Files
ThothII/backend/scripts/p1-manual-acceptance.mjs
T

158 lines
46 KiB
JavaScript
Executable File

#!/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 { access, chmod, lstat, mkdir, open, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
import net from "node:net";
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { promisify } from "node:util";
const exec = promisify(execFile); const modulePath=fileURLToPath(import.meta.url); const defaultRepositoryRoot=realpathSync(resolve(dirname(modulePath),"../.."));
const HEX64=/^[0-9a-f]{64}$/; const PORT=8791; const HOST="127.0.0.1";
export function fixedManualRoot(repositoryRoot=defaultRepositoryRoot){return join(realpathSync(repositoryRoot),".artifacts","manual-acceptance","p1");}
function below(parent,child){const rel=relative(parent,child);return rel!==""&&!rel.startsWith(`..${sep}`)&&rel!==".."&&!isAbsolute(rel);}
function noSymlinkExisting(repo,target){const rel=relative(repo,target);if(rel.startsWith("..")||isAbsolute(rel))throw new Error("root leaves repository");let cursor=repo;for(const part of rel.split(sep).filter(Boolean)){cursor=join(cursor,part);try{if(lstatSync(cursor).isSymbolicLink())throw new Error("owned root ancestor is a symlink");}catch(error){if(error.code==="ENOENT")break;throw error;}}}
async function atomicWrite(path,bytes,mode=0o600){await mkdir(dirname(path),{recursive:true});const staging=join(dirname(path),`.${basename(path)}.${randomBytes(12).toString("hex")}.tmp`);let h;try{h=await open(staging,"wx",mode);await h.writeFile(bytes);await h.sync();await h.close();h=undefined;await rename(staging,path);const fd=openSync(dirname(path),constants.O_RDONLY);try{fsyncSync(fd);}finally{closeSync(fd);}}finally{if(h)await h.close().catch(()=>{});await rm(staging,{force:true}).catch(()=>{});}}
function directorySync(path){const fd=openSync(path,constants.O_RDONLY);try{fsyncSync(fd);}finally{closeSync(fd);}}
async function exclusiveRecord(path,value,label){const bytes=`${JSON.stringify(value,null,2)}\n`;let handle,createdEntry;try{handle=await open(path,"wx",0o600);createdEntry=await handle.stat();await handle.chmod(0o600);await handle.writeFile(bytes);await handle.sync();await handle.close();handle=undefined;directorySync(dirname(path));return{path,bytes,dev:createdEntry.dev,ino:createdEntry.ino};}catch(error){if(handle)await handle.close().catch(()=>{});if(createdEntry)try{const current=await lstat(path);if(current.dev===createdEntry.dev&&current.ino===createdEntry.ino)await rm(path);}catch{}if(error.code==="EEXIST")throw new Error(`${label} already exists; operator inspection required`);throw error;}}
async function requireExactRecord(record){const entry=await lstat(record.path);if(!entry.isFile()||entry.isSymbolicLink()||(entry.mode&0o777)!==0o600||(record.dev!==undefined&&(entry.dev!==record.dev||entry.ino!==record.ino)))throw new Error("owned lifecycle record is unsafe");if(await readFile(record.path,"utf8")!==record.bytes)throw new Error("owned lifecycle record changed; operator inspection required");const after=await lstat(record.path);if(after.dev!==entry.dev||after.ino!==entry.ino)throw new Error("owned lifecycle record changed; operator inspection required");return after;}
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,root,owned,operation){const lifecycleNonce=randomBytes(32).toString("hex");return await exclusiveRecord(join(root,".backend.lifecycle.lock"),{schemaVersion:1,kind:"p1-manual-backend-lifecycle",status:"LOCKED",operation,lifecycleNonce,nonce:owned.nonce,root,repositoryRoot:repo},"backend lifecycle lock");}
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;}
`;}
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;}
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})];}
function quote(value){return `'${String(value).replaceAll("'",`'"'"'`)}'`;}
async function checkPrerequisites(repo){for(const path of ["scripts/p1-acceptance.sh","scripts/test-p1-acceptance.sh","backend/scripts/p1-acceptance.mjs","backend/dist/server.js"]){try{await access(join(repo,path));}catch{throw new Error(`Task 8 prerequisite is missing: ${path}`);}}for(const command of ["node","npm","git","curl","unzip","zipinfo","lsof"]){try{await run(command,[command==="unzip"||command==="lsof"?"-v":command==="zipinfo"?"-h":"--version"]);}catch{throw new Error(`missing prerequisite: ${command}`);}}const tht=join(repo,"harness",".venv","bin","tht");try{await access(tht,constants.X_OK);}catch{throw new Error("missing prerequisite: harness/.venv/bin/tht");}}
async function initializeGit(root){await run("git",["init","--bare","--initial-branch=main",join(root,"remote.git")],{cwd:root});await run("git",["clone",join(root,"remote.git"),join(root,"author")],{cwd:root});for(const [key,value]of [["user.name","P1 Manual Curator"],["user.email","p1-manual@example.invalid"]])await run("git",["config",key,value],{cwd:join(root,"author")});const evidence=join(root,"author","workspace-content","p1-filesystem","evidence");await mkdir(join(evidence,"domain"),{recursive:true});await writeFile(join(evidence,"guide.md"),"# P1 manually curated Evidence\n");await writeFile(join(evidence,"domain","table.md"),"# P1 curated table\n");await run("git",["add","workspace-content"],{cwd:join(root,"author")});await run("git",["commit","-m","Bootstrap P1 manual Evidence"],{cwd:join(root,"author")});await run("git",["push","origin","main"],{cwd:join(root,"author")});}
function requestFixtures(items){const result={"status.json":{method:"GET",path:"/workspace-registry/status"},"pull.json":{method:"POST",path:"/workspace-registry/pull"}};for(const workspace of items){const id=workspace.workspace.id;result[`validate-${id}.json`]={workspace};result[`publish-${id}.json`]={action:"create",workspace};result[`read-${id}.json`]={method:"GET",path:`/workspaces/${id}`};result[`export-${id}.json`]={method:"GET",path:`/workspaces/${id}/export`};}Object.assign(result,{"invalid-absolute.json":{workspace:{...items[0],evidence:{...items[0].evidence,source:{...items[0].evidence.source,uri:"/etc"}}}},"invalid-traversal.json":{workspace:{...items[0],evidence:{...items[0].evidence,source:{...items[0].evidence.source,uri:"workspace-content/p1-filesystem/evidence/../../p1-s3/evidence"}}}},"invalid-cross-workspace.json":{workspace:{...items[0],evidence:{...items[0].evidence,source:{...items[0].evidence.source,uri:"workspace-content/p1-s3/evidence"}}}},"invalid-protocol.json":{workspace:{...items[1],evidence:{...items[1].evidence,source:{...items[1].evidence.source,uris:["file:///etc/passwd"]}}}},"invalid-credential.json":{workspace:{...items[2],evidence:{...items[2].evidence,source:{...items[2].evidence.source,access_key:"CANARY-MUST-BE-REJECTED"}}}}});return result;}
function curlGet(url,output){return `#!/usr/bin/env bash\nset -euo pipefail\ncurl --fail-with-body --silent --show-error --output ${quote(output)} --write-out 'HTTP %{http_code}\\n' ${quote(url)}\n`;}
function curlPost(url,output,body){return `#!/usr/bin/env bash\nset -euo pipefail\ncurl --fail-with-body --silent --show-error --request POST --header 'content-type: application/json' --data-binary @${quote(body)} --output ${quote(output)} --write-out 'HTTP %{http_code}\\n' ${quote(url)}\n`;}
function curlPostEmpty(url,output){return `#!/usr/bin/env bash\nset -euo pipefail\ncurl --fail-with-body --silent --show-error --request POST --output ${quote(output)} --write-out 'HTTP %{http_code}\\n' ${quote(url)}\n`;}
function publishCurl(root,id,previousResponse){const descriptor=join(root,"fixtures/descriptors",`${id}.json`),body=join(root,"requests",`publish-${id}.concrete.json`),response=join(root,"responses",`publish-${id}.json`);return `#!/usr/bin/env bash
set -euo pipefail
node --input-type=module - ${quote(previousResponse)} ${quote(descriptor)} ${quote(body)} <<'NODE'
import { open, readFile, rename, stat } from "node:fs/promises";import { basename, dirname, join } from "node:path";import { randomBytes } from "node:crypto";
const [priorPath,descriptorPath,output]=process.argv.slice(2);const bounded=async(path)=>{let s;try{s=await stat(path);}catch{throw Error("required saved response is missing");}if(!s.isFile()||s.size<2||s.size>1048576)throw Error("saved response is unbounded");let value;try{value=JSON.parse(await readFile(path,"utf8"));}catch{throw Error("saved response is malformed JSON");}return value;};
const prior=await bounded(priorPath),workspace=await bounded(descriptorPath);const base=prior.head??prior.revision?.commit;if(!/^[0-9a-f]{40}$/.test(base??""))throw Error("saved response has no valid current base commit");const bytes=JSON.stringify({action:"create",workspace,baseCommit:base},null,2)+"\\n",tmp=join(dirname(output),"."+basename(output)+"."+randomBytes(8).toString("hex")+".tmp");const h=await open(tmp,"wx",0o600);try{await h.writeFile(bytes);await h.sync();}finally{await h.close();}await rename(tmp,output);
NODE
curl --fail-with-body --silent --show-error --request POST --header 'content-type: application/json' --data-binary @${quote(body)} --output ${quote(response)} --write-out 'HTTP %{http_code}\n' 'http://127.0.0.1:8791/workspaces/publish'
`;}
function httpCommands(root){const base="http://127.0.0.1:8791",entries=[];entries.push(["http-01-status.sh",curlGet(`${base}/workspace-registry/status`,join(root,"responses/status.json"))]);let n=2;for(const id of ["p1-filesystem","p1-http","p1-s3"])entries.push([`http-0${n++}-validate-${id}.sh`,curlPost(`${base}/workspaces/validate`,join(root,"responses",`validate-${id}.json`),join(root,"requests",`validate-${id}.json`))]);let prior=join(root,"responses/status.json");for(const id of ["p1-filesystem","p1-http","p1-s3"]){entries.push([`http-0${n++}-publish-${id}.sh`,publishCurl(root,id,prior)]);prior=join(root,"responses",`publish-${id}.json`);}entries.push([`http-0${n++}-pull.sh`,curlPostEmpty(`${base}/workspace-registry/pull`,join(root,"responses/pull.json"))]);for(const id of ["p1-filesystem","p1-http","p1-s3"])entries.push([`http-${String(n++).padStart(2,"0")}-read-${id}.sh`,curlGet(`${base}/workspaces/${id}`,join(root,"responses",`read-${id}.json`))]);for(const id of ["p1-filesystem","p1-http","p1-s3"])entries.push([`http-${String(n++).padStart(2,"0")}-export-${id}.sh`,curlGet(`${base}/workspaces/${id}/export`,join(root,"exports/raw",`${id}.zip`))]);for(const kind of ["absolute","traversal","cross-workspace","protocol","credential"])entries.push([`http-${String(n++).padStart(2,"0")}-invalid-${kind}.sh`,curlPost(`${base}/workspaces/validate`,join(root,"responses",`invalid-${kind}.json`),join(root,"requests",`invalid-${kind}.json`))]);return entries;}
function renderCommand(repo,root,n){const output=join(root,"rendered",`runtime-${n}.yaml`),response=join(root,"responses","read-p1-filesystem.json"),published=join(root,"responses","pull.json"),snapshots=join(root,"installation","registry","snapshots"),checkout=join(root,"installation","registry","repo");return `#!/usr/bin/env bash
set -euo pipefail
repo=${quote(repo)}
root=${quote(root)}
set -a
. ${quote(join(root,"installation","bindings.env"))}
set +a
node --input-type=module - "$root" ${quote(response)} ${quote(published)} ${quote(snapshots)} ${quote(checkout)} ${quote(output)} "$repo/backend/scripts/p1-render-snapshot.mjs" <<'NODE'
import { readFile, realpath, stat } from "node:fs/promises";
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
import { spawnSync } from "node:child_process";
const [root,readPath,publishPath,snapshots,checkout,output,renderer]=process.argv.slice(2);
const bounded=async(path)=>{let s;try{s=await stat(path);}catch{throw new Error("saved response is missing or unbounded");}if(!s.isFile()||s.size<2||s.size>1048576)throw new Error("saved response is missing or unbounded");let v;try{v=JSON.parse(await readFile(path,"utf8"));}catch{throw new Error("saved response is malformed JSON");}return v;};
const read=await bounded(readPath),published=await bounded(publishPath);const revision=read?.revision,commit=revision?.commit,snapshot=revision?.snapshotPath,publishedCommit=published?.head??published?.revision?.commit;
if(!/^[0-9a-f]{40}$/.test(commit??"")||commit!==publishedCommit)throw new Error("saved read/publish revisions differ");
if(typeof snapshot!=="string"||!isAbsolute(snapshot))throw new Error("snapshot path is not absolute");const canonical=await realpath(snapshot);const rel=relative(snapshots,canonical);if(rel.startsWith("..")||isAbsolute(rel)||dirname(canonical)!==resolve(snapshots,commit))throw new Error("snapshot escapes owned commit root");
const git=spawnSync("git",["-C",checkout,"rev-parse","HEAD"],{encoding:"utf8"});if(git.status!==0||git.stdout.trim()!==commit)throw new Error("saved revision differs from installed Git commit");
const child=spawnSync(process.execPath,[renderer,"--ownership",resolve(root,"ownership.json"),"--snapshot",canonical,"--output",output],{stdio:"inherit",env:process.env});if(child.status!==0)process.exit(child.status??1);
NODE
`;}
function guide(repo,root){const base=`http://${HOST}:${PORT}`;return `# P1 manual configuration walkthrough
Status: **PENDING**. The reviewer, not this helper, performs and judges every step. Never inspect raw secret-file contents.
1. Inspect \`${root}/ownership.json\`, the pre-publication Evidence tree under \`author/workspace-content/p1-filesystem/evidence\`, descriptor fixtures, and binding **paths and modes** in \`installation/bindings.env\`.
2. Run \`${repo}/scripts/p1-manual-acceptance.sh serve\`; verify only \`${HOST}:${PORT}\` listens (for example, \`lsof -nP -iTCP:${PORT} -sTCP:LISTEN\`).
3. Personally run each concrete \`commands/http-01-*.sh\` through \`commands/http-14-*.sh\` script, one at a time in numeric order: real curl status → three validates → three sequential publishes → pull → three reads → three exports against \`${base}\`. Each script saves the exact JSON response under \`responses/\` or ZIP bytes under \`exports/raw/\`; each publish derives its current base from the preceding bounded saved response. Do not advance on a non-2xx response.
4. Only after publish, run \`commands/git-inspect.sh <published-commit>\`: inspect \`git log\`, \`git ls-tree\`, \`git show <published-commit>:workspaces/<id>.yaml\`, and \`git show <published-commit>:workspace-content/<id>/evidence/...\` at that same commit.
5. Inspect generated \`workspace-docs\`, the immutable commit-addressed descriptor snapshot, and its \`snapshot.json\` manifest.
6. Run \`commands/extract-export.sh exports/raw/p1-filesystem.zip exports/extracted/p1-filesystem p1-filesystem\`, then the equivalent exact commands for \`p1-http\` and \`p1-s3\`; verify each manifest and descriptor identity, hashes, and absence of Evidence bytes and secret/canary material.
7. After saving \`responses/read-p1-filesystem.json\` and the final API/Git head in \`responses/pull.json\`, run \`commands/render-1.sh\`, \`commands/render-2.sh\`, then \`commands/diff-rendered.sh\`.
8. Inspect runtime identity, absolute reserved filesystem root, Evidence limits, and policy in the rendered YAML; do not inspect secret contents.
9. Personally execute \`${repo}/harness/.venv/bin/tht config check -c ${root}/rendered/runtime-1.yaml\` and the same command for \`runtime-2.yaml\` (or run \`commands/config-check.sh\`).
10. Personally run \`commands/http-15-*.sh\` through \`commands/http-19-*.sh\` to submit the invalid absolute, Evidence-URI traversal, cross-workspace, protocol, and credential validation requests; verify safe rejection, no Git/snapshot mutation, and no rejected canary outside the request fixture.
11. Run \`commands/secret-scan.sh\`; it excludes \`fixture-secrets\` and checks bounded bytes from every Git object, including unreachable blobs and dangling commits, for canary patterns without displaying secret contents.
12. Run \`commands/absence-check.sh\`; confirm no preprocessing, Evidence materialization, embedding, Qdrant, ACTIVE, or retention artifact exists.
13. Run \`${repo}/scripts/p1-manual-acceptance.sh stop\`; confirm \`backend.pid\` and the listener on port ${PORT} are gone.
14. Create \`${root}/VERDICT.md\` yourself with reviewer, UTC time, every checklist result, observations, and exactly either \`manual acceptance: PASS\` or \`manual acceptance: FAIL\`.
Preserve a failed lab by stopping it and leaving the owned root in place. Only \`cleanup\` removes this exact stopped lab.
`;}
function extractCommand(repo,root){return `#!/usr/bin/env bash
set -euo pipefail
zip=\${1:?zip required}; out=\${2:?new output required}; expected=\${3:?expected workspace id required}
node --input-type=module - "$zip" "$out" "$expected" ${quote(root)} ${quote(join(repo,"backend/package.json"))} <<'NODE'
import { execFileSync } from "node:child_process";
import { createHash, randomBytes } from "node:crypto";
import { createRequire } from "node:module";
import { lstat, mkdir, open, readFile, realpath, rename, rm } from "node:fs/promises";
import { basename, dirname, isAbsolute, join, relative } from "node:path";
const [zip,out,expected,root,packageJson]=process.argv.slice(2),allowed=new Set(["p1-filesystem","p1-http","p1-s3"]),base=join(root,"exports/extracted");let stagedZip,stagedDirectory;
const fail=message=>{throw new Error(message);},exact=(value,keys)=>value&&typeof value==="object"&&!Array.isArray(value)&&JSON.stringify(Object.keys(value).sort())===JSON.stringify([...keys].sort());
try{
if(!allowed.has(expected))fail("expected workspace identity is invalid");
const canonicalRoot=await realpath(root),baseEntry=await lstat(base);if(baseEntry.isSymbolicLink()||!baseEntry.isDirectory()||await realpath(base)!==base||!relative(canonicalRoot,base)||relative(canonicalRoot,base).startsWith("..")||isAbsolute(relative(canonicalRoot,base)))fail("unsafe owned extraction root");
if(dirname(out)!==base||basename(out).startsWith(".")||basename(out).length===0)fail("unsafe output path");try{await lstat(out);fail("unsafe output path");}catch(error){if(error.code!=="ENOENT")throw error;}
const source=await open(zip,"r");let sourceBytes;try{const stat=await source.stat();if(!stat.isFile()||stat.size<1||stat.size>33554432)fail("source ZIP is unbounded");sourceBytes=await source.readFile();if(sourceBytes.length!==stat.size)fail("source ZIP changed during staging");}finally{await source.close();}
stagedZip=join(root,"exports",".zip-stage-"+randomBytes(16).toString("hex")+".zip");const staged=await open(stagedZip,"wx",0o600);try{await staged.chmod(0o600);await staged.writeFile(sourceBytes);await staged.sync();}finally{await staged.close();}
const names=execFileSync("unzip",["-Z1",stagedZip],{encoding:"utf8",maxBuffer:1048576}).trim().split("\\n"),required=["manifest.json","workspace.yaml","contract.env.example","README.md"];
if(names.length!==4||new Set(names).size!==4||required.some(name=>!names.includes(name)))fail("unsafe-zip entries");
const listing=execFileSync("zipinfo",["-l",stagedZip],{encoding:"utf8",maxBuffer:1048576}),regular=listing.split("\\n").filter(line=>line.startsWith("-")).length;if(regular!==4)fail("ZIP contains a symlink or nonregular entry");
stagedDirectory=join(base,".extract-stage-"+randomBytes(16).toString("hex"));await mkdir(stagedDirectory,{mode:0o700});execFileSync("unzip",["-q",stagedZip,"-d",stagedDirectory],{stdio:"pipe",maxBuffer:1048576});
const bytes={};for(const name of required){const path=join(stagedDirectory,name),entry=await lstat(path);if(!entry.isFile()||entry.isSymbolicLink()||entry.size<1||entry.size>10485760)fail("extracted file is unsafe");bytes[name]=await readFile(path);}
const randomized=/(?:DWH|SIGNED|ACCESS|SECRET|SESSION)-[0-9a-f]{32}/,fixed=["CANARY","MUST","BE","REJECTED"].join("-");for(const name of required){const value=bytes[name].toString("latin1");if(value.includes("P1 manually curated Evidence")||value.includes("P1 curated table")||randomized.test(value)||value.includes(fixed))fail("export contains Evidence or secret canary bytes");}
let manifest;try{manifest=JSON.parse(bytes["manifest.json"].toString("utf8"));}catch{fail("export manifest schema mismatch");}const hashed=required.slice(1);if(!exact(manifest,["schema_version","workspace_id","files"])||manifest.schema_version!==1||manifest.workspace_id!==expected||!exact(manifest.files,hashed)||hashed.some(name=>!/^[0-9a-f]{64}$/.test(manifest.files[name])))fail("export manifest workspace identity or schema mismatch");for(const name of hashed)if(createHash("sha256").update(bytes[name]).digest("hex")!==manifest.files[name])fail("manifest hash mismatch");
const require=createRequire(packageJson),YAML=require("yaml");let descriptor;try{descriptor=YAML.parse(bytes["workspace.yaml"].toString("utf8"));}catch{fail("export descriptor is malformed");}if(!descriptor||descriptor.workspace?.id!==expected)fail("export descriptor workspace identity mismatch");
await rename(stagedDirectory,out);stagedDirectory=undefined;
}catch(error){console.error(error.message);process.exitCode=1;}finally{if(stagedDirectory)await rm(stagedDirectory,{recursive:true,force:true}).catch(()=>{});if(stagedZip)await rm(stagedZip,{force:true}).catch(()=>{});}
NODE
`;}
async function writeCommands(repo,root){const commands=join(root,"commands");for(const [name,body]of [...httpCommands(root),["render-1.sh",renderCommand(repo,root,1)],["render-2.sh",renderCommand(repo,root,2)],["diff-rendered.sh",`#!/bin/sh\nset -eu\ndiff -u ${quote(join(root,"rendered/runtime-1.yaml"))} ${quote(join(root,"rendered/runtime-2.yaml"))}\n`],["config-check.sh",`#!/bin/sh\nset -eu\n${quote(join(repo,"harness/.venv/bin/tht"))} config check -c ${quote(join(root,"rendered/runtime-1.yaml"))}\n${quote(join(repo,"harness/.venv/bin/tht"))} config check -c ${quote(join(root,"rendered/runtime-2.yaml"))}\n`],["git-inspect.sh",`#!/bin/sh\nset -eu\ncommit=\${1:?published commit required}\ncase "$commit" in *[!0-9a-f]*|'') exit 2;; esac\n[ \${#commit} -eq 40 ] || exit 2\ngit -C ${quote(join(root,"installation/registry/repo"))} log --oneline --decorate -10 "$commit"\ngit -C ${quote(join(root,"installation/registry/repo"))} ls-tree -r "$commit" -- workspaces workspace-content\ngit -C ${quote(join(root,"installation/registry/repo"))} show "$commit:workspaces/p1-filesystem.yaml"\ngit -C ${quote(join(root,"installation/registry/repo"))} show "$commit:workspace-content/p1-filesystem/evidence/guide.md"\n`],["extract-export.sh",extractCommand(repo,root)],["secret-scan.sh",`#!/usr/bin/env bash
set -euo pipefail
root=${quote(root)}
node --input-type=module - "$root" <<'NODE'
import { execFileSync } from "node:child_process";import { lstat, readFile, readdir } from "node:fs/promises";import { basename, join, relative } from "node:path";
const root=process.argv[2],randomized=/(?:DWH|SIGNED|ACCESS|SECRET|SESSION)-[0-9a-f]{32}/,fixed=["CANARY","MUST","BE","REJECTED"].join("-");let found=false;const containsCanary=text=>randomized.test(text)||text.includes(fixed);
async function walk(path){for(const entry of await readdir(path,{withFileTypes:true})){const child=join(path,entry.name),rel=relative(root,child);if(entry.isSymbolicLink()){console.error("unsafe symlink during secret scan: "+rel);found=true;continue;}if(entry.isDirectory()){if(rel==="fixture-secrets"||entry.name===".git")continue;await walk(child);}else if(entry.isFile()){const stat=await lstat(child);if(stat.size>33554432)throw Error("secret scan file too large: "+rel);if(containsCanary((await readFile(child)).toString("latin1"))&&rel!=="requests/invalid-credential.json"){console.error("secret canary found: "+rel);found=true;}}}}
function git(args,label){const listing=execFileSync("git",[...args,"cat-file","--batch-all-objects","--unordered","--batch-check=%(objectname) %(objecttype) %(objectsize)"],{encoding:"utf8",maxBuffer:16*1024*1024}).trim(),objects=listing?listing.split("\\n"):[];if(objects.length>100000)throw Error("too many Git objects to scan in "+label);let total=0;for(const line of objects){const [oid,type,sizeText]=line.split(" ");if(!/^[0-9a-f]{40,64}$/.test(oid??"")||!type||!/^\\d+$/.test(sizeText??""))throw Error("malformed Git object listing in "+label);if(type!=="blob")continue;const size=Number(sizeText);total+=size;if(!Number.isSafeInteger(size)||size>33554432||total>536870912)throw Error("Git blob scan bound exceeded in "+label);const blob=execFileSync("git",[...args,"cat-file","blob",oid],{maxBuffer:size+1});if(blob.length!==size)throw Error("Git blob size changed in "+label);if(containsCanary(blob.toString("latin1"))){console.error("secret canary found in Git blob: "+label+":"+oid);found=true;}}}
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"]);noSymlinkExisting(repo,root);await mkdir(dirname(root),{recursive:true,mode:0o700});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};}
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()));});}
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 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 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),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 lifecycle=await acquireLifecycle(repo,root,owned,"serve");let pidRecord,child,ready;try{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),owned=await readManualOwnership({repositoryRoot:repo}),root=owned.root,lifecycle=await acquireLifecycle(repo,root,owned,"stop");try{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);}}
export async function cleanupManual({repositoryRoot=defaultRepositoryRoot}={}){const repo=realpathSync(repositoryRoot),owned=await readManualOwnership({repositoryRoot:repo}),root=owned.root,lifecycle=await acquireLifecycle(repo,root,owned,"cleanup");let moved=false;try{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);moved=true;await removeExactRecord({path:join(tombstone,basename(lifecycle.path)),bytes:lifecycle.bytes,dev:lifecycle.dev,ino:lifecycle.ino});await rm(tombstone,{recursive:true});}finally{if(!moved)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;});