fix: use production entrypoint for P1 manual serve
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { execFile, spawn } from "node:child_process";
|
import { execFile, spawn } from "node:child_process";
|
||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
import { closeSync, constants, fsyncSync, lstatSync, openSync, realpathSync } from "node:fs";
|
import { closeSync, constants, fstatSync, fsyncSync, lstatSync, openSync, realpathSync } from "node:fs";
|
||||||
import { access, chmod, lstat, mkdir, open, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
import { access, chmod, lstat, mkdir, open, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
||||||
|
import http from "node:http";
|
||||||
import net from "node:net";
|
import net from "node:net";
|
||||||
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
const exec = promisify(execFile); const modulePath=fileURLToPath(import.meta.url); const defaultRepositoryRoot=realpathSync(resolve(dirname(modulePath),"../.."));
|
const exec = promisify(execFile); const modulePath=fileURLToPath(import.meta.url); const defaultRepositoryRoot=realpathSync(resolve(dirname(modulePath),"../.."));
|
||||||
@@ -20,25 +21,25 @@ async function requireExactRecord(record){const entry=await lstat(record.path);i
|
|||||||
async function removeExactRecord(record){await requireExactRecord(record);await requireExactRecord(record);await rm(record.path);directorySync(dirname(record.path));}
|
async function removeExactRecord(record){await requireExactRecord(record);await requireExactRecord(record);await rm(record.path);directorySync(dirname(record.path));}
|
||||||
async function replaceExactRecord(record,value){await requireExactRecord(record);const bytes=`${JSON.stringify(value,null,2)}\n`,staging=join(dirname(record.path),`.${basename(record.path)}.${randomBytes(12).toString("hex")}.tmp`);let handle;try{handle=await open(staging,"wx",0o600);await handle.chmod(0o600);await handle.writeFile(bytes);await handle.sync();await handle.close();handle=undefined;await requireExactRecord(record);await rename(staging,record.path);const entry=await lstat(record.path);directorySync(dirname(record.path));return{path:record.path,bytes,dev:entry.dev,ino:entry.ino};}finally{if(handle)await handle.close().catch(()=>{});await rm(staging,{force:true}).catch(()=>{});}}
|
async function replaceExactRecord(record,value){await requireExactRecord(record);const bytes=`${JSON.stringify(value,null,2)}\n`,staging=join(dirname(record.path),`.${basename(record.path)}.${randomBytes(12).toString("hex")}.tmp`);let handle;try{handle=await open(staging,"wx",0o600);await handle.chmod(0o600);await handle.writeFile(bytes);await handle.sync();await handle.close();handle=undefined;await requireExactRecord(record);await rename(staging,record.path);const entry=await lstat(record.path);directorySync(dirname(record.path));return{path:record.path,bytes,dev:entry.dev,ino:entry.ino};}finally{if(handle)await handle.close().catch(()=>{});await rm(staging,{force:true}).catch(()=>{});}}
|
||||||
async function acquireLifecycle(repo,operation){const root=fixedManualRoot(repo),lockDirectory=join(repo,".artifacts","manual-acceptance"),lockPath=join(repo,".artifacts","manual-acceptance",".p1.lifecycle.lock");noSymlinkExisting(repo,lockDirectory);await mkdir(lockDirectory,{recursive:true,mode:0o700});noSymlinkExisting(repo,lockPath);const lifecycleNonce=randomBytes(32).toString("hex"),record=await exclusiveRecord(lockPath,{schemaVersion:1,kind:"p1-manual-lifecycle",operation,lifecycleNonce,root,repositoryRoot:repo},"external lifecycle lock"),entry=await requireExactRecord(record);return{...record,dev:entry.dev,ino:entry.ino};}
|
async function acquireLifecycle(repo,operation){const root=fixedManualRoot(repo),lockDirectory=join(repo,".artifacts","manual-acceptance"),lockPath=join(repo,".artifacts","manual-acceptance",".p1.lifecycle.lock");noSymlinkExisting(repo,lockDirectory);await mkdir(lockDirectory,{recursive:true,mode:0o700});noSymlinkExisting(repo,lockPath);const lifecycleNonce=randomBytes(32).toString("hex"),record=await exclusiveRecord(lockPath,{schemaVersion:1,kind:"p1-manual-lifecycle",operation,lifecycleNonce,root,repositoryRoot:repo},"external lifecycle lock"),entry=await requireExactRecord(record);return{...record,dev:entry.dev,ino:entry.ino};}
|
||||||
function supervisorPath(root){return join(root,"installation/runtime/p1-backend-supervisor.mjs");}
|
function legacySupervisorPath(root){return join(root,"installation/runtime/p1-backend-supervisor.mjs");}
|
||||||
function readinessPath(root){return join(root,"installation/runtime/backend.ready");}
|
const CONTROL_PORT=8792;
|
||||||
function supervisorSource(repo,root){const configUrl=pathToFileURL(join(repo,"backend/dist/config.js")).href,appUrl=pathToFileURL(join(repo,"backend/dist/app.js")).href,runtime=join(root,"installation/runtime");return `import net from "node:net";
|
const PRELOAD_SOURCE=`import net from "node:net";
|
||||||
import { chmod, lstat, open, rename, rm } from "node:fs/promises";
|
const HOST="127.0.0.1",PORT=8792,HEX=/^[0-9a-f]{64}$/;
|
||||||
import { randomBytes } from "node:crypto";
|
const argv=process.argv.slice(2),noncePrefix="--p1-manual-nonce=",rootPrefix="--p1-root=",controlPrefix="--p1-control-nonce=";
|
||||||
import { basename, join } from "node:path";
|
if(argv.length!==3||!argv[0].startsWith(noncePrefix)||!argv[1].startsWith(rootPrefix)||!argv[2].startsWith(controlPrefix))throw new Error("manual control identity arguments refused");
|
||||||
const ROOT=${JSON.stringify(root)},REPO=${JSON.stringify(repo)},RUNTIME=${JSON.stringify(runtime)},READY=${JSON.stringify(readinessPath(root))};
|
const nonce=argv[0].slice(noncePrefix.length),root=argv[1].slice(rootPrefix.length),controlNonce=argv[2].slice(controlPrefix.length);
|
||||||
const expected=["--p1-manual-nonce=","--p1-root="+ROOT,"--p1-control-nonce="];const argv=process.argv.slice(2);if(argv.length!==3||!argv[0].startsWith(expected[0])||argv[1]!==expected[1]||!argv[2].startsWith(expected[2]))throw new Error("supervisor identity arguments refused");
|
if(!HEX.test(nonce)||!root.startsWith("/")||!HEX.test(controlNonce))throw new Error("manual control identity refused");
|
||||||
const ownershipNonce=argv[0].slice(expected[0].length),controlNonce=argv[2].slice(expected[2].length);if(!/^[0-9a-f]{64}$/.test(ownershipNonce)||!/^[0-9a-f]{64}$/.test(controlNonce))throw new Error("supervisor nonce refused");
|
let state="STARTING",stopping=false;
|
||||||
let app,control,readyOwned=false,shutting=false,controlPort;
|
const identity=()=>({status:state,pid:process.pid,nonce,controlNonce,root,control:{host:HOST,port:PORT}});
|
||||||
async function writeReady(){const value={schemaVersion:1,kind:"p1-manual-backend-ready",status:"READY",pid:process.pid,nonce:ownershipNonce,controlNonce,root:ROOT,repositoryRoot:REPO,control:{host:"127.0.0.1",port:controlPort}};const bytes=JSON.stringify(value,null,2)+"\\n",tmp=join(RUNTIME,"."+basename(READY)+"."+randomBytes(12).toString("hex")+".tmp"),h=await open(tmp,"wx",0o600);try{await h.chmod(0o600);await h.writeFile(bytes);await h.sync();}finally{await h.close();}await rename(tmp,READY);readyOwned=true;}
|
const control=net.createServer(socket=>{let bytes="";socket.setEncoding("utf8");socket.on("data",chunk=>{bytes+=chunk;if(bytes.length>2048)socket.destroy();});socket.on("end",()=>{let request;try{request=JSON.parse(bytes);}catch{socket.end();return;}if(request?.nonce!==controlNonce){socket.end();return;}if(request.action==="status"){socket.end(JSON.stringify(identity())+"\\n");return;}if(request.action==="ready"&&!stopping){state="READY";clearTimeout(watchdog);socket.end(JSON.stringify(identity())+"\\n");return;}if(request.action==="stop"&&!stopping){stopping=true;state="STOPPING";clearTimeout(watchdog);socket.end(JSON.stringify(identity())+"\\n",()=>{control.close();setImmediate(()=>process.exit(0));});return;}socket.end();});});
|
||||||
async function removeReady(){try{const entry=await lstat(READY);if(!entry.isFile()||entry.isSymbolicLink())return;await rm(READY);}catch(error){if(error.code!=="ENOENT")throw error;}}
|
await new Promise((resolve,reject)=>{control.once("error",reject);control.listen({host:HOST,port:PORT,exclusive:true},resolve);});
|
||||||
async function shutdown(){if(shutting)return;shutting=true;try{await app.close();}finally{await new Promise(resolve=>control.close(resolve));if(readyOwned)await removeReady();}}
|
const watchdog=setTimeout(()=>{if(state!=="STARTING")return;console.error("manual backend readiness watchdog expired");control.close(()=>process.exit(1));setTimeout(()=>process.exit(1),100).unref();},8000);
|
||||||
try{try{await lstat(READY);throw new Error("supervisor readiness already exists");}catch(error){if(error.code!=="ENOENT")throw error;}const [{loadConfig},{buildApp}]=await Promise.all([import(${JSON.stringify(configUrl)}),import(${JSON.stringify(appUrl)})]);const config=loadConfig(process.env);if(config.host!=="127.0.0.1"||config.port!==8791)throw new Error("supervisor bind refused");app=buildApp(config);await app.listen({host:config.host,port:config.port});control=net.createServer(socket=>{let bytes="";socket.setEncoding("utf8");socket.on("data",chunk=>{bytes+=chunk;if(bytes.length>1024)socket.destroy();});socket.on("end",async()=>{let request;try{request=JSON.parse(bytes);}catch{socket.end();return;}if(request?.nonce!==controlNonce){socket.end();return;}if(request.action==="status"){socket.end(JSON.stringify({status:"READY",pid:process.pid,nonce:ownershipNonce})+"\\n");return;}if(request.action!=="stop"){socket.end();return;}socket.end(JSON.stringify({status:"STOPPING",pid:process.pid})+"\\n");await shutdown();});});await new Promise((resolve,reject)=>{control.once("error",reject);control.listen({host:"127.0.0.1",port:0,exclusive:true},resolve);});controlPort=control.address().port;await writeReady();}catch(error){console.error("manual backend supervisor refused: "+error.message);try{if(app)await app.close();}catch{}try{if(control?.listening)await new Promise(resolve=>control.close(resolve));}catch{}if(readyOwned)await removeReady().catch(()=>{});process.exitCode=1;}
|
`;
|
||||||
`;}
|
const PRELOAD=`data:text/javascript;base64,${Buffer.from(PRELOAD_SOURCE,"utf8").toString("base64")}`;
|
||||||
async function controlRequest(control,payload){if(control?.host!==HOST||!Number.isSafeInteger(control?.port)||control.port<1||control.port>65535)throw new Error("backend control identity mismatch");return await new Promise((resolvePromise,reject)=>{const socket=net.createConnection({host:control.host,port:control.port}),timer=setTimeout(()=>socket.destroy(new Error("backend control timeout")),2000);let bytes="";socket.setEncoding("utf8");socket.on("connect",()=>socket.end(JSON.stringify(payload)));socket.on("data",chunk=>{bytes+=chunk;if(bytes.length>2048)socket.destroy(new Error("backend control response too large"));});socket.on("error",reject);socket.on("close",()=>{clearTimeout(timer);let value;try{value=JSON.parse(bytes);}catch{return reject(new Error("backend control response is malformed"));}resolvePromise(value);});});}
|
async function controlRequest(control,payload){if(control?.host!==HOST||!Number.isSafeInteger(control?.port)||control.port<1||control.port>65535)throw new Error("backend control identity mismatch");return await new Promise((resolvePromise,reject)=>{const socket=net.createConnection({host:control.host,port:control.port}),timer=setTimeout(()=>socket.destroy(new Error("backend control timeout")),2000);let bytes="";socket.setEncoding("utf8");socket.on("connect",()=>socket.end(JSON.stringify(payload)));socket.on("data",chunk=>{bytes+=chunk;if(bytes.length>2048)socket.destroy(new Error("backend control response too large"));});socket.on("error",reject);socket.on("close",()=>{clearTimeout(timer);let value;try{value=JSON.parse(bytes);}catch{return reject(new Error("backend control response is malformed"));}resolvePromise(value);});});}
|
||||||
|
|
||||||
function ownedValue(repo,root,nonce){return{schemaVersion:1,kind:"p1-manual-acceptance",nonce,repositoryRoot:repo,root,status:"PENDING",createdAt:new Date().toISOString(),listener:{host:HOST,port:PORT,state:"stopped"},resources:[root,{kind:"fastify",host:HOST,port:PORT}]};}
|
function ownedValue(repo,root,nonce,backendLog){return{schemaVersion:1,kind:"p1-manual-acceptance",nonce,repositoryRoot:repo,root,status:"PENDING",createdAt:new Date().toISOString(),listener:{host:HOST,port:PORT,state:"stopped"},backendLog,resources:[root,{kind:"fastify",host:HOST,port:PORT}]};}
|
||||||
export async function readManualOwnership({repositoryRoot=defaultRepositoryRoot}={}){const repo=realpathSync(repositoryRoot),root=fixedManualRoot(repo);noSymlinkExisting(repo,root);let rootEntry,ownershipEntry;try{rootEntry=await lstat(root);ownershipEntry=await lstat(join(root,"ownership.json"));}catch{throw new Error("manual ownership is missing");}if(!rootEntry.isDirectory()||rootEntry.isSymbolicLink()||await realpath(root)!==root||!ownershipEntry.isFile()||ownershipEntry.isSymbolicLink())throw new Error("manual ownership is unsafe");let value;try{value=JSON.parse(await readFile(join(root,"ownership.json"),"utf8"));}catch{throw new Error("manual ownership is malformed");}const expected={...ownedValue(repo,root,value.nonce),createdAt:value.createdAt,listener:value.listener};if(value.schemaVersion!==1||value.kind!=="p1-manual-acceptance"||!HEX64.test(value.nonce??"")||value.repositoryRoot!==repo||value.root!==root||value.status!=="PENDING"||value.listener?.host!==HOST||value.listener?.port!==PORT||!value.createdAt||JSON.stringify(value.resources)!==JSON.stringify(expected.resources))throw new Error("manual ownership identity mismatch");return value;}
|
export async function readManualOwnership({repositoryRoot=defaultRepositoryRoot}={}){const repo=realpathSync(repositoryRoot),root=fixedManualRoot(repo);noSymlinkExisting(repo,root);let rootEntry,ownershipEntry;try{rootEntry=await lstat(root);ownershipEntry=await lstat(join(root,"ownership.json"));}catch{throw new Error("manual ownership is missing");}if(!rootEntry.isDirectory()||rootEntry.isSymbolicLink()||await realpath(root)!==root||!ownershipEntry.isFile()||ownershipEntry.isSymbolicLink())throw new Error("manual ownership is unsafe");let value;try{value=JSON.parse(await readFile(join(root,"ownership.json"),"utf8"));}catch{throw new Error("manual ownership is malformed");}const expected={...ownedValue(repo,root,value.nonce,value.backendLog),createdAt:value.createdAt,listener:value.listener};if(value.schemaVersion!==1||value.kind!=="p1-manual-acceptance"||!HEX64.test(value.nonce??"")||value.repositoryRoot!==repo||value.root!==root||value.status!=="PENDING"||value.listener?.host!==HOST||value.listener?.port!==PORT||!value.createdAt||value.backendLog?.path!==join(root,"logs/backend.log")||!Number.isSafeInteger(value.backendLog?.dev)||!Number.isSafeInteger(value.backendLog?.ino)||JSON.stringify(value.resources)!==JSON.stringify(expected.resources))throw new Error("manual ownership identity mismatch");return value;}
|
||||||
async function run(executable,argv,options={}){return await exec(executable,argv,{...options,maxBuffer:2*1024*1024,encoding:"utf8"});}
|
async function run(executable,argv,options={}){return await exec(executable,argv,{...options,maxBuffer:2*1024*1024,encoding:"utf8"});}
|
||||||
function descriptor(id,source){return{workspace:{schema_version:3,id,name:`P1 ${id}`,language:"en"},dwh:{engine:"postgres",database:"postgres",schema:"public",supported_transports:["postgres_direct"]},semantic_index:{vector_store:{engine:"qdrant",collection:id,dimensions:1024,distance:"cosine"},embedding:{provider:"ollama_internal",model:"qwen3-embedding:0.6b",dimensions:1024}},llm_policy:{allowed:["zai/glm-5.2"]},evidence:{source,policy:{max_chunk_chars:4000,retain_published_generations:3}}};}
|
function descriptor(id,source){return{workspace:{schema_version:3,id,name:`P1 ${id}`,language:"en"},dwh:{engine:"postgres",database:"postgres",schema:"public",supported_transports:["postgres_direct"]},semantic_index:{vector_store:{engine:"qdrant",collection:id,dimensions:1024,distance:"cosine"},embedding:{provider:"ollama_internal",model:"qwen3-embedding:0.6b",dimensions:1024}},llm_policy:{allowed:["zai/glm-5.2"]},evidence:{source,policy:{max_chunk_chars:4000,retain_published_generations:3}}};}
|
||||||
function descriptors(){return[descriptor("p1-filesystem",{type:"filesystem",uri:"workspace-content/p1-filesystem/evidence",patterns:["**/*.md"],max_bytes:10485760}),descriptor("p1-http",{type:"http",uris:["https://evidence.example.test/guide.md"],authentication:"signed_urls_file",connect_timeout_ms:1250,read_timeout_ms:30001,max_bytes:12345,max_redirects:2,allow_private_hosts:false,max_cache_bytes:67890}),descriptor("p1-s3",{type:"s3",uri:"s3://p1-evidence/published/",endpoint_url:"https://s3.example.test/",region:"eu-west-1",credentials:"static_files",trusted_endpoint:true,allow_private_endpoint:false,allow_insecure_endpoint:false,max_bytes:12345,max_objects:33,max_pages:4,page_size:5})];}
|
function descriptors(){return[descriptor("p1-filesystem",{type:"filesystem",uri:"workspace-content/p1-filesystem/evidence",patterns:["**/*.md"],max_bytes:10485760}),descriptor("p1-http",{type:"http",uris:["https://evidence.example.test/guide.md"],authentication:"signed_urls_file",connect_timeout_ms:1250,read_timeout_ms:30001,max_bytes:12345,max_redirects:2,allow_private_hosts:false,max_cache_bytes:67890}),descriptor("p1-s3",{type:"s3",uri:"s3://p1-evidence/published/",endpoint_url:"https://s3.example.test/",region:"eu-west-1",credentials:"static_files",trusted_endpoint:true,allow_private_endpoint:false,allow_insecure_endpoint:false,max_bytes:12345,max_objects:33,max_pages:4,page_size:5})];}
|
||||||
@@ -140,18 +141,96 @@ function git(args,label){const listing=execFileSync("git",[...args,"cat-file","-
|
|||||||
await walk(root);git(["--git-dir",join(root,"remote.git")],"remote.git");git(["-C",join(root,"author")],"author");git(["-C",join(root,"installation/registry/repo")],"installed-registry");if(found)process.exit(1);console.log("no fixture secret canary outside fixture-secrets or in any bounded Git object");
|
await walk(root);git(["--git-dir",join(root,"remote.git")],"remote.git");git(["-C",join(root,"author")],"author");git(["-C",join(root,"installation/registry/repo")],"installed-registry");if(found)process.exit(1);console.log("no fixture secret canary outside fixture-secrets or in any bounded Git object");
|
||||||
NODE
|
NODE
|
||||||
`],["absence-check.sh",`#!/usr/bin/env bash\nset -euo pipefail\nroot=${quote(root)}\nif find "$root" -path '*/.git' -prune -o -type f \\( -iname '*preprocess*' -o -iname '*embedding*' -o -iname '*qdrant*' -o -iname '*retention*' -o -iname '*active*' \\) -print | grep .; then echo 'unexpected P1-scope artifact' >&2; exit 1; fi\necho 'no out-of-scope runtime artifact found'\n`]]){await atomicWrite(join(commands,name),body,0o700);await chmod(join(commands,name),0o700);}}
|
`],["absence-check.sh",`#!/usr/bin/env bash\nset -euo pipefail\nroot=${quote(root)}\nif find "$root" -path '*/.git' -prune -o -type f \\( -iname '*preprocess*' -o -iname '*embedding*' -o -iname '*qdrant*' -o -iname '*retention*' -o -iname '*active*' \\) -print | grep .; then echo 'unexpected P1-scope artifact' >&2; exit 1; fi\necho 'no out-of-scope runtime artifact found'\n`]]){await atomicWrite(join(commands,name),body,0o700);await chmod(join(commands,name),0o700);}}
|
||||||
export async function prepareManual(options={}){const unknown=Object.keys(options).filter(key=>!["repositoryRoot","skipBuild"].includes(key));if(unknown.length)throw new Error(`unknown or automated-run prepare input: ${unknown.join(", ")}`);const{repositoryRoot=defaultRepositoryRoot,skipBuild=false}=options;const repo=realpathSync(repositoryRoot),root=fixedManualRoot(repo);await checkPrerequisites(repo);if(!skipBuild)await run("npm",["--prefix",join(repo,"backend"),"run","build"]);const lifecycle=await acquireLifecycle(repo,"prepare");try{noSymlinkExisting(repo,root);try{await mkdir(root,{recursive:false,mode:0o700});}catch(error){if(error.code==="EEXIST")throw new Error("manual acceptance root already exists; stop/cleanup it explicitly");throw error;}const nonce=randomBytes(32).toString("hex");await atomicWrite(join(root,"ownership.json"),`${JSON.stringify(ownedValue(repo,root,nonce),null,2)}\n`);for(const path of ["installation/registry","installation/data","installation/runtime","fixture-secrets","fixtures/descriptors","requests","responses","exports/raw","exports/extracted","rendered","logs","commands"])await mkdir(join(root,path),{recursive:true,mode:path==="fixture-secrets"?0o700:0o755});await atomicWrite(supervisorPath(root),supervisorSource(repo,root),0o600);await initializeGit(root);const items=descriptors();for(const workspace of items)await atomicWrite(join(root,"fixtures/descriptors",`${workspace.workspace.id}.json`),`${JSON.stringify(workspace,null,2)}\n`);const secrets={"dwh-password":`DWH-${randomBytes(16).toString("hex")}`,"evidence-signed-urls.json":JSON.stringify([`https://evidence.example.test/guide.md?token=SIGNED-${randomBytes(16).toString("hex")}`]),"evidence-access":`ACCESS-${randomBytes(16).toString("hex")}`,"evidence-secret":`SECRET-${randomBytes(16).toString("hex")}`,"evidence-session":`SESSION-${randomBytes(16).toString("hex")}`};for(const[name,value]of Object.entries(secrets))await atomicWrite(join(root,"fixture-secrets",name),value,0o600);const env={};for(const workspace of items){const ns=workspace.workspace.id.toUpperCase().replaceAll("-","_"),prefix=`THT_WS_${ns}`;Object.assign(env,{[`${prefix}_DWH_TRANSPORT`]:"postgres_direct",[`${prefix}_DWH_HOST`]:"dwh.invalid",[`${prefix}_DWH_PORT`]:"5432",[`${prefix}_DWH_USER`]:"reader",[`${prefix}_DWH_PASSWORD_FILE`]:join(root,"fixture-secrets/dwh-password")});}Object.assign(env,{THT_WORKSPACE_SECRET_ROOTS:join(root,"fixture-secrets"),THT_WS_P1_HTTP_EVIDENCE_SIGNED_URLS_FILE:join(root,"fixture-secrets/evidence-signed-urls.json"),THT_WS_P1_S3_EVIDENCE_ACCESS_KEY_FILE:join(root,"fixture-secrets/evidence-access"),THT_WS_P1_S3_EVIDENCE_SECRET_KEY_FILE:join(root,"fixture-secrets/evidence-secret"),THT_WS_P1_S3_EVIDENCE_SESSION_TOKEN_FILE:join(root,"fixture-secrets/evidence-session")});await atomicWrite(join(root,"installation/bindings.env"),Object.entries(env).map(([k,v])=>`${k}=${quote(v)}`).join("\n")+"\n");await atomicWrite(join(root,"installation/base.yaml"),"{}\n");for(const[name,value]of Object.entries(requestFixtures(items)))await atomicWrite(join(root,"requests",name),`${JSON.stringify(value,null,2)}\n`);await writeCommands(repo,root);await atomicWrite(join(root,"GUIDE.md"),guide(repo,root),0o600);return{repositoryRoot:repo,root,nonce};}finally{await removeExactRecord(lifecycle);}}
|
export async function prepareManual(options={}){const unknown=Object.keys(options).filter(key=>!["repositoryRoot","skipBuild"].includes(key));if(unknown.length)throw new Error(`unknown or automated-run prepare input: ${unknown.join(", ")}`);const{repositoryRoot=defaultRepositoryRoot,skipBuild=false}=options;const repo=realpathSync(repositoryRoot),root=fixedManualRoot(repo);await checkPrerequisites(repo);if(!skipBuild)await run("npm",["--prefix",join(repo,"backend"),"run","build"]);const lifecycle=await acquireLifecycle(repo,"prepare");try{noSymlinkExisting(repo,root);try{await mkdir(root,{recursive:false,mode:0o700});}catch(error){if(error.code==="EEXIST")throw new Error("manual acceptance root already exists; stop/cleanup it explicitly");throw error;}const nonce=randomBytes(32).toString("hex");for(const path of ["installation/registry","installation/data","installation/runtime","fixture-secrets","fixtures/descriptors","requests","responses","exports/raw","exports/extracted","rendered","logs","commands"])await mkdir(join(root,path),{recursive:true,mode:path==="fixture-secrets"?0o700:0o755});const backendLogPath=join(root,"logs/backend.log"),backendLogHandle=await open(backendLogPath,"wx",0o600);let backendLogEntry;try{await backendLogHandle.chmod(0o600);await backendLogHandle.sync();backendLogEntry=await backendLogHandle.stat();}finally{await backendLogHandle.close();}directorySync(dirname(backendLogPath));const backendLog={path:backendLogPath,dev:backendLogEntry.dev,ino:backendLogEntry.ino};await atomicWrite(join(root,"ownership.json"),`${JSON.stringify(ownedValue(repo,root,nonce,backendLog),null,2)}\n`);await initializeGit(root);const items=descriptors();for(const workspace of items)await atomicWrite(join(root,"fixtures/descriptors",`${workspace.workspace.id}.json`),`${JSON.stringify(workspace,null,2)}\n`);const secrets={"dwh-password":`DWH-${randomBytes(16).toString("hex")}`,"evidence-signed-urls.json":JSON.stringify([`https://evidence.example.test/guide.md?token=SIGNED-${randomBytes(16).toString("hex")}`]),"evidence-access":`ACCESS-${randomBytes(16).toString("hex")}`,"evidence-secret":`SECRET-${randomBytes(16).toString("hex")}`,"evidence-session":`SESSION-${randomBytes(16).toString("hex")}`};for(const[name,value]of Object.entries(secrets))await atomicWrite(join(root,"fixture-secrets",name),value,0o600);const env={};for(const workspace of items){const ns=workspace.workspace.id.toUpperCase().replaceAll("-","_"),prefix=`THT_WS_${ns}`;Object.assign(env,{[`${prefix}_DWH_TRANSPORT`]:"postgres_direct",[`${prefix}_DWH_HOST`]:"dwh.invalid",[`${prefix}_DWH_PORT`]:"5432",[`${prefix}_DWH_USER`]:"reader",[`${prefix}_DWH_PASSWORD_FILE`]:join(root,"fixture-secrets/dwh-password")});}Object.assign(env,{THT_WORKSPACE_SECRET_ROOTS:join(root,"fixture-secrets"),THT_WS_P1_HTTP_EVIDENCE_SIGNED_URLS_FILE:join(root,"fixture-secrets/evidence-signed-urls.json"),THT_WS_P1_S3_EVIDENCE_ACCESS_KEY_FILE:join(root,"fixture-secrets/evidence-access"),THT_WS_P1_S3_EVIDENCE_SECRET_KEY_FILE:join(root,"fixture-secrets/evidence-secret"),THT_WS_P1_S3_EVIDENCE_SESSION_TOKEN_FILE:join(root,"fixture-secrets/evidence-session")});await atomicWrite(join(root,"installation/bindings.env"),Object.entries(env).map(([k,v])=>`${k}=${quote(v)}`).join("\n")+"\n");await atomicWrite(join(root,"installation/base.yaml"),"{}\n");for(const[name,value]of Object.entries(requestFixtures(items)))await atomicWrite(join(root,"requests",name),`${JSON.stringify(value,null,2)}\n`);await writeCommands(repo,root);await atomicWrite(join(root,"GUIDE.md"),guide(repo,root),0o600);return{repositoryRoot:repo,root,nonce};}finally{await removeExactRecord(lifecycle);}}
|
||||||
function portAvailable(){return new Promise((resolvePromise,reject)=>{const server=net.createServer();server.once("error",error=>error.code==="EADDRINUSE"?reject(new Error(`${HOST}:${PORT} is occupied`)):reject(error));server.listen({host:HOST,port:PORT,exclusive:true},()=>server.close(()=>resolvePromise()));});}
|
function portAvailable(port,label=`${HOST}:${port}`){return new Promise((resolvePromise,reject)=>{const server=net.createServer();server.once("error",error=>error.code==="EADDRINUSE"?reject(new Error(`${label} is occupied`)):reject(error));server.listen({host:HOST,port,exclusive:true},()=>server.close(()=>resolvePromise()));});}
|
||||||
|
async function requireCanonicalDirectory(path,label){const entry=await lstat(path);if(!entry.isDirectory()||entry.isSymbolicLink()||await realpath(path)!==path)throw new Error(`${label} directory identity is unsafe`);return entry;}
|
||||||
|
async function requireAbsent(path,label){try{await lstat(path);throw new Error(`${label} is legacy or unsafe`);}catch(error){if(error.code!=="ENOENT")throw error;}}
|
||||||
|
async function validateServeFilesystem(repo,root,owned){
|
||||||
|
if(root!==fixedManualRoot(repo))throw new Error("owned root identity is unsafe");
|
||||||
|
for(const [path,label] of [
|
||||||
|
[repo,"repository root"],[join(repo,".artifacts"),"artifact root"],[join(repo,".artifacts/manual-acceptance"),"manual root ancestor"],[root,"owned root"],
|
||||||
|
[join(root,"installation"),"owned installation"],[join(root,"installation/runtime"),"owned runtime"],[join(root,"installation/data"),"owned data"],
|
||||||
|
[join(root,"installation/registry"),"owned registry"],[join(root,"fixture-secrets"),"owned secrets"],[join(root,"logs"),"owned logs"],
|
||||||
|
[join(repo,"backend"),"backend root"],[join(repo,"backend/dist"),"backend distribution"],
|
||||||
|
])await requireCanonicalDirectory(path,label);
|
||||||
|
await requireAbsent(legacySupervisorPath(root),"legacy supervisor");
|
||||||
|
const script=join(repo,"backend/dist/server.js"),entry=await lstat(script);
|
||||||
|
if(!entry.isFile()||entry.isSymbolicLink()||entry.nlink!==1||await realpath(script)!==script)throw new Error("production server identity is unsafe");
|
||||||
|
const logPath=join(root,"logs/backend.log");
|
||||||
|
if(owned.backendLog?.path!==logPath)throw new Error("backend log ownership identity is unsafe");
|
||||||
|
return{script,logPath};
|
||||||
|
}
|
||||||
|
function openOwnedBackendLog(owned,logPath){
|
||||||
|
if(!Number.isInteger(constants.O_NOFOLLOW))throw new Error("backend log no-follow protection is unavailable");
|
||||||
|
let fd;
|
||||||
|
try{
|
||||||
|
fd=openSync(logPath,constants.O_WRONLY|constants.O_APPEND|constants.O_NOFOLLOW);
|
||||||
|
const entry=fstatSync(fd),pathEntry=lstatSync(logPath);
|
||||||
|
if(!entry.isFile()||(entry.mode&0o777)!==0o600||entry.nlink!==1||entry.dev!==owned.backendLog.dev||entry.ino!==owned.backendLog.ino||pathEntry.isSymbolicLink()||!pathEntry.isFile()||pathEntry.dev!==entry.dev||pathEntry.ino!==entry.ino)throw new Error("backend log identity is unsafe");
|
||||||
|
return fd;
|
||||||
|
}catch(error){if(fd!==undefined)closeSync(fd);throw error;}
|
||||||
|
}
|
||||||
|
async function ensureRuntimeDirectory(path){try{await mkdir(path,{mode:0o700});}catch(error){if(error.code!=="EEXIST")throw error;}const entry=await requireCanonicalDirectory(path,"owned runtime child");if((entry.mode&0o077)!==0)throw new Error("owned runtime child mode is unsafe");}
|
||||||
async function processStart(pid){return (await run("ps",["-p",String(pid),"-o","lstart="])).stdout.trim();}
|
async function processStart(pid){return (await run("ps",["-p",String(pid),"-o","lstart="])).stdout.trim();}
|
||||||
async function processArgs(pid){return (await run("ps",["-p",String(pid),"-o","command="])).stdout.trim();}
|
async function processArgs(pid){return (await run("ps",["-ww","-p",String(pid),"-o","command="])).stdout.trim();}
|
||||||
async function processCwd(pid){try{return await realpath(`/proc/${pid}/cwd`);}catch{try{const out=(await run("lsof",["-a","-p",String(pid),"-d","cwd","-Fn"])).stdout.split("\n").find(x=>x.startsWith("n"));return out?await realpath(out.slice(1)):"";}catch{return"";}}}
|
async function processCwd(pid){try{return await realpath(`/proc/${pid}/cwd`);}catch{try{const out=(await run("lsof",["-a","-p",String(pid),"-d","cwd","-Fn"])).stdout.split("\n").find(x=>x.startsWith("n"));return out?await realpath(out.slice(1)):"";}catch{return"";}}}
|
||||||
async function processExecutable(pid){try{return await realpath(`/proc/${pid}/exe`);}catch{try{const paths=(await run("lsof",["-a","-p",String(pid),"-d","txt","-Fn"])).stdout.split("\n").filter(x=>x.startsWith("n")).map(x=>x.slice(1));for(const path of paths){try{const canonical=await realpath(path);if(canonical===realpathSync(process.execPath))return canonical;}catch{}}return"";}catch{return"";}}}
|
async function processExecutable(pid){try{return await realpath(`/proc/${pid}/exe`);}catch{try{const paths=(await run("lsof",["-a","-p",String(pid),"-d","txt","-Fn"])).stdout.split("\n").filter(x=>x.startsWith("n")).map(x=>x.slice(1));for(const path of paths){try{const canonical=await realpath(path);if(canonical===realpathSync(process.execPath))return canonical;}catch{}}return"";}catch{return"";}}}
|
||||||
function alive(pid){try{process.kill(pid,0);return true;}catch{return false;}}
|
function alive(pid){try{process.kill(pid,0);return true;}catch{return false;}}
|
||||||
async function readPid(root){const path=join(root,"backend.pid"),entry=await lstat(path);if(!entry.isFile()||entry.isSymbolicLink()||(entry.mode&0o777)!==0o600)throw new Error("backend PID record is unsafe");const bytes=await readFile(path,"utf8");let value;try{value=JSON.parse(bytes);}catch{throw new Error("backend PID record is malformed");}return{path,bytes,value,dev:entry.dev,ino:entry.ino};}
|
async function readPid(root){const path=join(root,"backend.pid"),entry=await lstat(path);if(!entry.isFile()||entry.isSymbolicLink()||(entry.mode&0o777)!==0o600)throw new Error("backend PID record is unsafe");const bytes=await readFile(path,"utf8");let value;try{value=JSON.parse(bytes);}catch{throw new Error("backend PID record is malformed");}return{path,bytes,value,dev:entry.dev,ino:entry.ino};}
|
||||||
async function validateProcess(repo,root,owned,pidRecord){if(pidRecord.schemaVersion!==1||pidRecord.kind!=="p1-manual-backend"||pidRecord.status!=="RUNNING"||!Number.isSafeInteger(pidRecord.pid)||pidRecord.pid<2||!HEX64.test(pidRecord.reservationNonce??"")||pidRecord.nonce!==owned.nonce||pidRecord.root!==root||pidRecord.repositoryRoot!==repo||pidRecord.executable!==process.execPath||pidRecord.script!==supervisorPath(root)||!pidRecord.startIdentity||pidRecord.control?.host!==HOST||!Number.isSafeInteger(pidRecord.control?.port))throw new Error("backend PID identity mismatch");if(!alive(pidRecord.pid))throw new Error("backend PID is stale; operator inspection required");const[start,args,cwd,executable]=await Promise.all([processStart(pidRecord.pid),processArgs(pidRecord.pid),processCwd(pidRecord.pid),processExecutable(pidRecord.pid)]);const expectedArgs=[pidRecord.executable,pidRecord.script,`--p1-manual-nonce=${owned.nonce}`,`--p1-root=${root}`,`--p1-control-nonce=${pidRecord.reservationNonce}`].join(" ");if(start!==pidRecord.startIdentity||cwd!==repo||executable!==realpathSync(pidRecord.executable)||args!==expectedArgs)throw new Error("backend process identity mismatch; refusing cooperative control");return true;}
|
async function validateProcess(repo,root,owned,pidRecord){
|
||||||
|
const script=join(repo,"backend/dist/server.js");
|
||||||
|
if(pidRecord.schemaVersion!==1||pidRecord.kind!=="p1-manual-backend"||pidRecord.status!=="RUNNING"||!Number.isSafeInteger(pidRecord.pid)||pidRecord.pid<2||!HEX64.test(pidRecord.reservationNonce??"")||pidRecord.nonce!==owned.nonce||pidRecord.root!==root||pidRecord.repositoryRoot!==repo||pidRecord.executable!==process.execPath||pidRecord.preload!==PRELOAD||pidRecord.script!==script||!pidRecord.startIdentity||pidRecord.control?.host!==HOST||pidRecord.control?.port!==CONTROL_PORT)throw new Error("backend process identity mismatch; refusing cooperative control");
|
||||||
|
if(!alive(pidRecord.pid))throw new Error("backend PID is stale; operator inspection required");
|
||||||
|
const[start,args,cwd,executable]=await Promise.all([processStart(pidRecord.pid),processArgs(pidRecord.pid),processCwd(pidRecord.pid),processExecutable(pidRecord.pid)]);
|
||||||
|
const expectedArgs=[pidRecord.executable,"--import",pidRecord.preload,pidRecord.script,`--p1-manual-nonce=${owned.nonce}`,`--p1-root=${root}`,`--p1-control-nonce=${pidRecord.reservationNonce}`].join(" ");
|
||||||
|
if(start!==pidRecord.startIdentity||cwd!==repo||executable!==realpathSync(pidRecord.executable)||args!==expectedArgs)throw new Error("backend process identity mismatch; refusing cooperative control");return true;
|
||||||
|
}
|
||||||
async function waitForChildExit(child,milliseconds){if(!child||child.exitCode!==null||child.signalCode!==null)return true;return await Promise.race([new Promise(resolvePromise=>child.once("exit",()=>resolvePromise(true))),new Promise(resolvePromise=>setTimeout(()=>resolvePromise(child.exitCode!==null||child.signalCode!==null),milliseconds))]);}
|
async function waitForChildExit(child,milliseconds){if(!child||child.exitCode!==null||child.signalCode!==null)return true;return await Promise.race([new Promise(resolvePromise=>child.once("exit",()=>resolvePromise(true))),new Promise(resolvePromise=>setTimeout(()=>resolvePromise(child.exitCode!==null||child.signalCode!==null),milliseconds))]);}
|
||||||
export async function serveManual({repositoryRoot=defaultRepositoryRoot}={}){const repo=realpathSync(repositoryRoot),lifecycle=await acquireLifecycle(repo,"serve");let pidRecord,child,ready;try{const owned=await readManualOwnership({repositoryRoot:repo}),root=owned.root;if(owned.listener.host!==HOST||owned.listener.port!==PORT)throw new Error("non-loopback or unexpected bind refused");const reservationNonce=randomBytes(32).toString("hex");pidRecord=await exclusiveRecord(join(root,"backend.pid"),{schemaVersion:1,kind:"p1-manual-backend-reservation",status:"RESERVED",reservationNonce,nonce:owned.nonce,root,repositoryRoot:repo},"backend PID record");await portAvailable();await mkdir(join(root,"installation/runtime/home"),{recursive:true,mode:0o700});await mkdir(join(root,"installation/runtime/tmp"),{recursive:true,mode:0o700});try{await lstat(readinessPath(root));throw new Error("backend readiness record already exists; operator inspection required");}catch(error){if(error.code!=="ENOENT")throw error;}const inherited={};for(const key of ["PATH","LANG","LC_ALL","TZ"])if(process.env[key]!==undefined)inherited[key]=process.env[key];const env={...inherited,HOME:join(root,"installation/runtime/home"),TMPDIR:join(root,"installation/runtime/tmp"),HOST,PORT:String(PORT),AUTH_MODE:"none",THT_BIN:join(repo,"harness/.venv/bin/tht"),THT_HARNESS_DIR:join(repo,"harness"),THT_DATA_ROOT:join(root,"installation/data"),SETTINGS_FILE:join(root,"installation/data/settings.json"),MAINTENANCE_STATE_FILE:join(root,"installation/data/maintenance.json"),THT_WORKSPACE_REGISTRY_ROOT:join(root,"installation/registry"),THT_WORKSPACE_GIT_REMOTE:join(root,"remote.git"),THT_WORKSPACE_GIT_BRANCH:"main",THT_WORKSPACE_GIT_AUTHOR_NAME:"P1 Manual API Publisher",THT_WORKSPACE_GIT_AUTHOR_EMAIL:"p1-manual-api@example.invalid",THT_WORKSPACE_INSTALLATION_ID:"p1-manual-acceptance",THT_WORKSPACE_SECRET_ROOTS:join(root,"fixture-secrets"),THT_HOME:join(root,"installation/runtime/tht-home")};const stdout=openSync(join(root,"logs/backend.stdout.log"),constants.O_WRONLY|constants.O_CREAT|constants.O_APPEND,0o600),stderr=openSync(join(root,"logs/backend.stderr.log"),constants.O_WRONLY|constants.O_CREAT|constants.O_APPEND,0o600);try{child=spawn(process.execPath,[supervisorPath(root),`--p1-manual-nonce=${owned.nonce}`,`--p1-root=${root}`,`--p1-control-nonce=${reservationNonce}`],{cwd:repo,env,detached:true,stdio:["ignore",stdout,stderr]});}finally{closeSync(stdout);closeSync(stderr);}let start="";for(let n=0;n<40;n++){if(child.exitCode!==null)break;try{start=await processStart(child.pid);if(start)break;}catch{}await new Promise(r=>setTimeout(r,25));}if(!start)throw new Error("backend failed before process identity could be recorded");pidRecord=await replaceExactRecord(pidRecord,{schemaVersion:1,kind:"p1-manual-backend-reservation",status:"STARTING",pid:child.pid,reservationNonce,nonce:owned.nonce,root,repositoryRoot:repo,executable:process.execPath,script:supervisorPath(root),startIdentity:start});for(let n=0;n<100;n++){if(child.exitCode!==null)break;try{const entry=await lstat(readinessPath(root));if(!entry.isFile()||entry.isSymbolicLink()||(entry.mode&0o777)!==0o600)throw new Error("backend readiness is unsafe");const value=JSON.parse(await readFile(readinessPath(root),"utf8"));if(value.status!=="READY"||value.pid!==child.pid||value.nonce!==owned.nonce||value.controlNonce!==reservationNonce||value.root!==root||value.repositoryRoot!==repo||value.control?.host!==HOST||!Number.isSafeInteger(value.control?.port))throw new Error("backend readiness identity mismatch");const answer=await controlRequest(value.control,{action:"status",nonce:reservationNonce});if(answer.status==="READY"&&answer.pid===child.pid&&answer.nonce===owned.nonce){ready=value;break;}}catch{}await new Promise(r=>setTimeout(r,50));}if(!ready)throw new Error("backend readiness failed; inspect owned logs and starting PID record");pidRecord=await replaceExactRecord(pidRecord,{schemaVersion:1,kind:"p1-manual-backend",status:"RUNNING",pid:child.pid,reservationNonce,nonce:owned.nonce,root,repositoryRoot:repo,executable:process.execPath,script:supervisorPath(root),startIdentity:start,control:ready.control});child.unref();return child.pid;}catch(error){if(child&&ready){try{await controlRequest(ready.control,{action:"stop",nonce:ready.controlNonce});}catch{}await waitForChildExit(child,3000);}else if(child)await waitForChildExit(child,1000);if(pidRecord&&(!child||child.exitCode!==null||child.signalCode!==null))await removeExactRecord(pidRecord).catch(()=>{});throw error;}finally{await removeExactRecord(lifecycle);}}
|
async function healthStatus(){return await new Promise((resolvePromise,reject)=>{const request=http.get({host:HOST,port:PORT,path:"/health",timeout:500},response=>{const status=response.statusCode;response.resume();response.once("end",()=>resolvePromise(status));});request.once("timeout",()=>request.destroy(new Error("backend health readiness timeout")));request.once("error",reject);});}
|
||||||
export async function stopManual({repositoryRoot=defaultRepositoryRoot}={}){const repo=realpathSync(repositoryRoot),lifecycle=await acquireLifecycle(repo,"stop");try{const owned=await readManualOwnership({repositoryRoot:repo}),root=owned.root;let record;try{record=await readPid(root);}catch(error){if(error.code==="ENOENT")throw new Error("owned backend is not served");throw error;}await validateProcess(repo,root,owned,record.value);const answer=await controlRequest(record.value.control,{action:"stop",nonce:record.value.reservationNonce});if(answer.status!=="STOPPING"||answer.pid!==record.value.pid)throw new Error("backend cooperative stop acknowledgement mismatch; PID record retained");for(let n=0;n<100;n++){if(!alive(record.value.pid)){await removeExactRecord(record);return;}await new Promise(r=>setTimeout(r,100));}throw new Error("owned backend did not stop cooperatively; operator must intervene; PID record retained");}finally{await removeExactRecord(lifecycle);}}
|
function exactControlIdentity(answer,child,owned,root,reservationNonce){return answer?.pid===child.pid&&answer?.nonce===owned.nonce&&answer?.controlNonce===reservationNonce&&answer?.root===root&&answer?.control?.host===HOST&&answer?.control?.port===CONTROL_PORT;}
|
||||||
|
export async function serveManual({repositoryRoot=defaultRepositoryRoot}={}){
|
||||||
|
const repo=realpathSync(repositoryRoot),lifecycle=await acquireLifecycle(repo,"serve");let pidRecord,child,controlObserved=false,logFd;
|
||||||
|
try{
|
||||||
|
const owned=await readManualOwnership({repositoryRoot:repo}),root=owned.root;
|
||||||
|
if(owned.listener.host!==HOST||owned.listener.port!==PORT)throw new Error("non-loopback or unexpected bind refused");
|
||||||
|
const{script,logPath}=await validateServeFilesystem(repo,root,owned);
|
||||||
|
logFd=openOwnedBackendLog(owned,logPath);
|
||||||
|
const reservationNonce=randomBytes(32).toString("hex");
|
||||||
|
pidRecord=await exclusiveRecord(join(root,"backend.pid"),{schemaVersion:1,kind:"p1-manual-backend-reservation",status:"RESERVED",reservationNonce,nonce:owned.nonce,root,repositoryRoot:repo},"backend PID record");
|
||||||
|
await Promise.all([portAvailable(PORT),portAvailable(CONTROL_PORT,`${HOST}:${CONTROL_PORT} control port`)]);
|
||||||
|
await ensureRuntimeDirectory(join(root,"installation/runtime/home"));await ensureRuntimeDirectory(join(root,"installation/runtime/tmp"));await ensureRuntimeDirectory(join(root,"installation/runtime/tht-home"));
|
||||||
|
const inherited={};for(const key of ["PATH","LANG","LC_ALL","TZ"])if(process.env[key]!==undefined)inherited[key]=process.env[key];
|
||||||
|
const env={...inherited,HOME:join(root,"installation/runtime/home"),TMPDIR:join(root,"installation/runtime/tmp"),HOST,PORT:String(PORT),AUTH_MODE:"none",THT_BIN:join(repo,"harness/.venv/bin/tht"),THT_HARNESS_DIR:join(repo,"harness"),THT_DATA_ROOT:join(root,"installation/data"),SETTINGS_FILE:join(root,"installation/data/settings.json"),MAINTENANCE_STATE_FILE:join(root,"installation/data/maintenance.json"),THT_WORKSPACE_REGISTRY_ROOT:join(root,"installation/registry"),THT_WORKSPACE_GIT_REMOTE:join(root,"remote.git"),THT_WORKSPACE_GIT_BRANCH:"main",THT_WORKSPACE_GIT_AUTHOR_NAME:"P1 Manual API Publisher",THT_WORKSPACE_GIT_AUTHOR_EMAIL:"p1-manual-api@example.invalid",THT_WORKSPACE_INSTALLATION_ID:"p1-manual-acceptance",THT_WORKSPACE_SECRET_ROOTS:join(root,"fixture-secrets"),THT_HOME:join(root,"installation/runtime/tht-home")};
|
||||||
|
child=spawn(process.execPath,["--import",PRELOAD,script,`--p1-manual-nonce=${owned.nonce}`,`--p1-root=${root}`,`--p1-control-nonce=${reservationNonce}`],{cwd:repo,env,detached:true,stdio:["ignore",logFd,logFd]});
|
||||||
|
closeSync(logFd);logFd=undefined;
|
||||||
|
let start="";for(let n=0;n<80;n++){if(child.exitCode!==null)break;try{start=await processStart(child.pid);if(start)break;}catch{}await new Promise(r=>setTimeout(r,25));}
|
||||||
|
if(!start)throw new Error("backend failed before process identity could be recorded");
|
||||||
|
pidRecord=await replaceExactRecord(pidRecord,{schemaVersion:1,kind:"p1-manual-backend-reservation",status:"STARTING",pid:child.pid,reservationNonce,nonce:owned.nonce,root,repositoryRoot:repo,executable:process.execPath,preload:PRELOAD,script,startIdentity:start,control:{host:HOST,port:CONTROL_PORT}});
|
||||||
|
const deadline=Date.now()+7000;let readyAnswer;
|
||||||
|
while(Date.now()<deadline&&child.exitCode===null){
|
||||||
|
let status;try{status=await controlRequest({host:HOST,port:CONTROL_PORT},{action:"status",nonce:reservationNonce});}catch{await new Promise(r=>setTimeout(r,50));continue;}
|
||||||
|
if(!exactControlIdentity(status,child,owned,root,reservationNonce)||status.status!=="STARTING")throw new Error("backend control status identity mismatch");
|
||||||
|
controlObserved=true;
|
||||||
|
let httpCode;try{httpCode=await healthStatus();}catch{await new Promise(r=>setTimeout(r,50));continue;}
|
||||||
|
if(!Number.isSafeInteger(httpCode)||httpCode<200||httpCode>=300)throw new Error(`backend health readiness returned HTTP ${httpCode}`);
|
||||||
|
readyAnswer=await controlRequest({host:HOST,port:CONTROL_PORT},{action:"ready",nonce:reservationNonce});
|
||||||
|
if(!exactControlIdentity(readyAnswer,child,owned,root,reservationNonce)||readyAnswer.status!=="READY")throw new Error("backend READY acknowledgement identity mismatch");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(!readyAnswer)throw new Error("backend readiness failed; inspect owned backend log and starting PID record");
|
||||||
|
const runningValue={schemaVersion:1,kind:"p1-manual-backend",status:"RUNNING",pid:child.pid,reservationNonce,nonce:owned.nonce,root,repositoryRoot:repo,executable:process.execPath,preload:PRELOAD,script,startIdentity:start,control:{host:HOST,port:CONTROL_PORT}};
|
||||||
|
await validateProcess(repo,root,owned,runningValue);
|
||||||
|
pidRecord=await replaceExactRecord(pidRecord,runningValue);
|
||||||
|
child.unref();return child.pid;
|
||||||
|
}catch(error){
|
||||||
|
if(logFd!==undefined){closeSync(logFd);logFd=undefined;}
|
||||||
|
if(child&&controlObserved){try{await controlRequest({host:HOST,port:CONTROL_PORT},{action:"stop",nonce:pidRecord?JSON.parse(pidRecord.bytes).reservationNonce:undefined});}catch{}await waitForChildExit(child,3000);}
|
||||||
|
else if(child)await waitForChildExit(child,8500);
|
||||||
|
if(pidRecord&&(!child||child.exitCode!==null||child.signalCode!==null||!alive(child.pid)))await removeExactRecord(pidRecord).catch(()=>{});
|
||||||
|
throw error;
|
||||||
|
}finally{if(logFd!==undefined)closeSync(logFd);await removeExactRecord(lifecycle);}
|
||||||
|
}
|
||||||
|
export async function stopManual({repositoryRoot=defaultRepositoryRoot}={}){const repo=realpathSync(repositoryRoot),lifecycle=await acquireLifecycle(repo,"stop");try{const owned=await readManualOwnership({repositoryRoot:repo}),root=owned.root;let record;try{record=await readPid(root);}catch(error){if(error.code==="ENOENT")throw new Error("owned backend is not served");throw error;}await validateProcess(repo,root,owned,record.value);const answer=await controlRequest(record.value.control,{action:"stop",nonce:record.value.reservationNonce});if(answer.status!=="STOPPING"||answer.pid!==record.value.pid||answer.nonce!==owned.nonce||answer.controlNonce!==record.value.reservationNonce||answer.root!==root||answer.control?.host!==HOST||answer.control?.port!==CONTROL_PORT)throw new Error("backend cooperative stop acknowledgement mismatch; PID record retained");for(let n=0;n<100;n++){if(!alive(record.value.pid)){await removeExactRecord(record);return;}await new Promise(r=>setTimeout(r,100));}throw new Error("owned backend did not stop cooperatively; operator must intervene; PID record retained");}finally{await removeExactRecord(lifecycle);}}
|
||||||
export async function cleanupManual({repositoryRoot=defaultRepositoryRoot}={}){const repo=realpathSync(repositoryRoot),lifecycle=await acquireLifecycle(repo,"cleanup");try{const owned=await readManualOwnership({repositoryRoot:repo}),root=owned.root;try{const record=await readPid(root);if(record.value.status==="RUNNING"&&alive(record.value.pid)){await validateProcess(repo,root,owned,record.value);throw new Error("owned backend is live; run stop first");}throw new Error("stale or starting backend PID record requires operator inspection and stop validation");}catch(error){if(error.code!=="ENOENT")throw error;}if(root!==fixedManualRoot(repo)||!below(join(repo,".artifacts"),root))throw new Error("cleanup root identity mismatch");const tombstone=join(dirname(root),`.deleting-p1-${owned.nonce.slice(0,16)}`);await rename(root,tombstone);await rm(tombstone,{recursive:true});}finally{await removeExactRecord(lifecycle);}}
|
export async function cleanupManual({repositoryRoot=defaultRepositoryRoot}={}){const repo=realpathSync(repositoryRoot),lifecycle=await acquireLifecycle(repo,"cleanup");try{const owned=await readManualOwnership({repositoryRoot:repo}),root=owned.root;try{const record=await readPid(root);if(record.value.status==="RUNNING"&&alive(record.value.pid)){await validateProcess(repo,root,owned,record.value);throw new Error("owned backend is live; run stop first");}throw new Error("stale or starting backend PID record requires operator inspection and stop validation");}catch(error){if(error.code!=="ENOENT")throw error;}if(root!==fixedManualRoot(repo)||!below(join(repo,".artifacts"),root))throw new Error("cleanup root identity mismatch");const tombstone=join(dirname(root),`.deleting-p1-${owned.nonce.slice(0,16)}`);await rename(root,tombstone);await rm(tombstone,{recursive:true});}finally{await removeExactRecord(lifecycle);}}
|
||||||
async function main(){const[action,...rest]=process.argv.slice(2);if(rest.length||!["prepare","serve","stop","cleanup"].includes(action??""))throw new Error("usage: p1-manual-acceptance.mjs prepare|serve|stop|cleanup");if(action==="prepare")await prepareManual({skipBuild:true});if(action==="serve")await serveManual();if(action==="stop")await stopManual();if(action==="cleanup")await cleanupManual();console.log(`P1 manual acceptance ${action}: ${action==="prepare"?"PENDING":"complete"}`);}
|
async function main(){const[action,...rest]=process.argv.slice(2);if(rest.length||!["prepare","serve","stop","cleanup"].includes(action??""))throw new Error("usage: p1-manual-acceptance.mjs prepare|serve|stop|cleanup");if(action==="prepare")await prepareManual({skipBuild:true});if(action==="serve")await serveManual();if(action==="stop")await stopManual();if(action==="cleanup")await cleanupManual();console.log(`P1 manual acceptance ${action}: ${action==="prepare"?"PENDING":"complete"}`);}
|
||||||
if(process.argv[1]&&realpathSync(process.argv[1])===modulePath)main().catch(error=>{console.error(`p1 manual acceptance refused: ${error.message}`);process.exitCode=1;});
|
if(process.argv[1]&&realpathSync(process.argv[1])===modulePath)main().catch(error=>{console.error(`p1 manual acceptance refused: ${error.message}`);process.exitCode=1;});
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ test("prepare creates independent pending topology, fixtures, commands and guide
|
|||||||
assert.equal(run.root, fixedManualRoot(repo));
|
assert.equal(run.root, fixedManualRoot(repo));
|
||||||
const owned = await readManualOwnership({ repositoryRoot: repo });
|
const owned = await readManualOwnership({ repositoryRoot: repo });
|
||||||
assert.equal(owned.status, "PENDING"); assert.equal(owned.listener.host, "127.0.0.1"); assert.equal(owned.listener.port, 8791);
|
assert.equal(owned.status, "PENDING"); assert.equal(owned.listener.host, "127.0.0.1"); assert.equal(owned.listener.port, 8791);
|
||||||
for (const path of ["remote.git/HEAD", "author/.git", "installation/registry", "fixture-secrets/dwh-password", "fixtures/descriptors/p1-filesystem.json", "requests/status.json", "responses", "exports", "rendered", "logs", "commands/render-1.sh", "commands/render-2.sh", "installation/runtime/p1-backend-supervisor.mjs", "GUIDE.md"]) await lstat(join(run.root, path));
|
for (const path of ["remote.git/HEAD", "author/.git", "installation/registry", "fixture-secrets/dwh-password", "fixtures/descriptors/p1-filesystem.json", "requests/status.json", "responses", "exports", "rendered", "logs", "commands/render-1.sh", "commands/render-2.sh", "logs/backend.log", "GUIDE.md"]) await lstat(join(run.root, path));
|
||||||
await assert.rejects(lstat(join(run.root, "VERDICT.md")));
|
await assert.rejects(lstat(join(run.root, "VERDICT.md")));
|
||||||
const guide = await readFile(join(run.root, "GUIDE.md"), "utf8");
|
const guide = await readFile(join(run.root, "GUIDE.md"), "utf8");
|
||||||
let previous = -1; for (let n = 1; n <= 14; n++) { const at = guide.indexOf(`${n}. `); assert.ok(at > previous, `step ${n} ordered`); previous = at; }
|
let previous = -1; for (let n = 1; n <= 14; n++) { const at = guide.indexOf(`${n}. `); assert.ok(at > previous, `step ${n} ordered`); previous = at; }
|
||||||
@@ -102,19 +102,11 @@ test("cleanup removes only the exact stopped owned root and never creates verdic
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
async function installFakeServer(repo, { startupDelay = 0 } = {}) {
|
async function installFakeServer(repo, { startupDelay = 0, healthStatus = 200, marker } = {}) {
|
||||||
await writeFile(join(repo, "backend", "dist", "server.js"), `import http from "node:http";
|
await writeFile(join(repo, "backend", "dist", "server.js"), `import http from "node:http";
|
||||||
const server=http.createServer((req,res)=>{res.setHeader("content-type","application/json");res.end(JSON.stringify({status:"ok",ambient:process.env.THT_DWH_API_KEY,maintenance:process.env.MAINTENANCE_STATE_FILE,wrongMaintenance:process.env.THT_MAINTENANCE_STATE_FILE}));});
|
${marker ? `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(marker)}, "executed");` : ""}
|
||||||
|
const server=http.createServer((req,res)=>{res.statusCode=req.url==="/health"?${healthStatus}:200;res.setHeader("content-type","application/json");res.end(JSON.stringify({status:"ok",ambient:process.env.THT_DWH_API_KEY,maintenance:process.env.MAINTENANCE_STATE_FILE,wrongMaintenance:process.env.THT_MAINTENANCE_STATE_FILE}));});
|
||||||
setTimeout(()=>server.listen(Number(process.env.PORT),process.env.HOST),${startupDelay});
|
setTimeout(()=>server.listen(Number(process.env.PORT),process.env.HOST),${startupDelay});
|
||||||
process.on("SIGTERM",()=>server.listening?server.close(()=>process.exit(0)):process.exit(0));
|
|
||||||
`);
|
|
||||||
await writeFile(join(repo,"backend/dist/config.js"),`export const loadConfig=env=>({host:env.HOST,port:Number(env.PORT)});
|
|
||||||
`);
|
|
||||||
await writeFile(join(repo,"backend/dist/app.js"),`import http from "node:http";
|
|
||||||
export function buildApp(){let server;return{
|
|
||||||
async listen({port,host}){await new Promise(r=>setTimeout(r,${startupDelay}));server=http.createServer((req,res)=>{res.setHeader("content-type","application/json");res.end(JSON.stringify({status:"ok",ambient:process.env.THT_DWH_API_KEY,maintenance:process.env.MAINTENANCE_STATE_FILE,wrongMaintenance:process.env.THT_MAINTENANCE_STATE_FILE}));});await new Promise((resolve,reject)=>{server.once("error",reject);server.listen(port,host,resolve);});},
|
|
||||||
async close(){if(server?.listening)await new Promise((resolve,reject)=>server.close(error=>error?reject(error):resolve()));}
|
|
||||||
};}
|
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +204,14 @@ exec ${JSON.stringify(realGit)} "$@"
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("prepare records one regular 0600 backend log and no generated supervisor", async () => {
|
||||||
|
const repo=await fakeRepo(),run=await prepareManual({repositoryRoot:repo,skipBuild:true});
|
||||||
|
const owned=await readManualOwnership({repositoryRoot:repo}),entry=await lstat(join(run.root,"logs/backend.log"));
|
||||||
|
assert.equal(entry.isFile(),true); assert.equal(entry.isSymbolicLink(),false); assert.equal(entry.mode&0o777,0o600);
|
||||||
|
assert.deepEqual(owned.backendLog,{path:join(run.root,"logs/backend.log"),dev:entry.dev,ino:entry.ino});
|
||||||
|
await assert.rejects(lstat(join(run.root,"installation/runtime/p1-backend-supervisor.mjs")));
|
||||||
|
});
|
||||||
|
|
||||||
// A delayed real listener leaves the pre-fix port-check/spawn window open long enough for every
|
// A delayed real listener leaves the pre-fix port-check/spawn window open long enough for every
|
||||||
// overlapping call. The backend.pid reservation, rather than scheduler timing, must pick one owner.
|
// overlapping call. The backend.pid reservation, rather than scheduler timing, must pick one owner.
|
||||||
test("concurrent serves reserve one exact process and leave no orphan after stop", { concurrency: false }, async () => {
|
test("concurrent serves reserve one exact process and leave no orphan after stop", { concurrency: false }, async () => {
|
||||||
@@ -268,6 +268,52 @@ test("serve binds the one fixed loopback address, refuses a second PID, and guar
|
|||||||
await cleanupManual({repositoryRoot:repo});
|
await cleanupManual({repositoryRoot:repo});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("serve requires a 2xx HTTP health check and leaves no orphan on 503", { concurrency: false }, async () => {
|
||||||
|
const repo=await fakeRepo(); await installFakeServer(repo,{healthStatus:503}); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
|
||||||
|
const owned=await readManualOwnership({repositoryRoot:repo});
|
||||||
|
await assert.rejects(serveManual({repositoryRoot:repo}),/health|readiness/i);
|
||||||
|
await assert.rejects(lstat(join(run.root,"backend.pid")));
|
||||||
|
assert.deepEqual(await listenerPids(),[]); assert.deepEqual(await matchingManualServerPids(run.root,owned.nonce),[]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("serve launches exact server.js with immutable preload and fixed owned control port", { concurrency: false }, async () => {
|
||||||
|
const repo=await fakeRepo(); await installFakeServer(repo); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
|
||||||
|
const pid=await serveManual({repositoryRoot:repo}),record=JSON.parse(await readFile(join(run.root,"backend.pid"),"utf8"));
|
||||||
|
assert.equal(record.pid,pid); assert.equal(record.script,join(repo,"backend/dist/server.js"));
|
||||||
|
assert.equal(record.control.host,"127.0.0.1"); assert.equal(record.control.port,8792);
|
||||||
|
assert.match(record.preload,/^data:text\/javascript;base64,/);
|
||||||
|
const args=(await execFileAsync("ps",["-ww","-p",String(pid),"-o","command="])).stdout.trim();
|
||||||
|
assert.equal(args,[process.execPath,"--import",record.preload,record.script,`--p1-manual-nonce=${record.nonce}`,`--p1-root=${run.root}`,`--p1-control-nonce=${record.reservationNonce}`].join(" "));
|
||||||
|
await stopManual({repositoryRoot:repo});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("serve refuses legacy supervisor, runtime, server and log substitutions before code or outside writes", { concurrency: false }, async () => {
|
||||||
|
for(const kind of ["legacy-supervisor","runtime-symlink","server-symlink","log-symlink","log-replaced"]){
|
||||||
|
const repo=await fakeRepo(),marker=join(repo,`outside-${kind}.marker`); await installFakeServer(repo,{marker});
|
||||||
|
const run=await prepareManual({repositoryRoot:repo,skipBuild:true}),outside=join(repo,`outside-${kind}`); await mkdir(outside);
|
||||||
|
if(kind==="legacy-supervisor")await symlink(join(outside,"outside.mjs"),join(run.root,"installation/runtime/p1-backend-supervisor.mjs"));
|
||||||
|
if(kind==="runtime-symlink"){await rm(join(run.root,"installation/runtime"),{recursive:true});await symlink(outside,join(run.root,"installation/runtime"));}
|
||||||
|
if(kind==="server-symlink"){
|
||||||
|
const external=join(outside,"server.js"); await writeFile(external,`import {writeFileSync} from "node:fs";writeFileSync(${JSON.stringify(marker)},"bad");`);
|
||||||
|
await rm(join(repo,"backend/dist/server.js")); await symlink(external,join(repo,"backend/dist/server.js"));
|
||||||
|
}
|
||||||
|
if(kind==="log-symlink"){await rm(join(run.root,"logs/backend.log"));await symlink(join(outside,"captured.log"),join(run.root,"logs/backend.log"));}
|
||||||
|
if(kind==="log-replaced"){await rm(join(run.root,"logs/backend.log"));await writeFile(join(run.root,"logs/backend.log"),"",{mode:0o600});}
|
||||||
|
await assert.rejects(serveManual({repositoryRoot:repo}),/unsafe|identity|symlink|legacy|realpath|log/i,kind);
|
||||||
|
await assert.rejects(lstat(marker),undefined,`${kind} must refuse before server execution`);
|
||||||
|
assert.deepEqual(await readdir(outside),kind==="server-symlink"?["server.js"]:[]);
|
||||||
|
await assert.rejects(lstat(join(run.root,"backend.pid")));
|
||||||
|
await rm(run.root,{recursive:true,force:true});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("serve refuses an occupied fixed control port before spawning", { concurrency: false }, async () => {
|
||||||
|
const repo=await fakeRepo(),marker=join(repo,"server-executed"); await installFakeServer(repo,{marker}); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
|
||||||
|
const blocker=net.createServer(); await new Promise((resolvePromise,reject)=>blocker.once("error",reject).listen(8792,"127.0.0.1",resolvePromise));
|
||||||
|
try { await assert.rejects(serveManual({repositoryRoot:repo}),/8792.*occupied|control.*occupied/); } finally { await new Promise(resolvePromise=>blocker.close(resolvePromise)); }
|
||||||
|
await assert.rejects(lstat(marker)); await assert.rejects(lstat(join(run.root,"backend.pid")));
|
||||||
|
});
|
||||||
|
|
||||||
test("serve refuses an occupied fixed port and never creates a PID or verdict", { concurrency: false }, async () => {
|
test("serve refuses an occupied fixed port and never creates a PID or verdict", { concurrency: false }, async () => {
|
||||||
const repo=await fakeRepo(); await installFakeServer(repo); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
|
const repo=await fakeRepo(); await installFakeServer(repo); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
|
||||||
const blocker=net.createServer(); await new Promise((resolvePromise,reject)=>blocker.once("error",reject).listen(8791,"127.0.0.1",resolvePromise));
|
const blocker=net.createServer(); await new Promise((resolvePromise,reject)=>blocker.once("error",reject).listen(8791,"127.0.0.1",resolvePromise));
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ result. Automation never creates `VERDICT.md`, never records PASS, and never con
|
|||||||
|
|
||||||
From a clean repository checkout, Task 8 must already be implemented. Install Node/npm and Git,
|
From a clean repository checkout, Task 8 must already be implemented. Install Node/npm and Git,
|
||||||
`curl`, `unzip`/`zipinfo`, `lsof`, and the harness development environment so `harness/.venv/bin/tht` is executable.
|
`curl`, `unzip`/`zipinfo`, `lsof`, and the harness development environment so `harness/.venv/bin/tht` is executable.
|
||||||
Port `127.0.0.1:8791` must be free. The helper builds and serves only the production backend; it
|
Ports `127.0.0.1:8791` and `127.0.0.1:8792` must be free. The helper builds and serves only the
|
||||||
does not start Docker or the frontend.
|
production backend; it does not start Docker or the frontend.
|
||||||
|
|
||||||
## Lifecycle
|
## Lifecycle
|
||||||
|
|
||||||
@@ -24,19 +24,31 @@ Run these commands from the repository root:
|
|||||||
```
|
```
|
||||||
|
|
||||||
`prepare` exclusively creates `.artifacts/manual-acceptance/p1/`, with fresh Git history, fixtures,
|
`prepare` exclusively creates `.artifacts/manual-acceptance/p1/`, with fresh Git history, fixtures,
|
||||||
secret files, concrete request/inspection commands, and `GUIDE.md`. It leaves status `PENDING` and
|
secret files, concrete request/inspection commands, and `GUIDE.md`. It also creates the single regular
|
||||||
the server stopped. It refuses an existing root; use the guarded `stop` and `cleanup` actions rather
|
`logs/backend.log` with mode `0600` and records its exact path/device/inode ownership. It creates no
|
||||||
than deleting or reusing state manually.
|
supervisor or readiness-status program/file, leaves status `PENDING` and the server stopped, and
|
||||||
|
refuses an existing root; use the guarded `stop` and `cleanup` actions rather than deleting or reusing
|
||||||
|
state manually.
|
||||||
|
|
||||||
`serve` exclusively reserves the lifecycle and PID records before checking the fixed port, then
|
`serve` holds the external lifecycle lock, validates the canonical production
|
||||||
starts an owned Node supervisor that imports the production backend configuration and app in the same
|
`backend/dist/server.js`, every owned root/runtime/log ancestor, the absence of a legacy supervisor,
|
||||||
process and binds it to `127.0.0.1:8791`. Readiness and `backend.pid` bind that exact process to a
|
and the original log identity before spawning. The log is opened with no-follow semantics and its
|
||||||
random nonce and an ephemeral loopback control endpoint. `stop` revalidates the exact executable,
|
file descriptor is passed directly to the child. The child is the production Node entrypoint itself:
|
||||||
arguments, repository cwd/root, and process start identity, then requests shutdown over the
|
`node --import data:text/javascript;base64,<immutable-preload> backend/dist/server.js` followed by the
|
||||||
nonce-authenticated cooperative channel and requires the exact acknowledgement. It never sends a
|
three ownership/control arguments. The immutable preload owns only an authenticated fixed
|
||||||
numeric terminating signal. `serve`, `stop`, and `cleanup` are serialized; ambiguous, stale, or
|
`127.0.0.1:8792` control channel and a bounded startup watchdog; the application binds
|
||||||
starting records remain for operator inspection. `cleanup` removes only the exact stopped owned fixed
|
`127.0.0.1:8791` normally. Before writing the `RUNNING` PID record, the parent requires an exact
|
||||||
root. Foreign siblings and automated integration artifacts are outside its cleanup boundary.
|
nonce-bound control STATUS and a 2xx `GET /health`, then sends READY to disarm the watchdog. A startup
|
||||||
|
or non-2xx failure requests nonce-authenticated STOP (or lets the watchdog self-exit) and leaves no
|
||||||
|
listener or PID record.
|
||||||
|
|
||||||
|
`stop` revalidates the exact executable, immutable preload, production script, arguments, repository
|
||||||
|
cwd/root, and process start identity, then requests STOP over the nonce-authenticated cooperative
|
||||||
|
channel and requires the exact acknowledgement. The controlled process acknowledges and exits itself;
|
||||||
|
the production tool never sends a numeric terminating signal. `serve`, `stop`, and `cleanup` are
|
||||||
|
serialized; ambiguous, stale, or starting records remain for operator inspection. `cleanup` removes
|
||||||
|
only the exact stopped owned fixed root. Foreign siblings and automated integration artifacts are
|
||||||
|
outside its cleanup boundary.
|
||||||
|
|
||||||
After `prepare`, follow the 14 ordered steps in the generated absolute-path `GUIDE.md`. Personally run each generated `http-01` through `http-14` curl script in numeric order; they save the exact status, three validation, three sequential publication, pull, three read responses, and three ZIP exports. Each publication derives its current base commit with a bounded parser from the preceding saved API response, with no placeholder base. Run the five numbered negative validation scripts separately at checklist step 10. The render commands validate the bounded saved read response,
|
After `prepare`, follow the 14 ordered steps in the generated absolute-path `GUIDE.md`. Personally run each generated `http-01` through `http-14` curl script in numeric order; they save the exact status, three validation, three sequential publication, pull, three read responses, and three ZIP exports. Each publication derives its current base commit with a bounded parser from the preceding saved API response, with no placeholder base. Run the five numbered negative validation scripts separately at checklist step 10. The render commands validate the bounded saved read response,
|
||||||
its commit-addressed owned snapshot path, the saved publish commit, and the installed Git HEAD before
|
its commit-addressed owned snapshot path, the saved publish commit, and the installed Git HEAD before
|
||||||
|
|||||||
Reference in New Issue
Block a user