Files
ThothII/backend/scripts/p1-manual-acceptance.mjs
T
marcopan c7338969d7 fix: bind complete P1 manual dist graph and snapshot identity
prepare records an immutable manifest of every regular backend/dist file
(path/size/sha256/dev/ino) in the owned root and binds its record identity
in ownership; serve revalidates record and every file before spawn, passes
the manifest to the child on fd 4, and the immutable preload hash-verifies
all files at startup and serves only cached verified bytes for any import
below backend/dist, so imported dependency replacement is refused before
RUNNING or never executes. The render command validates the commit
snapshot.json manifest, binds snapshot bytes to the manifest digest and the
installed Git blob, and passes the expected digest to the renderer, which
revalidates head/files digest with bounded no-follow reads and renders only
verified bytes with lease release on refusal.
2026-08-10 17:36:24 +02:00

609 lines
83 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { execFile, spawn } from "node:child_process";
import { createHash, randomBytes } from "node:crypto";
import { closeSync, constants, fstatSync, fsyncSync, lstatSync, openSync, realpathSync } from "node:fs";
import { access, chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
import http from "node:http";
import net from "node:net";
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
const exec = promisify(execFile); const modulePath=fileURLToPath(import.meta.url); const defaultRepositoryRoot=realpathSync(resolve(dirname(modulePath),"../.."));
const HEX64=/^[0-9a-f]{64}$/; const PORT=8791; const HOST="127.0.0.1";
export function fixedManualRoot(repositoryRoot=defaultRepositoryRoot){return join(realpathSync(repositoryRoot),".artifacts","manual-acceptance","p1");}
function below(parent,child){const rel=relative(parent,child);return rel!==""&&!rel.startsWith(`..${sep}`)&&rel!==".."&&!isAbsolute(rel);}
function noSymlinkExisting(repo,target){const rel=relative(repo,target);if(rel.startsWith("..")||isAbsolute(rel))throw new Error("root leaves repository");let cursor=repo;for(const part of rel.split(sep).filter(Boolean)){cursor=join(cursor,part);try{if(lstatSync(cursor).isSymbolicLink())throw new Error("owned root ancestor is a symlink");}catch(error){if(error.code==="ENOENT")break;throw error;}}}
async function atomicWrite(path,bytes,mode=0o600){await mkdir(dirname(path),{recursive:true});const staging=join(dirname(path),`.${basename(path)}.${randomBytes(12).toString("hex")}.tmp`);let h;try{h=await open(staging,"wx",mode);await h.writeFile(bytes);await h.sync();await h.close();h=undefined;await rename(staging,path);const fd=openSync(dirname(path),constants.O_RDONLY);try{fsyncSync(fd);}finally{closeSync(fd);}}finally{if(h)await h.close().catch(()=>{});await rm(staging,{force:true}).catch(()=>{});}}
function directorySync(path){const fd=openSync(path,constants.O_RDONLY);try{fsyncSync(fd);}finally{closeSync(fd);}}
async function exclusiveRecord(path,value,label){const bytes=`${JSON.stringify(value,null,2)}\n`;let handle,createdEntry;try{handle=await open(path,"wx",0o600);createdEntry=await handle.stat();await handle.chmod(0o600);await handle.writeFile(bytes);await handle.sync();await handle.close();handle=undefined;directorySync(dirname(path));return{path,bytes,dev:createdEntry.dev,ino:createdEntry.ino};}catch(error){if(handle)await handle.close().catch(()=>{});if(createdEntry)try{const current=await lstat(path);if(current.dev===createdEntry.dev&&current.ino===createdEntry.ino)await rm(path);}catch{}if(error.code==="EEXIST")throw new Error(`${label} already exists; operator inspection required`);throw error;}}
async function requireExactRecord(record){const entry=await lstat(record.path);if(!entry.isFile()||entry.isSymbolicLink()||(entry.mode&0o777)!==0o600||(record.dev!==undefined&&(entry.dev!==record.dev||entry.ino!==record.ino)))throw new Error("owned lifecycle record is unsafe");if(await readFile(record.path,"utf8")!==record.bytes)throw new Error("owned lifecycle record changed; operator inspection required");const after=await lstat(record.path);if(after.dev!==entry.dev||after.ino!==entry.ino)throw new Error("owned lifecycle record changed; operator inspection required");return after;}
async function removeExactRecord(record){await requireExactRecord(record);await requireExactRecord(record);await rm(record.path);directorySync(dirname(record.path));}
async function replaceExactRecord(record,value){await requireExactRecord(record);const bytes=`${JSON.stringify(value,null,2)}\n`,staging=join(dirname(record.path),`.${basename(record.path)}.${randomBytes(12).toString("hex")}.tmp`);let handle;try{handle=await open(staging,"wx",0o600);await handle.chmod(0o600);await handle.writeFile(bytes);await handle.sync();await handle.close();handle=undefined;await requireExactRecord(record);await rename(staging,record.path);const entry=await lstat(record.path);directorySync(dirname(record.path));return{path:record.path,bytes,dev:entry.dev,ino:entry.ino};}finally{if(handle)await handle.close().catch(()=>{});await rm(staging,{force:true}).catch(()=>{});}}
function sameEntry(actual,expected){return actual.dev===expected.dev&&actual.ino===expected.ino;}
async function requirePathIdentity(path,expected,label){let entry;try{entry=await lstat(path);}catch{throw new Error(`${label} identity changed`);}if(entry.isSymbolicLink()||!sameEntry(entry,expected))throw new Error(`${label} identity changed`);return entry;}
async function acquireLifecycle(repo,operation){
const lockPath=join(repo,".p1-manual-acceptance.lifecycle.lock"),artifacts=join(repo,".artifacts"),manualParent=join(artifacts,"manual-acceptance"),root=fixedManualRoot(repo);
noSymlinkExisting(repo,manualParent);await mkdir(manualParent,{recursive:true,mode:0o700});noSymlinkExisting(repo,manualParent);
const repoEntry=await lstat(repo),artifactsEntry=await lstat(artifacts),parentEntry=await lstat(manualParent);
if(!repoEntry.isDirectory()||!artifactsEntry.isDirectory()||!parentEntry.isDirectory())throw new Error("lifecycle namespace identity is unsafe");
const lifecycleNonce=randomBytes(32).toString("hex"),record=await exclusiveRecord(lockPath,{schemaVersion:1,kind:"p1-manual-lifecycle",operation,lifecycleNonce,root,repositoryRoot:repo},"external lifecycle lock"),entry=await requireExactRecord(record);
return{...record,dev:entry.dev,ino:entry.ino,repoPath:repo,repoEntry,artifactsPath:artifacts,artifactsEntry,parentPath:manualParent,parentEntry,rootEntry:undefined};
}
async function requireLifecycleContext(lifecycle,{root=false}={}){
await requireExactRecord(lifecycle);await requirePathIdentity(lifecycle.repoPath,lifecycle.repoEntry,"repository root");await requirePathIdentity(lifecycle.artifactsPath,lifecycle.artifactsEntry,"artifact root");await requirePathIdentity(lifecycle.parentPath,lifecycle.parentEntry,"manual acceptance parent");
if(root&&lifecycle.rootEntry)await requirePathIdentity(join(lifecycle.parentPath,"p1"),lifecycle.rootEntry,"manual acceptance root");
}
async function bindLifecycleRoot(lifecycle,root){const entry=await lstat(root);if(!entry.isDirectory()||entry.isSymbolicLink())throw new Error("manual acceptance root identity is unsafe");lifecycle.rootEntry=entry;await requireLifecycleContext(lifecycle,{root:true});return entry;}
async function findRootByIdentity(repo,identity){
const artifacts=join(repo,".artifacts");let count=0;
for(const parent of await readdir(artifacts,{withFileTypes:true})){if(++count>1024)throw new Error("manual cleanup search bound exceeded");if(!parent.isDirectory()||parent.isSymbolicLink())continue;const candidate=join(artifacts,parent.name,"p1");try{const entry=await lstat(candidate);if(entry.isDirectory()&&!entry.isSymbolicLink()&&sameEntry(entry,identity))return candidate;}catch{}
}return undefined;
}
async function cleanupFailedPrepare(repo,lifecycle){if(!lifecycle.rootEntry)return;const candidate=await findRootByIdentity(repo,lifecycle.rootEntry);if(!candidate)return;const entry=await lstat(candidate);if(!sameEntry(entry,lifecycle.rootEntry)||entry.isSymbolicLink())throw new Error("failed prepare root identity changed");await rm(candidate,{recursive:true});}
function legacySupervisorPath(root){return join(root,"installation/runtime/p1-backend-supervisor.mjs");}
const CONTROL_PORT=8792;
const PRELOAD_SOURCE=`import net from "node:net";
import { createHash } from "node:crypto";
import { closeSync, constants, fstatSync, openSync, readFileSync, readSync, realpathSync } from "node:fs";
import { registerHooks } from "node:module";
import { dirname, join, sep } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const HOST="127.0.0.1",PORT=8792,HTTP_PORT=8791,HEX=/^[0-9a-f]{64}$/;
const argv=process.argv.slice(2),noncePrefix="--p1-manual-nonce=",rootPrefix="--p1-root=",controlPrefix="--p1-control-nonce=",shaPrefix="--p1-entry-sha256=",devPrefix="--p1-entry-dev=",inoPrefix="--p1-entry-ino=";
const prefixes=[noncePrefix,rootPrefix,controlPrefix,shaPrefix,devPrefix,inoPrefix];
if(argv.length!==6||argv.some((value,index)=>!value.startsWith(prefixes[index])))throw new Error("manual control identity arguments refused");
const nonce=argv[0].slice(noncePrefix.length),root=argv[1].slice(rootPrefix.length),controlNonce=argv[2].slice(controlPrefix.length),entrySha=argv[3].slice(shaPrefix.length),entryDev=argv[4].slice(devPrefix.length),entryIno=argv[5].slice(inoPrefix.length);
if(!HEX.test(nonce)||!root.startsWith("/")||!HEX.test(controlNonce)||!HEX.test(entrySha)||!/^[0-9]+$/.test(entryDev)||!/^[0-9]+$/.test(entryIno))throw new Error("manual control identity refused");
if(!Number.isInteger(constants.O_NOFOLLOW))throw new Error("manual distribution no-follow protection is unavailable");
const entryStat=fstatSync(3),entrySource=readFileSync(3);if(!entryStat.isFile()||String(entryStat.dev)!==entryDev||String(entryStat.ino)!==entryIno||createHash("sha256").update(entrySource).digest("hex")!==entrySha)throw new Error("manual entrypoint FD identity refused");
const manifestStat=fstatSync(4);if(!manifestStat.isFile()||manifestStat.size<1||manifestStat.size>8388608)throw new Error("manual distribution manifest FD identity refused");
let manifest;try{manifest=JSON.parse(readFileSync(4));}catch{throw new Error("manual distribution manifest is malformed");}
const entryPath=realpathSync(process.argv[1]),distRoot=dirname(entryPath);
if(manifest?.schemaVersion!==1||manifest.kind!=="p1-manual-dist-manifest"||manifest.root!==distRoot||!manifest.files||typeof manifest.files!=="object"||Array.isArray(manifest.files))throw new Error("manual distribution manifest identity refused");
const distEntries=Object.entries(manifest.files);if(distEntries.length<1||distEntries.length>20000)throw new Error("manual distribution manifest identity refused");
const distBytes=new Map();
for(const[rel,file]of distEntries){
if(typeof rel!=="string"||!rel||rel.startsWith("/")||rel.startsWith("..")||rel.includes("\\\\")||rel.includes("/./")||rel.endsWith("/")||!Number.isSafeInteger(file?.size)||file.size<1||file.size>33554432||!HEX.test(file?.sha256??"")||!/^[0-9]+$/.test(String(file?.dev))||!/^[0-9]+$/.test(String(file?.ino)))throw new Error("manual distribution manifest is malformed");
const path=join(distRoot,rel),fd=openSync(path,constants.O_RDONLY|constants.O_NOFOLLOW);
try{
const before=fstatSync(fd);
if(!before.isFile()||before.nlink!==1||String(before.dev)!==String(file.dev)||String(before.ino)!==String(file.ino)||before.size!==file.size)throw new Error("manual distribution module identity changed");
const bytes=Buffer.alloc(before.size);let offset=0;
while(offset<bytes.length){const n=readSync(fd,bytes,offset,bytes.length-offset,offset);if(n<1)throw new Error("manual distribution module changed while binding");offset+=n;}
const after=fstatSync(fd);
if(after.dev!==before.dev||after.ino!==before.ino||after.size!==before.size)throw new Error("manual distribution module changed while binding");
if(createHash("sha256").update(bytes).digest("hex")!==file.sha256)throw new Error("manual distribution module bytes changed");
distBytes.set(rel,bytes);
}finally{closeSync(fd);}
}
if(!distBytes.has("server.js")||createHash("sha256").update(entrySource).digest("hex")!==manifest.files["server.js"].sha256)throw new Error("manual entrypoint manifest identity refused");
const entryUrl=pathToFileURL(entryPath).href,distPrefix=distRoot+sep;
registerHooks({load(url,context,nextLoad){if(url===entryUrl)return{format:"module",shortCircuit:true,source:entrySource};let pathname;try{pathname=fileURLToPath(url);}catch{return nextLoad(url,context);}if(pathname.startsWith(distPrefix)){const rel=pathname.slice(distPrefix.length);const bytes=distBytes.get(rel);if(!bytes)throw new Error("manual distribution module refused");return{format:rel.endsWith(".json")?"json":"module",shortCircuit:true,source:bytes};}return nextLoad(url,context);}});
let state="STARTING",stopping=false,ownedListener,listenGeneration=0;
const listenerIdentity=()=>{const address=ownedListener?.listening?ownedListener.address():undefined;return{listening:Boolean(ownedListener?.listening&&address&&address.address===HOST&&address.port===HTTP_PORT),host:address?.address,port:address?.port,generation:listenGeneration};};
const originalListen=net.Server.prototype.listen;net.Server.prototype.listen=function(...args){const candidate=this;candidate.once("listening",()=>{const address=candidate.address();if(address&&address.address===HOST&&address.port===HTTP_PORT){ownedListener=candidate;listenGeneration++;}});candidate.on("close",()=>{if(ownedListener===candidate){ownedListener=undefined;if(state==="READY")state="LISTENER_CLOSED";}});return originalListen.apply(candidate,args);};
const identity=()=>({status:state,pid:process.pid,nonce,controlNonce,root,control:{host:HOST,port:PORT},listener:listenerIdentity()});
const control=net.createServer(socket=>{let bytes="";socket.setEncoding("utf8");socket.on("data",chunk=>{bytes+=chunk;if(bytes.length>2048)socket.destroy();});socket.on("end",()=>{let request;try{request=JSON.parse(bytes);}catch{socket.end();return;}if(request?.nonce!==controlNonce){socket.end();return;}if(request.action==="status"){socket.end(JSON.stringify(identity())+"\\n");return;}if(request.action==="ready"&&!stopping&&listenerIdentity().listening){state="READY";clearTimeout(watchdog);socket.end(JSON.stringify(identity())+"\\n");return;}if(request.action==="stop"&&!stopping){stopping=true;state="STOPPING";clearTimeout(watchdog);socket.end(JSON.stringify(identity())+"\\n",()=>{control.close();if(ownedListener?.listening)ownedListener.close(()=>process.exit(0));else setImmediate(()=>process.exit(0));});return;}socket.end(JSON.stringify(identity())+"\\n");});});
await new Promise((resolve,reject)=>{control.once("error",reject);control.listen({host:HOST,port:PORT,exclusive:true},resolve);});
const watchdog=setTimeout(()=>{if(state!=="STARTING")return;console.error("manual backend readiness watchdog expired");control.close(()=>process.exit(1));setTimeout(()=>process.exit(1),100).unref();},8000);
`;
const PRELOAD=`data:text/javascript;base64,${Buffer.from(PRELOAD_SOURCE,"utf8").toString("base64")}`;
async function controlRequest(control,payload){if(control?.host!==HOST||!Number.isSafeInteger(control?.port)||control.port<1||control.port>65535)throw new Error("backend control identity mismatch");return await new Promise((resolvePromise,reject)=>{const socket=net.createConnection({host:control.host,port:control.port}),timer=setTimeout(()=>socket.destroy(new Error("backend control timeout")),2000);let bytes="";socket.setEncoding("utf8");socket.on("connect",()=>socket.end(JSON.stringify(payload)));socket.on("data",chunk=>{bytes+=chunk;if(bytes.length>2048)socket.destroy(new Error("backend control response too large"));});socket.on("error",reject);socket.on("close",()=>{clearTimeout(timer);let value;try{value=JSON.parse(bytes);}catch{return reject(new Error("backend control response is malformed"));}resolvePromise(value);});});}
function ownedValue(repo,root,nonce,{backendLog=null,entrypoint,distManifest=null,stage="PREPARING",createdAt=new Date().toISOString()}={}){return{schemaVersion:1,kind:"p1-manual-acceptance",nonce,repositoryRoot:repo,root,status:"PENDING",stage,createdAt,listener:{host:HOST,port:PORT,state:"stopped"},backendLog,entrypoint,distManifest,resources:[root,{kind:"fastify",host:HOST,port:PORT}]};}
function validEntrypoint(value,repo){return value?.path===join(repo,"backend/dist/server.js")&&Number.isSafeInteger(value.dev)&&Number.isSafeInteger(value.ino)&&Number.isSafeInteger(value.size)&&value.size>0&&HEX64.test(value.sha256??"");}
async function readBoundEntrypoint(repo){
if(!Number.isInteger(constants.O_NOFOLLOW))throw new Error("production entrypoint no-follow protection is unavailable");const path=join(repo,"backend/dist/server.js");let handle;
try{handle=await open(path,constants.O_RDONLY|constants.O_NOFOLLOW);const before=await handle.stat(),pathEntry=await lstat(path);if(!before.isFile()||before.nlink!==1||pathEntry.isSymbolicLink()||!pathEntry.isFile()||!sameEntry(before,pathEntry))throw new Error("production server identity is unsafe");if(before.size<1||before.size>33554432)throw new Error("production entrypoint is unbounded");const bytes=Buffer.alloc(before.size);let offset=0;while(offset<bytes.length){const{bytesRead}=await handle.read(bytes,offset,bytes.length-offset,offset);if(bytesRead<1)throw new Error("production entrypoint changed while binding");offset+=bytesRead;}const after=await handle.stat();if(!sameEntry(before,after)||before.size!==bytes.length||after.size!==before.size)throw new Error("production entrypoint changed while binding");return{handle,identity:{path,dev:before.dev,ino:before.ino,size:before.size,sha256:createHash("sha256").update(bytes).digest("hex")},bytes};}catch(error){if(handle)await handle.close().catch(()=>{});throw error;}
}
async function requireEntrypointPathIdentity(entrypoint){const entry=await lstat(entrypoint.path);if(!entry.isFile()||entry.isSymbolicLink()||entry.nlink!==1||entry.dev!==entrypoint.dev||entry.ino!==entrypoint.ino||entry.size!==entrypoint.size)throw new Error("production entrypoint identity changed");const bytes=await readFile(entrypoint.path);if(bytes.length!==entrypoint.size||createHash("sha256").update(bytes).digest("hex")!==entrypoint.sha256)throw new Error("production entrypoint bytes changed");return entry;}
function validDistManifest(value,root){return value?.path===join(root,"installation","runtime","backend-dist.manifest.json")&&Number.isSafeInteger(value.dev)&&Number.isSafeInteger(value.ino)&&Number.isSafeInteger(value.size)&&value.size>0&&HEX64.test(value.sha256??"");}
function parseDistManifest(bytes,distRoot){let value;try{value=JSON.parse(bytes.toString("utf8"));}catch{throw new Error("production distribution manifest is malformed");}const files=value?.files;if(value?.schemaVersion!==1||value.kind!=="p1-manual-dist-manifest"||value.root!==distRoot||!files||typeof files!=="object"||Array.isArray(files))throw new Error("production distribution manifest is malformed");const entries=Object.entries(files);if(entries.length<1||entries.length>20000)throw new Error("production distribution manifest is malformed");for(const[rel,file]of entries){if(!/^[^./\\][^/\\]*(?:\/[^./\\][^/\\]*)*$/.test(rel)||!Number.isSafeInteger(file?.size)||file.size<1||file.size>33554432||!HEX64.test(file?.sha256??"")||!Number.isSafeInteger(file?.dev)||!Number.isSafeInteger(file?.ino))throw new Error("production distribution manifest is malformed");}return{value,files};}
async function buildDistManifest(repo){const dist=join(repo,"backend","dist"),files={};let count=0,total=0;async function walk(dir){for(const entry of await readdir(dir,{withFileTypes:true})){const path=join(dir,entry.name);if(entry.isSymbolicLink())throw new Error("production distribution contains a symlink");if(entry.isDirectory()){await walk(path);continue;}if(!entry.isFile())throw new Error("production distribution contains a nonregular entry");if(++count>20000)throw new Error("production distribution is unbounded");const rel=relative(dist,path).split(sep).join("/");let handle;try{handle=await open(path,constants.O_RDONLY|constants.O_NOFOLLOW);const before=await handle.stat();if(!before.isFile()||before.nlink!==1||before.size<1||before.size>33554432)throw new Error("production distribution module is unsafe");total+=before.size;if(total>536870912)throw new Error("production distribution is unbounded");const bytes=Buffer.alloc(before.size);let offset=0;while(offset<bytes.length){const{bytesRead}=await handle.read(bytes,offset,bytes.length-offset,offset);if(bytesRead<1)throw new Error("production distribution module changed while binding");offset+=bytesRead;}const after=await handle.stat();if(!sameEntry(before,after)||after.size!==before.size)throw new Error("production distribution module changed while binding");files[rel]={size:before.size,sha256:createHash("sha256").update(bytes).digest("hex"),dev:before.dev,ino:before.ino};}finally{if(handle)await handle.close().catch(()=>{});}}}await walk(dist);return{schemaVersion:1,kind:"p1-manual-dist-manifest",root:dist,files};}
async function readBoundDistManifest(repo,owned){
if(!Number.isInteger(constants.O_NOFOLLOW))throw new Error("production distribution manifest no-follow protection is unavailable");const distManifest=owned.distManifest;let handle;
try{handle=await open(distManifest.path,constants.O_RDONLY|constants.O_NOFOLLOW);const before=await handle.stat(),pathEntry=await lstat(distManifest.path);if(!before.isFile()||before.nlink!==1||pathEntry.isSymbolicLink()||!pathEntry.isFile()||!sameEntry(before,pathEntry)||before.dev!==distManifest.dev||before.ino!==distManifest.ino||before.size!==distManifest.size)throw new Error("production distribution manifest identity changed");if(before.size<1||before.size>8388608)throw new Error("production distribution manifest is unbounded");const bytes=Buffer.alloc(before.size);let offset=0;while(offset<bytes.length){const{bytesRead}=await handle.read(bytes,offset,bytes.length-offset,offset);if(bytesRead<1)throw new Error("production distribution manifest changed while binding");offset+=bytesRead;}const after=await handle.stat();if(!sameEntry(before,after)||before.size!==bytes.length||after.size!==before.size)throw new Error("production distribution manifest changed while binding");if(createHash("sha256").update(bytes).digest("hex")!==distManifest.sha256)throw new Error("production distribution manifest bytes changed");const{files}=parseDistManifest(bytes,join(repo,"backend","dist"));if(files["server.js"]?.sha256!==owned.entrypoint.sha256)throw new Error("production distribution manifest does not bind the entrypoint");return{handle,files};}catch(error){if(handle)await handle.close().catch(()=>{});throw error;}
}
async function validateDistFiles(repo,files){const dist=join(repo,"backend","dist");for(const[rel,file]of Object.entries(files)){const path=join(dist,...rel.split("/"));let handle;try{handle=await open(path,constants.O_RDONLY|constants.O_NOFOLLOW);const before=await handle.stat(),pathEntry=await lstat(path);if(!before.isFile()||before.nlink!==1||pathEntry.isSymbolicLink()||!pathEntry.isFile()||!sameEntry(before,pathEntry)||before.dev!==file.dev||before.ino!==file.ino||before.size!==file.size)throw new Error("production distribution module identity changed");const bytes=Buffer.alloc(before.size);let offset=0;while(offset<bytes.length){const{bytesRead}=await handle.read(bytes,offset,bytes.length-offset,offset);if(bytesRead<1)throw new Error("production distribution module changed while binding");offset+=bytesRead;}const after=await handle.stat();if(!sameEntry(before,after)||after.size!==before.size)throw new Error("production distribution module changed while binding");if(createHash("sha256").update(bytes).digest("hex")!==file.sha256)throw new Error("production distribution module bytes changed");}finally{if(handle)await handle.close().catch(()=>{});}}}
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 baseValid=value.schemaVersion===1&&value.kind==="p1-manual-acceptance"&&HEX64.test(value.nonce??"")&&value.repositoryRoot===repo&&value.root===root&&value.status==="PENDING"&&["PREPARING","READY"].includes(value.stage)&&value.listener?.host===HOST&&value.listener?.port===PORT&&value.listener?.state==="stopped"&&typeof value.createdAt==="string"&&validEntrypoint(value.entrypoint,repo)&&validDistManifest(value.distManifest,root)&&JSON.stringify(value.resources)===JSON.stringify([root,{kind:"fastify",host:HOST,port:PORT}]);const readyLog=value.backendLog?.path===join(root,"logs/backend.log")&&Number.isSafeInteger(value.backendLog?.dev)&&Number.isSafeInteger(value.backendLog?.ino);if(!baseValid||(value.stage==="READY"?!readyLog:value.backendLog!==null))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","python3"]){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 { createHash } from "node:crypto";
import { readFile, realpath, stat } from "node:fs/promises";
import { basename, dirname, isAbsolute, join, 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 HEX40=/^[0-9a-f]{40}$/,HEX64=/^[0-9a-f]{64}$/;
const bounded=async(path,label="saved response")=>{let s;try{s=await stat(path);}catch{throw new Error(label+" is missing or unbounded");}if(!s.isFile()||s.size<2||s.size>1048576)throw new Error(label+" is missing or unbounded");let v;try{v=JSON.parse(await readFile(path,"utf8"));}catch{throw new Error(label+" is malformed JSON");}return v;};
const boundedBytes=async(path)=>{let s;try{s=await stat(path);}catch{throw new Error("saved snapshot is missing or unbounded");}if(!s.isFile()||s.size<2||s.size>1048576)throw new Error("saved snapshot is missing or unbounded");return await readFile(path);};
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(!HEX40.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 id=basename(canonical).slice(0,-".yaml".length);if(!/^[a-z][a-z0-9-]{2,62}$/.test(id))throw new Error("snapshot workspace identity is invalid");
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 manifest=await bounded(join(snapshots,commit,"snapshot.json"),"snapshot manifest");const files=manifest?.files,revisions=manifest?.revisions;
if(manifest?.head!==commit||!files||typeof files!=="object"||Array.isArray(files))throw new Error("snapshot manifest identity is invalid");
const expected=files[id+".yaml"];if(!HEX64.test(expected??""))throw new Error("snapshot manifest digest is invalid");
const snapshotBytes=await boundedBytes(canonical);if(createHash("sha256").update(snapshotBytes).digest("hex")!==expected)throw new Error("snapshot bytes differ from manifest digest");
const entry=Array.isArray(revisions)?revisions.find(candidate=>candidate?.id===id):undefined;
if(!entry||!HEX40.test(entry?.blob??"")||entry.commit!==commit||typeof entry.snapshotPath!=="string"||resolve(entry.snapshotPath)!==canonical||(entry.state!=="operational"&&entry.state!=="migration_required"))throw new Error("snapshot manifest revision is invalid");
if(!HEX40.test(revision?.blob??"")||revision.blob!==entry.blob)throw new Error("saved revision blob differs from snapshot manifest");
const blobCheck=spawnSync("git",["-C",checkout,"rev-parse",commit+":workspaces/"+id+".yaml"],{encoding:"utf8"});
if(blobCheck.status!==0||blobCheck.stdout.trim()!==entry.blob)throw new Error("snapshot blob differs from installed Git commit");
const hashObject=spawnSync("git",["hash-object","--stdin"],{input:snapshotBytes,encoding:"utf8"});
if(hashObject.status!==0||hashObject.stdout.trim()!==entry.blob)throw new Error("snapshot bytes differ from Git blob");
const child=spawnSync(process.execPath,[renderer,"--ownership",resolve(root,"ownership.json"),"--snapshot",canonical,"--output",output,"--snapshot-sha256",expected],{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. Every lifecycle action uses the stable repository-root \`.p1-manual-acceptance.lifecycle.lock\`; successful prepare has advanced its ownership-first recovery record from \`PREPARING\` to \`READY\`.
1. Inspect \`${root}/ownership.json\`, including the bound production entrypoint identity and the complete \`backend/dist\` module manifest identity, 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 one production Node PID owns both \`${HOST}:${PORT}\` and its authenticated \`${HOST}:${CONTROL_PORT}\` control listener (for example, use \`lsof -nP -iTCP:${PORT} -sTCP:LISTEN\` and repeat for port ${CONTROL_PORT}). Serve executes the ownership-bound production entrypoint and complete verified \`backend/dist\` module graph from opened no-follow descriptors and publishes \`RUNNING\` only after the same authenticated child acknowledges its owned HTTP listener and passes bounded health checks.
3. Personally run each concrete \`commands/http-01-*.sh\` through \`commands/http-14-*.sh\` script, one at a time in numeric order: real curl status → three validates → three sequential publishes → pull → three reads → three exports against \`${base}\`. Each script saves the exact JSON response under \`responses/\` or ZIP bytes under \`exports/raw/\`; each publish derives its current base from the preceding bounded saved response. Do not advance on a non-2xx response.
4. Only after publish, run \`commands/git-inspect.sh <published-commit>\`: inspect \`git log\`, \`git ls-tree\`, \`git show <published-commit>:workspaces/<id>.yaml\`, and \`git show <published-commit>:workspace-content/<id>/evidence/...\` at that same commit.
5. Inspect generated \`workspace-docs\`, the immutable commit-addressed descriptor snapshot, and its \`snapshot.json\` manifest.
6. Run \`commands/extract-export.sh exports/raw/p1-filesystem.zip exports/extracted/p1-filesystem p1-filesystem\`, then the equivalent exact commands for \`p1-http\` and \`p1-s3\`; verify each manifest and descriptor identity, hashes, and absence of Evidence bytes and secret/canary material.
7. After saving \`responses/read-p1-filesystem.json\` and the final API/Git head in \`responses/pull.json\`, run \`commands/render-1.sh\`, \`commands/render-2.sh\`, then \`commands/diff-rendered.sh\`. The render commands bind the snapshot bytes to the commit\'s \`snapshot.json\` digest and Git blob identity; the renderer revalidates that digest and renders only the verified bytes through one opened no-follow \`rendered\` directory identity, refusing an ancestor swap.
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 only the direct \`fixture-secrets\` payload directory, scans bounded filesystem content and name/path bytes, discovers every bounded arbitrary \`.git\` repository plus the owned bare remote, and checks loose-ref names plus raw bounded bytes from every blob, commit, tree, and tag object, including unreachable objects. Findings and operational errors redact secret-bearing paths and values.
12. Run \`commands/absence-check.sh\`; confirm no file or directory represents preprocessing, Evidence materialization (including \`artifacts/evidence\`), embedding, Qdrant, ACTIVE, or retention state.
13. Run \`${repo}/scripts/p1-manual-acceptance.sh stop\`; confirm \`backend.pid\` and both listeners on ports ${PORT} and ${CONTROL_PORT} are gone.
14. Create \`${root}/VERDICT.md\` yourself with reviewer, UTC time, every checklist result, observations, and exactly either \`manual acceptance: PASS\` or \`manual acceptance: FAIL\`.
Preserve a failed lab by stopping it and leaving the owned root in place. Only \`cleanup\` removes this exact stopped lab.
`;}
function extractCommand(repo,root){return `#!/usr/bin/env bash
set -euo pipefail
zip=\${1:?zip required}; out=\${2:?new output required}; expected=\${3:?expected workspace id required}
python3 - "$zip" "$out" "$expected" ${quote(root)} ${quote(join(repo,"backend/package.json"))} <<'PY'
import hashlib
import io
import json
import os
import re
import secrets
import stat
import subprocess
import sys
import zipfile
zip_path, output_path, expected, root, package_json = sys.argv[1:]
allowed = {"p1-filesystem", "p1-http", "p1-s3"}
required = ["manifest.json", "workspace.yaml", "contract.env.example", "README.md"]
base = os.path.join(root, "exports", "extracted")
base_fd = None
archive_fd = None
stage_fd = None
archive_stage = None
extract_stage = None
published = False
def fail(message):
raise RuntimeError(message)
def exact(value, keys):
return isinstance(value, dict) and set(value) == set(keys)
def write_all(fd, data):
view = memoryview(data)
while view:
written = os.write(fd, view)
if written <= 0:
fail("anchored extraction write failed")
view = view[written:]
def read_exact_fd(fd, expected_size, limit, label):
if expected_size < 1 or expected_size > limit:
fail(label + " is unbounded")
chunks = []
remaining = expected_size
while remaining:
chunk = os.read(fd, min(1024 * 1024, remaining))
if not chunk:
fail(label + " changed while staging")
chunks.append(chunk)
remaining -= len(chunk)
if os.read(fd, 1):
fail(label + " changed while staging")
return b"".join(chunks)
def require_base_identity():
try:
current = os.stat(base, follow_symlinks=False)
except OSError:
fail("owned extraction root identity changed")
if (not stat.S_ISDIR(current.st_mode) or current.st_dev != base_identity.st_dev
or current.st_ino != base_identity.st_ino or os.path.realpath(base) != base):
fail("owned extraction root identity changed")
def remove_anchored_directory(name):
child_fd = None
try:
child_fd = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=base_fd)
for entry in os.listdir(child_fd):
if entry not in required:
fail("anchored extraction cleanup found an unexpected entry")
os.unlink(entry, dir_fd=child_fd)
os.fsync(child_fd)
except FileNotFoundError:
return
finally:
if child_fd is not None:
os.close(child_fd)
os.rmdir(name, dir_fd=base_fd)
os.fsync(base_fd)
try:
for flag in ("O_DIRECTORY", "O_NOFOLLOW"):
if not hasattr(os, flag):
fail("anchored extraction is unavailable on this platform")
if expected not in allowed:
fail("expected workspace identity is invalid")
if os.path.realpath(root) != root or os.path.dirname(output_path) != base:
fail("unsafe owned extraction root or output path")
output_name = os.path.basename(output_path)
if not output_name or output_name.startswith(".") or os.sep in output_name:
fail("unsafe output path")
base_fd = os.open(base, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
base_identity = os.fstat(base_fd)
if not stat.S_ISDIR(base_identity.st_mode):
fail("unsafe owned extraction root")
require_base_identity()
try:
os.stat(output_name, dir_fd=base_fd, follow_symlinks=False)
fail("unsafe output path")
except FileNotFoundError:
pass
# Open the caller's source exactly once, then consume only an owned staged copy.
source_fd = os.open(zip_path, os.O_RDONLY | os.O_NOFOLLOW)
try:
source_identity = os.fstat(source_fd)
if not stat.S_ISREG(source_identity.st_mode):
fail("source ZIP is unsafe")
source_bytes = read_exact_fd(source_fd, source_identity.st_size, 33554432, "source ZIP")
source_after = os.fstat(source_fd)
if (source_after.st_dev, source_after.st_ino, source_after.st_size) != (source_identity.st_dev, source_identity.st_ino, source_identity.st_size):
fail("source ZIP changed during staging")
finally:
os.close(source_fd)
archive_sha = hashlib.sha256(source_bytes).digest()
archive_stage = ".zip-stage-" + secrets.token_hex(16) + ".zip"
archive_fd = os.open(archive_stage, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=base_fd)
write_all(archive_fd, source_bytes)
os.fsync(archive_fd)
del source_bytes
os.lseek(archive_fd, 0, os.SEEK_SET)
staged_bytes = read_exact_fd(archive_fd, os.fstat(archive_fd).st_size, 33554432, "staged archive")
if hashlib.sha256(staged_bytes).digest() != archive_sha:
fail("staged archive SHA mismatch")
with zipfile.ZipFile(io.BytesIO(staged_bytes), "r") as archive:
infos = archive.infolist()
names = [entry.filename for entry in infos]
if len(names) != 4 or len(set(names)) != 4 or set(names) != set(required):
fail("unsafe-zip entries")
for entry in infos:
mode = (entry.external_attr >> 16) & 0xFFFF
if not stat.S_ISREG(mode) or entry.flag_bits & 1:
fail("ZIP contains a symlink or nonregular entry")
if entry.file_size < 1 or entry.file_size > 10485760:
fail("extracted file is unsafe")
payloads = {name: archive.read(name) for name in required}
if any(len(payloads[entry.filename]) != entry.file_size for entry in infos):
fail("extracted file size mismatch")
# Exercise the documented unzip prerequisite against the exact staged descriptor, not a path.
listing = subprocess.run(
["unzip", "-Z1", "/dev/fd/" + str(archive_fd)], pass_fds=(archive_fd,),
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=20, check=False,
)
if listing.returncode != 0 or listing.stdout.decode("utf8", "strict").splitlines() != names:
fail("unsafe-zip entries")
os.lseek(archive_fd, 0, os.SEEK_SET)
revalidated = read_exact_fd(archive_fd, len(staged_bytes), 33554432, "staged archive")
if hashlib.sha256(revalidated).digest() != archive_sha or revalidated != staged_bytes:
fail("staged archive SHA mismatch")
require_base_identity()
randomized = re.compile(br"(?:DWH|SIGNED|ACCESS|SECRET|SESSION)-[0-9a-f]{32}")
fixed = b"-".join([b"CANARY", b"MUST", b"BE", b"REJECTED"])
for data in payloads.values():
if b"P1 manually curated Evidence" in data or b"P1 curated table" in data or randomized.search(data) or fixed in data:
fail("export contains Evidence or secret canary bytes")
try:
manifest = json.loads(payloads["manifest.json"].decode("utf8"))
except Exception:
fail("export manifest schema mismatch")
hashed = required[1:]
files = manifest.get("files") if isinstance(manifest, dict) else None
if (not exact(manifest, ["schema_version", "workspace_id", "files"])
or manifest.get("schema_version") != 1 or manifest.get("workspace_id") != expected
or not exact(files, hashed)
or any(not isinstance(files[name], str) or not re.fullmatch(r"[0-9a-f]{64}", files[name]) for name in hashed)):
fail("export manifest workspace identity or schema mismatch")
for name in hashed:
if hashlib.sha256(payloads[name]).hexdigest() != files[name]:
fail("manifest hash mismatch")
yaml_helper = 'const fs=require("node:fs"),{createRequire}=require("node:module");try{const YAML=createRequire(process.argv[1])("yaml"),v=YAML.parse(fs.readFileSync(0,"utf8"));process.stdout.write(JSON.stringify(v?.workspace?.id??null));}catch{process.exit(2)}'
parsed = subprocess.run(["node", "-e", yaml_helper, package_json], input=payloads["workspace.yaml"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10, check=False)
try:
descriptor_id = json.loads(parsed.stdout.decode("utf8")) if parsed.returncode == 0 else None
except Exception:
descriptor_id = None
if descriptor_id != expected:
fail("export descriptor workspace identity mismatch")
extract_stage = ".extract-stage-" + secrets.token_hex(16)
os.mkdir(extract_stage, 0o700, dir_fd=base_fd)
stage_fd = os.open(extract_stage, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=base_fd)
for name in required:
fd = os.open(name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=stage_fd)
try:
write_all(fd, payloads[name])
os.fsync(fd)
finally:
os.close(fd)
os.fsync(stage_fd)
os.close(stage_fd)
stage_fd = None
require_base_identity()
try:
os.stat(output_name, dir_fd=base_fd, follow_symlinks=False)
fail("unsafe output path")
except FileNotFoundError:
pass
os.rename(extract_stage, output_name, src_dir_fd=base_fd, dst_dir_fd=base_fd)
extract_stage = None
published = True
os.fsync(base_fd)
require_base_identity()
except Exception as error:
if isinstance(error, RuntimeError):
print(str(error), file=sys.stderr)
else:
print("extraction operational failure (details redacted)", file=sys.stderr)
sys.exit_code = 1
finally:
if stage_fd is not None:
os.close(stage_fd)
if extract_stage is not None and base_fd is not None:
try:
remove_anchored_directory(extract_stage)
except Exception:
sys.exit_code = 1
if published and getattr(sys, "exit_code", 0) and base_fd is not None:
try:
remove_anchored_directory(output_name)
except Exception:
pass
if archive_fd is not None:
os.close(archive_fd)
if archive_stage is not None and base_fd is not None:
try:
os.unlink(archive_stage, dir_fd=base_fd)
os.fsync(base_fd)
except FileNotFoundError:
pass
if base_fd is not None:
os.close(base_fd)
if getattr(sys, "exit_code", 0):
raise SystemExit(sys.exit_code)
PY
`;}
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 { spawnSync } from "node:child_process";import { constants } from "node:fs";import { lstat, open, readdir } from "node:fs/promises";import { 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,filesystemCount=0,filesystemTotal=0,nameTotal=0;const gitDirs=new Set([join(root,"remote.git")]);const containsCanary=value=>randomized.test(value)||value.includes(fixed);const finding=kind=>{console.error("secret canary found in "+kind+" (path and value redacted)");found=true;};
async function maybeGitDir(path){try{const head=await lstat(join(path,"HEAD")),objects=await lstat(join(path,"objects"));if(head.isFile()&&objects.isDirectory()&&!head.isSymbolicLink()&&!objects.isSymbolicLink())gitDirs.add(path);}catch{}}
async function walk(path){for(const entry of await readdir(path,{withFileTypes:true})){if(++filesystemCount>200000)throw Error("bound");const child=join(path,entry.name),rel=relative(root,child),nameBytes=Buffer.from(entry.name),pathBytes=Buffer.from(rel);if(nameBytes.length>255||pathBytes.length>4096||(nameTotal+=nameBytes.length+pathBytes.length)>67108864)throw Error("bound");if(containsCanary(nameBytes.toString("latin1"))||containsCanary(pathBytes.toString("latin1")))finding("filesystem name bytes");if(entry.isSymbolicLink()){console.error("unsafe symlink during secret scan (path redacted)");found=true;continue;}if(entry.isDirectory()){if(entry.name===".git")await maybeGitDir(child);if(rel==="fixture-secrets")continue;await walk(child);continue;}if(!entry.isFile())throw Error("unsupported");let handle;try{handle=await open(child,constants.O_RDONLY|constants.O_NOFOLLOW);const before=await handle.stat();if(!before.isFile()||before.size>33554432)throw Error("bound");filesystemTotal+=before.size;if(filesystemTotal>1073741824)throw Error("bound");const bytes=await handle.readFile(),after=await handle.stat();if(bytes.length!==before.size||after.dev!==before.dev||after.ino!==before.ino||after.size!==before.size)throw Error("changed");const value=bytes.toString("latin1"),allowedRequest=rel==="requests/invalid-credential.json"&&value.includes(fixed)&&!randomized.test(value);if(containsCanary(value)&&!allowedRequest)finding("filesystem bytes");}finally{if(handle)await handle.close();}}}
function gitRun(args,options={}){const result=spawnSync("git",args,{...options,stdio:[options.input===undefined?"ignore":"pipe","pipe","pipe"]});if(result.error||result.status!==0)throw Error("git");return result.stdout;}
function scanGit(gitDir){const listing=gitRun(["--git-dir",gitDir,"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("bound");let total=0;for(const line of objects){const match=line.match(/^([0-9a-f]{40,64}) (blob|commit|tree|tag) (\\d+)$/);if(!match)throw Error("git");const[,oid,type,sizeText]=match,size=Number(sizeText);total+=size;if(!Number.isSafeInteger(size)||size>33554432||total>536870912)throw Error("bound");const raw=gitRun(["--git-dir",gitDir,"cat-file",type,oid],{maxBuffer:Math.max(1024,size+1)});if(raw.length!==size)throw Error("changed");if(containsCanary(raw.toString("latin1")))finding(type==="blob"?"Git blob":"Git object");}}
try{await walk(root);for(const gitDir of gitDirs)scanGit(gitDir);if(found)process.exitCode=1;else console.log("no fixture secret canary outside fixture-secrets or in any bounded Git object");}catch{console.error("secret scan operational failure (details redacted)");process.exitCode=2;}
NODE
`],["absence-check.sh",`#!/usr/bin/env bash
set -euo pipefail
root=${quote(root)}
node --input-type=module - "$root" <<'NODE'
import { readdir } from "node:fs/promises";import { join,relative } from "node:path";
const root=process.argv[2];let count=0,rejected=false;const artifactName=name=>name.toUpperCase()==="ACTIVE"||/(?:materiali[sz](?:e|ed|ation)|preprocess|embedding|qdrant|retention)/i.test(name);
async function walk(path){for(const entry of await readdir(path,{withFileTypes:true})){if(++count>200000)throw Error("bound");const child=join(path,entry.name),parts=relative(root,child).split("/");if(parts.some((part,index)=>part==="artifacts"&&parts[index+1]==="evidence")||artifactName(entry.name))rejected=true;if(entry.isSymbolicLink())continue;if(entry.isDirectory()&&entry.name!==".git")await walk(child);}}
try{await walk(root);if(rejected){console.error("unexpected out-of-scope P2+ artifact (path redacted)");process.exitCode=1;}else console.log("no out-of-scope runtime artifact found");}catch{console.error("out-of-scope artifact check failed safely (details redacted)");process.exitCode=2;}
NODE
`]]){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,repo=realpathSync(repositoryRoot),root=fixedManualRoot(repo),lifecycle=await acquireLifecycle(repo,"prepare");let entryBinding,ownershipCreated=false;
try{
await requireLifecycleContext(lifecycle);await checkPrerequisites(repo);if(!skipBuild)await run("npm",["--prefix",join(repo,"backend"),"run","build"]);await requireLifecycleContext(lifecycle);
entryBinding=await readBoundEntrypoint(repo);const entrypoint=entryBinding.identity;await entryBinding.handle.close();entryBinding=undefined;
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;}await bindLifecycleRoot(lifecycle,root);
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 requireLifecycleContext(lifecycle,{root:true});}
const distManifestValue=await buildDistManifest(repo),distManifestRecord=await exclusiveRecord(join(root,"installation/runtime/backend-dist.manifest.json"),distManifestValue,"production distribution manifest"),distManifest={path:distManifestRecord.path,dev:distManifestRecord.dev,ino:distManifestRecord.ino,size:distManifestRecord.bytes.length,sha256:createHash("sha256").update(distManifestRecord.bytes).digest("hex")};await requireLifecycleContext(lifecycle,{root:true});
const nonce=randomBytes(32).toString("hex"),createdAt=new Date().toISOString();await exclusiveRecord(join(root,"ownership.json"),ownedValue(repo,root,nonce,{entrypoint,distManifest,createdAt}),"manual ownership");ownershipCreated=true;await requireLifecycleContext(lifecycle,{root:true});
const backendLogPath=join(root,"logs/backend.log"),backendLogHandle=await open(backendLogPath,"wx",0o600);let backendLogEntry;try{await backendLogHandle.chmod(0o600);await backendLogHandle.sync();backendLogEntry=await backendLogHandle.stat();}finally{await backendLogHandle.close();}directorySync(dirname(backendLogPath));const backendLog={path:backendLogPath,dev:backendLogEntry.dev,ino:backendLogEntry.ino};
await requireLifecycleContext(lifecycle,{root:true});
try{await initializeGit(root);}catch(error){try{await requireLifecycleContext(lifecycle,{root:true});}catch{await cleanupFailedPrepare(repo,lifecycle);throw new Error("manual acceptance parent or root identity changed during prepare");}throw error;}await requireLifecycleContext(lifecycle,{root:true});
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);await requireLifecycleContext(lifecycle,{root:true});await atomicWrite(join(root,"ownership.json"),`${JSON.stringify(ownedValue(repo,root,nonce,{backendLog,entrypoint,distManifest,stage:"READY",createdAt}),null,2)}\n`);await requireLifecycleContext(lifecycle,{root:true});return{repositoryRoot:repo,root,nonce};
}catch(error){
if(entryBinding)await entryBinding.handle.close().catch(()=>{});
if(ownershipCreated){try{await requireLifecycleContext(lifecycle,{root:true});}catch{await cleanupFailedPrepare(repo,lifecycle).catch(()=>{});throw new Error("manual acceptance parent or root identity changed during prepare");}}
throw error;
}finally{await removeExactRecord(lifecycle);}
}
function portAvailable(port,label=`${HOST}:${port}`){return new Promise((resolvePromise,reject)=>{const server=net.createServer();server.once("error",error=>error.code==="EADDRINUSE"?reject(new Error(`${label} is occupied`)):reject(error));server.listen({host:HOST,port,exclusive:true},()=>server.close(()=>resolvePromise()));});}
async function requireCanonicalDirectory(path,label){const entry=await lstat(path);if(!entry.isDirectory()||entry.isSymbolicLink()||await realpath(path)!==path)throw new Error(`${label} directory identity is unsafe`);return entry;}
async function requireAbsent(path,label){try{await lstat(path);throw new Error(`${label} is legacy or unsafe`);}catch(error){if(error.code!=="ENOENT")throw error;}}
async function validateServeFilesystem(repo,root,owned){
if(root!==fixedManualRoot(repo))throw new Error("owned root identity is unsafe");
for(const [path,label] of [
[repo,"repository root"],[join(repo,".artifacts"),"artifact root"],[join(repo,".artifacts/manual-acceptance"),"manual root ancestor"],[root,"owned root"],
[join(root,"installation"),"owned installation"],[join(root,"installation/runtime"),"owned runtime"],[join(root,"installation/data"),"owned data"],
[join(root,"installation/registry"),"owned registry"],[join(root,"fixture-secrets"),"owned secrets"],[join(root,"logs"),"owned logs"],
[join(repo,"backend"),"backend root"],[join(repo,"backend/dist"),"backend distribution"],
])await requireCanonicalDirectory(path,label);
await requireAbsent(legacySupervisorPath(root),"legacy supervisor");
if(owned.stage!=="READY")throw new Error("manual acceptance preparation is incomplete");
const script=join(repo,"backend/dist/server.js");await requireEntrypointPathIdentity(owned.entrypoint);const manifestRecord=await readBoundDistManifest(repo,owned);try{await validateDistFiles(repo,manifestRecord.files);}finally{await manifestRecord.handle.close();}
const logPath=join(root,"logs/backend.log");
if(owned.backendLog?.path!==logPath)throw new Error("backend log ownership identity is unsafe");
return{script,logPath};
}
function openOwnedBackendLog(owned,logPath){
if(!Number.isInteger(constants.O_NOFOLLOW))throw new Error("backend log no-follow protection is unavailable");
let fd;
try{
fd=openSync(logPath,constants.O_WRONLY|constants.O_APPEND|constants.O_NOFOLLOW);
const entry=fstatSync(fd),pathEntry=lstatSync(logPath);
if(!entry.isFile()||(entry.mode&0o777)!==0o600||entry.nlink!==1||entry.dev!==owned.backendLog.dev||entry.ino!==owned.backendLog.ino||pathEntry.isSymbolicLink()||!pathEntry.isFile()||pathEntry.dev!==entry.dev||pathEntry.ino!==entry.ino)throw new Error("backend log identity is unsafe");
return fd;
}catch(error){if(fd!==undefined)closeSync(fd);throw error;}
}
async function ensureRuntimeDirectory(path){try{await mkdir(path,{mode:0o700});}catch(error){if(error.code!=="EEXIST")throw error;}const entry=await requireCanonicalDirectory(path,"owned runtime child");if((entry.mode&0o077)!==0)throw new Error("owned runtime child mode is unsafe");}
async function processStart(pid){return (await run("ps",["-p",String(pid),"-o","lstart="])).stdout.trim();}
async function processArgs(pid){return (await run("ps",["-ww","-p",String(pid),"-o","command="])).stdout.trim();}
async function processCwd(pid){try{return await realpath(`/proc/${pid}/cwd`);}catch{try{const out=(await run("lsof",["-a","-p",String(pid),"-d","cwd","-Fn"])).stdout.split("\n").find(x=>x.startsWith("n"));return out?await realpath(out.slice(1)):"";}catch{return"";}}}
async function processExecutable(pid){try{return await realpath(`/proc/${pid}/exe`);}catch{try{const paths=(await run("lsof",["-a","-p",String(pid),"-d","txt","-Fn"])).stdout.split("\n").filter(x=>x.startsWith("n")).map(x=>x.slice(1));for(const path of paths){try{const canonical=await realpath(path);if(canonical===realpathSync(process.execPath))return canonical;}catch{}}return"";}catch{return"";}}}
function alive(pid){try{process.kill(pid,0);return true;}catch{return false;}}
async function readPid(root){const path=join(root,"backend.pid"),entry=await lstat(path);if(!entry.isFile()||entry.isSymbolicLink()||(entry.mode&0o777)!==0o600)throw new Error("backend PID record is unsafe");const bytes=await readFile(path,"utf8");let value;try{value=JSON.parse(bytes);}catch{throw new Error("backend PID record is malformed");}return{path,bytes,value,dev:entry.dev,ino:entry.ino};}
async function validateProcess(repo,root,owned,pidRecord){
const script=join(repo,"backend/dist/server.js"),entrypoint=owned.entrypoint;
if(pidRecord.schemaVersion!==1||pidRecord.kind!=="p1-manual-backend"||pidRecord.status!=="RUNNING"||!Number.isSafeInteger(pidRecord.pid)||pidRecord.pid<2||!HEX64.test(pidRecord.reservationNonce??"")||pidRecord.nonce!==owned.nonce||pidRecord.root!==root||pidRecord.repositoryRoot!==repo||pidRecord.executable!==process.execPath||pidRecord.preload!==PRELOAD||pidRecord.script!==script||JSON.stringify(pidRecord.entrypoint)!==JSON.stringify(entrypoint)||!pidRecord.startIdentity||pidRecord.control?.host!==HOST||pidRecord.control?.port!==CONTROL_PORT)throw new Error("backend process identity mismatch; refusing cooperative control");
await requireEntrypointPathIdentity(entrypoint);const manifestRecord=await readBoundDistManifest(repo,owned);try{await validateDistFiles(repo,manifestRecord.files);}finally{await manifestRecord.handle.close();}if(!alive(pidRecord.pid))throw new Error("backend PID is stale; operator inspection required");
const[start,args,cwd,executable]=await Promise.all([processStart(pidRecord.pid),processArgs(pidRecord.pid),processCwd(pidRecord.pid),processExecutable(pidRecord.pid)]);
const expectedArgs=[pidRecord.executable,"--import",pidRecord.preload,pidRecord.script,`--p1-manual-nonce=${owned.nonce}`,`--p1-root=${root}`,`--p1-control-nonce=${pidRecord.reservationNonce}`,`--p1-entry-sha256=${entrypoint.sha256}`,`--p1-entry-dev=${entrypoint.dev}`,`--p1-entry-ino=${entrypoint.ino}`].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))]);}
async function healthStatus(){return await new Promise((resolvePromise,reject)=>{const request=http.get({host:HOST,port:PORT,path:"/health",timeout:500},response=>{const status=response.statusCode;response.resume();response.once("end",()=>resolvePromise(status));});request.once("timeout",()=>request.destroy(new Error("backend health readiness timeout")));request.once("error",reject);});}
function exactControlIdentity(answer,child,owned,root,reservationNonce){return answer?.pid===child.pid&&answer?.nonce===owned.nonce&&answer?.controlNonce===reservationNonce&&answer?.root===root&&answer?.control?.host===HOST&&answer?.control?.port===CONTROL_PORT;}
function exactOwnedListener(answer,generation){return answer?.listener?.listening===true&&answer.listener.host===HOST&&answer.listener.port===PORT&&Number.isSafeInteger(answer.listener.generation)&&answer.listener.generation>0&&(generation===undefined||answer.listener.generation===generation);}
export async function serveManual({repositoryRoot=defaultRepositoryRoot,beforeSpawn}={}){
const repo=realpathSync(repositoryRoot),lifecycle=await acquireLifecycle(repo,"serve");let pidRecord,child,controlObserved=false,logFd,entryBinding,manifestBinding;
try{
const owned=await readManualOwnership({repositoryRoot:repo}),root=owned.root;await bindLifecycleRoot(lifecycle,root);
if(owned.listener.host!==HOST||owned.listener.port!==PORT)throw new Error("non-loopback or unexpected bind refused");
const{script,logPath}=await validateServeFilesystem(repo,root,owned);await requireLifecycleContext(lifecycle,{root:true});
logFd=openOwnedBackendLog(owned,logPath);entryBinding=await readBoundEntrypoint(repo);if(JSON.stringify(entryBinding.identity)!==JSON.stringify(owned.entrypoint))throw new Error("production entrypoint identity changed");manifestBinding=await readBoundDistManifest(repo,owned);
const reservationNonce=randomBytes(32).toString("hex");pidRecord=await exclusiveRecord(join(root,"backend.pid"),{schemaVersion:1,kind:"p1-manual-backend-reservation",status:"RESERVED",reservationNonce,nonce:owned.nonce,root,repositoryRoot:repo},"backend PID record");
await Promise.all([portAvailable(PORT),portAvailable(CONTROL_PORT,`${HOST}:${CONTROL_PORT} control port`)]);await requireLifecycleContext(lifecycle,{root:true});
await ensureRuntimeDirectory(join(root,"installation/runtime/home"));await ensureRuntimeDirectory(join(root,"installation/runtime/tmp"));await ensureRuntimeDirectory(join(root,"installation/runtime/tht-home"));await requireLifecycleContext(lifecycle,{root:true});
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")};
if(beforeSpawn)await beforeSpawn({script,entrypoint:{...owned.entrypoint}});await requireLifecycleContext(lifecycle,{root:true});
const entryArgs=[`--p1-entry-sha256=${owned.entrypoint.sha256}`,`--p1-entry-dev=${owned.entrypoint.dev}`,`--p1-entry-ino=${owned.entrypoint.ino}`];
child=spawn(process.execPath,["--import",PRELOAD,script,`--p1-manual-nonce=${owned.nonce}`,`--p1-root=${root}`,`--p1-control-nonce=${reservationNonce}`,...entryArgs],{cwd:repo,env,detached:true,stdio:["ignore",logFd,logFd,entryBinding.handle.fd,manifestBinding.handle.fd]});
await entryBinding.handle.close();entryBinding=undefined;await manifestBinding.handle.close();manifestBinding=undefined;closeSync(logFd);logFd=undefined;
let start="";for(let n=0;n<80;n++){if(child.exitCode!==null)break;try{start=await processStart(child.pid);if(start)break;}catch{}await new Promise(r=>setTimeout(r,25));}
if(!start)throw new Error("backend failed before process identity could be recorded");await requireLifecycleContext(lifecycle,{root:true});
pidRecord=await replaceExactRecord(pidRecord,{schemaVersion:1,kind:"p1-manual-backend-reservation",status:"STARTING",pid:child.pid,reservationNonce,nonce:owned.nonce,root,repositoryRoot:repo,executable:process.execPath,preload:PRELOAD,script,entrypoint:owned.entrypoint,startIdentity:start,control:{host:HOST,port:CONTROL_PORT}});
const deadline=Date.now()+7000;let readyAnswer,listenerGeneration;
while(Date.now()<deadline&&child.exitCode===null){
let status;try{status=await controlRequest({host:HOST,port:CONTROL_PORT},{action:"status",nonce:reservationNonce});}catch{await new Promise(r=>setTimeout(r,50));continue;}
if(!exactControlIdentity(status,child,owned,root,reservationNonce)||status.status!=="STARTING")throw new Error("backend control status identity mismatch");controlObserved=true;
if(!exactOwnedListener(status)){await new Promise(r=>setTimeout(r,50));continue;}listenerGeneration=status.listener.generation;
await requireLifecycleContext(lifecycle,{root:true});await requireEntrypointPathIdentity(owned.entrypoint);
let httpCode;try{httpCode=await healthStatus();}catch{await new Promise(r=>setTimeout(r,50));continue;}if(!Number.isSafeInteger(httpCode)||httpCode<200||httpCode>=300)throw new Error(`backend health readiness returned HTTP ${httpCode}`);
readyAnswer=await controlRequest({host:HOST,port:CONTROL_PORT},{action:"ready",nonce:reservationNonce});if(!exactControlIdentity(readyAnswer,child,owned,root,reservationNonce)||readyAnswer.status!=="READY"||!exactOwnedListener(readyAnswer,listenerGeneration))throw new Error("backend READY listener acknowledgement identity mismatch");
const finalHealth=await healthStatus();if(!Number.isSafeInteger(finalHealth)||finalHealth<200||finalHealth>=300)throw new Error("backend final health readiness failed");
const finalStatus=await controlRequest({host:HOST,port:CONTROL_PORT},{action:"status",nonce:reservationNonce});if(!exactControlIdentity(finalStatus,child,owned,root,reservationNonce)||finalStatus.status!=="READY"||!exactOwnedListener(finalStatus,listenerGeneration))throw new Error("backend final listener identity mismatch");readyAnswer=finalStatus;break;
}
if(!readyAnswer)throw new Error("backend readiness failed; inspect owned backend log and starting PID record");
const runningValue={schemaVersion:1,kind:"p1-manual-backend",status:"RUNNING",pid:child.pid,reservationNonce,nonce:owned.nonce,root,repositoryRoot:repo,executable:process.execPath,preload:PRELOAD,script,entrypoint:owned.entrypoint,startIdentity:start,control:{host:HOST,port:CONTROL_PORT},listener:{host:HOST,port:PORT,generation:listenerGeneration}};
await requireLifecycleContext(lifecycle,{root:true});await validateProcess(repo,root,owned,runningValue);if(child.exitCode!==null||!alive(child.pid))throw new Error("backend exited before RUNNING publication");pidRecord=await replaceExactRecord(pidRecord,runningValue);await requireLifecycleContext(lifecycle,{root:true});
const publishedStatus=await controlRequest(runningValue.control,{action:"status",nonce:reservationNonce});if(!exactControlIdentity(publishedStatus,child,owned,root,reservationNonce)||publishedStatus.status!=="READY"||!exactOwnedListener(publishedStatus,listenerGeneration)){await removeExactRecord(pidRecord);throw new Error("backend listener changed during RUNNING publication");}
child.unref();return child.pid;
}catch(error){
if(entryBinding)await entryBinding.handle.close().catch(()=>{});if(manifestBinding)await manifestBinding.handle.close().catch(()=>{});if(logFd!==undefined){closeSync(logFd);logFd=undefined;}
if(child&&controlObserved){try{const value=pidRecord?JSON.parse(pidRecord.bytes):undefined;await controlRequest({host:HOST,port:CONTROL_PORT},{action:"stop",nonce:value?.reservationNonce});}catch{}await waitForChildExit(child,3000);}else if(child)await waitForChildExit(child,8500);
if(pidRecord&&(!child||child.exitCode!==null||child.signalCode!==null||!alive(child.pid)))await removeExactRecord(pidRecord).catch(()=>{});throw error;
}finally{if(entryBinding)await entryBinding.handle.close().catch(()=>{});if(manifestBinding)await manifestBinding.handle.close().catch(()=>{});if(logFd!==undefined)closeSync(logFd);await removeExactRecord(lifecycle);}
}
export async function stopManual({repositoryRoot=defaultRepositoryRoot}={}){const repo=realpathSync(repositoryRoot),lifecycle=await acquireLifecycle(repo,"stop");try{const owned=await readManualOwnership({repositoryRoot:repo}),root=owned.root;if(owned.stage!=="READY")throw new Error("owned backend was never prepared");await bindLifecycleRoot(lifecycle,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 requireLifecycleContext(lifecycle,{root:true});await validateProcess(repo,root,owned,record.value);const answer=await controlRequest(record.value.control,{action:"stop",nonce:record.value.reservationNonce});if(answer.status!=="STOPPING"||answer.pid!==record.value.pid||answer.nonce!==owned.nonce||answer.controlNonce!==record.value.reservationNonce||answer.root!==root||answer.control?.host!==HOST||answer.control?.port!==CONTROL_PORT)throw new Error("backend cooperative stop acknowledgement mismatch; PID record retained");for(let n=0;n<100;n++){if(!alive(record.value.pid)){await requireLifecycleContext(lifecycle,{root:true});await removeExactRecord(record);await requireLifecycleContext(lifecycle,{root:true});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);}}
const ANCHORED_REMOVE_SOURCE=String.raw`import os,stat,sys
parent,parent_dev,parent_ino,root_dev,root_ino,tomb=sys.argv[1:]
pfd=rfd=None
def die(): raise RuntimeError("anchored cleanup refused")
def clear(fd):
names=os.listdir(fd)
if len(names)>200000: die()
for name in names:
if name in (".",".."): die()
item=os.stat(name,dir_fd=fd,follow_symlinks=False)
if stat.S_ISDIR(item.st_mode):
child=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=fd)
try: clear(child)
finally: os.close(child)
os.rmdir(name,dir_fd=fd)
elif stat.S_ISREG(item.st_mode) or stat.S_ISLNK(item.st_mode): os.unlink(name,dir_fd=fd)
else: die()
try:
pfd=os.open(parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW)
ps=os.fstat(pfd)
if (ps.st_dev,ps.st_ino)!=(int(parent_dev),int(parent_ino)): die()
rfd=os.open("p1",os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=pfd)
rs=os.fstat(rfd)
if (rs.st_dev,rs.st_ino)!=(int(root_dev),int(root_ino)): die()
try: os.stat(tomb,dir_fd=pfd,follow_symlinks=False); die()
except FileNotFoundError: pass
os.rename("p1",tomb,src_dir_fd=pfd,dst_dir_fd=pfd);os.fsync(pfd)
clear(rfd);os.close(rfd);rfd=None;os.rmdir(tomb,dir_fd=pfd);os.fsync(pfd)
except Exception:
print("anchored cleanup refused (details redacted)",file=sys.stderr);raise SystemExit(1)
finally:
if rfd is not None: os.close(rfd)
if pfd is not None: os.close(pfd)
`;
async function anchoredRemoveOwnedRoot(lifecycle,owned){const tomb=`.deleting-p1-${owned.nonce.slice(0,16)}`;try{await run("python3",["-c",ANCHORED_REMOVE_SOURCE,lifecycle.parentPath,String(lifecycle.parentEntry.dev),String(lifecycle.parentEntry.ino),String(lifecycle.rootEntry.dev),String(lifecycle.rootEntry.ino),tomb]);}catch{throw new Error("anchored cleanup refused; owned identities changed");}}
export async function cleanupManual({repositoryRoot=defaultRepositoryRoot}={}){const repo=realpathSync(repositoryRoot),lifecycle=await acquireLifecycle(repo,"cleanup");try{const owned=await readManualOwnership({repositoryRoot:repo}),root=owned.root;await bindLifecycleRoot(lifecycle,root);try{const record=await readPid(root);if(record.value.status==="RUNNING"&&alive(record.value.pid)){await validateProcess(repo,root,owned,record.value);throw new Error("owned backend is live; run stop first");}throw new Error("stale or starting backend PID record requires operator inspection and stop validation");}catch(error){if(error.code!=="ENOENT")throw error;}if(root!==fixedManualRoot(repo)||!below(join(repo,".artifacts"),root))throw new Error("cleanup root identity mismatch");await requireLifecycleContext(lifecycle,{root:true});await anchoredRemoveOwnedRoot(lifecycle,owned);await requireLifecycleContext(lifecycle);}finally{await removeExactRecord(lifecycle);}}
async function main(){const[action,...rest]=process.argv.slice(2);if(rest.length||!["prepare","serve","stop","cleanup"].includes(action??""))throw new Error("usage: p1-manual-acceptance.mjs prepare|serve|stop|cleanup");if(action==="prepare")await prepareManual();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;});