fix: serialize P1 manual server ownership

This commit is contained in:
2026-08-09 21:51:45 +02:00
parent 3fe5a7b5e9
commit 6b2fd71018
3 changed files with 213 additions and 52 deletions
+64 -23
View File
@@ -5,7 +5,7 @@ import { closeSync, constants, fsyncSync, lstatSync, openSync, realpathSync } fr
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 } from "node:url";
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),"../.."));
@@ -14,6 +14,29 @@ export function fixedManualRoot(repositoryRoot=defaultRepositoryRoot){return joi
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"});}
@@ -65,52 +88,70 @@ Status: **PENDING**. The reviewer, not this helper, performs and judges every st
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 <zip> <new-output-dir>\` to safely extract the ZIP; verify manifest hashes and absence of Evidence bytes and secret/canary material.
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 for canary patterns without displaying secret contents.
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.
`;}
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",`#!/usr/bin/env bash\nset -euo pipefail\nzip=\${1:?zip required}; out=\${2:?new output required}\n[[ "$out" == ${quote(join(root,"exports/extracted"))}/* && ! -e "$out" ]] || { echo unsafe-output >&2; exit 2; }\nentries=$(unzip -Z1 "$zip"); [[ "$entries" == $'README.md\\ncontract.env.example\\nmanifest.json\\nworkspace.yaml' || "$entries" == $'manifest.json\\nworkspace.yaml\\ncontract.env.example\\nREADME.md' ]] || { echo unsafe-zip >&2; exit 2; }\nregular=$(zipinfo -l "$zip" | awk '$1 ~ /^-/ { n += 1 } END { print n + 0 }'); [[ "$regular" == 4 ]] || { echo 'ZIP contains a symlink or nonregular entry' >&2; exit 2; }\nmkdir -m 700 "$out"; unzip -q "$zip" -d "$out"\nnode --input-type=module - "$out" <<'NODE'
import {createHash} from 'node:crypto';import {readFile} from 'node:fs/promises';import {join} from 'node:path';
try {
const out=process.argv[2],names=['manifest.json','workspace.yaml','contract.env.example','README.md'],hashed=names.slice(1),hex64=/^[0-9a-f]{64}$/,fixed=['CANARY','MUST','BE','REJECTED'].join('-');
const bytes=Object.fromEntries(await Promise.all(names.map(async name=>[name,await readFile(join(out,name))])));
for(const name of names){const text=bytes[name].toString('latin1');if(text.includes('P1 manually curated Evidence')||text.includes('P1 curated table')||/(?:DWH|SIGNED|ACCESS|SECRET|SESSION)-[0-9a-f]{32}/.test(text)||text.includes(fixed))throw Error('export contains Evidence or secret canary bytes');}
let m;try{m=JSON.parse(bytes['manifest.json'].toString('utf8'));}catch{throw Error('export manifest schema mismatch');}
const exact=(value,keys)=>value&&typeof value==='object'&&!Array.isArray(value)&&JSON.stringify(Object.keys(value).sort())===JSON.stringify([...keys].sort());
if(!exact(m,['schema_version','workspace_id','files'])||m.schema_version!==1||!/^[a-z][a-z0-9-]{2,62}$/.test(m.workspace_id)||!exact(m.files,hashed)||hashed.some(name=>!hex64.test(m.files[name])))throw Error('export manifest schema mismatch');
for(const name of hashed)if(createHash('sha256').update(bytes[name]).digest('hex')!==m.files[name])throw Error('manifest hash mismatch');
} catch(error) { console.error(error.message); process.exit(1); }
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
`],["secret-scan.sh",`#!/usr/bin/env bash
`;}
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 objects=execFileSync("git",[...args,"rev-list","--objects","--all"],{encoding:"utf8",maxBuffer:4*1024*1024}).trim().split("\\n").filter(Boolean);for(const line of objects){const oid=line.split(" ",1)[0],type=execFileSync("git",[...args,"cat-file","-t",oid],{encoding:"utf8"}).trim();if(type!=="blob")continue;const size=Number(execFileSync("git",[...args,"cat-file","-s",oid],{encoding:"utf8"}));if(!Number.isSafeInteger(size)||size>33554432)throw Error("Git blob is too large to scan in "+label);const blob=execFileSync("git",[...args,"cat-file","blob",oid],{maxBuffer:33554433});if(containsCanary(blob.toString("latin1"))){console.error("secret canary found in reachable 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 reachable Git blobs");
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 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};}
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())throw new Error("backend PID record is unsafe");let value;try{value=JSON.parse(await readFile(path,"utf8"));}catch{throw new Error("backend PID record is malformed");}return value;}
async function validateProcess(repo,root,owned,pidRecord){if(!Number.isSafeInteger(pidRecord.pid)||pidRecord.pid<2||pidRecord.nonce!==owned.nonce||pidRecord.root!==root||pidRecord.repositoryRoot!==repo||pidRecord.executable!==process.execPath||pidRecord.script!==join(repo,"backend/dist/server.js")||!pidRecord.startIdentity)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)]);if(start!==pidRecord.startIdentity||cwd!==repo||executable!==realpathSync(pidRecord.executable)||!args.includes(pidRecord.script)||!args.includes(`--p1-manual-nonce=${owned.nonce}`)||!args.includes(`--p1-root=${root}`))throw new Error("backend process identity mismatch; refusing to signal");return true;}
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");try{await lstat(join(root,"backend.pid"));throw new Error("backend PID record already exists; stale/live identity must be resolved");}catch(error){if(error.code!=="ENOENT")throw error;}await portAvailable();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);await mkdir(join(root,"installation/runtime/home"),{recursive:true,mode:0o700});await mkdir(join(root,"installation/runtime/tmp"),{recursive:true,mode:0o700});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 child=spawn(process.execPath,[join(repo,"backend/dist/server.js"),`--p1-manual-nonce=${owned.nonce}`,`--p1-root=${root}`],{cwd:repo,env,detached:true,stdio:["ignore",stdout,stderr]});closeSync(stdout);closeSync(stderr);child.unref();let start="";for(let n=0;n<20;n++){try{start=await processStart(child.pid);if(start)break;}catch{}await new Promise(r=>setTimeout(r,50));}if(!start)throw new Error("backend failed before PID identity could be recorded");await atomicWrite(join(root,"backend.pid"),`${JSON.stringify({schemaVersion:1,pid:child.pid,nonce:owned.nonce,root,repositoryRoot:repo,executable:process.execPath,script:join(repo,"backend/dist/server.js"),startIdentity:start},null,2)}\n`);let ready=false;for(let n=0;n<50;n++){if(!alive(child.pid))break;try{const response=await fetch(`http://${HOST}:${PORT}/health`,{signal:AbortSignal.timeout(250)});if(response.ok){ready=true;break;}}catch{}await new Promise(r=>setTimeout(r,100));}if(!ready)throw new Error("backend readiness failed; inspect owned logs and use stop after identity review");return child.pid;}
export async function stopManual({repositoryRoot=defaultRepositoryRoot}={}){const repo=realpathSync(repositoryRoot),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);process.kill(record.pid,"SIGTERM");for(let n=0;n<100;n++){if(!alive(record.pid)){await rm(join(root,"backend.pid"));return;}await new Promise(r=>setTimeout(r,100));}throw new Error("owned backend did not stop after TERM; operator must intervene; PID record retained");}
export async function cleanupManual({repositoryRoot=defaultRepositoryRoot}={}){const repo=realpathSync(repositoryRoot),owned=await readManualOwnership({repositoryRoot:repo}),root=owned.root;try{const record=await readPid(root);if(alive(record.pid)){await validateProcess(repo,root,owned,record);throw new Error("owned backend is live; run stop first");}throw new Error("stale 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});}
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;});
+135 -23
View File
@@ -22,6 +22,7 @@ async function fakeRepo() {
await mkdir(dirname(join(root, path)), { recursive: true });
await writeFile(join(root, path), path.endsWith(".sh") ? "#!/bin/sh\n" : "export {};\n", { mode: 0o700 });
}
await symlink(new URL("../node_modules", import.meta.url).pathname, join(root, "backend", "node_modules"), "dir");
await mkdir(join(root, "harness", ".venv", "bin"), { recursive: true });
await writeFile(join(root, "harness", ".venv", "bin", "tht"), "#!/bin/sh\n", { mode: 0o700 });
await chmod(join(root, "harness", ".venv", "bin", "tht"), 0o700);
@@ -71,13 +72,13 @@ 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", "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", "installation/runtime/p1-backend-supervisor.mjs", "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; }
assert.doesNotMatch(guide, /cat .*fixture-secrets|show.*secret contents/i);
assert.doesNotMatch(guide, /cat .*fixture-secrets|show.*secret contents/i); for(const id of ["p1-filesystem","p1-http","p1-s3"])assert.match(guide,new RegExp(`extract-export\\.sh[^\\n]+${id}`));
const traversal=JSON.parse(await readFile(join(run.root,"requests","invalid-traversal.json"),"utf8")); assert.match(traversal.workspace.evidence.source.uri,/\.\./);
const bindings=await readFile(join(run.root,"installation","bindings.env"),"utf8"); assert.match(bindings,new RegExp(`^THT_WORKSPACE_SECRET_ROOTS=.*fixture-secrets`,"m")); const scan=await readFile(join(run.root,"commands","secret-scan.sh"),"utf8"),extract=await readFile(join(run.root,"commands","extract-export.sh"),"utf8"); assert.match(scan,/rev-list/); assert.match(scan,/cat-file/); assert.match(scan,/installed-registry/); assert.match(extract,/ZIP contains a symlink or nonregular entry/);
const bindings=await readFile(join(run.root,"installation","bindings.env"),"utf8"); assert.match(bindings,new RegExp(`^THT_WORKSPACE_SECRET_ROOTS=.*fixture-secrets`,"m")); const scan=await readFile(join(run.root,"commands","secret-scan.sh"),"utf8"),extract=await readFile(join(run.root,"commands","extract-export.sh"),"utf8"); assert.match(scan,/batch-all-objects/); assert.match(scan,/cat-file/); assert.match(scan,/installed-registry/); assert.match(extract,/ZIP contains a symlink or nonregular entry/);
const pubFs=await readFile(join(run.root,"commands","http-05-publish-p1-filesystem.sh"),"utf8"),pubHttp=await readFile(join(run.root,"commands","http-06-publish-p1-http.sh"),"utf8"),pubS3=await readFile(join(run.root,"commands","http-07-publish-p1-s3.sh"),"utf8"); assert.match(pubFs,/responses\/status\.json/); assert.match(pubHttp,/responses\/publish-p1-filesystem\.json/); assert.match(pubS3,/responses\/publish-p1-http\.json/); assert.doesNotMatch(pubFs,/REPLACE_WITH/);
const render = await readFile(join(run.root, "commands", "render-1.sh"), "utf8");
for(const name of await readdir(join(run.root,"commands")))if(name.endsWith(".sh"))await execFileAsync("bash",["-n",join(run.root,"commands",name)]);
@@ -101,14 +102,85 @@ test("cleanup removes only the exact stopped owned root and never creates verdic
});
async function installFakeServer(repo) {
async function installFakeServer(repo, { startupDelay = 0 } = {}) {
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}));});
server.listen(Number(process.env.PORT),process.env.HOST);
process.on("SIGTERM",()=>server.close(()=>process.exit(0)));
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()));}
};}
`);
}
async function matchingManualServerPids(root, nonce) {
const { stdout } = await execFileAsync("ps", ["ax", "-o", "pid=,command="]);
const nonceArg = `--p1-manual-nonce=${nonce}`, rootArg = `--p1-root=${root}`;
return stdout.split("\n").filter(line => line.includes(nonceArg) && line.includes(rootArg))
.map(line => Number(line.trim().match(/^(\d+)/)?.[1])).filter(Number.isSafeInteger);
}
async function listenerPids() {
try {
const { stdout } = await execFileAsync("lsof", ["-nP", "-t", "-iTCP:8791", "-sTCP:LISTEN"]);
return [...new Set(stdout.trim().split("\n").filter(Boolean).map(Number))];
} catch (error) {
if (error.code === 1) return [];
throw error;
}
}
// 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 () => {
const repo=await fakeRepo(); await installFakeServer(repo,{startupDelay:400});
const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
const owned=await readManualOwnership({repositoryRoot:repo}); let winner;
try {
const results=await Promise.allSettled(Array.from({length:12},()=>serveManual({repositoryRoot:repo})));
const fulfilled=results.filter(result=>result.status==="fulfilled");
assert.equal(fulfilled.length,1,`one serve fulfills: ${results.map(result=>result.status).join(",")}`);
assert.equal(results.filter(result=>result.status==="rejected").length,11);
winner=fulfilled[0].value;
const pidPath=join(run.root,"backend.pid"),record=JSON.parse(await readFile(pidPath,"utf8")),entry=await lstat(pidPath);
assert.equal(entry.mode&0o777,0o600); assert.equal(record.status,"RUNNING");
assert.match(record.reservationNonce,/^[0-9a-f]{64}$/); assert.equal(record.pid,winner);
assert.equal(record.nonce,owned.nonce); assert.equal(record.root,run.root); assert.equal(record.repositoryRoot,repo);
assert.deepEqual(await matchingManualServerPids(run.root,owned.nonce),[winner]);
assert.deepEqual(await listenerPids(),[winner]); assert.doesNotThrow(()=>process.kill(winner,0));
await stopManual({repositoryRoot:repo});
await assert.rejects(lstat(pidPath)); assert.deepEqual(await matchingManualServerPids(run.root,owned.nonce),[]);
assert.deepEqual(await listenerPids(),[]); assert.throws(()=>process.kill(winner,0));
await cleanupManual({repositoryRoot:repo}); await assert.rejects(lstat(run.root));
} finally {
for(const pid of await matchingManualServerPids(run.root,owned.nonce))try{process.kill(pid,"SIGTERM");}catch{}
await new Promise(resolvePromise=>setTimeout(resolvePromise,50));
for(const pid of await matchingManualServerPids(run.root,owned.nonce))try{process.kill(pid,"SIGKILL");}catch{}
await rm(run.root,{recursive:true,force:true});
}
});
test("cooperative stop is serialized and production never sends a numeric terminating signal", { concurrency: false }, async () => {
const source=await readFile(new URL("./p1-manual-acceptance.mjs",import.meta.url),"utf8");
assert.doesNotMatch(source,/process\.kill\([^,]+,\s*["']SIG(?:TERM|KILL|INT)/);
const repo=await fakeRepo(); await installFakeServer(repo); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
const owned=await readManualOwnership({repositoryRoot:repo}); const pid=await serveManual({repositoryRoot:repo});
const record=JSON.parse(await readFile(join(run.root,"backend.pid"),"utf8")); assert.equal(record.control.host,"127.0.0.1");
const unauthorized=await new Promise((resolvePromise,reject)=>{const socket=net.createConnection(record.control),timer=setTimeout(()=>socket.destroy(new Error("control timeout")),1000);let bytes="";socket.setEncoding("utf8");socket.on("connect",()=>socket.end(JSON.stringify({action:"stop",nonce:"0".repeat(64)})));socket.on("data",chunk=>bytes+=chunk);socket.on("error",reject);socket.on("close",()=>{clearTimeout(timer);resolvePromise(bytes);});});
assert.equal(unauthorized,""); assert.doesNotThrow(()=>process.kill(pid,0));
const stopped=await Promise.allSettled([stopManual({repositoryRoot:repo}),stopManual({repositoryRoot:repo})]);
assert.equal(stopped.filter(result=>result.status==="fulfilled").length,1);
assert.equal(stopped.filter(result=>result.status==="rejected").length,1);
await assert.rejects(lstat(join(run.root,"backend.pid"))); assert.deepEqual(await listenerPids(),[]);
assert.deepEqual(await matchingManualServerPids(run.root,owned.nonce),[]); assert.throws(()=>process.kill(pid,0));
await cleanupManual({repositoryRoot:repo});
});
test("serve binds the one fixed loopback address, refuses a second PID, and guarded stop removes identity", { concurrency: false }, async () => {
const repo=await fakeRepo(); await installFakeServer(repo); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
const priorAmbient=process.env.THT_DWH_API_KEY; process.env.THT_DWH_API_KEY="AMBIENT-MUST-NOT-PASS"; const pid=await serveManual({repositoryRoot:repo}); assert.equal(Number.isSafeInteger(pid),true);
@@ -128,7 +200,7 @@ test("serve refuses an occupied fixed port and never creates a PID or verdict",
test("serve and cleanup refuse stale or mismatched PID records without signaling", async () => {
const repo=await fakeRepo(); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
await writeFile(join(run.root,"backend.pid"),JSON.stringify({pid:999999,nonce:"wrong"}));
await writeFile(join(run.root,"backend.pid"),JSON.stringify({pid:999999,nonce:"wrong"}),{mode:0o600});
await assert.rejects(serveManual({repositoryRoot:repo}),/PID record/);
await assert.rejects(stopManual({repositoryRoot:repo}),/identity mismatch/);
await assert.rejects(cleanupManual({repositoryRoot:repo}),/identity|stale/);
@@ -166,12 +238,12 @@ async function stopTestProcess(child) {
test("live foreign executable, cwd, start and args mismatches are never signaled", async () => {
const repo=await fakeRepo(); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
const owned=JSON.parse(await readFile(join(run.root,"ownership.json"),"utf8"));
const script=join(repo,"backend/dist/server.js"),nonceArg=`--p1-manual-nonce=${owned.nonce}`,rootArg=`--p1-root=${run.root}`;
await writeFile(script,"setInterval(()=>{},1000);\n");
const script=join(run.root,"installation/runtime/p1-backend-supervisor.mjs"),nonceArg=`--p1-manual-nonce=${owned.nonce}`,rootArg=`--p1-root=${run.root}`,reservationNonce="a".repeat(64),controlArg=`--p1-control-nonce=${reservationNonce}`;
await writeFile(script,"setInterval(()=>{},1000);\n",{mode:0o600});
const cases=[
["executable",()=>spawn("bash",["-c","while :; do sleep 1; done",script,nonceArg,rootArg],{cwd:repo,stdio:"ignore"}),{}],
["cwd",()=>spawn(process.execPath,[script,nonceArg,rootArg],{cwd:tmpdir(),stdio:"ignore"}),{}],
["start",()=>spawn(process.execPath,[script,nonceArg,rootArg],{cwd:repo,stdio:"ignore"}),{startIdentity:"foreign-start"}],
["executable",()=>spawn("bash",["-c","while :; do sleep 1; done",script,nonceArg,rootArg,controlArg],{cwd:repo,stdio:"ignore"}),{}],
["cwd",()=>spawn(process.execPath,[script,nonceArg,rootArg,controlArg],{cwd:tmpdir(),stdio:"ignore"}),{}],
["start",()=>spawn(process.execPath,[script,nonceArg,rootArg,controlArg],{cwd:repo,stdio:"ignore"}),{startIdentity:"foreign-start"}],
["args",()=>spawn(process.execPath,[script],{cwd:repo,stdio:"ignore"}),{}],
];
for(const [name,start,override] of cases){
@@ -179,9 +251,9 @@ test("live foreign executable, cwd, start and args mismatches are never signaled
try {
let actualStart=""; for(let n=0;n<50&&!actualStart;n++){try{actualStart=await processStartIdentity(child.pid);}catch{} if(!actualStart)await new Promise(r=>setTimeout(r,20));}
assert.ok(actualStart,`live ${name} process started`);
const record={schemaVersion:1,pid:child.pid,nonce:owned.nonce,root:run.root,repositoryRoot:repo,executable:process.execPath,script,startIdentity:actualStart,...override};
await writeFile(join(run.root,"backend.pid"),JSON.stringify(record));
await assert.rejects(stopManual({repositoryRoot:repo}),/process identity mismatch|refusing to signal/);
const record={schemaVersion:1,kind:"p1-manual-backend",status:"RUNNING",pid:child.pid,reservationNonce,nonce:owned.nonce,root:run.root,repositoryRoot:repo,executable:process.execPath,script,startIdentity:actualStart,control:{host:"127.0.0.1",port:1},...override};
await writeFile(join(run.root,"backend.pid"),JSON.stringify(record),{mode:0o600});
await assert.rejects(stopManual({repositoryRoot:repo}),/process identity mismatch|refusing cooperative control/);
assert.doesNotThrow(()=>process.kill(child.pid,0));
await rm(join(run.root,"backend.pid"));
} finally { await stopTestProcess(child); await rm(join(run.root,"backend.pid"),{force:true}); }
@@ -203,10 +275,10 @@ test("generated render command validates saved responses and owned snapshot befo
const sha256=bytes=>createHash("sha256").update(bytes).digest("hex");
async function makeExportZip(directory,name,{payloads={},manifest,extra=false,symlinkReadme=false}={}) {
async function makeExportZip(directory,name,{payloads={},manifest,workspaceId="p1-filesystem",extra=false,symlinkReadme=false}={}) {
const source=join(directory,`${name}-source`),zip=join(directory,`${name}.zip`); await mkdir(source,{recursive:true});
const files={"workspace.yaml":"workspace: safe\n","contract.env.example":"SAFE=path\n","README.md":"# Safe\n",...payloads};
const value=manifest??{schema_version:1,workspace_id:"p1-filesystem",files:Object.fromEntries(Object.entries(files).map(([n,b])=>[n,sha256(b)]))};
const files={"workspace.yaml":`workspace:\n id: ${workspaceId}\n`,"contract.env.example":"SAFE=path\n","README.md":"# Safe\n",...payloads};
const value=manifest??{schema_version:1,workspace_id:workspaceId,files:Object.fromEntries(Object.entries(files).map(([n,b])=>[n,sha256(b)]))};
await writeFile(join(source,"manifest.json"),JSON.stringify(value));
for(const [file,bytes] of Object.entries(files))if(!(symlinkReadme&&file==="README.md"))await writeFile(join(source,file),bytes);
if(symlinkReadme)await symlink("workspace.yaml",join(source,"README.md"));
@@ -217,9 +289,9 @@ async function makeExportZip(directory,name,{payloads={},manifest,extra=false,sy
test("generated ZIP verifier enforces exact manifest mapping, hashes, entries and regular files", async () => {
const repo=await fakeRepo(); const run=await prepareManual({repositoryRoot:repo,skipBuild:true}),extract=join(run.root,"commands/extract-export.sh");
const invoke=async(name,options={})=>execFileAsync("bash",[extract,await makeExportZip(run.root,name,options),join(run.root,"exports/extracted",name)],{cwd:repo});
const invoke=async(name,options={})=>execFileAsync("bash",[extract,await makeExportZip(run.root,name,options),join(run.root,"exports/extracted",name),"p1-filesystem"],{cwd:repo});
await invoke("valid");
const safe={"workspace.yaml":"workspace: safe\n","contract.env.example":"SAFE=path\n","README.md":"# Safe\n"};
const safe={"workspace.yaml":"workspace:\n id: p1-filesystem\n","contract.env.example":"SAFE=path\n","README.md":"# Safe\n"};
const hashes=Object.fromEntries(Object.entries(safe).map(([n,b])=>[n,sha256(b)]));
await assert.rejects(invoke("missing-map",{manifest:{schema_version:1,workspace_id:"p1-filesystem",files:{"workspace.yaml":hashes["workspace.yaml"],"contract.env.example":hashes["contract.env.example"]}}}),/manifest/i);
await assert.rejects(invoke("short-hash",{manifest:{schema_version:1,workspace_id:"p1-filesystem",files:{...hashes,"README.md":"abc"}}}),/manifest/i);
@@ -227,15 +299,43 @@ test("generated ZIP verifier enforces exact manifest mapping, hashes, entries an
await assert.rejects(invoke("nonregular",{symlinkReadme:true}),/symlink|nonregular/);
});
test("generated ZIP verifier binds identity, stages source once, and rejects symlink output ancestry", async () => {
const repo=await fakeRepo(); const run=await prepareManual({repositoryRoot:repo,skipBuild:true}),extract=join(run.root,"commands/extract-export.sh");
for(const id of ["p1-filesystem","p1-http","p1-s3"]){
const zip=await makeExportZip(run.root,`valid-${id}`,{workspaceId:id});
await execFileAsync("bash",[extract,zip,join(run.root,"exports/extracted",id),id],{cwd:repo});
}
const wrong=await makeExportZip(run.root,"wrong-valid-id",{workspaceId:"p1-http"});
await assert.rejects(execFileAsync("bash",[extract,wrong,join(run.root,"exports/extracted/wrong-id"),"p1-s3"],{cwd:repo}),/workspace.*identity|workspace_id/i);
const descriptorMismatch=await makeExportZip(run.root,"descriptor-mismatch",{workspaceId:"p1-http",payloads:{"workspace.yaml":"workspace:\n id: p1-s3\n"}});
await assert.rejects(execFileAsync("bash",[extract,descriptorMismatch,join(run.root,"exports/extracted/descriptor-mismatch"),"p1-http"],{cwd:repo}),/workspace.*identity|descriptor/i);
const outside=join(repo,"outside-extract"); await mkdir(outside); await rm(join(run.root,"exports/extracted"),{recursive:true}); await symlink(outside,join(run.root,"exports/extracted"));
const safe=await makeExportZip(run.root,"symlink-parent");
await assert.rejects(execFileAsync("bash",[extract,safe,join(run.root,"exports/extracted/escape"),"p1-filesystem"],{cwd:repo}),/symlink|owned|unsafe/i);
assert.deepEqual(await readdir(outside),[]); await rm(join(run.root,"exports/extracted")); await mkdir(join(run.root,"exports/extracted"));
const original=await makeExportZip(run.root,"replace-original"),replacement=await makeExportZip(run.root,"replace-malicious",{extra:true});
const bin=join(run.root,"swap-bin"),markerPath=join(run.root,"swap-once"); await mkdir(bin);
const realUnzip=(await execFileAsync("which",["unzip"])).stdout.trim();
await writeFile(join(bin,"unzip"),`#!/bin/sh
if [ ! -e "$P1_SWAP_MARKER" ]; then cp "$P1_SWAP_REPLACEMENT" "$P1_SWAP_ORIGINAL"; : > "$P1_SWAP_MARKER"; fi
exec ${realUnzip} "$@"
`,{mode:0o700});
await execFileAsync("bash",[extract,original,join(run.root,"exports/extracted/staged-source"),"p1-filesystem"],{cwd:repo,env:{...process.env,PATH:`${bin}:${process.env.PATH}`,P1_SWAP_MARKER:markerPath,P1_SWAP_REPLACEMENT:replacement,P1_SWAP_ORIGINAL:original}});
await lstat(markerPath); await lstat(join(run.root,"exports/extracted/staged-source/manifest.json"));
const generated=await readFile(extract,"utf8"); assert.match(generated,/open\(zip,["']r["']\)/); assert.match(generated,/stage/i);
});
test("generated ZIP verifier scans all four extracted byte streams for Evidence and canaries", async () => {
const repo=await fakeRepo(); const run=await prepareManual({repositoryRoot:repo,skipBuild:true}),extract=join(run.root,"commands/extract-export.sh");
const markers=["P1 manually curated Evidence","DWH-"+"d".repeat(32),"CANARY-MUST-BE-REJECTED"];
for(const marker of markers)for(const target of ["manifest.json","workspace.yaml","contract.env.example","README.md"]){
const name=`scan-${markers.indexOf(marker)}-${target.replaceAll(".","-")}`,payloads=target==="manifest.json"?{}:{[target]:marker};
const files={"workspace.yaml":"workspace: safe\n","contract.env.example":"SAFE=path\n","README.md":"# Safe\n",...payloads};
const files={"workspace.yaml":"workspace:\n id: p1-filesystem\n","contract.env.example":"SAFE=path\n","README.md":"# Safe\n",...payloads};
const manifest={schema_version:1,workspace_id:target==="manifest.json"?marker:"p1-filesystem",files:Object.fromEntries(Object.entries(files).map(([n,b])=>[n,sha256(b)]))};
const zip=await makeExportZip(run.root,name,{payloads,manifest});
await assert.rejects(execFileAsync("bash",[extract,zip,join(run.root,"exports/extracted",name)],{cwd:repo}),error=>/Evidence|canary/.test(error.stderr)&&!error.stderr.includes(marker),`${target} must reject ${marker.slice(0,8)}`);
await assert.rejects(execFileAsync("bash",[extract,zip,join(run.root,"exports/extracted",name),"p1-filesystem"],{cwd:repo}),error=>/Evidence|canary/.test(error.stderr)&&!error.stderr.includes(marker),`${target} must reject ${marker.slice(0,8)}`);
}
});
@@ -246,12 +346,24 @@ test("generated secret scan excludes only the exact request fixture and hides fi
await assert.rejects(execFileAsync("bash",[scan],{cwd:repo}),error=>/secret canary found/.test(error.stderr)&&!error.stderr.includes(canary));
});
test("generated secret scan checks unreachable blobs and dangling commits without printing values", async () => {
for(const kind of ["unreachable-blob","dangling-commit"]){
const value=kind==="unreachable-blob"?"SECRET-"+"e".repeat(32):"SECRET-"+"f".repeat(32);
const repo=await fakeRepo(); const run=await prepareManual({repositoryRoot:repo,skipBuild:true}); const author=join(run.root,"author"),installed=join(run.root,"installation/registry/repo"),scan=join(run.root,"commands/secret-scan.sh");
await execFileAsync("git",["clone",join(run.root,"remote.git"),installed]); const file=join(author,"dangling-secret"); await writeFile(file,value);
if(kind==="unreachable-blob"){await execFileAsync("git",["hash-object","-w",file],{cwd:author}); await rm(file);}
else {await execFileAsync("git",["add","dangling-secret"],{cwd:author}); await execFileAsync("git",["commit","-m","dangling secret"],{cwd:author}); await execFileAsync("git",["reset","--hard","HEAD^"],{cwd:author});}
await assert.rejects(execFileAsync("bash",[scan],{cwd:repo}),error=>/Git blob/.test(error.stderr)&&!error.stderr.includes(value),`${kind} must be scanned`);
await rm(run.root,{recursive:true,force:true});
}
});
test("generated secret scan checks randomized and fixed canaries in reachable Git without printing values", async () => {
for(const canary of ["DWH-"+"c".repeat(32),"CANARY-MUST-BE-REJECTED"]){
const repo=await fakeRepo(); const run=await prepareManual({repositoryRoot:repo,skipBuild:true}); const installed=join(run.root,"installation/registry/repo"),author=join(run.root,"author"),scan=join(run.root,"commands/secret-scan.sh");
await execFileAsync("git",["clone",join(run.root,"remote.git"),installed]);
await writeFile(join(author,"temporary-secret"),canary); await execFileAsync("git",["add","temporary-secret"],{cwd:author}); await execFileAsync("git",["commit","-m","temporary canary"],{cwd:author}); await execFileAsync("git",["rm","temporary-secret"],{cwd:author}); await execFileAsync("git",["commit","-m","remove canary"],{cwd:author});
await assert.rejects(execFileAsync("bash",[scan],{cwd:repo}),error=>/reachable Git blob/.test(error.stderr)&&!error.stderr.includes(canary));
await assert.rejects(execFileAsync("bash",[scan],{cwd:repo}),error=>/Git blob/.test(error.stderr)&&!error.stderr.includes(canary));
await rm(run.root,{recursive:true,force:true});
}
});
+14 -6
View File
@@ -28,18 +28,26 @@ secret files, concrete request/inspection commands, and `GUIDE.md`. It leaves st
the server stopped. It refuses an existing root; use the guarded `stop` and `cleanup` actions rather
than deleting or reusing state manually.
`serve` starts only `node backend/dist/server.js`, bound to `127.0.0.1:8791`, and saves logs and a
guarded PID identity inside the owned root. `stop` sends TERM only after validating the ownership
nonce, executable, command token, repository cwd/root, and recorded process start identity. It never
uses `pkill`. `cleanup` refuses a live or ambiguous process and removes only the exact owned fixed
`serve` exclusively reserves the lifecycle and PID records before checking the fixed port, then
starts an owned Node supervisor that imports the production backend configuration and app in the same
process and binds it to `127.0.0.1:8791`. Readiness and `backend.pid` bind that exact process to a
random nonce and an ephemeral loopback control endpoint. `stop` revalidates the exact executable,
arguments, repository cwd/root, and process start identity, then requests shutdown over the
nonce-authenticated cooperative channel and requires the exact acknowledgement. It never sends a
numeric terminating signal. `serve`, `stop`, and `cleanup` are serialized; ambiguous, stale, or
starting records remain for operator inspection. `cleanup` removes only the exact stopped owned fixed
root. Foreign siblings and automated integration artifacts are outside its cleanup boundary.
After `prepare`, follow the 14 ordered steps in the generated absolute-path `GUIDE.md`. Personally run each generated `http-01` through `http-14` curl script in numeric order; they save the exact status, three validation, three sequential publication, pull, three read responses, and three ZIP exports. Each publication derives its current base commit with a bounded parser from the preceding saved API response, with no placeholder base. Run the five numbered negative validation scripts separately at checklist step 10. The render commands validate the bounded saved read response,
its commit-addressed owned snapshot path, the saved publish commit, and the installed Git HEAD before
calling the acceptance-only production renderer. The renderer imports the built `ThtRunner`, resolves
bindings from environment paths, copies one lease atomically with mode `0600`, and releases it in
`finally`. Do not inspect or print raw secret-file contents; only inspect ownership/mode/path metadata
and canary absence outside `fixture-secrets`.
`finally`. For each exported ZIP, invoke the generated extractor with the exact expected workspace ID
(`p1-filesystem`, `p1-http`, or `p1-s3`); it stages one immutable owned copy, confines extraction,
and binds both the manifest and parsed descriptor identity to that expected ID. The generated secret
scan checks bounded bytes from every Git object, including unreachable blobs and dangling commits.
Do not inspect or print raw secret-file contents; only inspect ownership/mode/path metadata and canary
absence outside `fixture-secrets`.
## Failures and verdict