test: add P1 manual configuration walkthrough
This commit is contained in:
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env node
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { closeSync, constants, fsyncSync, lstatSync, openSync, realpathSync } from "node:fs";
|
||||
import { access, chmod, lstat, mkdir, open, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
||||
import net from "node:net";
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const exec = promisify(execFile); const modulePath=fileURLToPath(import.meta.url); const defaultRepositoryRoot=realpathSync(resolve(dirname(modulePath),"../.."));
|
||||
const HEX64=/^[0-9a-f]{64}$/; const PORT=8791; const HOST="127.0.0.1";
|
||||
export function fixedManualRoot(repositoryRoot=defaultRepositoryRoot){return join(realpathSync(repositoryRoot),".artifacts","manual-acceptance","p1");}
|
||||
function below(parent,child){const rel=relative(parent,child);return rel!==""&&!rel.startsWith(`..${sep}`)&&rel!==".."&&!isAbsolute(rel);}
|
||||
function noSymlinkExisting(repo,target){const rel=relative(repo,target);if(rel.startsWith("..")||isAbsolute(rel))throw new Error("root leaves repository");let cursor=repo;for(const part of rel.split(sep).filter(Boolean)){cursor=join(cursor,part);try{if(lstatSync(cursor).isSymbolicLink())throw new Error("owned root ancestor is a symlink");}catch(error){if(error.code==="ENOENT")break;throw error;}}}
|
||||
async function atomicWrite(path,bytes,mode=0o600){await mkdir(dirname(path),{recursive:true});const staging=join(dirname(path),`.${basename(path)}.${randomBytes(12).toString("hex")}.tmp`);let h;try{h=await open(staging,"wx",mode);await h.writeFile(bytes);await h.sync();await h.close();h=undefined;await rename(staging,path);const fd=openSync(dirname(path),constants.O_RDONLY);try{fsyncSync(fd);}finally{closeSync(fd);}}finally{if(h)await h.close().catch(()=>{});await rm(staging,{force:true}).catch(()=>{});}}
|
||||
function ownedValue(repo,root,nonce){return{schemaVersion:1,kind:"p1-manual-acceptance",nonce,repositoryRoot:repo,root,status:"PENDING",createdAt:new Date().toISOString(),listener:{host:HOST,port:PORT,state:"stopped"},resources:[root,{kind:"fastify",host:HOST,port:PORT}]};}
|
||||
export async function readManualOwnership({repositoryRoot=defaultRepositoryRoot}={}){const repo=realpathSync(repositoryRoot),root=fixedManualRoot(repo);noSymlinkExisting(repo,root);let rootEntry,ownershipEntry;try{rootEntry=await lstat(root);ownershipEntry=await lstat(join(root,"ownership.json"));}catch{throw new Error("manual ownership is missing");}if(!rootEntry.isDirectory()||rootEntry.isSymbolicLink()||await realpath(root)!==root||!ownershipEntry.isFile()||ownershipEntry.isSymbolicLink())throw new Error("manual ownership is unsafe");let value;try{value=JSON.parse(await readFile(join(root,"ownership.json"),"utf8"));}catch{throw new Error("manual ownership is malformed");}const expected={...ownedValue(repo,root,value.nonce),createdAt:value.createdAt,listener:value.listener};if(value.schemaVersion!==1||value.kind!=="p1-manual-acceptance"||!HEX64.test(value.nonce??"")||value.repositoryRoot!==repo||value.root!==root||value.status!=="PENDING"||value.listener?.host!==HOST||value.listener?.port!==PORT||!value.createdAt||JSON.stringify(value.resources)!==JSON.stringify(expected.resources))throw new Error("manual ownership identity mismatch");return value;}
|
||||
async function run(executable,argv,options={}){return await exec(executable,argv,{...options,maxBuffer:2*1024*1024,encoding:"utf8"});}
|
||||
function descriptor(id,source){return{workspace:{schema_version:3,id,name:`P1 ${id}`,language:"en"},dwh:{engine:"postgres",database:"postgres",schema:"public",supported_transports:["postgres_direct"]},semantic_index:{vector_store:{engine:"qdrant",collection:id,dimensions:1024,distance:"cosine"},embedding:{provider:"ollama_internal",model:"qwen3-embedding:0.6b",dimensions:1024}},llm_policy:{allowed:["zai/glm-5.2"]},evidence:{source,policy:{max_chunk_chars:4000,retain_published_generations:3}}};}
|
||||
function descriptors(){return[descriptor("p1-filesystem",{type:"filesystem",uri:"workspace-content/p1-filesystem/evidence",patterns:["**/*.md"],max_bytes:10485760}),descriptor("p1-http",{type:"http",uris:["https://evidence.example.test/guide.md"],authentication:"signed_urls_file",connect_timeout_ms:1250,read_timeout_ms:30001,max_bytes:12345,max_redirects:2,allow_private_hosts:false,max_cache_bytes:67890}),descriptor("p1-s3",{type:"s3",uri:"s3://p1-evidence/published/",endpoint_url:"https://s3.example.test/",region:"eu-west-1",credentials:"static_files",trusted_endpoint:true,allow_private_endpoint:false,allow_insecure_endpoint:false,max_bytes:12345,max_objects:33,max_pages:4,page_size:5})];}
|
||||
function quote(value){return `'${String(value).replaceAll("'",`'"'"'`)}'`;}
|
||||
async function checkPrerequisites(repo){for(const path of ["scripts/p1-acceptance.sh","scripts/test-p1-acceptance.sh","backend/scripts/p1-acceptance.mjs","backend/dist/server.js"]){try{await access(join(repo,path));}catch{throw new Error(`Task 8 prerequisite is missing: ${path}`);}}for(const command of ["node","npm","git","curl","unzip","zipinfo","lsof"]){try{await run(command,[command==="unzip"||command==="lsof"?"-v":command==="zipinfo"?"-h":"--version"]);}catch{throw new Error(`missing prerequisite: ${command}`);}}const tht=join(repo,"harness",".venv","bin","tht");try{await access(tht,constants.X_OK);}catch{throw new Error("missing prerequisite: harness/.venv/bin/tht");}}
|
||||
async function initializeGit(root){await run("git",["init","--bare","--initial-branch=main",join(root,"remote.git")],{cwd:root});await run("git",["clone",join(root,"remote.git"),join(root,"author")],{cwd:root});for(const [key,value]of [["user.name","P1 Manual Curator"],["user.email","p1-manual@example.invalid"]])await run("git",["config",key,value],{cwd:join(root,"author")});const evidence=join(root,"author","workspace-content","p1-filesystem","evidence");await mkdir(join(evidence,"domain"),{recursive:true});await writeFile(join(evidence,"guide.md"),"# P1 manually curated Evidence\n");await writeFile(join(evidence,"domain","table.md"),"# P1 curated table\n");await run("git",["add","workspace-content"],{cwd:join(root,"author")});await run("git",["commit","-m","Bootstrap P1 manual Evidence"],{cwd:join(root,"author")});await run("git",["push","origin","main"],{cwd:join(root,"author")});}
|
||||
function requestFixtures(items){const result={"status.json":{method:"GET",path:"/workspace-registry/status"},"pull.json":{method:"POST",path:"/workspace-registry/pull"}};for(const workspace of items){const id=workspace.workspace.id;result[`validate-${id}.json`]={workspace};result[`publish-${id}.json`]={action:"create",workspace};result[`read-${id}.json`]={method:"GET",path:`/workspaces/${id}`};result[`export-${id}.json`]={method:"GET",path:`/workspaces/${id}/export`};}Object.assign(result,{"invalid-absolute.json":{workspace:{...items[0],evidence:{...items[0].evidence,source:{...items[0].evidence.source,uri:"/etc"}}}},"invalid-traversal.json":{workspace:{...items[0],evidence:{...items[0].evidence,source:{...items[0].evidence.source,uri:"workspace-content/p1-filesystem/evidence/../../p1-s3/evidence"}}}},"invalid-cross-workspace.json":{workspace:{...items[0],evidence:{...items[0].evidence,source:{...items[0].evidence.source,uri:"workspace-content/p1-s3/evidence"}}}},"invalid-protocol.json":{workspace:{...items[1],evidence:{...items[1].evidence,source:{...items[1].evidence.source,uris:["file:///etc/passwd"]}}}},"invalid-credential.json":{workspace:{...items[2],evidence:{...items[2].evidence,source:{...items[2].evidence.source,access_key:"CANARY-MUST-BE-REJECTED"}}}}});return result;}
|
||||
function curlGet(url,output){return `#!/usr/bin/env bash\nset -euo pipefail\ncurl --fail-with-body --silent --show-error --output ${quote(output)} --write-out 'HTTP %{http_code}\\n' ${quote(url)}\n`;}
|
||||
function curlPost(url,output,body){return `#!/usr/bin/env bash\nset -euo pipefail\ncurl --fail-with-body --silent --show-error --request POST --header 'content-type: application/json' --data-binary @${quote(body)} --output ${quote(output)} --write-out 'HTTP %{http_code}\\n' ${quote(url)}\n`;}
|
||||
function curlPostEmpty(url,output){return `#!/usr/bin/env bash\nset -euo pipefail\ncurl --fail-with-body --silent --show-error --request POST --output ${quote(output)} --write-out 'HTTP %{http_code}\\n' ${quote(url)}\n`;}
|
||||
function publishCurl(root,id,previousResponse){const descriptor=join(root,"fixtures/descriptors",`${id}.json`),body=join(root,"requests",`publish-${id}.concrete.json`),response=join(root,"responses",`publish-${id}.json`);return `#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
node --input-type=module - ${quote(previousResponse)} ${quote(descriptor)} ${quote(body)} <<'NODE'
|
||||
import { open, readFile, rename, stat } from "node:fs/promises";import { basename, dirname, join } from "node:path";import { randomBytes } from "node:crypto";
|
||||
const [priorPath,descriptorPath,output]=process.argv.slice(2);const bounded=async(path)=>{let s;try{s=await stat(path);}catch{throw Error("required saved response is missing");}if(!s.isFile()||s.size<2||s.size>1048576)throw Error("saved response is unbounded");let value;try{value=JSON.parse(await readFile(path,"utf8"));}catch{throw Error("saved response is malformed JSON");}return value;};
|
||||
const prior=await bounded(priorPath),workspace=await bounded(descriptorPath);const base=prior.head??prior.revision?.commit;if(!/^[0-9a-f]{40}$/.test(base??""))throw Error("saved response has no valid current base commit");const bytes=JSON.stringify({action:"create",workspace,baseCommit:base},null,2)+"\\n",tmp=join(dirname(output),"."+basename(output)+"."+randomBytes(8).toString("hex")+".tmp");const h=await open(tmp,"wx",0o600);try{await h.writeFile(bytes);await h.sync();}finally{await h.close();}await rename(tmp,output);
|
||||
NODE
|
||||
curl --fail-with-body --silent --show-error --request POST --header 'content-type: application/json' --data-binary @${quote(body)} --output ${quote(response)} --write-out 'HTTP %{http_code}\n' 'http://127.0.0.1:8791/workspaces/publish'
|
||||
`;}
|
||||
function httpCommands(root){const base="http://127.0.0.1:8791",entries=[];entries.push(["http-01-status.sh",curlGet(`${base}/workspace-registry/status`,join(root,"responses/status.json"))]);let n=2;for(const id of ["p1-filesystem","p1-http","p1-s3"])entries.push([`http-0${n++}-validate-${id}.sh`,curlPost(`${base}/workspaces/validate`,join(root,"responses",`validate-${id}.json`),join(root,"requests",`validate-${id}.json`))]);let prior=join(root,"responses/status.json");for(const id of ["p1-filesystem","p1-http","p1-s3"]){entries.push([`http-0${n++}-publish-${id}.sh`,publishCurl(root,id,prior)]);prior=join(root,"responses",`publish-${id}.json`);}entries.push([`http-0${n++}-pull.sh`,curlPostEmpty(`${base}/workspace-registry/pull`,join(root,"responses/pull.json"))]);for(const id of ["p1-filesystem","p1-http","p1-s3"])entries.push([`http-${String(n++).padStart(2,"0")}-read-${id}.sh`,curlGet(`${base}/workspaces/${id}`,join(root,"responses",`read-${id}.json`))]);for(const id of ["p1-filesystem","p1-http","p1-s3"])entries.push([`http-${String(n++).padStart(2,"0")}-export-${id}.sh`,curlGet(`${base}/workspaces/${id}/export`,join(root,"exports/raw",`${id}.zip`))]);for(const kind of ["absolute","traversal","cross-workspace","protocol","credential"])entries.push([`http-${String(n++).padStart(2,"0")}-invalid-${kind}.sh`,curlPost(`${base}/workspaces/validate`,join(root,"responses",`invalid-${kind}.json`),join(root,"requests",`invalid-${kind}.json`))]);return entries;}
|
||||
function renderCommand(repo,root,n){const output=join(root,"rendered",`runtime-${n}.yaml`),response=join(root,"responses","read-p1-filesystem.json"),published=join(root,"responses","pull.json"),snapshots=join(root,"installation","registry","snapshots"),checkout=join(root,"installation","registry","repo");return `#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
repo=${quote(repo)}
|
||||
root=${quote(root)}
|
||||
set -a
|
||||
. ${quote(join(root,"installation","bindings.env"))}
|
||||
set +a
|
||||
node --input-type=module - "$root" ${quote(response)} ${quote(published)} ${quote(snapshots)} ${quote(checkout)} ${quote(output)} "$repo/backend/scripts/p1-render-snapshot.mjs" <<'NODE'
|
||||
import { readFile, realpath, stat } from "node:fs/promises";
|
||||
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
const [root,readPath,publishPath,snapshots,checkout,output,renderer]=process.argv.slice(2);
|
||||
const bounded=async(path)=>{let s;try{s=await stat(path);}catch{throw new Error("saved response is missing or unbounded");}if(!s.isFile()||s.size<2||s.size>1048576)throw new Error("saved response is missing or unbounded");let v;try{v=JSON.parse(await readFile(path,"utf8"));}catch{throw new Error("saved response is malformed JSON");}return v;};
|
||||
const read=await bounded(readPath),published=await bounded(publishPath);const revision=read?.revision,commit=revision?.commit,snapshot=revision?.snapshotPath,publishedCommit=published?.head??published?.revision?.commit;
|
||||
if(!/^[0-9a-f]{40}$/.test(commit??"")||commit!==publishedCommit)throw new Error("saved read/publish revisions differ");
|
||||
if(typeof snapshot!=="string"||!isAbsolute(snapshot))throw new Error("snapshot path is not absolute");const canonical=await realpath(snapshot);const rel=relative(snapshots,canonical);if(rel.startsWith("..")||isAbsolute(rel)||dirname(canonical)!==resolve(snapshots,commit))throw new Error("snapshot escapes owned commit root");
|
||||
const git=spawnSync("git",["-C",checkout,"rev-parse","HEAD"],{encoding:"utf8"});if(git.status!==0||git.stdout.trim()!==commit)throw new Error("saved revision differs from installed Git commit");
|
||||
const child=spawnSync(process.execPath,[renderer,"--ownership",resolve(root,"ownership.json"),"--snapshot",canonical,"--output",output],{stdio:"inherit",env:process.env});if(child.status!==0)process.exit(child.status??1);
|
||||
NODE
|
||||
`;}
|
||||
function guide(repo,root){const base=`http://${HOST}:${PORT}`;return `# P1 manual configuration walkthrough
|
||||
|
||||
Status: **PENDING**. The reviewer, not this helper, performs and judges every step. Never inspect raw secret-file contents.
|
||||
|
||||
1. Inspect \`${root}/ownership.json\`, the pre-publication Evidence tree under \`author/workspace-content/p1-filesystem/evidence\`, descriptor fixtures, and binding **paths and modes** in \`installation/bindings.env\`.
|
||||
2. Run \`${repo}/scripts/p1-manual-acceptance.sh serve\`; verify only \`${HOST}:${PORT}\` listens (for example, \`lsof -nP -iTCP:${PORT} -sTCP:LISTEN\`).
|
||||
3. Personally run each concrete \`commands/http-01-*.sh\` through \`commands/http-14-*.sh\` script, one at a time in numeric order: real curl status → three validates → three sequential publishes → pull → three reads → three exports against \`${base}\`. Each script saves the exact JSON response under \`responses/\` or ZIP bytes under \`exports/raw/\`; each publish derives its current base from the preceding bounded saved response. Do not advance on a non-2xx response.
|
||||
4. Only after publish, run \`commands/git-inspect.sh <published-commit>\`: inspect \`git log\`, \`git ls-tree\`, \`git show <published-commit>:workspaces/<id>.yaml\`, and \`git show <published-commit>:workspace-content/<id>/evidence/...\` at that same commit.
|
||||
5. Inspect generated \`workspace-docs\`, the immutable commit-addressed descriptor snapshot, and its \`snapshot.json\` manifest.
|
||||
6. Run \`commands/extract-export.sh <zip> <new-output-dir>\` to safely extract the ZIP; verify manifest 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.
|
||||
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'\nimport {createHash} from 'node:crypto';import {readFile} from 'node:fs/promises';import {join} from 'node:path';const out=process.argv[2],m=JSON.parse(await readFile(join(out,'manifest.json')));for(const [n,h]of Object.entries(m.files)){const b=await readFile(join(out,n));if(createHash('sha256').update(b).digest('hex')!==h)throw Error('manifest hash mismatch');const text=b.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))throw Error('export contains Evidence or secret canary bytes');}\nNODE\n`],["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],pattern=/(?:DWH|SIGNED|ACCESS|SECRET|SESSION)-[0-9a-f]{32}/;let found=false;
|
||||
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(pattern.test((await readFile(child)).toString("latin1"))&&!rel.endsWith("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(pattern.test(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");
|
||||
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({repositoryRoot=defaultRepositoryRoot,skipBuild=false}={}){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};}
|
||||
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 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;});
|
||||
@@ -0,0 +1,135 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { chmod, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import net from "node:net";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
import {
|
||||
cleanupManual, fixedManualRoot, prepareManual, readManualOwnership, serveManual, stopManual,
|
||||
} from "./p1-manual-acceptance.mjs";
|
||||
|
||||
const roots = [];
|
||||
async function fakeRepo() {
|
||||
const root = await realpath(await mkdtemp(join(tmpdir(), "p1-manual-repo-")));
|
||||
roots.push(root);
|
||||
for (const path of ["scripts/p1-acceptance.sh", "scripts/test-p1-acceptance.sh", "backend/scripts/p1-acceptance.mjs", "backend/dist/server.js"]) {
|
||||
await mkdir(dirname(join(root, path)), { recursive: true });
|
||||
await writeFile(join(root, path), path.endsWith(".sh") ? "#!/bin/sh\n" : "export {};\n", { mode: 0o700 });
|
||||
}
|
||||
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);
|
||||
await mkdir(join(root, "harness", "workspaces"), { recursive: true });
|
||||
return root;
|
||||
}
|
||||
test.afterEach(async () => Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))));
|
||||
|
||||
test("prepare refuses a pre-existing or symlink fixed root", async () => {
|
||||
const repo = await fakeRepo(); const root = fixedManualRoot(repo);
|
||||
await mkdir(root, { recursive: true });
|
||||
await assert.rejects(prepareManual({ repositoryRoot: repo, skipBuild: true }), /already exists/);
|
||||
await rm(root, { recursive: true });
|
||||
const target = `${root}-target`; await mkdir(target, { recursive: true }); await symlink(target, root);
|
||||
await assert.rejects(prepareManual({ repositoryRoot: repo, skipBuild: true }), /already exists|symlink/);
|
||||
});
|
||||
|
||||
test("prepare requires Task 8 and prerequisites before creating state", async () => {
|
||||
const repo = await fakeRepo(); await rm(join(repo, "scripts", "p1-acceptance.sh"));
|
||||
await assert.rejects(prepareManual({ repositoryRoot: repo, skipBuild: true }), /Task 8/);
|
||||
await assert.rejects(lstat(fixedManualRoot(repo)));
|
||||
});
|
||||
|
||||
test("prepare creates independent pending topology, fixtures, commands and guide without verdict", async () => {
|
||||
const repo = await fakeRepo(); const run = await prepareManual({ repositoryRoot: repo, skipBuild: true });
|
||||
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));
|
||||
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);
|
||||
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 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)]);
|
||||
assert.match(render, /read-p1-filesystem\.json/); assert.match(render, /responses\/pull\.json/); assert.doesNotMatch(render, /responses\/publish-p1-filesystem\.json/); assert.match(render, /snapshotPath/); assert.match(render, /p1-render-snapshot\.mjs/);
|
||||
});
|
||||
|
||||
test("cleanup rejects unowned, live, mismatched and symlink state and preserves siblings", async () => {
|
||||
const repo = await fakeRepo(); const integration = join(repo, ".artifacts", "p1-integration"); const sibling = join(repo, ".artifacts", "manual-acceptance", "foreign");
|
||||
await mkdir(integration, { recursive: true }); await writeFile(join(integration, "sentinel"), "keep");
|
||||
await mkdir(sibling, { recursive: true }); await writeFile(join(sibling, "sentinel"), "keep");
|
||||
await assert.rejects(cleanupManual({ repositoryRoot: repo }), /ownership|root/);
|
||||
const run = await prepareManual({ repositoryRoot: repo, skipBuild: true });
|
||||
const ownershipPath = join(run.root, "ownership.json"); const owned = JSON.parse(await readFile(ownershipPath)); owned.root += "-wrong"; await writeFile(ownershipPath, JSON.stringify(owned));
|
||||
await assert.rejects(cleanupManual({ repositoryRoot: repo }), /identity/); assert.equal((await lstat(run.root)).isDirectory(), true);
|
||||
assert.equal(await readFile(join(integration, "sentinel"), "utf8"), "keep"); assert.equal(await readFile(join(sibling, "sentinel"), "utf8"), "keep");
|
||||
});
|
||||
|
||||
test("cleanup removes only the exact stopped owned root and never creates verdict", async () => {
|
||||
const repo = await fakeRepo(); const run = await prepareManual({ repositoryRoot: repo, skipBuild: true });
|
||||
await cleanupManual({ repositoryRoot: repo }); await assert.rejects(lstat(run.root));
|
||||
});
|
||||
|
||||
|
||||
async function installFakeServer(repo) {
|
||||
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)));
|
||||
`);
|
||||
}
|
||||
|
||||
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);
|
||||
const health=await (await fetch("http://127.0.0.1:8791/health")).json(); assert.equal(health.status,"ok"); assert.equal(health.ambient,undefined); assert.equal(health.wrongMaintenance,undefined); assert.equal(health.maintenance,join(run.root,"installation/data/maintenance.json")); if(priorAmbient===undefined)delete process.env.THT_DWH_API_KEY;else process.env.THT_DWH_API_KEY=priorAmbient;
|
||||
await assert.rejects(serveManual({repositoryRoot:repo}),/PID record/);
|
||||
await stopManual({repositoryRoot:repo}); await assert.rejects(lstat(join(run.root,"backend.pid")));
|
||||
await assert.rejects(fetch("http://127.0.0.1:8791/health",{signal:AbortSignal.timeout(200)}));
|
||||
await cleanupManual({repositoryRoot:repo});
|
||||
});
|
||||
|
||||
test("serve refuses an occupied fixed port and never creates a PID or verdict", { concurrency: false }, async () => {
|
||||
const repo=await fakeRepo(); await installFakeServer(repo); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
|
||||
const blocker=net.createServer(); await new Promise((resolvePromise,reject)=>blocker.once("error",reject).listen(8791,"127.0.0.1",resolvePromise));
|
||||
try { await assert.rejects(serveManual({repositoryRoot:repo}),/occupied/); } finally { await new Promise(resolvePromise=>blocker.close(resolvePromise)); }
|
||||
await assert.rejects(lstat(join(run.root,"backend.pid"))); await assert.rejects(lstat(join(run.root,"VERDICT.md")));
|
||||
});
|
||||
|
||||
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 assert.rejects(serveManual({repositoryRoot:repo}),/PID record/);
|
||||
await assert.rejects(stopManual({repositoryRoot:repo}),/identity mismatch/);
|
||||
await assert.rejects(cleanupManual({repositoryRoot:repo}),/identity|stale/);
|
||||
assert.equal((await lstat(run.root)).isDirectory(),true);
|
||||
});
|
||||
|
||||
test("generated render command validates saved responses and owned snapshot before renderer", async () => {
|
||||
const repo=await fakeRepo(); const run=await prepareManual({repositoryRoot:repo,skipBuild:true}); const script=join(run.root,"commands/render-1.sh"), output=join(run.root,"rendered/runtime-1.yaml");
|
||||
const invoke=()=>execFileAsync("bash",[script],{cwd:repo});
|
||||
await assert.rejects(invoke(),/saved response is missing/);
|
||||
await writeFile(join(run.root,"responses/read-p1-filesystem.json"),"{"); await writeFile(join(run.root,"responses/pull.json"),"{}");
|
||||
await assert.rejects(invoke(),/malformed JSON/);
|
||||
const a="a".repeat(40),b="b".repeat(40),outside=join(repo,"outside.yaml"); await writeFile(outside,"x");
|
||||
await writeFile(join(run.root,"responses/read-p1-filesystem.json"),JSON.stringify({revision:{commit:a,snapshotPath:outside}})); await writeFile(join(run.root,"responses/pull.json"),JSON.stringify({head:b}));
|
||||
await assert.rejects(invoke(),/revisions differ/);
|
||||
await writeFile(join(run.root,"responses/pull.json"),JSON.stringify({head:a})); await assert.rejects(invoke(),/snapshot escapes/);
|
||||
await assert.rejects(lstat(output));
|
||||
});
|
||||
|
||||
|
||||
test("generated secret scan checks reachable Git blobs without printing contents", async () => {
|
||||
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 execFileAsync("bash",[scan],{cwd:repo});
|
||||
const canary="DWH-"+"c".repeat(32); 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));
|
||||
});
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { closeSync, constants, fsyncSync, lstatSync, openSync, realpathSync } from "node:fs";
|
||||
import { chmod, lstat, mkdir, open, readFile, realpath, rename, rm } from "node:fs/promises";
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// This acceptance-only adapter deliberately imports the built production runner.
|
||||
import { ThtRunner } from "../dist/tht/tht-runner.js";
|
||||
|
||||
const modulePath = fileURLToPath(import.meta.url);
|
||||
const defaultRepositoryRoot = realpathSync(resolve(dirname(modulePath), "../.."));
|
||||
const HEX40 = /^[0-9a-f]{40}$/;
|
||||
const HEX64 = /^[0-9a-f]{64}$/;
|
||||
|
||||
function fixedRoot(repositoryRoot) { return join(realpathSync(repositoryRoot), ".artifacts", "manual-acceptance", "p1"); }
|
||||
function below(parent, child) { const rel = relative(parent, child); return rel !== "" && !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel); }
|
||||
function assertNoSymlinks(root, path, allowMissingLeaf = false) {
|
||||
const rel = relative(root, path);
|
||||
if (rel.startsWith("..") || isAbsolute(rel)) throw new Error("path is outside owned root");
|
||||
let cursor = root;
|
||||
for (const [index, part] of rel.split(sep).filter(Boolean).entries()) {
|
||||
cursor = join(cursor, part);
|
||||
try { if (lstatSync(cursor).isSymbolicLink()) throw new Error("owned path contains a symlink"); }
|
||||
catch (error) {
|
||||
if (allowMissingLeaf && error.code === "ENOENT" && index === rel.split(sep).filter(Boolean).length - 1) return;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function ownership(repositoryRoot, ownershipPath) {
|
||||
const root = fixedRoot(repositoryRoot);
|
||||
const expected = join(root, "ownership.json");
|
||||
if (resolve(ownershipPath) !== expected) throw new Error("ownership path is not owned");
|
||||
const rootEntry = await lstat(root); const ownershipEntry = await lstat(expected);
|
||||
if (!rootEntry.isDirectory() || rootEntry.isSymbolicLink() || !ownershipEntry.isFile() || ownershipEntry.isSymbolicLink()) throw new Error("ownership is unsafe");
|
||||
if (await realpath(root) !== root) throw new Error("ownership root is not canonical");
|
||||
let value; try { value = JSON.parse(await readFile(expected, "utf8")); } catch { throw new Error("ownership is malformed"); }
|
||||
if (value?.schemaVersion !== 1 || value.kind !== "p1-manual-acceptance" || !HEX64.test(value.nonce ?? "")
|
||||
|| value.repositoryRoot !== realpathSync(repositoryRoot) || value.root !== root || value.status !== "PENDING"
|
||||
|| value.listener?.host !== "127.0.0.1" || value.listener?.port !== 8791) throw new Error("ownership identity mismatch");
|
||||
return { root, value };
|
||||
}
|
||||
async function atomicCopy(source, output) {
|
||||
const staging = join(dirname(output), `.${basename(output)}.${randomBytes(12).toString("hex")}.tmp`);
|
||||
let handle;
|
||||
try {
|
||||
const bytes = await readFile(source);
|
||||
handle = await open(staging, "wx", 0o600); await handle.writeFile(bytes); await handle.sync(); await handle.close(); handle = undefined;
|
||||
await chmod(staging, 0o600); await rename(staging, output);
|
||||
const directory = openSync(dirname(output), constants.O_RDONLY); try { fsyncSync(directory); } finally { closeSync(directory); }
|
||||
} finally { if (handle) await handle.close().catch(() => {}); await rm(staging, { force: true }).catch(() => {}); }
|
||||
}
|
||||
|
||||
export async function renderOwnedSnapshot({ repositoryRoot = defaultRepositoryRoot, ownershipPath, snapshotPath, outputPath, env = process.env }) {
|
||||
const repo = realpathSync(repositoryRoot); const { root } = await ownership(repo, resolve(repo, ownershipPath));
|
||||
const snapshot = resolve(repo, snapshotPath); const output = resolve(repo, outputPath);
|
||||
const snapshotsRoot = join(root, "installation", "registry", "snapshots");
|
||||
const renderedRoot = join(root, "rendered");
|
||||
if (!isAbsolute(snapshotPath) || !below(snapshotsRoot, snapshot)) throw new Error("snapshot is not an owned absolute path");
|
||||
const match = /^([0-9a-f]{40})\/([a-z][a-z0-9-]{2,62})\.yaml$/.exec(relative(snapshotsRoot, snapshot).split(sep).join("/"));
|
||||
if (!match || !HEX40.test(match[1])) throw new Error("snapshot is not commit addressed");
|
||||
assertNoSymlinks(root, snapshot); const snapshotEntry = await lstat(snapshot);
|
||||
if (!snapshotEntry.isFile() || snapshotEntry.isSymbolicLink() || await realpath(snapshot) !== snapshot) throw new Error("snapshot is unsafe");
|
||||
if (!below(renderedRoot, output) || dirname(output) !== renderedRoot || !output.endsWith(".yaml")) throw new Error("output is not an owned rendered path");
|
||||
assertNoSymlinks(root, dirname(output));
|
||||
try { if ((await lstat(output)).isSymbolicLink()) throw new Error("output is unsafe"); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
||||
await mkdir(join(snapshotsRoot, "runtime"), { recursive: true, mode: 0o700 });
|
||||
const prior = {};
|
||||
for (const [key, value] of Object.entries(env)) { prior[key] = process.env[key]; if (value === undefined) delete process.env[key]; else process.env[key] = value; }
|
||||
const runner = new ThtRunner({
|
||||
thtBin: join(repo, "harness", ".venv", "bin", "tht"), harnessDir: join(repo, "harness"),
|
||||
configPath: join(root, "installation", "base.yaml"), dataRoot: join(root, "installation", "data"),
|
||||
runtimeSnapshotRoot: join(snapshotsRoot, "runtime"), secretRoots: [join(root, "fixture-secrets")],
|
||||
semanticRuntime: { internalQdrantUrl: "http://qdrant:6333", internalEmbeddingUrl: "http://embedding:11434", internalEmbeddingModel: "qwen3-embedding:0.6b", internalEmbeddingDimensions: 1024 },
|
||||
});
|
||||
let lease;
|
||||
try { lease = runner.acquireWorkspaceRuntime(snapshot); await atomicCopy(lease.path, output); }
|
||||
finally {
|
||||
if (lease) lease.release();
|
||||
for (const key of Object.keys(env)) { if (prior[key] === undefined) delete process.env[key]; else process.env[key] = prior[key]; }
|
||||
}
|
||||
return output;
|
||||
}
|
||||
function parseArgs(argv) {
|
||||
if (argv.length !== 6) throw new Error("usage: p1-render-snapshot.mjs --ownership PATH --snapshot ABSOLUTE_PATH --output PATH");
|
||||
const result = {}; for (let i=0;i<argv.length;i+=2) { if (!["--ownership","--snapshot","--output"].includes(argv[i]) || result[argv[i]]) throw new Error("invalid arguments"); result[argv[i]]=argv[i+1]; }
|
||||
if (!result["--ownership"] || !result["--snapshot"] || !result["--output"]) throw new Error("missing arguments"); return result;
|
||||
}
|
||||
if (process.argv[1] && realpathSync(process.argv[1]) === modulePath) {
|
||||
try { const args=parseArgs(process.argv.slice(2)); await renderOwnedSnapshot({ ownershipPath:args["--ownership"], snapshotPath:args["--snapshot"], outputPath:args["--output"] }); console.log(`rendered ${resolve(args["--output"])}`); }
|
||||
catch(error) { console.error(`p1 render refused: ${error.message}`); process.exitCode=1; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { chmod, lstat, mkdir, mkdtemp, readFile, realpath, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { renderOwnedSnapshot } from "./p1-render-snapshot.mjs";
|
||||
|
||||
const roots=[];
|
||||
async function fixture() {
|
||||
const repo=await realpath(await mkdtemp(join(tmpdir(),"p1-render-repo-"))); roots.push(repo);
|
||||
const root=join(repo,".artifacts/manual-acceptance/p1"); const commit="a".repeat(40); const snapshot=join(root,"installation/registry/snapshots",commit,"p1-filesystem.yaml");
|
||||
for (const p of [dirname(snapshot),join(root,"rendered"),join(root,"installation/registry/snapshots/runtime"),join(root,"installation/data"),join(root,"fixture-secrets"),join(repo,"harness")]) await mkdir(p,{recursive:true,mode:0o700});
|
||||
await writeFile(join(root,"ownership.json"),JSON.stringify({schemaVersion:1,kind:"p1-manual-acceptance",nonce:"b".repeat(64),repositoryRoot:repo,root,status:"PENDING",listener:{host:"127.0.0.1",port:8791}}));
|
||||
await writeFile(join(root,"installation/base.yaml"),"{}\n");
|
||||
const secret=join(root,"fixture-secrets/dwh-password"); await writeFile(secret,"not-inspected",{mode:0o600});
|
||||
await writeFile(snapshot,`workspace:
|
||||
schema_version: 3
|
||||
id: p1-filesystem
|
||||
name: P1 filesystem
|
||||
language: en
|
||||
dwh:
|
||||
engine: postgres
|
||||
database: postgres
|
||||
schema: public
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store: {engine: qdrant, collection: p1-filesystem, dimensions: 1024, distance: cosine}
|
||||
embedding: {provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024}
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
evidence:
|
||||
source: {type: filesystem, uri: workspace-content/p1-filesystem/evidence, patterns: ["**/*.md"], max_bytes: 10485760}
|
||||
policy: {max_chunk_chars: 4000, retain_published_generations: 3}
|
||||
`);
|
||||
const env={THT_WS_P1_FILESYSTEM_DWH_TRANSPORT:"postgres_direct",THT_WS_P1_FILESYSTEM_DWH_HOST:"dwh.invalid",THT_WS_P1_FILESYSTEM_DWH_PORT:"5432",THT_WS_P1_FILESYSTEM_DWH_USER:"reader",THT_WS_P1_FILESYSTEM_DWH_PASSWORD_FILE:secret};
|
||||
return {repo,root,snapshot,env};
|
||||
}
|
||||
test.afterEach(async()=>Promise.all(roots.splice(0).map(r=>rm(r,{recursive:true,force:true}))));
|
||||
|
||||
test("renderer copies a production lease deterministically with mode 0600 and no leases",async()=>{ const f=await fixture(); const one=join(f.root,"rendered/one.yaml"),two=join(f.root,"rendered/two.yaml"); await renderOwnedSnapshot({repositoryRoot:f.repo,ownershipPath:join(f.root,"ownership.json"),snapshotPath:f.snapshot,outputPath:one,env:{...process.env,...f.env}}); await renderOwnedSnapshot({repositoryRoot:f.repo,ownershipPath:join(f.root,"ownership.json"),snapshotPath:f.snapshot,outputPath:two,env:{...process.env,...f.env}}); assert.deepEqual(await readFile(one),await readFile(two)); assert.equal((await lstat(one)).mode&0o777,0o600); assert.deepEqual(await (await import("node:fs/promises")).readdir(join(f.root,"installation/registry/snapshots/runtime")),[]); });
|
||||
|
||||
test("renderer rejects unowned, symlink, and out-of-root paths",async()=>{ const f=await fixture(); const outside=join(f.repo,"outside.yaml"); await writeFile(outside,"x"); await assert.rejects(renderOwnedSnapshot({repositoryRoot:f.repo,ownershipPath:join(f.root,"ownership.json"),snapshotPath:outside,outputPath:join(f.root,"rendered/x.yaml"),env:f.env}),/owned|snapshot/); const link=join(dirname(f.snapshot),"linked.yaml"); await symlink(f.snapshot,link); await assert.rejects(renderOwnedSnapshot({repositoryRoot:f.repo,ownershipPath:join(f.root,"ownership.json"),snapshotPath:link,outputPath:join(f.root,"rendered/x.yaml"),env:f.env}),/snapshot|symlink/); await assert.rejects(renderOwnedSnapshot({repositoryRoot:f.repo,ownershipPath:join(f.root,"ownership.json"),snapshotPath:f.snapshot,outputPath:outside,env:f.env}),/output/); });
|
||||
|
||||
test("renderer releases its lease when atomic output fails",async()=>{ const f=await fixture(); const output=join(f.root,"rendered/existing"); await mkdir(output); await assert.rejects(renderOwnedSnapshot({repositoryRoot:f.repo,ownershipPath:join(f.root,"ownership.json"),snapshotPath:f.snapshot,outputPath:output,env:{...process.env,...f.env}})); assert.deepEqual(await (await import("node:fs/promises")).readdir(join(f.root,"installation/registry/snapshots/runtime")),[]); });
|
||||
Reference in New Issue
Block a user