fix: use production entrypoint for P1 manual serve

This commit is contained in:
2026-08-09 22:33:18 +02:00
parent e9755c6feb
commit bd1f4083e9
3 changed files with 188 additions and 51 deletions
+104 -25
View File
@@ -1,11 +1,12 @@
#!/usr/bin/env node
import { execFile, spawn } from "node:child_process";
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 http from "node:http";
import net from "node:net";
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";
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 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};}
function supervisorPath(root){return join(root,"installation/runtime/p1-backend-supervisor.mjs");}
function readinessPath(root){return join(root,"installation/runtime/backend.ready");}
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";
import { chmod, lstat, open, rename, rm } from "node:fs/promises";
import { randomBytes } from "node:crypto";
import { basename, join } from "node:path";
const ROOT=${JSON.stringify(root)},REPO=${JSON.stringify(repo)},RUNTIME=${JSON.stringify(runtime)},READY=${JSON.stringify(readinessPath(root))};
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");
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 app,control,readyOwned=false,shutting=false,controlPort;
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;}
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;}}
async function shutdown(){if(shutting)return;shutting=true;try{await app.close();}finally{await new Promise(resolve=>control.close(resolve));if(readyOwned)await removeReady();}}
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;}
`;}
function legacySupervisorPath(root){return join(root,"installation/runtime/p1-backend-supervisor.mjs");}
const CONTROL_PORT=8792;
const PRELOAD_SOURCE=`import net from "node:net";
const HOST="127.0.0.1",PORT=8792,HEX=/^[0-9a-f]{64}$/;
const argv=process.argv.slice(2),noncePrefix="--p1-manual-nonce=",rootPrefix="--p1-root=",controlPrefix="--p1-control-nonce=";
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 nonce=argv[0].slice(noncePrefix.length),root=argv[1].slice(rootPrefix.length),controlNonce=argv[2].slice(controlPrefix.length);
if(!HEX.test(nonce)||!root.startsWith("/")||!HEX.test(controlNonce))throw new Error("manual control identity refused");
let state="STARTING",stopping=false;
const identity=()=>({status:state,pid:process.pid,nonce,controlNonce,root,control:{host:HOST,port:PORT}});
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();});});
await new Promise((resolve,reject)=>{control.once("error",reject);control.listen({host:HOST,port:PORT,exclusive:true},resolve);});
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);
`;
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);});});}
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}]};}
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;}
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,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"});}
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})];}
@@ -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");
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);}}
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);}}
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()));});}
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(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 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 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;}}
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))]);}
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);}}
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);}}
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);});}
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);}}
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;});
+58 -12
View File
@@ -72,7 +72,7 @@ test("prepare creates independent pending topology, fixtures, commands and guide
assert.equal(run.root, fixedManualRoot(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);
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")));
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; }
@@ -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";
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});
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
// 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 () => {
@@ -268,6 +268,52 @@ test("serve binds the one fixed loopback address, refuses a second PID, and guar
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 () => {
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));