fix: harden P1 manual acceptance audit gates

This commit is contained in:
2026-08-09 23:32:42 +02:00
parent 7d8fb065b5
commit 96362929d7
6 changed files with 398 additions and 121 deletions
+131 -6
View File
@@ -82,8 +82,9 @@ test("prepare creates independent pending topology, fixtures, commands and guide
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); for(const id of ["p1-filesystem","p1-http","p1-s3"])assert.match(guide,new RegExp(`extract-export\\.sh[^\\n]+${id}`));
for(const contract of [/stable repository-root/,/ownership-first recovery/,/one production Node PID owns both/,/opened no-follow descriptor/,/arbitrary `.git` repository/,/name\/path bytes/,/artifacts\/evidence/,/opened no-follow `rendered` directory/])assert.match(guide,contract);
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,/batch-all-objects/); 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,/maybeGitDir/); 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)]);
@@ -149,7 +150,7 @@ exec ${JSON.stringify(realGit)} "$@"
const preparing=prepareManual({repositoryRoot:repo,skipBuild:true});
for(let n=0;n<200;n++){try{await lstat(entered);break;}catch{} await new Promise(r=>setTimeout(r,10));}
await lstat(entered);
const lock=join(repo,".artifacts/manual-acceptance/.p1.lifecycle.lock");
const lock=join(repo,".p1-manual-acceptance.lifecycle.lock");
const lockEntry=await lstat(lock); assert.equal(lockEntry.isFile(),true); assert.equal(lockEntry.mode&0o777,0o600);
const lockBytes=await readFile(lock,"utf8"),lockValue=JSON.parse(lockBytes);
assert.deepEqual(Object.keys(lockValue).sort(),["kind","lifecycleNonce","operation","repositoryRoot","root","schemaVersion"].sort());
@@ -165,9 +166,9 @@ exec ${JSON.stringify(realGit)} "$@"
test("the external lifecycle lock prevents old-root/new-root ABA and ownership is read only under lock", async () => {
const source=await readFile(new URL("./p1-manual-acceptance.mjs",import.meta.url),"utf8");
assert.match(source,/\.artifacts["'],["']manual-acceptance["'],["']\.p1\.lifecycle\.lock/);
assert.match(source,/\.p1-manual-acceptance\.lifecycle\.lock/);
const repo=await fakeRepo(),run=await prepareManual({repositoryRoot:repo,skipBuild:true});
const lockPath=join(repo,".artifacts/manual-acceptance/.p1.lifecycle.lock"),nonce="f".repeat(64),bytes=`${nonce}\n`;
const lockPath=join(repo,".p1-manual-acceptance.lifecycle.lock"),nonce="f".repeat(64),bytes=`${nonce}\n`;
const handle=await open(lockPath,"wx",0o600); await handle.writeFile(bytes); await handle.sync();
try {
const old=JSON.parse(await readFile(join(run.root,"ownership.json"),"utf8"));
@@ -197,7 +198,7 @@ exec ${JSON.stringify(realGit)} "$@"
preparing=prepareManual({repositoryRoot:repo,skipBuild:true});
for(let n=0;n<200;n++){try{await lstat(entered);break;}catch{} await new Promise(r=>setTimeout(r,10));}
await lstat(entered);
const lock=join(repo,".artifacts/manual-acceptance/.p1.lifecycle.lock"),bytes=await readFile(lock);
const lock=join(repo,".p1-manual-acceptance.lifecycle.lock"),bytes=await readFile(lock);
const original=await lstat(lock),replacement=join(dirname(lock),".replacement-lifecycle-lock");
await writeFile(replacement,bytes,{mode:0o600}); const replacementEntry=await lstat(replacement);
assert.notEqual(replacementEntry.ino,original.ino); await rename(replacement,lock); await writeFile(release,"go");
@@ -288,7 +289,7 @@ test("serve launches exact server.js with immutable preload and fixed owned cont
assert.equal(record.control.host,"127.0.0.1"); assert.equal(record.control.port,8792);
assert.match(record.preload,/^data:text\/javascript;base64,/);
const args=(await execFileAsync("ps",["-ww","-p",String(pid),"-o","command="])).stdout.trim();
assert.equal(args,[process.execPath,"--import",record.preload,record.script,`--p1-manual-nonce=${record.nonce}`,`--p1-root=${run.root}`,`--p1-control-nonce=${record.reservationNonce}`].join(" "));
assert.equal(args,[process.execPath,"--import",record.preload,record.script,`--p1-manual-nonce=${record.nonce}`,`--p1-root=${run.root}`,`--p1-control-nonce=${record.reservationNonce}`,`--p1-entry-sha256=${record.entrypoint.sha256}`,`--p1-entry-dev=${record.entrypoint.dev}`,`--p1-entry-ino=${record.entrypoint.ino}`].join(" "));
await stopManual({repositoryRoot:repo});
});
@@ -538,3 +539,127 @@ test("generated secret scan checks randomized and fixed canaries in reachable Gi
await rm(run.root,{recursive:true,force:true});
}
});
test("prepare publishes cleanable ownership before lab population", { concurrency: false }, async () => {
const repo=await fakeRepo(),realGit=(await execFileAsync("which",["git"])).stdout.trim(),bin=join(repo,"failing-bin");
await installFakeServer(repo); await mkdir(bin);
await writeFile(join(bin,"git"),`#!/bin/sh
if [ "$1" = init ]; then exit 71; fi
exec ${JSON.stringify(realGit)} "$@"
`,{mode:0o700});
const prior=process.env.PATH; process.env.PATH=`${bin}:${prior}`;
try { await assert.rejects(prepareManual({repositoryRoot:repo,skipBuild:true})); }
finally { process.env.PATH=prior; }
const owned=await readManualOwnership({repositoryRoot:repo});
assert.equal(owned.stage,"PREPARING");
await cleanupManual({repositoryRoot:repo});
await assert.rejects(lstat(fixedManualRoot(repo)));
});
test("stable repo-root lifecycle namespace survives manual-parent rename and cleans partial prepare", { concurrency: false }, async () => {
const repo=await fakeRepo(),realGit=(await execFileAsync("which",["git"])).stdout.trim();
const bin=join(repo,"rename-lock-bin"),entered=join(repo,"rename-entered"),release=join(repo,"rename-release");
await installFakeServer(repo); await mkdir(bin);
await writeFile(join(bin,"git"),`#!/bin/sh
if [ "$1" = init ] && [ ! -e ${JSON.stringify(entered)} ]; then
: > ${JSON.stringify(entered)}
while [ ! -e ${JSON.stringify(release)} ]; do sleep 0.01; done
fi
exec ${JSON.stringify(realGit)} "$@"
`,{mode:0o700});
const prior=process.env.PATH; process.env.PATH=`${bin}:${prior}`; let preparing;
const parent=join(repo,".artifacts/manual-acceptance"),moved=join(repo,".artifacts/manual-acceptance-moved");
try {
preparing=prepareManual({repositoryRoot:repo,skipBuild:true});
for(let n=0;n<300;n++){try{await lstat(entered);break;}catch{} await new Promise(r=>setTimeout(r,10));}
await lstat(entered); await rename(parent,moved); await mkdir(parent,{recursive:true}); await writeFile(join(parent,"public-sibling"),"keep"); await writeFile(join(moved,"moved-sibling"),"keep");
await assert.rejects(cleanupManual({repositoryRoot:repo}),/lifecycle lock|operator inspection/i);
await writeFile(release,"go"); await assert.rejects(preparing,/identity|changed|unsafe|manual/i); preparing=undefined;
assert.equal(await readFile(join(parent,"public-sibling"),"utf8"),"keep");
assert.equal(await readFile(join(moved,"moved-sibling"),"utf8"),"keep");
await assert.rejects(lstat(join(moved,"p1")));
await assert.rejects(lstat(join(repo,".p1-manual-acceptance.lifecycle.lock")));
} finally { process.env.PATH=prior; await writeFile(release,"go").catch(()=>{}); if(preparing)await preparing.catch(()=>{}); }
});
test("all four lifecycle operations serialize on the stable repo-root lock", async () => {
const repo=await fakeRepo(); await installFakeServer(repo); await prepareManual({repositoryRoot:repo,skipBuild:true});
const lock=join(repo,".p1-manual-acceptance.lifecycle.lock"); await writeFile(lock,"foreign",{mode:0o600});
try {
for(const operation of [prepareManual,serveManual,stopManual,cleanupManual])
await assert.rejects(operation({repositoryRoot:repo,skipBuild:true}),/lifecycle lock|operator inspection/i);
} finally { await rm(lock,{force:true}); }
});
test("prepare binds production entry bytes and serve rejects a regular replacement", { concurrency: false }, async () => {
const repo=await fakeRepo(),malicious=join(repo,"malicious-executed"); await installFakeServer(repo);
const run=await prepareManual({repositoryRoot:repo,skipBuild:true}),owned=await readManualOwnership({repositoryRoot:repo}),script=join(repo,"backend/dist/server.js");
const prepared=await readFile(script); assert.equal(owned.entrypoint.sha256,sha256(prepared));
await rm(script); await writeFile(script,`import {writeFileSync} from "node:fs";writeFileSync(${JSON.stringify(malicious)},"bad");setInterval(()=>{},1000);`);
await assert.rejects(serveManual({repositoryRoot:repo}),/entrypoint|production server.*identity/i);
await assert.rejects(lstat(malicious)); await assert.rejects(lstat(join(run.root,"backend.pid")));
});
test("opened production FD prevents deterministic check-spawn replacement execution", { concurrency: false }, async () => {
const repo=await fakeRepo(),safe=join(repo,"safe-executed"),malicious=join(repo,"malicious-executed"); await installFakeServer(repo,{marker:safe});
const run=await prepareManual({repositoryRoot:repo,skipBuild:true}),script=join(repo,"backend/dist/server.js"),replacement=join(repo,"replacement-server.js");
await writeFile(replacement,`import {writeFileSync} from "node:fs";writeFileSync(${JSON.stringify(malicious)},"bad");setInterval(()=>{},1000);`);
await assert.rejects(serveManual({repositoryRoot:repo,beforeSpawn:async()=>rename(replacement,script)}),/entrypoint|identity|changed/i);
await assert.rejects(lstat(malicious)); await assert.rejects(lstat(join(run.root,"backend.pid")));
});
test("foreign 8791 health can never make a delayed authenticated child RUNNING", { concurrency: false }, async () => {
const repo=await fakeRepo(),entered=join(repo,"entry-loaded"); await installFakeServer(repo,{startupDelay:700,marker:entered});
const run=await prepareManual({repositoryRoot:repo,skipBuild:true}),owned=await readManualOwnership({repositoryRoot:repo});
const serving=serveManual({repositoryRoot:repo});
for(let n=0;n<300;n++){try{await lstat(entered);break;}catch{} await new Promise(r=>setTimeout(r,5));}
await lstat(entered); const foreign=net.createServer((socket)=>socket.end("HTTP/1.1 200 OK\r\nContent-Length: 7\r\n\r\nforeign"));
await new Promise((resolvePromise,reject)=>foreign.once("error",reject).listen(8791,"127.0.0.1",resolvePromise));
try {
await assert.rejects(serving,/readiness|listener|entrypoint|backend failed/i);
await assert.rejects(lstat(join(run.root,"backend.pid")));
assert.equal((await listenerPids()).includes(process.pid),true);
assert.deepEqual(await matchingManualServerPids(run.root,owned.nonce),[]);
} finally { await new Promise(resolvePromise=>foreign.close(resolvePromise)); }
});
test("absence gate rejects evidence and every P2 materialization artifact name", async () => {
for(const rel of ["artifacts/evidence/generation/chunk.md","responses/materialization","responses/preprocess-state","responses/embedding-cache","responses/qdrant-state","responses/ACTIVE","responses/retention-policy"]){
const repo=await fakeRepo(); await installFakeServer(repo); const run=await prepareManual({repositoryRoot:repo,skipBuild:true}),check=join(run.root,"commands/absence-check.sh"),target=join(run.root,rel);
if(rel.endsWith("materialization"))await mkdir(target,{recursive:true}); else {await mkdir(dirname(target),{recursive:true}); await writeFile(target,"safe");}
await assert.rejects(execFileAsync("bash",[check],{cwd:repo}),/out-of-scope/i,rel);
await rm(run.root,{recursive:true,force:true});
}
});
test("secret scan discovers every arbitrary git repository including unreachable objects", async () => {
const repo=await fakeRepo(); await installFakeServer(repo); const run=await prepareManual({repositoryRoot:repo,skipBuild:true}),gitdir=join(run.root,"responses/.git"),scan=join(run.root,"commands/secret-scan.sh"),canary="SECRET-"+"9".repeat(32);
await execFileAsync("git",["init","--bare",gitdir]); const blob=join(run.root,"responses/canary-blob"); await writeFile(blob,canary); await execFileAsync("git",["--git-dir",gitdir,"hash-object","-w",blob]); await rm(blob);
await assert.rejects(execFileAsync("bash",[scan],{cwd:repo}),error=>/Git object|secret canary/.test(error.stderr)&&!error.stderr.includes(canary));
});
test("secret scan bounds and scans filesystem names plus loose ref names", async () => {
for(const kind of ["file","directory","loose-ref"]){
const repo=await fakeRepo(); await installFakeServer(repo); const run=await prepareManual({repositoryRoot:repo,skipBuild:true}),scan=join(run.root,"commands/secret-scan.sh"),canary="SESSION-"+"8".repeat(32);
if(kind==="file")await writeFile(join(run.root,"responses",canary),"safe");
if(kind==="directory")await mkdir(join(run.root,"responses",canary));
if(kind==="loose-ref"){const ref=join(run.root,"author/.git/refs/heads",canary);await mkdir(dirname(ref),{recursive:true});await writeFile(ref,"0".repeat(40)+"\n");}
await assert.rejects(execFileAsync("bash",[scan],{cwd:repo}),error=>/secret canary/.test(error.stderr)&&!error.stderr.includes(canary),kind);
await rm(run.root,{recursive:true,force:true});
}
});
test("extractor and scanner operational diagnostics redact canary-bearing paths", async () => {
const repo=await fakeRepo(); await installFakeServer(repo); const run=await prepareManual({repositoryRoot:repo,skipBuild:true}),canary="SECRET-"+"7".repeat(32),extract=join(run.root,"commands/extract-export.sh"),scan=join(run.root,"commands/secret-scan.sh");
const missing=join(run.root,"exports/raw",`${canary}.zip`),output=join(run.root,"exports/extracted",canary);
await assert.rejects(execFileAsync("bash",[extract,missing,output,"p1-filesystem"],{cwd:repo}),error=>!error.stderr.includes(canary)&&/redacted|refused|unsafe/i.test(error.stderr));
const oversized=join(run.root,"responses","oversized"); const handle=await open(oversized,"w"); await handle.truncate(33554433); await handle.close();
await assert.rejects(execFileAsync("bash",[scan],{cwd:repo}),error=>!error.stderr.includes(canary)&&/redacted|failed|bound/i.test(error.stderr));
});
test("public prepare build is inside the stable lifecycle transaction", async()=>{
const wrapper=await readFile(new URL("../../scripts/p1-manual-acceptance.sh",import.meta.url),"utf8"),source=await readFile(new URL("./p1-manual-acceptance.mjs",import.meta.url),"utf8");
assert.doesNotMatch(wrapper,/npm .*run build/); assert.match(source,/action==="prepare"\)await prepareManual\(\)/);
});