v1.87.0.0 feat: add verified CSO audits and replayable repair bundles (#2852)

* feat(cso): add verified audits and replayable repair bundles

* fix(cso): harden qualification and setup boundaries

* fix(cso): assemble security canaries at runtime

* fix(cso): bound release proof and maintenance work

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): require complete evaluation reports

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): replay expired snapshots from supplied source

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* test(cso): synchronize DNS cancellation assertion

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* chore(ship): exempt repository owner from liveness proof

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* test(cso): make recheck retention overlap deterministic

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* chore: bump version and changelog (v1.85.0.0)

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): pass native release gates

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* chore: move release to v1.86.0.0

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): resolve rechecks by finding

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* chore: move release to v1.87.0.0

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): pass macOS and Windows release gates

Normalize BSD wc output, compare Windows paths by filesystem identity, preserve portable snapshot race coverage, and narrow POSIX-only Windows fixtures.

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): harden native verification gates

* fix(cso): refine Windows native diagnostics

* test(cso): isolate Windows Git startup failure

* test(cso): stabilize Windows native diagnostics

* fix(cso): support hardened Git on Windows

* fix(cso): close final verification gaps

* test(cso): bound cold Docker fixture setup

* fix(cso): restore cross-platform free-suite gates

---------

Co-authored-by: OpenAI Codex <noreply@openai.com>
This commit is contained in:
Garry Tan
2026-09-14 15:14:58 -07:00
committed by GitHub
co-authored by OpenAI Codex
parent 9f81911136
commit 4a3c6a8a3c
160 changed files with 24697 additions and 2288 deletions
+165
View File
@@ -0,0 +1,165 @@
import * as fs from 'node:fs';
import { join } from 'node:path';
import { tmpdir, userInfo } from 'node:os';
import { randomBytes } from 'node:crypto';
import { CsoError, sha256 } from './contracts';
import { discardAtomicNoReplaceTemp, recoverAtomicNoReplaceJson, secureDirectory } from './state';
import { atomicWriteSync } from '../fs-atomic';
export const GROUP_LIMITS = { cpu: 2, memoryMiB: 4096, pids: 256, writableMiB: 2048, outputBytes: 1024 * 1024 } as const;
export const ROLE_LIMITS = {
anchor: {cpu:.05,memoryMiB:64,pids:8,writableMiB:16},
app: {cpu:.85,memoryMiB:2304,pids:96,writableMiB:1264},
verifier: {cpu:.55,memoryMiB:512,pids:32,writableMiB:256},
tests: {cpu:.55,memoryMiB:1280,pids:64,writableMiB:1024},
postgres: {cpu:.25,memoryMiB:1024,pids:96,writableMiB:512},
browser: {cpu:.30,memoryMiB:512,pids:16,writableMiB:256},
} as const;
export type Role = keyof typeof ROLE_LIMITS;
export interface Lease { endpoint: string; slot: number; path: string; runId: string; ownerPid: number; expiresAt: number; token:string; supervised:boolean }
function alive(pid: number): boolean { try { process.kill(pid,0); return true; } catch { return false; } }
function processIdentity(pid:number):string|undefined{if(process.platform!=='linux')return;try{const raw=fs.readFileSync(`/proc/${pid}/stat`,'utf8'),tail=raw.slice(raw.lastIndexOf(')')+2).trim().split(/\s+/);return /^\d+$/.test(tail[19]??'')?`linux:${tail[19]}`:undefined;}catch{return;}}
function sameDirectory(left:fs.Stats,right:fs.Stats):boolean{return left.dev===right.dev&&left.ino===right.ino&&left.uid===right.uid&&left.mode===right.mode;}
function sameFile(left:fs.Stats,right:fs.Stats):boolean{return left.dev===right.dev&&left.ino===right.ino&&left.uid===right.uid&&left.mode===right.mode&&left.nlink===right.nlink;}
function privateDirectory(path:string,label:string):fs.Stats{const stat=fs.lstatSync(path);if(!stat.isDirectory()||stat.isSymbolicLink()||(process.getuid&&stat.uid!==process.getuid())||(stat.mode&0o077)!==0)throw new CsoError('UNSAFE_PATH',`${label} is not a private owned directory`);return stat;}
function privateFile(path:string,label:string):fs.Stats{const stat=fs.lstatSync(path);if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1||(process.getuid&&stat.uid!==process.getuid())||(stat.mode&0o077)!==0||stat.size>1024*1024)throw new CsoError('UNSAFE_PATH',`${label} is not a private regular file`);return stat;}
type Claim={path:string;token:string;identity:fs.Stats;pid:number;processIdentity:string|null};
type ClaimOwner={pid:number;processIdentity:string|null;token:string;createdAt:number};
function validateClaimOwner(value:unknown,expectedToken?:string,publisherPid?:number):ClaimOwner{
if(!value||typeof value!=='object'||Array.isArray(value))throw new CsoError('INCOMPATIBLE_INPUT','Reproduction recovery owner is invalid');
const owner=value as Record<string,unknown>;
if(Object.keys(owner).sort().join(',')!=='createdAt,pid,processIdentity,token'||!Number.isSafeInteger(owner.pid)||Number(owner.pid)<=1||
typeof owner.token!=='string'||!/^[a-f0-9]{32}$/.test(owner.token)||(expectedToken!==undefined&&owner.token!==expectedToken)||
!Number.isFinite(owner.createdAt)||Number(owner.createdAt)<0||!(owner.processIdentity===null||(typeof owner.processIdentity==='string'&&/^linux:\d+$/.test(owner.processIdentity)))||
(publisherPid!==undefined&&Number(owner.pid)!==publisherPid))throw new CsoError('INCOMPATIBLE_INPUT','Reproduction recovery owner is invalid');
return{pid:Number(owner.pid),processIdentity:owner.processIdentity as string|null,token:owner.token,createdAt:Number(owner.createdAt)};
}
function inspectClaim(path:string,expectedToken?:string):Claim{
const before=privateFile(path,'Reproduction recovery claim');if(before.size<=0||before.size>4096)throw new CsoError('UNSAFE_PATH','Reproduction recovery claim has an invalid size');
let owner:any;try{owner=JSON.parse(fs.readFileSync(path,'utf8'));}catch{throw new CsoError('INCOMPATIBLE_INPUT','Reproduction recovery owner is invalid');}
const after=privateFile(path,'Reproduction recovery claim');if(!sameFile(before,after))throw new CsoError('INCOMPATIBLE_INPUT','Reproduction recovery owner is invalid');
owner=validateClaimOwner(owner,expectedToken);
return{path,token:owner.token,identity:after,pid:owner.pid,processIdentity:owner.processIdentity};
}
function releaseClaim(claim:Claim):void{
const current=inspectClaim(claim.path,claim.token);if(!sameFile(current.identity,claim.identity))throw new CsoError('PERSISTENCE_FAILED','Reproduction recovery ownership changed');
const final=privateFile(claim.path,'Reproduction recovery claim');if(!sameFile(final,claim.identity))throw new CsoError('PERSISTENCE_FAILED','Reproduction recovery ownership changed');
fs.unlinkSync(claim.path);
}
function acquireClaim(parent:string,expected:fs.Stats):Claim{
const path=join(parent,'.recovery'),assertParent=()=>{const current=privateDirectory(parent,'Reproduction lease slot');if(!sameDirectory(expected,current))throw new CsoError('INSUFFICIENT_CAPACITY','Reproduction lease changed during recovery');};
const recoverPublications=()=>{
const pattern=/^\.recovery\.tmp\.(\d{1,10})\.[a-f0-9]{8}$/;
for(const name of fs.readdirSync(parent)){
const match=name.match(pattern);if(!match)continue;
const publisherPid=Number(match[1]),temporary=join(parent,name),options={label:'Reproduction recovery claim',maxBytes:4096,
validate:(value:unknown,pid:number)=>{validateClaimOwner(value,undefined,pid);}};
assertParent();if(fs.existsSync(path))recoverAtomicNoReplaceJson(path,options);if(fs.existsSync(temporary))discardAtomicNoReplaceTemp(temporary,publisherPid,options);assertParent();
}
};
for(let attempt=0;attempt<64;attempt++){
assertParent();recoverPublications();const token=randomBytes(16).toString('hex');
try{
atomicWriteSync(path,JSON.stringify({pid:process.pid,processIdentity:processIdentity(process.pid)??null,token,createdAt:Date.now()})+'\n',{mode:0o600,noReplace:true});
const claim=inspectClaim(path,token);try{assertParent();}catch(error){try{releaseClaim(claim);}catch{}throw error;}return claim;
}catch(error:any){
if(error instanceof CsoError)throw error;
if(error?.code!=='EEXIST')throw new CsoError('PERSISTENCE_FAILED','Reproduction recovery claim could not be created');
}
assertParent();const observed=inspectClaim(path),isAlive=alive(observed.pid),identity=isAlive?processIdentity(observed.pid):undefined;
if(isAlive&&!(typeof observed.processIdentity==='string'&&identity!==undefined&&identity!==observed.processIdentity))throw new CsoError('INSUFFICIENT_CAPACITY','Another helper is recovering the reproduction lease');
try{releaseClaim(observed);}catch(error){if(error instanceof CsoError&&error.code==='PERSISTENCE_FAILED')continue;throw error;}
}
throw new CsoError('INSUFFICIENT_CAPACITY','Reproduction recovery claim changed repeatedly');
}
/** One host-user pool shared by every workspace/state root on this machine. */
export function machinePoolRoot():string{
const uid=process.getuid?.()??userInfo().uid;
return secureDirectory(join(fs.realpathSync(tmpdir()),`gstack-cso-pool-${uid}`));
}
function reclaimSlot(path:string,pool:string,slot:number,observed:fs.Stats,expectedToken?:string):boolean{
let claim:Claim;try{claim=acquireClaim(path,observed);}catch(error){if(error instanceof CsoError&&error.code==='INSUFFICIENT_CAPACITY')return false;throw error;}
try{const current=privateDirectory(path,'Reproduction lease slot');if(!sameDirectory(observed,current)){releaseClaim(claim);return false;}if(expectedToken){privateFile(join(path,'lease.json'),'Reproduction lease');const lease=JSON.parse(fs.readFileSync(join(path,'lease.json'),'utf8'));if(lease.token!==expectedToken){releaseClaim(claim);return false;}}
const tomb=join(pool,`.slot-${slot}.stale-${process.pid}-${randomBytes(8).toString('hex')}`);fs.renameSync(path,tomb);const moved=privateDirectory(tomb,'Reproduction lease tomb');if(!sameDirectory(observed,moved))throw new CsoError('SNAPSHOT_RACE','Reproduction lease changed while quarantined');fs.mkdirSync(path,{mode:0o700});releaseClaim({...claim,path:join(tomb,'.recovery')});for(const name of fs.readdirSync(tomb)){if(!['lease.json','lease.token'].includes(name)&&!/^lease\.json\.tmp\.\d+\.[a-f0-9]{8}$/.test(name)&&!/^\.recovery\.tmp\.\d+\.[a-f0-9]{8}$/.test(name))throw new CsoError('UNSAFE_PATH','Stale reproduction lease contains an unexpected object');privateFile(join(tomb,name),'Stale reproduction lease file');fs.unlinkSync(join(tomb,name));}fs.rmdirSync(tomb);return true;
}catch(error){if(error instanceof CsoError)throw error;return false;}
}
function slotControl(pool:string,slot:number):{path:string;stat:fs.Stats}{
const path=join(pool,`.slot-${slot}.control`);
try{fs.mkdirSync(path,{mode:0o700});}catch(error:any){if(error?.code!=='EEXIST')throw new CsoError('PERSISTENCE_FAILED','Reproduction slot control directory could not be created');}
const stat=privateDirectory(path,'Reproduction slot control directory');for(const name of fs.readdirSync(path))if(name!=='.recovery'&&!/^\.recovery\.tmp\.\d+\.[a-f0-9]{8}$/.test(name))throw new CsoError('UNSAFE_PATH','Reproduction slot control directory contains an unexpected object');return{path,stat};
}
function writeLease(lease:Lease):void{
const keys=Object.keys(lease).sort().join(','),expected='endpoint,expiresAt,ownerPid,path,runId,slot,supervised,token';
if(keys!==expected||!/^unix:\/\/[/.A-Za-z0-9_-]+$/.test(lease.endpoint)||![0,1].includes(lease.slot)||
!/^[A-Za-z0-9_.-]{1,100}$/.test(lease.runId)||lease.ownerPid!==process.pid||!Number.isSafeInteger(lease.expiresAt)||
!/^[a-f0-9]{32}$/.test(lease.token)||typeof lease.supervised!=='boolean')
throw new CsoError('PERSISTENCE_FAILED','Reproduction lease metadata is invalid');
const expectedPath=join(machinePoolRoot(),sha256(lease.endpoint).slice(0,24),`slot-${lease.slot}`);
if(lease.path!==expectedPath)throw new CsoError('PERSISTENCE_FAILED','Reproduction lease path is invalid');
// This exact helper-owned schema contains only control metadata. In
// particular, its random capability may resemble a wallet address and must
// remain byte-identical to lease.token; untrusted reports still use writeJson.
atomicWriteSync(join(lease.path,'lease.json'),JSON.stringify(lease)+'\n',{mode:0o600});
}
export function admit(endpoint: string, runId: string, deadline: number): Lease {
if (!/^unix:\/\/[/.A-Za-z0-9_-]+$/.test(endpoint)) throw new CsoError('ISOLATION_FAILED','Only a pinned local Unix Docker endpoint is admitted on this host');
const pool = secureDirectory(join(machinePoolRoot(),sha256(endpoint).slice(0,24)));
for (let slot=0;slot<2;slot++) {
const path=join(pool,`slot-${slot}`),control=slotControl(pool,slot);let mutation:Claim;
try{mutation=acquireClaim(control.path,control.stat);}catch(error){if(error instanceof CsoError&&error.code==='INSUFFICIENT_CAPACITY')continue;throw error;}
try{
try {
fs.mkdirSync(path,{mode:0o700});
} catch(error:any) {
if(error?.code!=='EEXIST')throw new CsoError('PERSISTENCE_FAILED','Reproduction lease slot could not be created');
try {
const observed=privateDirectory(path,'Reproduction lease slot');
const old = JSON.parse(fs.readFileSync(join(path,'lease.json'),'utf8'));
// A supervised lease is removed only after its watchdog or owner has
// confirmed exact-resource cleanup. This preserves the two-group cap
// through supervisor death and daemon outages.
if (old.supervised === true || (typeof old.ownerPid === 'number' && alive(old.ownerPid))) continue;
// Unsupervised stale slots cannot have created containers: supervision
// is acknowledged before the anchor create call.
if(!reclaimSlot(path,pool,slot,observed,typeof old.token==='string'?old.token:undefined))continue;
} catch(recoveryError) {
if(recoveryError instanceof CsoError)throw recoveryError;
// No live initializer can publish into this path while this stable
// slot-control claim is held. Recover a crashed partial publication
// only after the compatibility grace period.
let stat:fs.Stats;try{stat=privateDirectory(path,'Reproduction lease slot');}catch(statError){if(statError instanceof CsoError)throw statError;continue;}
if(Date.now()-stat.mtimeMs<=5000)continue;
if(!reclaimSlot(path,pool,slot,stat))continue;
}
}
// Both authenticated records become visible as one logical publication
// when the stable slot-control claim is released.
const lease:Lease={endpoint,slot,path,runId,ownerPid:process.pid,expiresAt:deadline,token:randomBytes(16).toString('hex'),supervised:false};writeLease(lease);fs.writeFileSync(join(path,'lease.token'),lease.token+'\n',{mode:0o600,flag:'wx'});return lease;
}finally{releaseClaim(mutation);}
}
throw new CsoError('INSUFFICIENT_CAPACITY','Two reproduction groups are already admitted for this Docker endpoint');
}
export function markSupervised(lease:Lease):void{
const current=JSON.parse(fs.readFileSync(join(lease.path,'lease.json'),'utf8'));
if(current.token!==lease.token||current.ownerPid!==lease.ownerPid)throw new CsoError('INSUFFICIENT_CAPACITY','Reproduction lease changed before watchdog supervision');
lease.supervised=true;writeLease(lease);
}
export function release(lease: Lease): void {
let observed:fs.Stats;try{observed=privateDirectory(lease.path,'Reproduction lease slot');}catch(error:any){if(error?.code==='ENOENT')throw new CsoError('PERSISTENCE_FAILED','Exact reproduction lease was already missing');throw error;}
const claim=acquireClaim(lease.path,observed);
try{
const currentStat=privateDirectory(lease.path,'Reproduction lease slot');if(!sameDirectory(observed,currentStat))throw new CsoError('PERSISTENCE_FAILED','Reproduction lease changed before exact release');
const names=fs.readdirSync(lease.path).filter(name=>name!=='.recovery'&&!/^\.recovery\.tmp\.\d+\.[a-f0-9]{8}$/.test(name)).sort();if(names.join('\0')!=='lease.json\0lease.token')throw new CsoError('PERSISTENCE_FAILED','Reproduction lease contents changed before exact release');
privateFile(join(lease.path,'lease.json'),'Reproduction lease');privateFile(join(lease.path,'lease.token'),'Reproduction lease token');
const current=JSON.parse(fs.readFileSync(join(lease.path,'lease.json'),'utf8')),token=fs.readFileSync(join(lease.path,'lease.token'),'utf8').trim();
if(current.runId!==lease.runId||current.ownerPid!==lease.ownerPid||current.token!==lease.token||token!==lease.token)throw new CsoError('PERSISTENCE_FAILED','Reproduction lease ownership changed before exact release');
fs.unlinkSync(join(lease.path,'lease.token'));fs.unlinkSync(join(lease.path,'lease.json'));releaseClaim(claim);fs.rmdirSync(lease.path);
if(fs.existsSync(lease.path))throw new CsoError('PERSISTENCE_FAILED','Exact reproduction lease removal could not be proven');
}catch(error){try{if(fs.existsSync(claim.path))releaseClaim(claim);}catch{}if(error instanceof CsoError)throw error;throw new CsoError('PERSISTENCE_FAILED','Exact reproduction lease removal failed');}
}
export function total(roles: Role[]) {
const value = roles.reduce((a,r) => ({cpu:a.cpu+ROLE_LIMITS[r].cpu,memoryMiB:a.memoryMiB+ROLE_LIMITS[r].memoryMiB,pids:a.pids+ROLE_LIMITS[r].pids,writableMiB:a.writableMiB+ROLE_LIMITS[r].writableMiB}), {cpu:0,memoryMiB:0,pids:0,writableMiB:0});
if (value.cpu > GROUP_LIMITS.cpu || value.memoryMiB > GROUP_LIMITS.memoryMiB || value.pids > GROUP_LIMITS.pids || value.writableMiB > GROUP_LIMITS.writableMiB)
throw new CsoError('INSUFFICIENT_CAPACITY','Requested sidecars exceed the aggregate reproduction-group limit');
return value;
}
+16
View File
@@ -0,0 +1,16 @@
import * as fs from 'node:fs';
import { CsoError } from './contracts';
/** Read one caller-supplied control file without following or blocking on a raced special file. */
export function readBoundedStable(path:string,max:number,label:string):Buffer{
let named:fs.Stats,fd:number|undefined;try{named=fs.lstatSync(path);}catch{throw new CsoError('MISSING_INPUT',`${label} does not exist`);}
if(named.isSymbolicLink()||!named.isFile()||named.nlink!==1||named.size>max)throw new CsoError('MISSING_INPUT',`${label} must be one bounded regular file`);
try{
fd=fs.openSync(path,fs.constants.O_RDONLY|(fs.constants.O_NOFOLLOW??0)|(fs.constants.O_NONBLOCK??0));const opened=fs.fstatSync(fd);
if(!opened.isFile()||opened.nlink!==1||opened.dev!==named.dev||opened.ino!==named.ino||opened.mode!==named.mode||opened.size!==named.size)throw new CsoError('SNAPSHOT_RACE',`${label} changed before it could be read`);
const data=Buffer.alloc(max+1);let bytes=0,count=0;while(bytes<data.length&&(count=fs.readSync(fd,data,bytes,data.length-bytes,null))>0)bytes+=count;
const after=fs.fstatSync(fd),current=fs.lstatSync(path);if(bytes>max)throw new CsoError('MISSING_INPUT',`${label} exceeds the ${max}-byte limit`);
if(!current.isFile()||current.isSymbolicLink()||current.nlink!==1||current.dev!==opened.dev||current.ino!==opened.ino||current.mode!==opened.mode||after.size!==opened.size||after.mtimeMs!==opened.mtimeMs||after.ctimeMs!==opened.ctimeMs)throw new CsoError('SNAPSHOT_RACE',`${label} changed while it was read`);
return data.subarray(0,bytes);
}catch(error){if(error instanceof CsoError)throw error;const code=(error as NodeJS.ErrnoException).code;if(['ELOOP','ENOENT','ENOTDIR','ENXIO'].includes(code??''))throw new CsoError('SNAPSHOT_RACE',`${label} changed before it could be opened`);throw new CsoError('MISSING_INPUT',`${label} is missing or unreadable`);}finally{if(fd!==undefined)fs.closeSync(fd);}
}
+707
View File
@@ -0,0 +1,707 @@
import * as fs from 'node:fs';
import { createHash, randomBytes } from 'node:crypto';
import { join, resolve, sep } from 'node:path';
import { atomicWriteSync } from '../fs-atomic';
import { CsoError } from './contracts';
import { discardAtomicNoReplaceTemp, privateRoot, recoverAtomicNoReplaceJson, secureDirectory, withLock as withStateLock } from './state';
export const DEFAULT_PUBLIC_ARCHIVE_CACHE_BYTES = 10 * 1024 * 1024 * 1024;
const METADATA_VERSION = 1;
const METADATA_LIMIT = 4096;
const COPY_BUFFER_BYTES = 64 * 1024;
const MAX_CACHE_DIRECTORY_ENTRIES = 100_000;
const CACHE_LOCK_PROTOCOL = 'immutable-cache-lease-set-v3';
const SHA256 = /^[a-f0-9]{64}$/;
const RELATIVE_STAGE_PATH = /^(?!\/)(?!.*(?:^|\/)\.\.?(?:\/|$))(?!.*\\)[^\0-\x1f\x7f]+$/;
export interface PublicArchiveCacheOptions {
/** Defaults to the private CSO state namespace. */
root?: string;
/** Existing directory populated by the constrained acquisition step. */
stagingRoot: string;
/** Persistent archive-byte ceiling. Defaults to 10 GiB. */
maxBytes?: number;
/** Per-archive ceiling. Defaults to maxBytes. */
maxEntryBytes?: number;
/** Deterministic clock for tests. */
now?: () => number;
}
/** Optional cooperative bounds for synchronous cache work. */
export interface CacheOperationControl {
/** Absolute Unix timestamp in milliseconds. */
deadline?: number;
/** Checked between bounded filesystem operations. */
signal?: AbortSignal;
}
export type CacheOperationInput = CacheOperationControl | number | undefined;
type NormalizedCacheOperationControl = Readonly<{ deadline?: number; signal?: AbortSignal }>;
export interface PublicArchiveCacheEntry {
sha256: string;
path: string;
bytes: number;
createdAt: number;
lastAccessedAt: number;
}
export interface PublicArchiveCacheStats {
entries: number;
bytes: number;
maxBytes: number;
}
export interface MaterializedArchive {
sha256: string;
path: string;
bytes: number;
}
interface Metadata {
version: 1;
sha256: string;
bytes: number;
createdAt: number;
lastAccessedAt: number;
}
interface StableStat {
dev: number;
ino: number;
size: number;
mode: number;
nlink: number;
mtimeMs: number;
ctimeMs: number;
uid: number;
}
function fail(code: ConstructorParameters<typeof CsoError>[0], message: string): never {
throw new CsoError(code, message);
}
function operationControl(input?: CacheOperationInput): NormalizedCacheOperationControl {
const value = typeof input === 'number' ? { deadline: input } : input ?? {};
if (!value || typeof value !== 'object' || Array.isArray(value)) fail('INVALID_ARGUMENT', 'Cache operation control must be an object or absolute deadline');
if (value.deadline !== undefined && (!Number.isSafeInteger(value.deadline) || value.deadline <= 0))
fail('INVALID_ARGUMENT', 'Cache deadline must be an absolute millisecond timestamp');
if (value.signal !== undefined && typeof value.signal.aborted !== 'boolean')
fail('INVALID_ARGUMENT', 'Cache cancellation signal is invalid');
const control = Object.freeze({ ...(value.deadline === undefined ? {} : { deadline: value.deadline }),
...(value.signal === undefined ? {} : { signal: value.signal }) });
checkOperation(control);
return control;
}
function checkOperation(control: NormalizedCacheOperationControl): void {
if (control.signal?.aborted) fail('CANCELLED', 'Archive-cache operation was cancelled');
if (control.deadline !== undefined && Date.now() >= control.deadline)
fail('DEADLINE', 'Archive-cache operation reached its deadline');
}
function boundedDirectoryNames(path: string, label: string, control: NormalizedCacheOperationControl): string[] {
checkOperation(control);
const directory = fs.opendirSync(path), names: string[] = [];
try {
for (;;) {
checkOperation(control);
const entry = directory.readSync();
if (!entry) break;
if (names.length >= MAX_CACHE_DIRECTORY_ENTRIES) fail('INSUFFICIENT_CAPACITY', `${label} exceeds the cache entry limit`);
names.push(entry.name);
}
} finally { directory.closeSync(); }
checkOperation(control);
return names;
}
function assertEmptyDirectory(path: string, control: NormalizedCacheOperationControl): void {
checkOperation(control);
const directory = fs.opendirSync(path);
try { if (directory.readSync()) fail('UNSAFE_PATH', 'Archive materialization directory must be empty'); }
finally { directory.closeSync(); }
checkOperation(control);
}
function boundedPositiveInteger(value: number, name: string): number {
if (!Number.isSafeInteger(value) || value <= 0) fail('INVALID_ARGUMENT', `${name} must be a positive safe integer`);
return value;
}
function expectedDigest(value: string): string {
if (!SHA256.test(value)) fail('INVALID_ARGUMENT', 'Archive SHA-256 must be 64 lowercase hexadecimal characters');
return value;
}
function stagedRelativePath(value: string): string {
if (typeof value !== 'string' || value.length > 4096 || !RELATIVE_STAGE_PATH.test(value))
fail('UNSAFE_PATH', 'Staged archive path must be a contained relative path');
const parts = value.split('/');
if (parts.some(part => !part || part === '.' || part === '..')) fail('UNSAFE_PATH', 'Staged archive path must be a contained relative path');
return value;
}
function stableStat(stat: fs.Stats): StableStat {
return {
dev: stat.dev, ino: stat.ino, size: stat.size, mode: stat.mode, nlink: stat.nlink,
mtimeMs: stat.mtimeMs, ctimeMs: stat.ctimeMs, uid: stat.uid,
};
}
function sameStat(left: StableStat, right: StableStat): boolean {
return left.dev === right.dev && left.ino === right.ino && left.size === right.size &&
left.mode === right.mode && left.nlink === right.nlink && left.mtimeMs === right.mtimeMs &&
left.ctimeMs === right.ctimeMs && left.uid === right.uid;
}
function sameRenamedInode(left: StableStat, right: StableStat): boolean {
return left.dev === right.dev && left.ino === right.ino && left.size === right.size &&
left.mode === right.mode && left.nlink === right.nlink && left.mtimeMs === right.mtimeMs && left.uid === right.uid;
}
function assertOwnedRegular(stat: fs.Stats, label: string, maxBytes: number, immutable = false): void {
if (!stat.isFile() || stat.isSymbolicLink()) fail('UNSAFE_PATH', `${label} must be a regular file`);
if (stat.nlink !== 1) fail('UNSAFE_PATH', `${label} must not be hard-linked`);
if (process.getuid && stat.uid !== process.getuid()) fail('UNSAFE_PATH', `${label} must be owned by the current user`);
if (stat.size > maxBytes) fail('INSUFFICIENT_CAPACITY', `${label} exceeds its byte limit`);
if (immutable && (stat.mode & 0o222) !== 0) fail('INCOMPATIBLE_INPUT', `${label} is unexpectedly writable`);
}
function assertExistingDirectory(path: string, label: string): string {
const requested = resolve(path);
let requestedStat: fs.Stats;
try { requestedStat = fs.lstatSync(requested); }
catch { fail('MISSING_INPUT', `${label} does not exist`); }
if (requestedStat!.isSymbolicLink()) fail('UNSAFE_PATH', `${label} must not be a symlink`);
let canonical: string;
try { canonical = fs.realpathSync(requested); }
catch { fail('MISSING_INPUT', `${label} does not exist`); }
const stat = fs.lstatSync(canonical!);
if (!stat.isDirectory() || stat.isSymbolicLink()) fail('UNSAFE_PATH', `${label} must be a directory`);
if (process.getuid && stat.uid !== process.getuid()) fail('UNSAFE_PATH', `${label} must be owned by the current user`);
if ((stat.mode & 0o022) !== 0) fail('UNSAFE_PATH', `${label} must not be writable by another user`);
return canonical!;
}
function assertContainedAncestors(root: string, relativePath: string, control: NormalizedCacheOperationControl): string {
const parts = relativePath.split('/');
let cursor = root;
for (const part of parts.slice(0, -1)) {
checkOperation(control);
cursor = join(cursor, part);
let stat: fs.Stats;
try { stat = fs.lstatSync(cursor); }
catch { fail('MISSING_INPUT', `Staged archive directory is missing: ${part}`); }
if (!stat.isDirectory() || stat.isSymbolicLink()) fail('UNSAFE_PATH', 'Staged archive has a symlink or non-directory ancestor');
if (process.getuid && stat.uid !== process.getuid()) fail('UNSAFE_PATH', 'Staged archive ancestor has an unexpected owner');
if ((stat.mode & 0o022) !== 0) fail('UNSAFE_PATH', 'Staged archive ancestor is writable by another user');
}
const path = resolve(root, ...parts);
if (path !== root && !path.startsWith(`${root}${sep}`)) fail('UNSAFE_PATH', 'Staged archive escaped its staging directory');
return path;
}
function openNoFollow(path: string, flags: number, mode?: number): number {
const noFollow = (fs.constants as Record<string, number>).O_NOFOLLOW ?? 0;
const closeOnExec = (fs.constants as Record<string, number>).O_CLOEXEC ?? 0;
try { return fs.openSync(path, flags | noFollow | closeOnExec, mode); }
catch { fail('UNSAFE_PATH', 'Archive file could not be opened without following links'); }
}
function readMetadata(path: string, digest: string, control: NormalizedCacheOperationControl): Metadata {
checkOperation(control);
let stat: fs.Stats;
try { stat = fs.lstatSync(path); }
catch { fail('INCOMPATIBLE_INPUT', `Cache metadata is missing for ${digest}`); }
assertOwnedRegular(stat!, 'Cache metadata', METADATA_LIMIT);
if ((stat!.mode & 0o077) !== 0) fail('INCOMPATIBLE_INPUT', 'Cache metadata permissions are not private');
let value: unknown;
try { checkOperation(control); value = JSON.parse(fs.readFileSync(path, 'utf8')); checkOperation(control); }
catch (error) {
if (error instanceof CsoError) throw error;
fail('INCOMPATIBLE_INPUT', `Cache metadata is invalid for ${digest}`);
}
const record = value as Partial<Metadata>;
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).sort().join(',') !== 'bytes,createdAt,lastAccessedAt,sha256,version' ||
record.version !== METADATA_VERSION || record.sha256 !== digest || !Number.isSafeInteger(record.bytes) || Number(record.bytes) < 0 ||
!Number.isSafeInteger(record.createdAt) || Number(record.createdAt) < 0 || !Number.isSafeInteger(record.lastAccessedAt) ||
Number(record.lastAccessedAt) < Number(record.createdAt)) fail('INCOMPATIBLE_INPUT', `Cache metadata is invalid for ${digest}`);
return record as Metadata;
}
function writeMetadata(path: string, metadata: Metadata, noReplace = false): void {
try { atomicWriteSync(path, `${JSON.stringify(metadata)}\n`, { mode: 0o600, noReplace }); }
catch { fail('PERSISTENCE_FAILED', 'Cache metadata could not be written atomically'); }
}
function removeRegular(path: string, label: string): void {
const stat = fs.lstatSync(path);
assertOwnedRegular(stat, label, Number.MAX_SAFE_INTEGER);
try { fs.unlinkSync(path); }
catch { fail('PERSISTENCE_FAILED', `${label} could not be removed`); }
}
function existsNoFollow(path: string): boolean {
try { fs.lstatSync(path); return true; }
catch (error: any) {
if (error?.code === 'ENOENT') return false;
fail('INCOMPATIBLE_INPUT', 'Cache object could not be inspected safely');
}
}
/**
* A content-addressed cache for already-acquired public archives. It never
* performs downloads, runs package managers, or executes archive content.
*/
export class PublicArchiveCache {
readonly root: string;
readonly stagingRoot: string;
readonly maxBytes: number;
readonly maxEntryBytes: number;
private readonly entriesDir: string;
private readonly metadataDir: string;
private readonly incomingDir: string;
private readonly recoveryDir: string;
private readonly lockDir: string;
private readonly clock: () => number;
constructor(options: PublicArchiveCacheOptions) {
if (!options || typeof options !== 'object') fail('INVALID_ARGUMENT', 'Cache options are required');
this.maxBytes = boundedPositiveInteger(options.maxBytes ?? DEFAULT_PUBLIC_ARCHIVE_CACHE_BYTES, 'maxBytes');
this.maxEntryBytes = boundedPositiveInteger(options.maxEntryBytes ?? this.maxBytes, 'maxEntryBytes');
if (this.maxEntryBytes > this.maxBytes) fail('INVALID_ARGUMENT', 'maxEntryBytes cannot exceed maxBytes');
this.clock = options.now ?? Date.now;
const base = options.root ? resolve(options.root) : join(privateRoot(), 'public-cache');
this.root = secureDirectory(base);
this.entriesDir = secureDirectory(join(this.root, 'entries'));
this.metadataDir = secureDirectory(join(this.root, 'metadata'));
this.incomingDir = secureDirectory(join(this.root, 'incoming'));
this.recoveryDir = secureDirectory(join(this.root, 'recovery'));
this.lockDir = join(this.root, '.lock');
this.stagingRoot = assertExistingDirectory(options.stagingRoot, 'Archive staging directory');
if (this.root === this.stagingRoot || this.root.startsWith(`${this.stagingRoot}${sep}`) || this.stagingRoot.startsWith(`${this.root}${sep}`))
fail('UNSAFE_PATH', 'Archive staging and cache directories must be separate');
}
/** Promote a verified staging file. The staging file is never deleted. */
promote(stagedPath: string, sha256: string, operation?: CacheOperationInput): PublicArchiveCacheEntry {
const control = operationControl(operation);
const digest = expectedDigest(sha256), relativePath = stagedRelativePath(stagedPath);
const source = assertContainedAncestors(this.stagingRoot, relativePath, control);
return this.withLock(() => {
checkOperation(control);
this.cleanIncoming(control);
this.recoverInterruptedOperations(control);
let initial: fs.Stats;
try { initial = fs.lstatSync(source); }
catch { fail('MISSING_INPUT', 'Staged archive is missing'); }
assertOwnedRegular(initial!, 'Staged archive', this.maxEntryBytes);
if ((initial!.mode & 0o022) !== 0) fail('UNSAFE_PATH', 'Staged archive must not be writable by another user');
const target = this.entryPath(digest), metadataPath = this.metadataPath(digest);
const targetExists = existsNoFollow(target), metadataExists = existsNoFollow(metadataPath);
if (targetExists !== metadataExists) fail('INCOMPATIBLE_INPUT', `Cache entry is incomplete for ${digest}`);
if (targetExists) {
const staged = this.hashFile(source, this.maxEntryBytes, false, stableStat(initial!), control);
if (staged.digest !== digest) fail('INCOMPATIBLE_INPUT', 'Staged archive does not match its caller-provided SHA-256');
const metadata = this.verifiedEntry(digest, control);
return this.touch(metadata, control);
}
// Authenticate the complete staged object before it is allowed to
// displace any already-verified cache entry. Copying below hashes it a
// second time so a staging race still fails closed.
const authenticated = this.hashFile(source, this.maxEntryBytes, false, stableStat(initial!), control);
if (authenticated.digest !== digest) fail('INCOMPATIBLE_INPUT', 'Staged archive does not match its caller-provided SHA-256');
this.evictToFit(initial!.size, control);
const incoming = join(this.incomingDir, `.incoming-${process.pid}-${randomBytes(12).toString('hex')}`);
let promoted = false;
try {
const staged = this.copyAndHash(source, incoming, this.maxEntryBytes, stableStat(initial!), control);
if (staged.digest !== digest) fail('INCOMPATIBLE_INPUT', 'Staged archive does not match its caller-provided SHA-256');
if (staged.bytes !== initial!.size) fail('SNAPSHOT_RACE', 'Staged archive changed during promotion');
checkOperation(control);
fs.chmodSync(incoming, 0o400);
// A hard-link followed by unlink is an atomic no-replace publication on
// the cache filesystem. rename(2) would silently replace a raced target.
try { fs.linkSync(incoming, target); fs.unlinkSync(incoming); }
catch { fail('PERSISTENCE_FAILED', 'Verified archive could not be promoted atomically'); }
promoted = true;
const now = this.timestamp();
const metadata: Metadata = { version: 1, sha256: digest, bytes: staged.bytes, createdAt: now, lastAccessedAt: now };
try { checkOperation(control); writeMetadata(metadataPath, metadata, true); }
catch (error) {
try { this.discardObject(target, 'entry', digest); } catch {}
throw error;
}
return this.entry(metadata);
} finally {
if (!promoted && existsNoFollow(incoming)) {
const stat = fs.lstatSync(incoming);
if (stat.isFile() && !stat.isSymbolicLink()) fs.unlinkSync(incoming);
}
}
}, control);
}
/** Return a cache hit only after hashing every byte and validating metadata. */
get(sha256: string, operation?: CacheOperationInput): PublicArchiveCacheEntry | undefined {
const control = operationControl(operation);
const digest = expectedDigest(sha256);
return this.withLock(() => {
checkOperation(control);
this.cleanIncoming(control);
this.recoverInterruptedOperations(control);
const targetExists = existsNoFollow(this.entryPath(digest)), metadataExists = existsNoFollow(this.metadataPath(digest));
if (!targetExists && !metadataExists) return undefined;
if (targetExists !== metadataExists) fail('INCOMPATIBLE_INPUT', `Cache entry is incomplete for ${digest}`);
return this.touch(this.verifiedEntry(digest, control), control);
}, control);
}
/** Inspect capacity without treating entries as execution-ready cache hits. */
stats(operation?: CacheOperationInput): PublicArchiveCacheStats {
const control = operationControl(operation);
return this.withLock(() => {
checkOperation(control);
this.cleanIncoming(control);
this.recoverInterruptedOperations(control);
const entries = this.inventory(control);
let bytes = 0;
for (const entry of entries) { checkOperation(control); bytes += entry.bytes; }
return { entries: entries.length, bytes, maxBytes: this.maxBytes };
}, control);
}
/**
* Copy a complete digest set into one run-owned directory while holding the
* cache lock. Callers mount these immutable copies, never eviction-prone
* cache paths. Every source and every copy is fully hashed in the same
* critical section.
*/
materialize(digests: string[], destinationRoot: string, operation?: CacheOperationInput): MaterializedArchive[] {
const control = operationControl(operation);
if (!Array.isArray(digests) || !digests.length)
fail('INVALID_ARGUMENT', 'Archive materialization requires at least one SHA-256 digest');
if (digests.length > MAX_CACHE_DIRECTORY_ENTRIES)
fail('INSUFFICIENT_CAPACITY', 'Archive materialization exceeds the cache entry limit');
const selectedSet=new Set<string>();for(const digest of digests){checkOperation(control);if(typeof digest!=='string')fail('INVALID_ARGUMENT', 'Archive materialization requires SHA-256 digest strings');selectedSet.add(expectedDigest(digest));}
checkOperation(control);const selected=[...selectedSet].sort();checkOperation(control);
const destination = assertExistingDirectory(destinationRoot, 'Archive materialization directory');
assertEmptyDirectory(destination, control);
if (destination === this.root || destination.startsWith(`${this.root}${sep}`) || this.root.startsWith(`${destination}${sep}`) ||
destination === this.stagingRoot || destination.startsWith(`${this.stagingRoot}${sep}`) || this.stagingRoot.startsWith(`${destination}${sep}`))
fail('UNSAFE_PATH', 'Archive materialization directory must be separate from cache and staging roots');
return this.withLock(() => {
checkOperation(control);
this.cleanIncoming(control);
this.recoverInterruptedOperations(control);
const created: string[] = [], result: MaterializedArchive[] = [];
try {
for (const digest of selected) {
checkOperation(control);
const metadata = this.verifiedEntry(digest, control), source = this.entryPath(digest), initial = fs.lstatSync(source);
assertOwnedRegular(initial, 'Cached archive', this.maxEntryBytes, true);
const target = join(destination, digest);
let copied: { digest: string; bytes: number };
try { copied = this.copyAndHash(source, target, this.maxEntryBytes, stableStat(initial), control); }
catch (error) {
if (existsNoFollow(target)) try { removeRegular(target, 'Incomplete run-owned archive copy'); } catch {}
throw error;
}
created.push(target);
if (copied.digest !== digest || copied.bytes !== metadata.bytes) fail('SNAPSHOT_RACE', 'Cached archive changed while its run-owned copy was materialized');
fs.chmodSync(target, 0o400);
const verified = this.hashFile(target, this.maxEntryBytes, true, undefined, control);
if (verified.digest !== digest || verified.bytes !== metadata.bytes) fail('SNAPSHOT_RACE', 'Run-owned archive copy failed verification');
result.push(Object.freeze({ sha256: digest, path: target, bytes: verified.bytes }));
}
return result;
} catch (error) {
for (const path of created.reverse()) { try { removeRegular(path, 'Incomplete run-owned archive copy'); } catch {} }
throw error;
}
}, control);
}
private timestamp(): number {
const value = this.clock();
if (!Number.isSafeInteger(value) || value < 0) fail('PERSISTENCE_FAILED', 'Cache clock returned an invalid timestamp');
return value;
}
private entryPath(digest: string): string { return join(this.entriesDir, digest); }
private metadataPath(digest: string): string { return join(this.metadataDir, `${digest}.json`); }
private entry(metadata: Metadata): PublicArchiveCacheEntry {
return Object.freeze({ sha256: metadata.sha256, path: this.entryPath(metadata.sha256), bytes: metadata.bytes,
createdAt: metadata.createdAt, lastAccessedAt: metadata.lastAccessedAt });
}
private touch(metadata: Metadata, control: NormalizedCacheOperationControl): PublicArchiveCacheEntry {
checkOperation(control);
const updated: Metadata = { ...metadata, lastAccessedAt: Math.max(metadata.lastAccessedAt, this.timestamp()) };
checkOperation(control);
writeMetadata(this.metadataPath(metadata.sha256), updated);
return this.entry(updated);
}
private verifiedEntry(digest: string, control: NormalizedCacheOperationControl): Metadata {
checkOperation(control);
const metadata = readMetadata(this.metadataPath(digest), digest, control);
const result = this.hashFile(this.entryPath(digest), this.maxEntryBytes, true, undefined, control);
if (result.digest !== digest || result.bytes !== metadata.bytes) fail('INCOMPATIBLE_INPUT', `Cached archive failed SHA-256 verification: ${digest}`);
return metadata;
}
private hashFile(path: string, maxBytes: number, immutable: boolean, expected: StableStat | undefined,
control: NormalizedCacheOperationControl): { digest: string; bytes: number } {
checkOperation(control);
const fd = openNoFollow(path, fs.constants.O_RDONLY);
try {
const beforeStat = fs.fstatSync(fd);
assertOwnedRegular(beforeStat, immutable ? 'Cached archive' : 'Staged archive', maxBytes, immutable);
const before = stableStat(beforeStat), hash = createHash('sha256'), buffer = Buffer.allocUnsafe(COPY_BUFFER_BYTES);
if (expected && !sameStat(expected, before)) fail('SNAPSHOT_RACE', 'Archive changed before it could be verified');
let bytes = 0;
for (;;) {
checkOperation(control);
const read = fs.readSync(fd, buffer, 0, buffer.length, null);
if (!read) break;
bytes += read;
if (bytes > maxBytes) fail('INSUFFICIENT_CAPACITY', 'Archive exceeded its byte limit while being read');
hash.update(buffer.subarray(0, read));
checkOperation(control);
}
checkOperation(control);
const after = stableStat(fs.fstatSync(fd));
if (!sameStat(before, after) || bytes !== before.size) fail('SNAPSHOT_RACE', 'Archive changed while it was being verified');
return { digest: hash.digest('hex'), bytes };
} finally { fs.closeSync(fd); }
}
private copyAndHash(source: string, destination: string, maxBytes: number, expected: StableStat,
control: NormalizedCacheOperationControl): { digest: string; bytes: number } {
checkOperation(control);
const sourceFd = openNoFollow(source, fs.constants.O_RDONLY);
let destinationFd: number | undefined;
try {
const beforeStat = fs.fstatSync(sourceFd);
assertOwnedRegular(beforeStat, 'Staged archive', maxBytes);
const before = stableStat(beforeStat), hash = createHash('sha256'), buffer = Buffer.allocUnsafe(COPY_BUFFER_BYTES);
if (!sameStat(expected, before)) fail('SNAPSHOT_RACE', 'Staged archive changed before promotion');
destinationFd = openNoFollow(destination, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
let bytes = 0;
for (;;) {
checkOperation(control);
const read = fs.readSync(sourceFd, buffer, 0, buffer.length, null);
if (!read) break;
bytes += read;
if (bytes > expected.size) fail('SNAPSHOT_RACE', 'Staged archive grew during promotion');
if (bytes > maxBytes) fail('INSUFFICIENT_CAPACITY', 'Archive exceeded its byte limit during promotion');
hash.update(buffer.subarray(0, read));
let offset = 0;
while (offset < read) {
checkOperation(control);
const written = fs.writeSync(destinationFd, buffer, offset, read - offset);
if (written <= 0) fail('PERSISTENCE_FAILED', 'Archive copy stopped before the current chunk was written');
offset += written;
checkOperation(control);
}
}
checkOperation(control);
fs.fsyncSync(destinationFd);
checkOperation(control);
const after = stableStat(fs.fstatSync(sourceFd));
if (!sameStat(before, after) || bytes !== before.size) fail('SNAPSHOT_RACE', 'Staged archive changed during promotion');
return { digest: hash.digest('hex'), bytes };
} finally {
if (destinationFd !== undefined) fs.closeSync(destinationFd);
fs.closeSync(sourceFd);
}
}
private inventory(control: NormalizedCacheOperationControl): Metadata[] {
checkOperation(control);
const entryNames = boundedDirectoryNames(this.entriesDir, 'Cache entries directory', control),
metadataNames = boundedDirectoryNames(this.metadataDir, 'Cache metadata directory', control);
const entrySet=new Set<string>(),metadataSet=new Set<string>();
for (const name of entryNames) {checkOperation(control);if (!SHA256.test(name)) fail('INCOMPATIBLE_INPUT', 'Cache entries directory contains an unexpected object');entrySet.add(name);}
for (const name of metadataNames) {checkOperation(control);if (!/^[a-f0-9]{64}\.json$/.test(name)) fail('INCOMPATIBLE_INPUT', 'Cache metadata directory contains an unexpected object');metadataSet.add(name.slice(0,-5));}
if (entrySet.size !== metadataSet.size) fail('INCOMPATIBLE_INPUT', 'Cache entries and metadata are inconsistent');
for(const name of entrySet){checkOperation(control);if(!metadataSet.has(name))
fail('INCOMPATIBLE_INPUT', 'Cache entries and metadata are inconsistent');
}
const inventory = entryNames.map(digest => {
checkOperation(control);
const stat = fs.lstatSync(this.entryPath(digest));
assertOwnedRegular(stat, 'Cached archive', this.maxEntryBytes, true);
const metadata = readMetadata(this.metadataPath(digest), digest, control);
if (metadata.bytes !== stat.size) fail('INCOMPATIBLE_INPUT', `Cache size metadata is inconsistent for ${digest}`);
return metadata;
});
checkOperation(control);
return inventory;
}
private evictToFit(incomingBytes: number, control: NormalizedCacheOperationControl): void {
if (!Number.isSafeInteger(incomingBytes) || incomingBytes < 0 || incomingBytes > this.maxBytes)
fail('INSUFFICIENT_CAPACITY', 'Archive cannot fit within the public-cache limit');
checkOperation(control);
const entries = this.inventory(control);checkOperation(control);entries.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt || a.createdAt - b.createdAt || a.sha256.localeCompare(b.sha256));
checkOperation(control);
let total = 0;
for (const item of entries) { checkOperation(control); total += item.bytes; }
for (const item of entries) {
checkOperation(control);
if (total + incomingBytes <= this.maxBytes) break;
const archive = this.moveToRecovery(this.entryPath(item.sha256), 'entry', item.sha256);
const metadata = this.moveToRecovery(this.metadataPath(item.sha256), 'metadata', item.sha256);
removeRegular(archive, 'Evicted cache archive');
removeRegular(metadata, 'Evicted cache metadata');
total -= item.bytes;
}
if (total + incomingBytes > this.maxBytes) fail('INSUFFICIENT_CAPACITY', 'Archive cache could not free enough verified capacity');
}
private cleanIncoming(control: NormalizedCacheOperationControl): void {
const incomingNames = boundedDirectoryNames(this.incomingDir, 'Cache incoming directory', control);
let publishedByInode:Map<string,string[]>|undefined;
for (const name of incomingNames) {
checkOperation(control);
if (!/^\.incoming-\d+-[a-f0-9]{24}$/.test(name)) fail('INCOMPATIBLE_INPUT', 'Cache incoming directory contains an unexpected object');
const incoming = join(this.incomingDir, name), stat = fs.lstatSync(incoming);
if (!stat.isFile() || stat.isSymbolicLink() || ![1, 2].includes(stat.nlink) || stat.size > this.maxEntryBytes ||
(process.getuid && stat.uid !== process.getuid())) fail('UNSAFE_PATH', 'Incomplete cache archive is not a bounded regular file');
if (stat.nlink === 2) {
if(!publishedByInode){
publishedByInode=new Map();
for(const entry of boundedDirectoryNames(this.entriesDir, 'Cache entries directory', control)){
checkOperation(control);
if (!SHA256.test(entry)) fail('INCOMPATIBLE_INPUT', 'Cache entries directory contains an unexpected object');
const candidate=fs.lstatSync(this.entryPath(entry)),key=`${candidate.dev}:${candidate.ino}`,matches=publishedByInode.get(key)??[];
matches.push(entry);publishedByInode.set(key,matches);
}
}
const matches=publishedByInode.get(`${stat.dev}:${stat.ino}`)??[];
if (matches.length !== 1) fail('UNSAFE_PATH', 'Incoming archive hard link does not match one published cache entry');
const target = fs.lstatSync(this.entryPath(matches[0]));
if (!target.isFile() || target.isSymbolicLink() || target.nlink !== 2 || target.size > this.maxEntryBytes ||
(process.getuid && target.uid !== process.getuid()) || (target.mode & 0o222) !== 0)
fail('SNAPSHOT_RACE', 'Incoming archive link count or identity changed during recovery');
}
try { fs.unlinkSync(incoming); } catch { fail('PERSISTENCE_FAILED', 'Incomplete cache archive could not be removed'); }
}
}
/**
* Recover only artifacts whose names and inode types prove they belong to an
* interrupted cache transaction. Unknown objects remain a hard failure.
*/
private recoverInterruptedOperations(control: NormalizedCacheOperationControl): void {
for (const name of boundedDirectoryNames(this.recoveryDir, 'Cache recovery directory', control)) {
checkOperation(control);
if (!/^\.recovery-(?:entry|metadata)-[a-f0-9]{64}-\d+-[a-f0-9]{24}$/.test(name))
fail('INCOMPATIBLE_INPUT', 'Cache recovery directory contains an unexpected object');
removeRegular(join(this.recoveryDir, name), 'Interrupted cache transaction');
}
const entries = boundedDirectoryNames(this.entriesDir, 'Cache entries directory', control),
metadataObjects = boundedDirectoryNames(this.metadataDir, 'Cache metadata directory', control);
for (const name of metadataObjects) {
checkOperation(control);
if (/^[a-f0-9]{64}\.json\.tmp\.\d+\.[a-f0-9]{8}$/.test(name)) this.recoverMetadataTemp(name, control);
else if (!/^[a-f0-9]{64}\.json$/.test(name)) fail('INCOMPATIBLE_INPUT', 'Cache metadata directory contains an unexpected object');
}
const metadata = boundedDirectoryNames(this.metadataDir, 'Cache metadata directory', control);
for (const name of entries) if (!SHA256.test(name)) fail('INCOMPATIBLE_INPUT', 'Cache entries directory contains an unexpected object');
for (const name of metadata) if (!/^[a-f0-9]{64}\.json$/.test(name)) fail('INCOMPATIBLE_INPUT', 'Cache metadata directory contains an unexpected object');
const entrySet=new Set<string>(),metadataSet=new Set<string>(),digests=new Set<string>();
for(const name of entries){checkOperation(control);entrySet.add(name);digests.add(name);}
for(const name of metadata){checkOperation(control);const digest=name.slice(0,-5);metadataSet.add(digest);digests.add(digest);}
for (const digest of digests) {
checkOperation(control);
if (entrySet.has(digest) === metadataSet.has(digest)) continue;
if (entrySet.has(digest)) this.discardObject(this.entryPath(digest), 'entry', digest);
else this.discardObject(this.metadataPath(digest), 'metadata', digest);
}
}
private recoveryPath(kind: 'entry' | 'metadata', digest: string): string {
return join(this.recoveryDir, `.recovery-${kind}-${digest}-${process.pid}-${randomBytes(12).toString('hex')}`);
}
private moveToRecovery(path: string, kind: 'entry' | 'metadata', digest: string): string {
const before = fs.lstatSync(path);
assertOwnedRegular(before, kind === 'entry' ? 'Cached archive' : 'Cache metadata', kind === 'entry' ? this.maxEntryBytes : METADATA_LIMIT, kind === 'entry');
if (kind === 'metadata' && (before.mode & 0o077) !== 0) fail('UNSAFE_PATH', 'Cache metadata permissions are not private');
const destination = this.recoveryPath(kind, digest);
try { fs.renameSync(path, destination); }
catch { fail('PERSISTENCE_FAILED', 'Interrupted cache object could not be quarantined atomically'); }
const after = fs.lstatSync(destination);
// rename(2) can update ctime; stable inode identity, content size, mode,
// link count, mtime, and ownership prove the moved object is the one read.
if (!sameRenamedInode(stableStat(before), stableStat(after))) fail('SNAPSHOT_RACE', 'Cache object changed while it was quarantined');
return destination;
}
private discardObject(path: string, kind: 'entry' | 'metadata', digest: string): void {
const quarantined = this.moveToRecovery(path, kind, digest);
removeRegular(quarantined, 'Interrupted cache transaction');
}
private recoverMetadataTemp(name: string, control: NormalizedCacheOperationControl): void {
checkOperation(control);
const path = join(this.metadataDir, name), stat = fs.lstatSync(path);
if (!stat.isFile() || stat.isSymbolicLink() || ![1, 2].includes(stat.nlink) || stat.size > METADATA_LIMIT ||
(process.getuid && stat.uid !== process.getuid()) || (stat.mode & 0o077) !== 0)
fail('UNSAFE_PATH', 'Interrupted cache metadata write is not a private regular file');
if (stat.nlink === 2) {
const target = this.metadataPath(name.slice(0, 64));
let targetStat: fs.Stats;
try { targetStat = fs.lstatSync(target); } catch { fail('INCOMPATIBLE_INPUT', 'Hard-linked metadata temp has no published target'); }
if (!targetStat!.isFile() || targetStat!.isSymbolicLink() || targetStat!.dev !== stat.dev || targetStat!.ino !== stat.ino || targetStat!.nlink !== 2)
fail('UNSAFE_PATH', 'Interrupted metadata hard link does not match its published target');
}
try { fs.unlinkSync(path); } catch { fail('PERSISTENCE_FAILED', 'Interrupted cache metadata write could not be removed'); }
}
private withLock<T>(callback: () => T, control: NormalizedCacheOperationControl): T {
checkOperation(control);
const marker = `${JSON.stringify({ protocol: CACHE_LOCK_PROTOCOL })}\n`;
const options={label:'Cache lock protocol',maxBytes:METADATA_LIMIT,validate:(value:unknown)=>{
if(!value||typeof value!=='object'||Array.isArray(value)||Object.keys(value).join(',')!=='protocol'||(value as any).protocol!==CACHE_LOCK_PROTOCOL)
fail('INCOMPATIBLE_INPUT','Archive-cache lock protocol is invalid');
}};
const tempPattern=/^\.lock\.tmp\.(\d{1,10})\.[a-f0-9]{8}$/;
for(const name of boundedDirectoryNames(this.root, 'Cache root directory', control)){
checkOperation(control);
const match=name.match(tempPattern);if(!match)continue;
const temporary=join(this.root,name),publisherPid=Number(match[1]);
if(existsNoFollow(this.lockDir))recoverAtomicNoReplaceJson(this.lockDir,options);
if(existsNoFollow(temporary))discardAtomicNoReplaceTemp(temporary,publisherPid,options);
}
try { atomicWriteSync(this.lockDir, marker, { mode: 0o600, noReplace: true }); }
catch (error: any) {
if (error?.code !== 'EEXIST') fail('PERSISTENCE_FAILED', 'Archive-cache lock protocol could not be initialized');
recoverAtomicNoReplaceJson(this.lockDir,options);
const stat = fs.lstatSync(this.lockDir);
if (stat.isDirectory() && !stat.isSymbolicLink())
fail('INSUFFICIENT_CAPACITY', 'A legacy archive-cache helper may still own or initialize this cache; its lock was left intact');
assertOwnedRegular(stat, 'Cache lock protocol', METADATA_LIMIT);
if ((stat.mode & 0o077) !== 0) fail('UNSAFE_PATH', 'Cache lock protocol permissions are not private');
let protocol:unknown;try{protocol=JSON.parse(fs.readFileSync(this.lockDir,'utf8')).protocol;}catch{}
if(protocol!==CACHE_LOCK_PROTOCOL)fail('INCOMPATIBLE_INPUT','Archive-cache lock protocol is invalid');
}
const result=withStateLock(this.root,()=>{checkOperation(control);return callback();});
if(result&&typeof (result as any).then==='function')fail('PERSISTENCE_FAILED','Archive-cache operation unexpectedly became asynchronous');
return result as T;
}
}
export function publicArchiveCacheRoot(): string {
return join(privateRoot(), 'public-cache');
}
+508
View File
@@ -0,0 +1,508 @@
#!/usr/bin/env bun
import * as fs from 'node:fs';
import * as os from 'node:os';
import { randomBytes } from 'node:crypto';
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
import {
ABI, ApplicationModel, CoverageRecord, CsoError, FindingV3, PreparationProof, RunPolicy, RunReportV3, SnapshotEntry, SnapshotManifest, SubmissionV3,
canonical, completeness, importLegacy, object, relativePath, renderReport, rootCauseIdentity, sha256, snapshotPathHandle, snapshotPathHandleId, snapshotPathId, snapshotReference, string, strings, validateCoverage, validateFinding, validateVerificationRequest,
} from './contracts';
import { capture, containedFile, assertSnapshot } from './snapshot';
import { assertStateOutside, event, finalizeReplayTemporary, loadReport, newRun, privateRoot, readJson, repoId, requireTime, retention, runDirectory, saveReport, secureDirectory, withLock, writeHelperJson, writeJson, writeJsonExclusive } from './state';
import { dockerEndpoint, dockerProbe, ISOLATION_POLICY_HASH } from './docker';
import { executable, git, redact, sanitizeForJson, sanitizeHelperForJson } from './process';
import { inspectPreparation } from './preparation';
import { assertRuntimeCompatible, RUNTIME_CATALOG, selectRuntime, validateRuntimeCatalog, type RuntimeCatalog } from './runtime-catalog';
import { PublicArchiveCache, publicArchiveCacheRoot } from './cache';
import { admitPreparationRuntime, admitPreparationSidecar, PreparationExecutor, type DependencyClosure, type RailsDatabaseSelection } from './preparation-executor';
import { DockerPreparationSandboxRunner } from './preparation-docker';
import { importSarif, SCANNER_IDS, ScannerId } from './scanners';
import { SCANNER_CATALOG, selectScanner, validateScannerCatalog, type ScannerCatalog } from './scanner-catalog';
import { executeScanner, scannerCoverage, validateScannerRequest } from './scanner-executor';
import { canonicalStartPlan, canonicalTestPlan, DockerVerificationExecutor, makeReviewArtifact, patchHash, validateRepairBundle, validateReviewArtifact, VerificationAttemptError, verificationHarnessHash, verifyRepair, type VerificationExecutor } from './verification';
import { readBoundedStable } from './bounded-file';
import { assertionWitnessReplayHash, runAssertionWitnessChild } from './witness';
import { historyForPath } from './history';
import { catalogImageProvisioningPolicy, inspectCatalogImages, openLocalCatalogImageSession, provisionCatalogImages, qualifiedCatalogImages, type CatalogImageSessionFactory } from './image-provisioning';
const VERSION = '3.0.0';
const RETENTION_MAINTENANCE_MS=1_000,RETENTION_MAX_ENTRIES=100_000,REPLAY_LOOKUP_MAX_ENTRIES=10_000;
const productionCatalogImageSession:CatalogImageSessionFactory=deadline=>openLocalCatalogImageSession(process.env,deadline);
/** Internal qualification seam. The public entrypoint below always supplies committed dependencies. */
export interface CsoCliDependencies {
readonly runtimeCatalog: RuntimeCatalog;
readonly scannerCatalog?: ScannerCatalog;
readonly catalogImageSession?: CatalogImageSessionFactory;
readonly watchdogPath: () => string;
}
const GENERATION_LOCK_FD=(()=>{
// Source-mode developer/test runs use Bun directly. For installed builds,
// this catches accidental direct use of the internal payload. It is not an
// authentication mechanism against the trusted same-user host: that user
// can reproduce an inherited descriptor or environment value. The public
// native launcher is the security boundary because it scrubs runtime and
// loader variables before Bun starts.
if(/^bun(?:\.exe)?$/i.test(basename(process.execPath)))return undefined;
if(process.platform==='win32'){
if(process.env.GSTACK_CSO_GENERATION_GUARD!=='inherited-windows-generation-handle-v3')throw new CsoError('ISOLATION_FAILED','Direct use of the internal CSO payload is unsupported; invoke gstack-cso-launcher');
delete process.env.GSTACK_CSO_GENERATION_GUARD;return undefined;
}
const raw=process.env.GSTACK_CSO_GENERATION_LOCK_FD;if(raw===undefined||!/^(?:[3-9]|[1-9][0-9]+)$/.test(raw))throw new CsoError('ISOLATION_FAILED','Direct use of the internal CSO payload is unsupported; invoke gstack-cso-launcher');const fd=Number(raw);let stat:fs.Stats;try{stat=fs.fstatSync(fd);}catch{throw new CsoError('ISOLATION_FAILED','The launcher publication lease was not preserved by the runtime');}const install=fs.statSync(dirname(process.execPath));if(!stat.isDirectory()||stat.dev!==install.dev||stat.ino!==install.ino)throw new CsoError('ISOLATION_FAILED','The launcher publication lease does not name the helper installation directory');delete process.env.GSTACK_CSO_GENERATION_LOCK_FD;return fd;
})();
void GENERATION_LOCK_FD;
const HELP = `gstack-cso ${VERSION} (helper ABI ${ABI})
Usage:
gstack-cso start --repo PATH [--comprehensive] [--diff] [--base REF] [--budget SECONDS] [--offline] [--infra|--code|--skills|--supply-chain|--owasp|--scope DOMAIN]
gstack-cso doctor --repo PATH
gstack-cso provision-images [--setup-summary] [--per-image-seconds 5..300]
gstack-cso resume RUN
gstack-cso inspect RUN
gstack-cso read RUN PATH_OR_HANDLE
gstack-cso history RUN [PATH_OR_HANDLE]
gstack-cso submit RUN SUBMISSION.json
gstack-cso scan RUN gitleaks|osv|semgrep|zizmor|trivy|schemathesis [REQUEST.json]
gstack-cso scanner-outcome RUN ARTIFACT_ID
gstack-cso import-sarif RUN REPORT.sarif
gstack-cso record-review RUN REQUEST.json --producer ID
gstack-cso test-plan RUN node|bun|python|rails
gstack-cso runtime-plan RUN node|bun|python|rails --port PORT
gstack-cso verify RUN REQUEST.json
gstack-cso patch-hash REQUEST.json
gstack-cso finish RUN
gstack-cso replay BUNDLE_ID [--source MATCHING_SOURCE]
gstack-cso recheck FINDING --repo PATH [--run RUN]
gstack-cso import-v2 REPORT.json
gstack-cso inspect-v2 IMPORT_ID
gstack-cso schema
Static runs never execute application code. Target and scanner execution is Docker-only and requires a qualified immutable catalog.`;
const SCHEMA = {
version:3,
scanner:{profile:'optional exact qualified scanner profile ID',api:{runtimeProfile:'qualified app runtime ID; comprehensive mode only',port:'1024..65535',start:{executable:'absolute in-container path',args:['helper-derived literal argv with optional inspect handles']},control:{name:'legitimate control',path:'/path',method:'GET|POST|PUT|PATCH|DELETE',expected:{status:'100..599',includes:'optional',excludes:'optional'}},boundaryFiles:['untransformed snapshot-relative path or inspect handle'],schema:'reviewed OpenAPI 3.0/3.1 JSON; internal references; no server overrides, hooks, callbacks or external examples',operationIds:['1..20 unique declared path operation IDs'],seed:'optional 1..2147483647',maxExamples:'optional 1..100'}},
submission:{application:{actors:['string'],assets:['string'],entrypoints:['string'],tenantBoundaries:['string'],sensitiveOperations:['string'],invariants:['string']},findings:[{title:'string',rootCause:'stable root cause',location:{path:'exact path or opaque handle returned by inspect',line:'positive integer',symbol:'string'},advisoryIds:['normalized advisory identity or empty'],severity:'critical|high|medium|low|informational',confidence:'high|medium|low',confidenceRationale:'why available evidence supports that confidence',evidence:'supported|hypothesis',attackerControl:'specific input/control',impact:'specific consequence',scenario:'concrete attacker scenario',trace:['entrypoint','caller','sink'],references:['supporting source/advisory reference'],recommendation:'concrete root-cause repair',challenge:{reviewer:'identity or exact sequential fallback label',independent:'boolean',mode:'independent_agent|sequential_fallback',callers:'checked callers',controls:'checked controls',counterevidence:'checked counterevidence',conclusion:'reasoned outcome'},dependency:{affectedVersion:'optional exact range/version',reachability:'reachable|unreachable|unknown',exposure:'production/build/development context',exploitation:'published exploitation evidence or unknown'}}],coverage:[{domain:'string',scope:'string',status:'assessed|partial|not_assessed|not_applicable',method:'string',gaps:['required for partial/not_assessed'],exclusions:['string'],evidence:['required for assessed/partial/not_applicable'],tool:{name:'optional',version:'exact',freshness:'timestamp/status',outcome:'string'}}],gaps:['string'],modelUsage:{source:'host-reported source',tokens:'nonnegative integer',cost:'optional finite nonnegative number'},recheck:{findingId:'original stable ID',outcome:'open|resolved|unknown',evidence:[{kind:'caller|security_boundary',path:'fresh snapshot path or inspect handle',line:'positive integer',observation:'fresh source observation'}],rootCause:'same root cause'}},
verification:{findingId:'stable ID',runtimeProfile:'qualified runtime ID',port:'1024..65535',start:{executable:'absolute in-container path',args:['literal argv']},legitimate:[{name:'control name',path:'/numeric-loopback-relative path',method:'GET|POST|PUT|PATCH|DELETE',headers:{'optional-name':'value'},body:'optional body',expected:{status:'100..599',includes:'optional',excludes:'optional'}}],security:{name:'security assertion',path:'/path',method:'GET|POST|PUT|PATCH|DELETE',expected:{status:'fixed status',includes:'optional',excludes:'optional'},vulnerable:{status:'provably mutually-exclusive vulnerable status',includes:'optional',excludes:'optional'}},existingTests:[{executable:'must exactly match the helper-derived canonical stack suite',args:['helper-derived argv']}],testFiles:['exact immutable canonical path or inspect handle'],fixtures:{'relative/path':'content mounted read-only at /fixtures'},boundaryFiles:['snapshot path or inspect handle for the security boundary'],changes:[{path:'snapshot path or inspect handle',beforeSha256:'hash or null',after:'replacement or null',effect:'source|configuration|dependency'}],review:{artifactId:'helper-issued after record-review',reviewer:'self-attested identity distinct from producer',independent:'self-attested boolean',rootCauseRepaired:'self-attested boolean',featurePreserved:'self-attested boolean',boundaryMocks:'self-attested boolean',rationale:'specific review',reviewedPatchHash:'canonical patch hash'}},
helperOwned:['run/report completeness','stable IDs','reproduction outcome','repair result and label','current-source closure','verification/bundle hashes'],
};
function emit(value: unknown): void { process.stdout.write((typeof value === 'string' ? redact(value) : JSON.stringify(sanitizeHelperForJson(value),null,2)) + '\n'); }
function persistableArtifact<T>(value:T,label:string):T{
const sanitized=sanitizeHelperForJson(value) as T;
if(canonical(sanitized)!==canonical(value))throw new CsoError('REDACTION_FAILED',`${label} contains material that cannot be persisted without changing its authenticated identity`);
return sanitized;
}
function rejectUnexpected(value:Record<string,unknown>,allowed:readonly string[],name:string):void{
for(const key of Object.keys(value))if(!allowed.includes(key))throw new CsoError('INVALID_SCHEMA',`Unexpected ${name} field: ${key}`);
}
function need(args:string[],flag:string):string { const i=args.indexOf(flag); if(i<0||i===args.length-1||args[i+1].startsWith('--')) throw new CsoError('INVALID_ARGUMENT',`${flag} requires a value`); const v=args[i+1]; args.splice(i,2); return v; }
function take(args:string[],flag:string):boolean { const i=args.indexOf(flag); if(i<0)return false;args.splice(i,1);return true; }
function callerPath(value:string):string {
if(isAbsolute(value))return resolve(value);
const cwd=process.env.GSTACK_CSO_CALLER_CWD;
if(!cwd||!isAbsolute(cwd))throw new CsoError('INVALID_ARGUMENT','Relative paths require the trusted gstack-cso launcher');
return resolve(cwd,value);
}
function readInput(path:string,max=1024*1024):unknown {
let data:Buffer;try{data=readBoundedStable(callerPath(path),max,'Input file');}catch(error){if(error instanceof CsoError)throw error;throw new CsoError('MISSING_INPUT',`Input file does not exist: ${path}`);}
try{return JSON.parse(data.toString('utf8'));}catch{throw new CsoError('INVALID_SCHEMA','Input is not valid JSON');}
}
function model(v:unknown):ApplicationModel {
const x=object(v,'application model');rejectUnexpected(x,['actors','assets','entrypoints','tenantBoundaries','sensitiveOperations','invariants'],'application model'); const out:ApplicationModel={actors:strings(x.actors,'actors'),assets:strings(x.assets,'assets'),entrypoints:strings(x.entrypoints,'entrypoints'),tenantBoundaries:strings(x.tenantBoundaries,'tenantBoundaries'),sensitiveOperations:strings(x.sensitiveOperations,'sensitiveOperations'),invariants:strings(x.invariants,'invariants')};
if(Object.values(out).some(a=>!a.length))throw new CsoError('INVALID_SCHEMA','Every application-model dimension needs at least one evidence-backed entry');return out;
}
function planned(scope:string):CoverageRecord[]{
const domains = scope==='infra'?['secrets','dependencies','ci-cd','infrastructure','integrations']
: scope==='code'?['llm-agentic-mcp','owasp-2025','stride','data-classification']
: scope==='skills'?['skill-supply-chain'] : scope==='supply-chain'?['dependencies'] : scope==='owasp'?['owasp-2025']
: scope.startsWith('domain:')?[scope.slice(7)]
:['secrets','dependencies','ci-cd','infrastructure','integrations','llm-agentic-mcp','skill-supply-chain','owasp-2025','stride','data-classification'];
return ['application-model','attack-surface',...domains].map(domain=>({domain,scope,status:'not_assessed',method:'pending investigation',gaps:['Assessment has not been submitted'],exclusions:[],evidence:[]}));
}
function snapshotCoverage(manifest:Awaited<ReturnType<typeof capture>>,scope:string):CoverageRecord{
const omitted=manifest.entries.filter(entry=>!entry.executionHash),excluded=omitted.filter(entry=>entry.transformation?.startsWith('excluded:'));
// Classify every unexplained omission as a material coverage gap. Coverage
// must not depend on transformation prose retaining a particular prefix.
const unread=omitted.filter(entry=>!excluded.includes(entry));
const deleted=manifest.deletedPaths??[],captured=manifest.entries.filter(entry=>entry.executionHash).length,missing=unread.length+deleted.length;
return {domain:'snapshot-inputs',scope,status:missing?(captured?'partial':'not_assessed'):'assessed',method:'fail-closed captured source inventory',
gaps:[...unread.map(entry=>`${publicSnapshotPath(manifest,entry.path).path}: in-scope source payload was unread and withheld from static and runtime assessment`),...deleted.map(item=>`${publicSnapshotPath(manifest,item.path).path}: tracked source is deleted from the worktree; only retained history is available for assessment`)],
exclusions:excluded.map(entry=>`${publicSnapshotPath(manifest,entry.path).path}: ${entry.transformation}`),
evidence:[`${captured} sanitized execution input${captured===1?'':'s'} captured; ${excluded.length} explicit non-executable exclusion${excluded.length===1?'':'s'}; ${unread.length} unread in-scope input${unread.length===1?'':'s'}; ${deleted.length} tracked deletion${deleted.length===1?'':'s'}`]};
}
function historyCoverage(status:any,scope:string):CoverageRecord{return status?.status==='captured'
?{domain:'history-inputs',scope,status:'assessed',method:'bounded helper-retained Git history',gaps:[],exclusions:[],evidence:[`Captured ${String(status.commits??'bounded commits')} from ${String(status.range??'the pinned source range')}`]}
:{domain:'history-inputs',scope,status:'not_assessed',method:'bounded helper-retained Git history',gaps:[typeof status?.gap==='string'&&status.gap.trim()?status.gap:'Historical evidence was not safely retained'],exclusions:[],evidence:[]};}
function helperOwnedCoverage(domain:string):boolean{return domain==='snapshot-inputs'||domain==='history-inputs'||domain==='runtime-readiness'||domain.startsWith('scanner:')||domain.startsWith('preparation:')||domain.startsWith('execution:');}
function parseStart(args:string[]):{repo:string;policy:RunPolicy;comparisonBase?:string}{
const repo=callerPath(need(args,'--repo')),comprehensive=take(args,'--comprehensive'),diff=take(args,'--diff'),offline=take(args,'--offline');
const scopeFlags=['--infra','--code','--skills','--supply-chain','--owasp'].filter(f=>args.includes(f));
const named=args.includes('--scope')?need(args,'--scope'):undefined;
if(scopeFlags.length+(named?1:0)>1)throw new CsoError('INVALID_ARGUMENT','Select only one scope');
for(const f of scopeFlags)take(args,f);
const explicitBase=args.includes('--base'),base=explicitBase?need(args,'--base'):'origin/main';
const rawBudget=args.includes('--budget')?need(args,'--budget'):String(comprehensive?1800:600),budgetSeconds=Number(rawBudget);
if(!Number.isInteger(budgetSeconds)||budgetSeconds<120||budgetSeconds>(comprehensive?1800:600))throw new CsoError('INVALID_ARGUMENT',`Budget must be an integer from 120 to ${comprehensive?1800:600} seconds`);
if(args.length)throw new CsoError('INVALID_ARGUMENT',`Unknown argument: ${args[0]}`);
if(!fs.existsSync(repo)||!fs.statSync(repo).isDirectory())throw new CsoError('MISSING_INPUT','Repository directory does not exist');
const scope=named?`domain:${string(named,'scope',100)}`:scopeFlags[0]?.slice(2)??'default';
return {repo,policy:{mode:comprehensive?'comprehensive':'daily',scope,diff,base,offline,budgetSeconds,maxWorkers:3,maxRepairs:3},...(diff||explicitBase?{comparisonBase:base}:{})};
}
async function start(args:string[],dependencies:CsoCliDependencies,parent?:RunReportV3['parent'],requiredAncestor?:string,startedAt=new Date()):Promise<RunReportV3>{
const {repo,policy,comparisonBase}=parseStart(args),createdAt=startedAt;assertStateOutside(repo);if(!parent)retention(createdAt.getTime(),{deadlineMs:Math.min(createdAt.getTime()+RETENTION_MAINTENANCE_MS,createdAt.getTime()+policy.budgetSeconds*1000-60_000),maxEntries:RETENTION_MAX_ENTRIES});const run=newRun(repo);
let manifest:Awaited<ReturnType<typeof capture>>;try{manifest=await capture(repo,run.dir,comparisonBase,requiredAncestor,{deadlineMs:createdAt.getTime()+policy.budgetSeconds*1000-60_000});}catch(error){fs.rmSync(run.dir,{recursive:true,force:true});throw error;}
const report:RunReportV3={schemaVersion:3,runId:run.runId,repoId:run.repoId,createdAt:createdAt.toISOString(),deadline:new Date(createdAt.getTime()+policy.budgetSeconds*1000).toISOString(),status:'running',completeness:'not assessed',policy,
source:{root:repo,snapshotHash:manifest.executionHash,originalHash:manifest.originalHash,baseCommit:manifest.baseCommit,
transformations:[...manifest.entries.filter(entry=>entry.transformation).map(entry=>({path:publicSnapshotPath(manifest,entry.path).path,handling:entry.transformation!})),...(manifest.deletedPaths??[]).map(item=>({path:publicSnapshotPath(manifest,item.path).path,handling:'tracked source deleted; retained history only'}))]},
application:{actors:[],assets:[],entrypoints:[],tenantBoundaries:[],sensitiveOperations:[],invariants:[]},coverage:[snapshotCoverage(manifest,policy.scope),historyCoverage(readJson(join(run.dir,'history-status.json')),policy.scope),...planned(policy.scope)],findings:[],gaps:[],events:[],...(parent?{parent}:{})};
event(report,'snapshot',`Captured ${manifest.entries.length} source entries; ${manifest.entries.filter(e=>e.transformation).length+(manifest.deletedPaths?.length??0)} transformations disclosed`);
if(policy.mode==='comprehensive'){
const plan=inspectPreparation(join(run.dir,'snapshot'));writeJson(join(run.dir,'preparation.json'),plan);
const c:CoverageRecord={domain:'runtime-readiness',scope:plan.stack,status:plan.status==='ready'?'partial':'not_assessed',method:'inert lockfile and runtime-catalog inspection',gaps:plan.prerequisites.map(p=>p.message),exclusions:[],evidence:[`Preparation metadata: ${plan.status}`]};
try{validateRuntimeCatalog(dependencies.runtimeCatalog);selectRuntime(plan.runtimeProfile,platform(),dependencies.runtimeCatalog);c.evidence.push(`Qualified runtime catalog: ${dependencies.runtimeCatalog.revision}`);}catch(error:any){c.gaps.push(error?.message?.startsWith('MISSING_QUALIFIED_RUNTIME')?error.message:'Runtime catalog validation failed');}
try{const runtimeHome=secureDirectory(join(run.dir,'home')),endpoint=await dockerEndpoint(runtimeHome);const probe=await dockerProbe(endpoint,runtimeHome);c.evidence.push(`Local Docker ${probe.version} admitted at ${endpoint.uri}`);}catch(error:any){c.gaps.push(error instanceof CsoError?error.message:'Local Docker isolation admission failed');}
if(plan.status==='ready'&&!c.gaps.length)c.status='assessed';else if(c.evidence.length>1)c.status='partial';
report.coverage.push(c);
}
saveReport(run.dir,report);return report;
}
async function doctor(args:string[],dependencies:CsoCliDependencies){
const started=Date.now(),repo=callerPath(need(args,'--repo'));if(args.length)throw new CsoError('INVALID_ARGUMENT',`Unknown argument: ${args[0]}`);
const staticCheck=(async()=>{let home='';try{const stat=fs.statSync(repo);if(!stat.isDirectory())throw new CsoError('MISSING_INPUT','Repository path is not a directory');const g=executable('git');home=secureDirectory(fs.mkdtempSync(join(fs.realpathSync(os.tmpdir()),'gstack-cso-doctor-git-')));if((await git(repo,['rev-parse','--is-inside-work-tree'],home)).trim()!=='true')throw new CsoError('MISSING_INPUT','Repository path is not a Git working tree');return{capability:'static-snapshot',status:'ready',detail:g};}catch(e:any){return{capability:'static-snapshot',status:'missing',detail:e instanceof CsoError?e.message:'Repository path is missing or unreadable'};}finally{if(home)fs.rmSync(home,{recursive:true,force:true});}})();
let preparation:ReturnType<typeof inspectPreparation>|undefined;
try{const stat=fs.statSync(repo);if(!stat.isDirectory())throw new CsoError('MISSING_INPUT','Repository path is not a directory');preparation=inspectPreparation(repo);}catch{}
const scannerCatalog=dependencies.scannerCatalog??SCANNER_CATALOG,imageSession=dependencies.catalogImageSession??productionCatalogImageSession,deadline=started+30_000;
let targetPlatform:'linux/amd64'|'linux/arm64'|undefined,entries:ReturnType<typeof qualifiedCatalogImages>=[];
try{targetPlatform=platform();entries=qualifiedCatalogImages(dependencies.runtimeCatalog,scannerCatalog,targetPlatform);}catch{}
const inspectedPromise=inspectCatalogImages(entries,imageSession,deadline);
const checks:any[]=[await staticCheck];
checks.push(preparation?{capability:'application-preparation',status:preparation.status==='ready'?'ready':'missing',detail:{stack:preparation.stack,prerequisites:preparation.prerequisites}}:{capability:'application-preparation',status:'missing',detail:'Repository source is unavailable for inert preparation inspection'});
const inspected=await inspectedPromise;checks.push({capability:'local-docker-isolation',status:inspected.docker.status,detail:inspected.docker.detail});
try{
if(!preparation||preparation.status!=='ready')throw new CsoError('PREREQUISITE','Resolve the application-preparation prerequisites first');
if(!targetPlatform)throw new CsoError('PREREQUISITE','Qualified runtimes require an amd64/arm64 Linux Docker platform');
validateRuntimeCatalog(dependencies.runtimeCatalog);const runtime=selectRuntime(preparation.runtimeProfile,targetPlatform,dependencies.runtimeCatalog),availability=inspected.images.find(item=>item.kind==='runtime'&&item.id===runtime.id),requiredSidecars:Array<Record<string,unknown>>=[],prerequisites:string[]=[];
if(!availability||availability.status!=='available')prerequisites.push(availability?.reason??'Exact qualified runtime image is not present in the local Docker daemon; rerun setup with Docker and public registry access');
if(preparation.stack==='rails'&&preparation.database?.selected==='postgresql'){
const sidecar=selectRuntime('postgresql',targetPlatform,dependencies.runtimeCatalog),sidecarAvailability=inspected.images.find(item=>item.kind==='runtime'&&item.id===sidecar.id),sidecarReady=sidecarAvailability?.status==='available',prerequisite=sidecarReady?undefined:sidecarAvailability?.reason??'Exact qualified PostgreSQL sidecar image is not present in the local Docker daemon; rerun setup with Docker and public registry access';
if(prerequisite)prerequisites.push(prerequisite);requiredSidecars.push({kind:'postgresql',profile:sidecar.id,image:sidecar.image,platform:targetPlatform,availability:sidecarReady?'available':'unavailable',...(prerequisite?{prerequisite}:{})});
}
const ready=!prerequisites.length;checks.push({capability:'qualified-runtimes',status:ready?'ready':'missing',detail:{catalog:dependencies.runtimeCatalog.revision,profile:runtime.id,image:runtime.image,platform:targetPlatform,availability:ready?'available':'unavailable',...(requiredSidecars.length?{requiredSidecars}:{}),...(prerequisites.length?{prerequisite:prerequisites[0],prerequisites}:{})}});
}catch(error:any){checks.push({capability:'qualified-runtimes',status:'missing',detail:error instanceof CsoError?error.message:error?.message??'Qualified runtime catalog is invalid'});}
for(const id of SCANNER_IDS){try{
if(!targetPlatform)throw new CsoError('PREREQUISITE','Scanner containers require an amd64/arm64 Linux Docker platform');validateScannerCatalog(scannerCatalog);
const profile=selectScanner(id,targetPlatform,undefined,scannerCatalog),availability=inspected.images.find(item=>item.kind==='scanner'&&item.id===profile.id);
if(!availability||availability.status!=='available')checks.push({capability:`scanner:${id}`,status:'missing',detail:{catalog:scannerCatalog.revision,profile:profile.id,image:profile.image,version:profile.version,qualifiedAt:profile.qualifiedAt,availability:'unavailable',prerequisite:availability?.reason??'Exact qualified scanner image is not present in the local Docker daemon; rerun setup with Docker and public registry access'}});
else checks.push({capability:`scanner:${id}`,status:'ready',detail:{catalog:scannerCatalog.revision,profile:profile.id,image:profile.image,version:profile.version,qualifiedAt:profile.qualifiedAt,availability:'available'}});
}catch(error:any){checks.push({capability:`scanner:${id}`,status:'missing',detail:error instanceof CsoError?error.message:error?.message??'Qualified scanner catalog is invalid'});}}
return{schemaVersion:3,downloads:false,elapsedMs:Date.now()-started,checks};
}
async function provisionImages(args:string[],dependencies:CsoCliDependencies):Promise<unknown>{
const setupSummary=take(args,'--setup-summary'),requestedSeconds=args.includes('--per-image-seconds')?need(args,'--per-image-seconds'):undefined;if(args.length)throw new CsoError('INVALID_ARGUMENT',`Unknown argument: ${args[0]}`);
if(requestedSeconds!==undefined)catalogImageProvisioningPolicy(0,requestedSeconds);
let targetPlatform:'linux/amd64'|'linux/arm64';
try{targetPlatform=platform();}catch(error){
const reason=error instanceof CsoError?error.message:'Qualified image provisioning requires an amd64/arm64 Linux Docker platform',result={schemaVersion:1,status:'not_available',downloads:true,platform:'unsupported',requested:0,inspected:0,alreadyPresent:0,downloaded:0,deadlineReached:false,unavailable:[],summary:`Qualified CSO images were not preloaded: ${reason}. Static audits remain available.`};
return setupSummary?result.summary:result;
}
const scannerCatalog=dependencies.scannerCatalog??SCANNER_CATALOG,entries=qualifiedCatalogImages(dependencies.runtimeCatalog,scannerCatalog,targetPlatform),policy=catalogImageProvisioningPolicy(entries.length,requestedSeconds),deadline=Date.now()+policy.aggregateMs,result=await provisionCatalogImages(entries,targetPlatform,dependencies.catalogImageSession??productionCatalogImageSession,deadline,policy.perImageMs);
return setupSummary?result.summary:result;
}
function run(args:string[]){if(!args.length)throw new CsoError('INVALID_ARGUMENT','Run ID is required');return {dir:runDirectory(args.shift()!),report:null as any};}
function recoveryEvents(dir:string):string[]{const out:string[]=[];let visited=0;const walk=(at:string,depth:number)=>{if(depth>6||visited++>4000)return;let entries:fs.Dirent[];try{entries=fs.readdirSync(at,{withFileTypes:true});}catch{return;}for(const entry of entries){if(!/^[A-Za-z0-9._-]{1,120}$/.test(entry.name))continue;const path=join(at,entry.name);let stat:fs.Stats;try{stat=fs.lstatSync(path);}catch{continue;}if(stat.isSymbolicLink())continue;if(stat.isDirectory()){walk(path,depth+1);continue;}if(!['attempt.event','watchdog.event'].includes(entry.name)||!stat.isFile()||stat.size>8192)continue;try{const message=redact(fs.readFileSync(path,'utf8').trim());if(message&&!out.includes(message))out.push(message);}catch{}}};for(const name of ['supervision','preparation-execution']){const root=join(dir,name);if(!fs.existsSync(root))continue;const stat=fs.lstatSync(root);if(stat.isSymbolicLink()||!stat.isDirectory())throw new CsoError('UNSAFE_PATH','Watchdog recovery state is not a private directory');walk(root,0);}return out;}
function publicSnapshotPath(manifest:SnapshotManifest,path:string):{path:string;displayPath?:string}{
const entry=manifest.entries.find(item=>item.path===path),handle=snapshotPathHandle(entry?.pathId??snapshotPathId(manifest.root,path));let displayPath:string;
try{displayPath=redact(path);}catch{displayPath='[sensitive path withheld]';}
return displayPath===path?{path}:{path:handle,displayPath};
}
function publicSnapshotManifest(manifest:SnapshotManifest):Record<string,unknown>{
return {...manifest,entries:manifest.entries.map(entry=>{const {path,pathId:_,...rest}=entry;return{...rest,...publicSnapshotPath(manifest,path)};}),
...(manifest.deletedPaths?.length?{deletedPaths:manifest.deletedPaths.map(item=>publicSnapshotPath(manifest,item.path))}:{}),
...(manifest.changedPaths?{changedPaths:manifest.changedPaths.map(path=>publicSnapshotPath(manifest,path).path)}:{})};
}
function resolveSnapshotPath(manifest:SnapshotManifest,value:unknown,requireEntry:boolean,label='Snapshot path',allowDeleted=false):{path:string;entry?:SnapshotEntry}{
const reference=snapshotReference(value),handleId=snapshotPathHandleId(reference);
if(handleId){const entry=manifest.entries.find(item=>item.pathId===handleId);if(entry)return{path:entry.path,entry};const deleted=manifest.deletedPaths?.find(item=>item.pathId===handleId),changed=manifest.changedPaths?.find(path=>snapshotPathId(manifest.root,path)===handleId);if(allowDeleted&&deleted)return{path:deleted.path};if(!requireEntry&&changed)return{path:changed};throw new CsoError('INVALID_SCHEMA',`${label} handle is outside the retained inventory: ${reference}`);}
const path=relativePath(reference),entry=manifest.entries.find(item=>item.path===path),deleted=manifest.deletedPaths?.find(item=>item.path===path),changed=manifest.changedPaths?.includes(path);if(entry)return{path,entry};if(allowDeleted&&deleted)return{path};if(!requireEntry&&changed)return{path};throw new CsoError('INVALID_SCHEMA',`${label} is outside the retained inventory: ${reference}`);
}
type BoundRecheckEvidence={kind:'caller'|'security_boundary';path:string;line:number;observation:string;sourceState:'present'|'absent';snapshotHash:string;sourceHash?:string;executionHash?:string};
type BoundRecheckClaim={findingId:string;outcome:'open'|'resolved'|'unknown';evidence:BoundRecheckEvidence[];rootCause:string};
function assertRecheckLine(runDir:string,path:string,entry:SnapshotEntry,line:number,label:string):void{
if(!entry.executionHash)throw new CsoError('INVALID_SCHEMA',`${label} must reference source available in the fresh execution snapshot`);
const body=readBoundedStable(containedFile(join(runDir,'snapshot'),path),64*1024*1024,label).toString('utf8'),lines=body.length?(body.endsWith('\n')?body.slice(0,-1):body).split('\n').length:0;
if(line>lines)throw new CsoError('INVALID_SCHEMA',`${label} line is outside the fresh source file`);
}
function originalBoundary(report:RunReportV3):{dir:string;report:RunReportV3;manifest:SnapshotManifest;finding:FindingV3;path:string}{
if(!report.parent)throw new CsoError('INVALID_SCHEMA','Recheck evidence requires a linked original finding');
const dir=runDirectory(report.parent.runId),original=loadReport(dir),manifest=readJson(join(dir,'snapshot.json')) as SnapshotManifest,
finding=original.findings.find(item=>item.id===report.parent!.findingId);
if(report.repoId!==original.repoId)throw new CsoError('INCOMPATIBLE_INPUT','Recheck repository identity differs from the original audit');
if(!finding)throw new CsoError('MISSING_INPUT','Original recheck finding no longer exists');
const path=resolveSnapshotPath(manifest,finding.location.path,false,'Original finding boundary',true).path;
return{dir,report:original,manifest,finding,path};
}
function bindRecheckEvidence(value:unknown,runDir:string,manifest:SnapshotManifest,boundary:ReturnType<typeof originalBoundary>,outcome:BoundRecheckClaim['outcome']):BoundRecheckEvidence[]{
if(!Array.isArray(value)||value.length<1||value.length>20)throw new CsoError('INVALID_SCHEMA','Recheck evidence must contain 1 to 20 fresh source observations');
const evidence=value.map((raw,index)=>{
const item=object(raw,`recheck evidence[${index}]`);rejectUnexpected(item,['kind','path','line','observation'],`recheck evidence[${index}]`);
const kind=string(item.kind,`recheck evidence[${index}].kind`,32);if(!['caller','security_boundary'].includes(kind))throw new CsoError('INVALID_SCHEMA','Recheck evidence kind must be caller or security_boundary');
if(!Number.isSafeInteger(item.line)||item.line<1)throw new CsoError('INVALID_SCHEMA',`recheck evidence[${index}].line must be a positive integer`);
const observation=redact(string(item.observation,`recheck evidence[${index}].observation`,2048)),reference=snapshotReference(item.path);
let selected:{path:string;entry?:SnapshotEntry}|undefined;
try{selected=resolveSnapshotPath(manifest,reference,true,`recheck evidence[${index}].path`);}catch(error){
if(kind!=='security_boundary')throw error;
let old:{path:string};try{old=resolveSnapshotPath(boundary.manifest,reference,false,`recheck evidence[${index}].path`,true);}catch{throw error;}
if(old.path!==boundary.path||manifest.entries.some(entry=>entry.path===boundary.path))throw error;
if(item.line!==boundary.finding.location.line)throw new CsoError('INVALID_SCHEMA','Absent security-boundary evidence must cite the original finding line');
return{kind:'security_boundary' as const,path:snapshotPathHandle(snapshotPathId(manifest.root,boundary.path)),line:item.line as number,observation,sourceState:'absent' as const,snapshotHash:manifest.originalHash};
}
if(!selected.entry||selected.entry.originalHash==='not-read')throw new CsoError('INVALID_SCHEMA','Recheck evidence must reference freshly captured readable source');
assertRecheckLine(runDir,selected.path,selected.entry,item.line as number,`recheck evidence[${index}]`);
if(kind==='security_boundary'&&selected.path!==boundary.path)throw new CsoError('INVALID_SCHEMA','Security-boundary evidence must reference the original finding location');
return{kind:kind as BoundRecheckEvidence['kind'],path:snapshotPathHandle(selected.entry.pathId),line:item.line as number,observation,sourceState:'present' as const,snapshotHash:manifest.originalHash,sourceHash:selected.entry.originalHash,executionHash:selected.entry.executionHash};
});
const identities=new Set(evidence.map(item=>canonical(item)));if(identities.size!==evidence.length)throw new CsoError('INVALID_SCHEMA','Recheck evidence contains duplicate observations');
if(outcome==='resolved'&&(!evidence.some(item=>item.kind==='caller'&&item.sourceState==='present')||!evidence.some(item=>item.kind==='security_boundary')))
throw new CsoError('INVALID_SCHEMA','Resolved closure requires fresh caller evidence and evidence for the original security boundary');
return evidence;
}
function validateBoundRecheckClaim(value:unknown,runDir:string,manifest:SnapshotManifest,boundary:ReturnType<typeof originalBoundary>):BoundRecheckClaim{
const raw=object(value,'retained recheck claim');rejectUnexpected(raw,['findingId','outcome','evidence','rootCause'],'retained recheck claim');
const findingId=string(raw.findingId,'retained recheck findingId'),rootCause=string(raw.rootCause,'retained recheck rootCause'),outcome=raw.outcome;
if(!['open','resolved','unknown'].includes(outcome as string))throw new CsoError('INVALID_SCHEMA','Retained recheck outcome is invalid');
if(!Array.isArray(raw.evidence)||raw.evidence.length<1||raw.evidence.length>20)throw new CsoError('INVALID_SCHEMA','Retained recheck evidence is invalid');
const evidence=raw.evidence.map((itemRaw,index)=>{
const item=object(itemRaw,`retained recheck evidence[${index}]`);rejectUnexpected(item,['kind','path','line','observation','sourceState','snapshotHash','sourceHash','executionHash'],`retained recheck evidence[${index}]`);
const kind=string(item.kind,`retained recheck evidence[${index}].kind`,32),sourceState=string(item.sourceState,`retained recheck evidence[${index}].sourceState`,16);
if(!['caller','security_boundary'].includes(kind)||!['present','absent'].includes(sourceState))throw new CsoError('INVALID_SCHEMA','Retained recheck evidence type is invalid');
if(!Number.isSafeInteger(item.line)||item.line<1)throw new CsoError('INVALID_SCHEMA','Retained recheck evidence line is invalid');
const observation=string(item.observation,`retained recheck evidence[${index}].observation`,2048),reference=snapshotReference(item.path);
if(item.snapshotHash!==manifest.originalHash)throw new CsoError('INCOMPATIBLE_INPUT','Retained recheck evidence is not bound to the complete fresh snapshot inventory');
if(sourceState==='present'){
const selected=resolveSnapshotPath(manifest,reference,true,'Retained recheck evidence path');
if(!selected.entry||selected.entry.originalHash==='not-read'||typeof item.sourceHash!=='string'||item.sourceHash!==selected.entry.originalHash||typeof item.executionHash!=='string'||item.executionHash!==selected.entry.executionHash)
throw new CsoError('INCOMPATIBLE_INPUT','Retained recheck evidence is not bound to the fresh snapshot');
assertRecheckLine(runDir,selected.path,selected.entry,item.line as number,`retained recheck evidence[${index}]`);
if(kind==='security_boundary'&&selected.path!==boundary.path)throw new CsoError('INCOMPATIBLE_INPUT','Retained security-boundary evidence changed location');
return{kind:kind as BoundRecheckEvidence['kind'],path:snapshotPathHandle(selected.entry.pathId),line:item.line as number,observation,sourceState:'present' as const,snapshotHash:item.snapshotHash as string,sourceHash:item.sourceHash,executionHash:item.executionHash};
}
if(kind!=='security_boundary'||item.sourceHash!==undefined||item.executionHash!==undefined)throw new CsoError('INVALID_SCHEMA','Only an absent original security boundary can use absent evidence');
const old=resolveSnapshotPath(boundary.manifest,reference,false,'Retained absent security boundary',true);
if(old.path!==boundary.path||manifest.entries.some(entry=>entry.path===boundary.path)||item.line!==boundary.finding.location.line)throw new CsoError('INCOMPATIBLE_INPUT','Retained absent-boundary evidence does not match the fresh snapshot');
return{kind:'security_boundary' as const,path:snapshotPathHandle(snapshotPathId(manifest.root,boundary.path)),line:item.line as number,observation,sourceState:'absent' as const,snapshotHash:item.snapshotHash as string};
});
if(outcome==='resolved'&&(!evidence.some(item=>item.kind==='caller'&&item.sourceState==='present')||!evidence.some(item=>item.kind==='security_boundary')))
throw new CsoError('INVALID_SCHEMA','Resolved closure lacks caller or original security-boundary evidence');
if(new Set(evidence.map(item=>canonical(item))).size!==evidence.length)throw new CsoError('INVALID_SCHEMA','Retained recheck evidence contains duplicates');
return{findingId,outcome:outcome as BoundRecheckClaim['outcome'],evidence,rootCause};
}
function requireReportingTime(report:RunReportV3):void{if(Date.now()>=Date.parse(report.deadline))throw new CsoError('DEADLINE','Audit deadline reached; no further evidence can be accepted');}
function submit(args:string[]){
const {dir}=run(args);if(args.length!==1)throw new CsoError('INVALID_ARGUMENT','submit requires one JSON file');const rawInput=object(readInput(args[0]),'submission');rejectUnexpected(rawInput,['application','findings','coverage','gaps','modelUsage','recheck'],'submission');const input=rawInput as SubmissionV3;
return withLock(dir,()=>{const report=loadReport(dir),manifest=readJson(join(dir,'snapshot.json'));assertSnapshot(dir,manifest);requireReportingTime(report);if(report.status!=='running')throw new CsoError('INVALID_SCHEMA','Only a running audit accepts evidence');
if(input.findings!==undefined&&!Array.isArray(input.findings))throw new CsoError('INVALID_SCHEMA','submission.findings must be an array');
if(input.coverage!==undefined&&!Array.isArray(input.coverage))throw new CsoError('INVALID_SCHEMA','submission.coverage must be an array');
if(input.application)report.application=model(input.application);
for(const raw of input.findings??[]){const sourceFinding=validateFinding(raw),sourceLocation=resolveSnapshotPath(manifest,sourceFinding.location.path,false,'Finding path',true);if(report.policy.diff&&(!Array.isArray(manifest.changedPaths)||!manifest.changedPaths.includes(sourceLocation.path)))throw new CsoError('INVALID_SCHEMA',`Diff-scope finding root cause is outside the captured changed paths: ${sourceFinding.location.path}`);const safeRaw=object(sanitizeForJson(raw),'finding'),safeLocation=object(safeRaw.location,'location');safeLocation.path=publicSnapshotPath(manifest,sourceLocation.path).path;const f=validateFinding(safeRaw),normalizedLocation=resolveSnapshotPath(manifest,f.location.path,false,'Finding path',true);if(normalizedLocation.path!==sourceLocation.path)throw new CsoError('INVALID_SCHEMA','Finding path identity changed during redaction');if(report.policy.mode==='daily'&&f.evidence==='hypothesis')throw new CsoError('INVALID_SCHEMA','Daily reports contain supported findings only');const old=report.findings.findIndex(x=>x.fingerprint===f.fingerprint);if(old<0){report.findings.push(f);event(report,'early-finding',`${f.severity} ${f.evidence} finding ${f.id}`);}else report.findings[old]={...f,reproduction:report.findings[old].reproduction,repair:report.findings[old].repair,closure:report.findings[old].closure,verificationId:report.findings[old].verificationId,reproductionAttemptId:report.findings[old].reproductionAttemptId,verificationAssurance:report.findings[old].verificationAssurance};}
for(const raw of input.coverage??[]){const c=validateCoverage(raw);if(helperOwnedCoverage(c.domain))throw new CsoError('INVALID_SCHEMA',`Coverage domain is helper-owned: ${c.domain}`);const i=report.coverage.findIndex(x=>x.domain===c.domain&&x.scope===c.scope);if(i<0)report.coverage.push(c);else report.coverage[i]=c;}
if(input.gaps)report.gaps=strings(input.gaps,'gaps');
if(input.modelUsage){const u=object(input.modelUsage,'model usage');rejectUnexpected(u,['source','tokens','cost'],'model usage');if(!Number.isInteger(u.tokens)||u.tokens<0||('cost'in u&&(typeof u.cost!=='number'||!Number.isFinite(u.cost)||u.cost<0)))throw new CsoError('INVALID_SCHEMA','Model usage must be host-reported finite nonnegative numbers');report.modelUsage={source:string(u.source,'usage source'),tokens:u.tokens,...(typeof u.cost==='number'?{cost:u.cost}:{})};}
let recheckClaim:BoundRecheckClaim|undefined;
if(input.recheck){const rawClaim=object(input.recheck,'recheck claim');rejectUnexpected(rawClaim,['findingId','outcome','evidence','rootCause'],'recheck claim');if(!report.parent||input.recheck.findingId!==report.parent.findingId)throw new CsoError('INVALID_SCHEMA','Recheck claim must target the linked original finding');const outcome=input.recheck.outcome;if(!['open','resolved','unknown'].includes(outcome))throw new CsoError('INVALID_SCHEMA','Recheck needs an open, resolved, or unknown outcome');const boundary=originalBoundary(report),claim={findingId:string(input.recheck.findingId,'findingId'),outcome,evidence:bindRecheckEvidence(input.recheck.evidence,dir,manifest,boundary,outcome),rootCause:string(input.recheck.rootCause,'rootCause')};recheckClaim=claim;}
// A claim can close a prior finding. Publish it only after every report
// mutation it relies on is durably accepted, so a failed submission never
// leaves closure evidence behind.
saveReport(dir,report);if(recheckClaim)writeHelperJson(join(dir,'recheck-claim.json'),recheckClaim);return {runId:report.runId,findings:report.findings.length,completeness:report.completeness};});
}
function finish(args:string[]){const {dir}=run(args);if(args.length)throw new CsoError('INVALID_ARGUMENT','finish takes only a run ID');return withLock(dir,()=>{
const report=loadReport(dir),manifest=readJson(join(dir,'snapshot.json'));assertSnapshot(dir,manifest);for(const c of report.coverage)if(c.status==='not_assessed'&&!c.domain.startsWith('scanner:')&&!report.gaps.includes(`${c.domain}: not assessed`))report.gaps.push(`${c.domain}: not assessed`);
if(!report.application.actors.length&&!report.gaps.includes('Application model was not completed'))report.gaps.push('Application model was not completed');report.completeness=completeness(report);
const persistTerminal=()=>{report.status='finished';if(!report.events.some(e=>e.kind==='terminal'))event(report,'terminal','Audit finished and retained according to the private-state policy');saveReport(dir,report);return{runId:report.runId,status:report.status,completeness:report.completeness,report:'report.md'};};
if(report.parent&&fs.existsSync(join(dir,'recheck-claim.json'))){const boundary=originalBoundary(report),claim=validateBoundRecheckClaim(readJson(join(dir,'recheck-claim.json')),dir,manifest,boundary);if(claim.outcome==='resolved'){
if(report.completeness!=='complete')throw new CsoError('INVALID_SCHEMA','Partial or incompatible rechecks cannot establish closure');
const originalDir=boundary.dir;return withLock(originalDir,()=>{const original=loadReport(originalDir),finding=original.findings.find(f=>f.id===claim.findingId);
if(report.repoId!==original.repoId)throw new CsoError('INCOMPATIBLE_INPUT','Recheck repository identity differs from the original audit');
const survivingVariant=!!finding&&report.findings.some(candidate=>candidate.fingerprint===finding.fingerprint||rootCauseIdentity(candidate.rootCause)===rootCauseIdentity(finding.rootCause)||(candidate.advisoryIds.length>0&&candidate.advisoryIds.some(id=>finding.advisoryIds.includes(id))));
if(!finding||finding.id!==boundary.finding.id||rootCauseIdentity(finding.rootCause)!==rootCauseIdentity(claim.rootCause)||survivingVariant)throw new CsoError('INVALID_SCHEMA','Closure needs matching root cause, fresh caller and original-boundary evidence, and no surviving root-cause or advisory variant');
const result=persistTerminal();if(finding.closure!=='resolved'){finding.closure='resolved';event(original,'closure',`Fresh recheck ${report.runId} resolved ${finding.id}`);saveReport(originalDir,original);}return result;});
}}return persistTerminal();});}
async function inspect(args:string[]){const {dir}=run(args);if(args.length)throw new CsoError('INVALID_ARGUMENT','inspect takes one run ID');const report=loadReport(dir),manifest=readJson(join(dir,'snapshot.json')) as SnapshotManifest;assertSnapshot(dir,manifest);const rawSensitive=readJson(join(dir,'sensitive-evidence.json')),sensitiveEvidence=Array.isArray(rawSensitive)?rawSensitive.map(item=>{if(!item||typeof item!=='object'||typeof item.path!=='string')return item;const id=snapshotPathHandleId(item.path),exact=id?manifest.entries.find(entry=>entry.pathId===id)?.path:item.path;if(!exact)throw new CsoError('INCOMPATIBLE_INPUT','Sensitive-evidence path handle is outside the snapshot');return{...item,...publicSnapshotPath(manifest,exact)};}):rawSensitive;emit({report,manifest:publicSnapshotManifest(manifest),history:readJson(join(dir,'history-status.json')),sensitiveEvidence,preparation:fs.existsSync(join(dir,'preparation.json'))?readJson(join(dir,'preparation.json')):undefined,recovery:recoveryEvents(dir)});}
async function read(args:string[]){const {dir}=run(args);if(args.length!==1)throw new CsoError('INVALID_ARGUMENT','read requires one path or opaque handle');const manifest=readJson(join(dir,'snapshot.json')) as SnapshotManifest;assertSnapshot(dir,manifest);const selected=resolveSnapshotPath(manifest,args[0],true),full=containedFile(join(dir,'readable'),selected.path),data=readBoundedStable(full,1024*1024,'Snapshot path');emit(data.toString('utf8'));}
async function history(args:string[]){const {dir}=run(args),manifest=readJson(join(dir,'snapshot.json')) as SnapshotManifest;assertSnapshot(dir,manifest);let selected:string|undefined;if(args.length)selected=resolveSnapshotPath(manifest,args.shift(),false,'History path',true).path;if(args.length)throw new CsoError('INVALID_ARGUMENT','history accepts at most one path or opaque handle');const status=readJson(join(dir,'history-status.json'));if(status.status!=='captured'||!fs.existsSync(join(dir,'history.txt')))throw new CsoError('MISSING_INPUT',status.gap||'Historical evidence was not retained');const raw=fs.readFileSync(join(dir,'history.txt'),'utf8');if(!selected){emit(raw);return;}
const displayPath=publicSnapshotPath(manifest,selected).displayPath??selected,retained=historyForPath(raw,displayPath);emit(retained??`No retained patch hunks for ${displayPath}`);}
function resume(args:string[]){const {dir}=run(args);if(args.length)throw new CsoError('INVALID_ARGUMENT','resume takes one run ID');return withLock(dir,()=>{const report=loadReport(dir),manifest=readJson(join(dir,'snapshot.json'));if(report.status==='finished')throw new CsoError('INVALID_SCHEMA','A finished audit cannot be resumed');assertSnapshot(dir,manifest);for(const message of recoveryEvents(dir))if(!report.events.some(e=>e.kind==='watchdog-recovery'&&e.message===message))event(report,'watchdog-recovery',message);if(Date.now()>=Date.parse(report.deadline)){report.status='interrupted';event(report,'deadline','Original budget is exhausted; resume did not replenish it');saveReport(dir,report);throw new CsoError('DEADLINE','Original run budget is exhausted');}report.status='running';event(report,'resume','Continued retained snapshot under original policy');saveReport(dir,report);return {runId:report.runId,deadline:report.deadline,policy:report.policy,recovery:recoveryEvents(dir)};});}
function importV2(args:string[]){if(args.length!==1)throw new CsoError('INVALID_ARGUMENT','import-v2 requires one report file');const legacy=sanitizeForJson(importLegacy(readInput(args[0]))) as ReturnType<typeof importLegacy>,id=sha256(JSON.stringify(legacy)),dir=secureDirectory(join(privateRoot(),'legacy-imports'));writeJson(join(dir,`${id}.json`),legacy);return {id,path:`legacy-imports/${id}.json`,warning:legacy.warning,report:legacy};}
function inspectV2(args:string[]){if(args.length!==1||!/^[a-f0-9]{64}$/.test(args[0]??''))throw new CsoError('INVALID_ARGUMENT','inspect-v2 requires the 64-character import ID');const id=args[0],file=join(secureDirectory(join(privateRoot(),'legacy-imports')),`${id}.json`);if(!fs.existsSync(file))throw new CsoError('MISSING_INPUT','Legacy report import was not found or expired');const report=readJson(file);if(report?.schemaVersion!==2||report?.readOnly!==true||!Array.isArray(report?.findings))throw new CsoError('INCOMPATIBLE_INPUT','Stored legacy report is incompatible');if(sha256(JSON.stringify(report))!==id)throw new CsoError('INCOMPATIBLE_INPUT','Stored legacy report identity is inconsistent');return{id,report};}
async function scanner(args:string[],sarif=false){
const {dir}=run(args);
if(sarif?args.length!==1:(args.length<1||args.length>2||!SCANNER_IDS.includes(args[0] as ScannerId)))throw new CsoError('INVALID_ARGUMENT',sarif?'import-sarif needs one file':'scan requires a supported scanner ID and optional request JSON');
const id=args[0] as ScannerId,request=!sarif?validateScannerRequest(args[1]?readInput(args[1]):{},id):undefined;
// A live writer owns the lock throughout the bounded scan. Finish, submit and
// concurrent imports cannot replace coverage or retire the run underneath it.
return await withLock(dir,async()=>{
const report=loadReport(dir);requireTime(report);if(report.status!=='running')throw new CsoError('INVALID_SCHEMA','Scanner evidence can only enter a running audit');
let record:any;
if(sarif){
const file=callerPath(args[0]);
try{
const data=readBoundedStable(file,1024*1024,'SARIF file'),out=importSarif(data.toString('utf8'),{sourceRoot:'/source'});record={outcome:out,coverage:scannerCoverage(out,report.policy.scope),provenance:{kind:'untrusted SARIF import',sourceHash:sha256(data)}};
}catch(error){if(error instanceof CsoError)throw error;throw new CsoError('MISSING_INPUT','SARIF file is missing or unreadable');}
}else{
event(report,`scanner-attempt:${id}`,'Started bounded scanner collection; completion requires an immutable outcome artifact');saveReport(dir,report);
record=await executeScanner({id,runId:report.runId,runDir:dir,manifest:readJson(join(dir,'snapshot.json')),policy:report.policy,executionDeadline:Date.parse(report.deadline)-60_000,platform:platform(),request,watchdogPath:join(dirname(process.execPath),'gstack-cso-watchdog')});
}
const outcome=record.outcome,originalCount=outcome.candidates.length;
// Normalization can expand a 1 MiB scanner payload. Retain supported-size
// candidate evidence and disclose omissions instead of saving unreadable state.
while(Buffer.byteLength(JSON.stringify(record,null,2))>950_000&&outcome.candidates.length)outcome.candidates.splice(Math.max(0,outcome.candidates.length-Math.max(1,Math.ceil(outcome.candidates.length/4))));
if(outcome.candidates.length<originalCount){outcome.status='partial';outcome.gaps.push({code:'OUTPUT_LIMIT',message:`${originalCount-outcome.candidates.length} scanner candidates withheld to fit the bounded immutable artifact`});record.coverage=scannerCoverage(outcome,report.policy.scope);}
if(Buffer.byteLength(JSON.stringify(record,null,2))>1024*1024)throw new CsoError('PERSISTENCE_FAILED','Scanner result exceeds the private artifact limit; no saved report is claimed');
record=persistableArtifact(record,'Scanner outcome');const artifactId=`${outcome.tool}-${sha256(canonical(record)).slice(0,16)}-${randomBytes(8).toString('hex')}`,file=join(dir,'scanner-outcomes',`${artifactId}.json`);
writeJsonExclusive(file,record);
record.coverage.evidence.push(`Immutable outcome: ${artifactId}`);
report.coverage.push(record.coverage);event(report,'scanner-outcome',`${artifactId}: ${outcome.status}; ${outcome.candidates.length} candidates`);saveReport(dir,report);
return {...outcome,artifactId,artifact:`scanner-outcomes/${artifactId}.json`,provenance:record.provenance};
});
}
function scannerOutcome(args:string[]){const {dir}=run(args);if(args.length!==1||!/^[a-z0-9-]{1,40}-[a-f0-9]{16}-[a-f0-9]{16}$/.test(args[0]))throw new CsoError('INVALID_ARGUMENT','scanner-outcome requires one immutable scanner artifact ID');return readJson(join(dir,'scanner-outcomes',`${args[0]}.json`));}
function recheckOriginalDirectory(repo:string,findingId:string,requestedRun?:string):{dir:string;runId:string}{
const currentRepoId=repoId(repo),root=privateRoot(),repoDir=join(root,currentRepoId),runPattern=/^\d{13}-[a-f0-9]{16}$/;
if(requestedRun){
if(!runPattern.test(requestedRun))throw new CsoError('INVALID_ARGUMENT','Run identifier must be the ID returned by start');
const dir=join(repoDir,requestedRun);if(!fs.existsSync(dir))throw new CsoError('MISSING_INPUT','Original run was not found for the current repository or has expired');
return{dir:secureDirectory(dir),runId:requestedRun};
}
if(!fs.existsSync(repoDir))throw new CsoError('MISSING_INPUT','No finished original audit contains this finding in the current repository');
const matches:{dir:string;runId:string}[]=[],directory=fs.opendirSync(secureDirectory(repoDir));let visited=0;
try{let entry:fs.Dirent|null;while((entry=directory.readSync())!==null){
if(++visited>REPLAY_LOOKUP_MAX_ENTRIES)throw new CsoError('INSUFFICIENT_CAPACITY',`Recheck lookup exceeded ${REPLAY_LOOKUP_MAX_ENTRIES} private state entries`);
if(!entry.isDirectory()||!runPattern.test(entry.name))continue;const dir=join(repoDir,entry.name),reportPath=join(dir,'report.json');if(!fs.existsSync(reportPath))continue;const report=loadReport(dir);
if(report.runId!==entry.name||report.repoId!==currentRepoId)throw new CsoError('INCOMPATIBLE_INPUT','Retained original audit identity does not match its repository state path');
if(report.status==='finished'&&!report.parent&&report.findings.some(f=>f.id===findingId))matches.push({dir,runId:entry.name});
}}finally{directory.closeSync();}
if(!matches.length)throw new CsoError('MISSING_INPUT','No finished original audit contains this finding in the current repository');
if(matches.length>1)throw new CsoError('INVALID_ARGUMENT',`Finding matches ${matches.length} finished original audits; use --run RUN to select one`);
return matches[0];
}
async function recheck(args:string[],dependencies:CsoCliDependencies){const startedAt=new Date();if(!args.length)throw new CsoError('INVALID_ARGUMENT','recheck requires a finding ID');const findingId=args.shift()!;if(!/^[a-f0-9]{32}$/.test(findingId))throw new CsoError('INVALID_ARGUMENT','Finding identifier must be the 32-character ID reported by CSO');const repo=callerPath(need(args,'--repo')),requestedRun=args.includes('--run')?need(args,'--run'):undefined;if(args.length)throw new CsoError('INVALID_ARGUMENT',`Unknown recheck argument: ${args[0]}`);if(!fs.existsSync(repo)||!fs.statSync(repo).isDirectory())throw new CsoError('MISSING_INPUT','Repository directory does not exist');assertStateOutside(repo);retention(startedAt.getTime(),{deadlineMs:startedAt.getTime()+RETENTION_MAINTENANCE_MS,maxEntries:RETENTION_MAX_ENTRIES});const selected=recheckOriginalDirectory(repo,findingId,requestedRun),runId=selected.runId,originalDir=selected.dir;return await withLock(originalDir,async()=>{const original=loadReport(originalDir);if(original.runId!==runId||original.repoId!==repoId(repo))throw new CsoError('INCOMPATIBLE_INPUT','Retained original audit identity does not match its repository state path');const finding=original.findings.find(f=>f.id===findingId);if(!finding)throw new CsoError('MISSING_INPUT','Original finding does not exist');if(original.status!=='finished'||original.parent)throw new CsoError('INVALID_SCHEMA','Recheck requires a finished original audit');
// Keep the original immutable while the fresh snapshot is captured and
// until its child lineage report has been durably published.
const oldManifest=readJson(join(originalDir,'snapshot.json'));
const preserveBase=original.policy.diff||Boolean(original.source.baseCommit),report=await start(['--repo',repo,...(original.policy.mode==='comprehensive'?['--comprehensive']:[]),...(original.policy.diff?['--diff']:[]),...(preserveBase?['--base',original.policy.base]:[]),'--budget',String(original.policy.budgetSeconds),...(original.policy.offline?['--offline']:[]),...(original.policy.scope==='default'?[]:original.policy.scope.startsWith('domain:')?['--scope',original.policy.scope.slice(7)]:[`--${original.policy.scope}`])],dependencies,{runId,findingId,kind:'recheck'},oldManifest.headCommit,startedAt);
return {runId:report.runId,parent:report.parent};});}
function recordReview(args:string[]){
const {dir}=run(args);if(!args.length)throw new CsoError('INVALID_ARGUMENT','record-review requires a request JSON file and --producer ID');const raw=readInput(args.shift()!),producer=need(args,'--producer');if(args.length)throw new CsoError('INVALID_ARGUMENT',`Unknown record-review argument: ${args[0]}`);const request=validateVerificationRequest(raw);
return withLock(dir,()=>{const report=loadReport(dir);requireTime(report);if(report.policy.mode!=='comprehensive'||report.status!=='running'||!report.findings.some(f=>f.id===request.findingId&&f.evidence==='supported'))throw new CsoError('MISSING_INPUT','Review artifact must target a supported finding in a running comprehensive audit');const artifact=persistableArtifact(makeReviewArtifact(report.runId,request,string(producer,'producer identity',200)),'Repair review artifact');writeJsonExclusive(join(dir,'reviews',`${artifact.id}.json`),artifact);event(report,'repair-review',`Self-attested review artifact ${artifact.id} bound the proposed repair; reviewer independence is not host-verifiable`);saveReport(dir,report);return{reviewArtifactId:artifact.id,reviewAssurance:artifact.assurance,patchHash:artifact.patchHash,requestHash:artifact.requestHash};});
}
function publicPlanArgument(manifest:SnapshotManifest,arg:string,paths:string[]):string{
for(const path of [...paths].sort((a,b)=>b.length-a.length)){const reference=publicSnapshotPath(manifest,path).path;if(reference===path)continue;if(arg===path)return reference;if(arg===`./${path}`)return `./${reference}`;}
return arg;
}
function publicTestPlan(manifest:SnapshotManifest,plan:ReturnType<typeof canonicalTestPlan>){return{...plan,commands:plan.commands.map(command=>({...command,args:command.args.map(arg=>publicPlanArgument(manifest,arg,plan.files))})),files:plan.files.map(path=>publicSnapshotPath(manifest,path).path)};}
function publicStartPlan(manifest:SnapshotManifest,plan:ReturnType<typeof canonicalStartPlan>){return{...plan,command:{...plan.command,args:plan.command.args.map(arg=>publicPlanArgument(manifest,arg,plan.entrypointFiles))},entrypointFiles:plan.entrypointFiles.map(path=>publicSnapshotPath(manifest,path).path)};}
function testPlan(args:string[]){const {dir}=run(args);if(args.length!==1||!['node','bun','python','rails'].includes(args[0]))throw new CsoError('INVALID_ARGUMENT','test-plan requires one supported stack');const stack=args[0] as 'node'|'bun'|'python'|'rails',manifest=readJson(join(dir,'snapshot.json')) as SnapshotManifest;assertSnapshot(dir,manifest);const preparation=inspectPreparation(join(dir,'snapshot'),stack);if(preparation.status!=='ready')throw new CsoError('PREREQUISITE',preparation.prerequisites.map(item=>item.message).join('; ')||`${stack} preparation metadata is incomplete`);return{stack,runtimeProfile:preparation.runtimeProfile,...publicTestPlan(manifest,canonicalTestPlan(join(dir,'snapshot'),stack))};}
function runtimePlan(args:string[]){const {dir}=run(args);if(!args.length||!['node','bun','python','rails'].includes(args[0]))throw new CsoError('INVALID_ARGUMENT','runtime-plan requires one supported stack and --port PORT');const stack=args.shift() as 'node'|'bun'|'python'|'rails',rawPort=need(args,'--port');if(args.length)throw new CsoError('INVALID_ARGUMENT',`Unknown runtime-plan argument: ${args[0]}`);const port=Number(rawPort);if(!Number.isInteger(port)||port<1024||port>65535)throw new CsoError('INVALID_ARGUMENT','--port must be an integer from 1024 to 65535');const manifest=readJson(join(dir,'snapshot.json')) as SnapshotManifest;assertSnapshot(dir,manifest);const preparation=inspectPreparation(join(dir,'snapshot'),stack);if(preparation.status!=='ready')throw new CsoError('PREREQUISITE',preparation.prerequisites.map(item=>item.message).join('; ')||`${stack} preparation metadata is incomplete`);return{stack,runtimeProfile:preparation.runtimeProfile,start:publicStartPlan(manifest,canonicalStartPlan(join(dir,'snapshot'),stack,port)),tests:publicTestPlan(manifest,canonicalTestPlan(join(dir,'snapshot'),stack))};}
function platform(): 'linux/amd64'|'linux/arm64'{if(!['linux','darwin'].includes(process.platform)||!['x64','arm64'].includes(process.arch))throw new CsoError('PREREQUISITE','Contained target execution requires a Linux or macOS host with amd64/arm64 Linux Docker images');return process.arch==='arm64'?'linux/arm64':'linux/amd64';}
function watchdog():string{const p=join(dirname(process.execPath),process.platform==='win32'?'gstack-cso-watchdog.exe':'gstack-cso-watchdog');if(!fs.existsSync(p))throw new CsoError('ISOLATION_FAILED','Trusted detached watchdog is missing');return p;}
function closureStateFile(dir:string,findingId:string,phase:'before'|'after',plan:unknown):string{
return join(dir,'dependency-closures',`${findingId}-${phase}-${sha256(canonical(plan)).slice(0,16)}.json`);
}
function retainClosure(path:string,closure:DependencyClosure):void{
if(fs.existsSync(path)){if(canonical(readJson(path))!==canonical(closure))throw new CsoError('INCOMPATIBLE_INPUT','Retained dependency closure conflicts with this preparation plan');return;}
writeJsonExclusive(path,persistableArtifact(closure,'Dependency closure'));
}
function bindArchiveHashes(target:string[],closures:{before:DependencyClosure;after:DependencyClosure}):void{
const hashes=[...new Set([...closures.before.archives,...closures.after.archives].map(archive=>archive.sha256))].sort();target.splice(0,target.length,...hashes);
}
function preparedVerificationExecutor(options:{dir:string;findingId:string;runtimeProfile:string;stack:'node'|'bun'|'python'|'rails';targetPlatform:'linux/amd64'|'linux/arm64';deadline:number;offline:boolean;runtimeCatalog:RuntimeCatalog;preparation:PreparationExecutor;delegate:VerificationExecutor;beforePlan:ReturnType<typeof inspectPreparation>;beforeAdmission:ReturnType<typeof admitPreparationRuntime>;beforeClosure:DependencyClosure;closures:{before:DependencyClosure;after:DependencyClosure};archiveHashes:string[];proofs:{before:PreparationProof;after:PreparationProof};replay?:{before:DependencyClosure;after:DependencyClosure};persistClosures:boolean;}):VerificationExecutor{
let beforeProjectToolchainHash:string|undefined;
return {observe:async(source,phase,request,runtime,verifier,work,control,_execution,testEvidence,witness)=>{
const plan=phase==='before'?options.beforePlan:inspectPreparation(source,options.stack);
if(plan.status!=='ready')throw new CsoError('PREREQUISITE',plan.prerequisites.map(item=>item.message).join('; ')||`${options.stack} dependency metadata is not ready`);
const admission=phase==='before'?options.beforeAdmission:admitPreparationRuntime({plan,platform:options.targetPlatform,profile:options.runtimeProfile,catalog:options.runtimeCatalog});
if(admission.runtime.id!==runtime.id||admission.runtime.image!==runtime.image)throw new CsoError('INCOMPATIBLE_INPUT','Prepared verification runtime changed between source phases');
const state=closureStateFile(options.dir,options.findingId,phase,plan),supplied=options.replay?.[phase],retained=!supplied&&fs.existsSync(state)?readJson(state) as DependencyClosure:undefined;
const closure=phase==='before'?(supplied??options.beforeClosure):await options.preparation.acquire({plan,admission,snapshot:source,deadline:options.deadline,offline:options.offline||Boolean(supplied),existingClosure:supplied??retained});
options.closures[phase]=closure;
bindArchiveHashes(options.archiveHashes,options.closures);
if(options.persistClosures)retainClosure(state,closure);
let database:RailsDatabaseSelection|undefined;
if(options.stack==='rails'){
const selected=plan.database?.selected;
if(!selected)throw new CsoError('PREREQUISITE','Rails automatic verification could not select one locked database adapter from static test configuration');
database=selected==='postgresql'?{adapter:'postgresql',sidecar:admitPreparationSidecar({platform:options.targetPlatform,catalog:options.runtimeCatalog})}:{adapter:'sqlite'};
}
const prepared=await options.preparation.prepareOffline({plan,admission,snapshot:source,closure,deadline:options.deadline,database});
try{
options.proofs[phase]={schemaVersion:1,dependencyClosureHash:prepared.dependencyClosureHash,configurationHash:prepared.configurationHash,
sourceProjectionHash:prepared.sourceProjectionHash,preparedManifestHash:prepared.preparedManifestHash,preparedDependencyHash:prepared.preparedDependencyHash,receiptHash:prepared.receiptHash,
executionEnvironmentHash:sha256(canonical(prepared.executionEnvironment)),databaseHash:prepared.databaseHash,
transformations:prepared.transformations};
const sourceTests=canonicalTestPlan(source,options.stack),preparedTests=canonicalTestPlan(prepared.preparedRoot,options.stack),
sourceStart=canonicalStartPlan(source,options.stack,request.port),preparedStart=canonicalStartPlan(prepared.preparedRoot,options.stack,request.port);
if(sourceTests.signature!==preparedTests.signature||sourceStart.signature!==preparedStart.signature||verificationHarnessHash(request,source)!==verificationHarnessHash(request,prepared.preparedRoot))throw new CsoError('ISOLATION_FAILED','Offline lifecycle execution changed the canonical start, test, or harness inputs');
if(sourceTests.toolchain==='project'){
if(phase==='before')beforeProjectToolchainHash=prepared.preparedDependencyHash;
else if(!beforeProjectToolchainHash||prepared.preparedDependencyHash!==beforeProjectToolchainHash)throw new CsoError('ASSERTION_FAILED','Offline preparation changed the project-installed test toolchain between source phases');
}
const protectedPaths=new Set([...request.boundaryFiles,...request.testFiles,...sourceStart.entrypointFiles,...request.changes.map(item=>item.path)]);
if(prepared.transformations.some(item=>protectedPaths.has(item.path)))throw new CsoError('ISOLATION_FAILED','Synthetic preparation transformation overlaps a security boundary, startup input, or test input');
return await options.delegate.observe(prepared.preparedRoot,phase,request,runtime,verifier,work,control,
{environment:prepared.executionEnvironment,database:prepared.database},testEvidence,witness);
}
finally{await options.preparation.dispose(prepared);}
}};
}
async function verify(args:string[],dependencies:CsoCliDependencies){const {dir}=run(args);if(args.length!==1)throw new CsoError('INVALID_ARGUMENT','verify requires one request JSON file');const raw=readInput(args[0]),request=validateVerificationRequest(raw);
return await withLock(dir,async()=>{const report=loadReport(dir),manifest=readJson(join(dir,'snapshot.json')) as SnapshotManifest;assertSnapshot(dir,manifest);requireTime(report);if(report.policy.mode!=='comprehensive'||report.status!=='running')throw new CsoError('INVALID_SCHEMA','Only a running comprehensive audit can request target execution');const finding=report.findings.find(f=>f.id===request.findingId&&f.evidence==='supported');if(!finding)throw new CsoError('MISSING_INPUT','Verification must target a supported finding in this run');
if(!request.review.artifactId)throw new CsoError('MISSING_INPUT','Verification requires a separately persisted independent repair-review artifact');const reviewArtifact=validateReviewArtifact(readJson(join(dir,'reviews',`${request.review.artifactId}.json`)),report.runId,request);
const findingPath=resolveSnapshotPath(manifest,finding.location.path,true,'Finding path').path;if(!request.boundaryFiles.some(path=>resolveSnapshotPath(manifest,path,true,'Boundary path').path===findingPath))throw new CsoError('INVALID_SCHEMA','Boundary files must include the finding location');
const attempts=report.events.filter(e=>e.kind===`verification-attempt:${finding.id}`).length;if(attempts>=3)throw new CsoError('DEADLINE','Three bounded harness/repair attempts have already been used for this finding');if(report.findings.filter(f=>['runtime_tested','tested'].includes(f.repair)).length>=3)throw new CsoError('INSUFFICIENT_CAPACITY','This run already produced three runtime-tested repairs');
const targetPlatform=platform();let runtime;try{runtime=selectRuntime(request.runtimeProfile,targetPlatform,dependencies.runtimeCatalog);}catch(error:any){throw new CsoError('PREREQUISITE',error?.message||'Qualified runtime is unavailable');}const verifier=runtime;
if(!['node','bun','python','rails'].includes(runtime.stack))throw new CsoError('INCOMPATIBLE_INPUT','Application verification requires an application runtime profile');const plan=inspectPreparation(join(dir,'snapshot'),runtime.stack as any);if(plan.status!=='ready')throw new CsoError('PREREQUISITE',plan.prerequisites.map(p=>p.message).join('; ')||'Runtime preparation metadata is incomplete');assertRuntimeCompatible(plan,runtime);writeJson(join(dir,`preparation-${runtime.stack}.json`),plan);
const endpoint=await dockerEndpoint(secureDirectory(join(dir,'home'))),watchdogPath=dependencies.watchdogPath(),attemptDeadline=Math.min(Date.now()+300_000,Date.parse(report.deadline)-60_000);
const admission=admitPreparationRuntime({plan,platform:targetPlatform,profile:runtime.id,catalog:dependencies.runtimeCatalog}),staging=secureDirectory(join(dir,'archive-staging')),
runner=new DockerPreparationSandboxRunner({endpoint,watchdogPath,runRoot:dir,controlRoot:secureDirectory(join(dir,'preparation-execution')),admission}),
preparation=new PreparationExecutor({cache:new PublicArchiveCache({root:publicArchiveCacheRoot(),stagingRoot:staging}),runner,materializationRoot:secureDirectory(join(dir,'archive-materializations'))});
const beforeState=closureStateFile(dir,finding.id,'before',plan),retainedBefore=fs.existsSync(beforeState)?readJson(beforeState) as DependencyClosure:undefined;
const beforeClosure=await preparation.acquire({plan,admission,snapshot:join(dir,'snapshot'),deadline:attemptDeadline,offline:report.policy.offline,existingClosure:retainedBefore});retainClosure(beforeState,beforeClosure);
const closures={before:beforeClosure,after:beforeClosure},archiveHashes=[...new Set(beforeClosure.archives.map(archive=>archive.sha256))].sort(),proofs={} as {before:PreparationProof;after:PreparationProof},delegate=new DockerVerificationExecutor(endpoint,watchdogPath,attemptDeadline,()=>{event(report,`verification-attempt:${finding.id}`,`Started bounded repair verification attempt ${attempts+1}`);saveReport(dir,report);});
const executor=preparedVerificationExecutor({dir,findingId:finding.id,runtimeProfile:runtime.id,stack:runtime.stack as 'node'|'bun'|'python'|'rails',targetPlatform,deadline:attemptDeadline,offline:report.policy.offline,runtimeCatalog:dependencies.runtimeCatalog,preparation,delegate,beforePlan:plan,beforeAdmission:admission,beforeClosure,closures,archiveHashes,proofs,persistClosures:true});
let result:Awaited<ReturnType<typeof verifyRepair>>;try{result=await verifyRepair({runId:report.runId,runDir:dir,manifest,rawRequest:raw,runtime,verifier,policyHash:ISOLATION_POLICY_HASH,auditPolicyHash:sha256(canonical(report.policy)),archives:archiveHashes,dependencyClosures:closures,preparation:proofs,reviewArtifact,executor,watchdogPath,attemptDeadline});}catch(error){if(error instanceof VerificationAttemptError){finding.reproduction=error.attempt.reproduction;finding.repair=error.attempt.repair;finding.reproductionAttemptId=error.attempt.id;event(report,error.attempt.repair==='proposed'?'repair-candidate':'verification-failed',error.attempt.repair==='proposed'?`${error.attempt.id}: external repair assertions passed; helper-authenticated external assertion witness required before certification`:`${error.attempt.id}: ${error.attempt.reproduction}; repair validation failed without issuing a bundle`);saveReport(dir,report);}throw error;}
finding.reproduction=result.manifest.before.security==='intended_failure'&&result.manifest.before.booted&&result.manifest.before.legitimate?'reproduced':result.manifest.before.security==='pass'?'disproved':result.manifest.before.booted?'inconclusive':'blocked';
finding.repair=result.manifest.result==='tested'?'tested':result.manifest.result==='runtime_tested'?'runtime_tested':'failed';if(['tested','runtime_tested'].includes(result.manifest.result)){finding.verificationId=result.manifest.id;finding.verificationAssurance={assertions:result.manifest.assertionAssurance!,testCompletion:result.manifest.testCompletionAssurance,review:result.manifest.reviewAssurance};delete finding.reproductionAttemptId;}event(report,'verification',`${result.manifest.id}: ${result.manifest.result}; assertion assurance ${result.manifest.assertionAssurance}; test completion assurance ${result.manifest.testCompletionAssurance}; review assurance ${result.manifest.reviewAssurance}`);saveReport(dir,report);return{result:result.manifest.result,verification:result.manifest,bundle:`bundles/${result.bundle.id}.json`};});}
function replayBundle(stored:{path:string;dir:string},id:string):any{const bundle=validateRepairBundle(readJson(stored.path),id),report=loadReport(stored.dir),finding=report.findings.find(f=>f.verificationId===id&&['tested','runtime_tested'].includes(f.repair));if(!['tested','runtime_tested'].includes(bundle.verification.result)||!finding)throw new CsoError('INCOMPATIBLE_INPUT','Only a helper-recorded runtime-tested or host-reviewed repair bundle can be replayed');return bundle;}
async function withReplayBundle<T>(id:string,deadline:number,fn:(stored:{path:string;dir:string},bundle:any)=>Promise<T>):Promise<T>{
if(!/^[a-f0-9]{32}$/.test(id))throw new CsoError('INVALID_ARGUMENT','Bundle identifier must be the 32-character ID returned by verify');
let visited=0,stored:{path:string;dir:string}|undefined;const admit=()=>{if(Date.now()>=deadline)throw new CsoError('DEADLINE','Replay exhausted its five-minute budget while locating the recorded bundle');if(++visited>REPLAY_LOOKUP_MAX_ENTRIES)throw new CsoError('INSUFFICIENT_CAPACITY',`Replay bundle lookup exceeded ${REPLAY_LOOKUP_MAX_ENTRIES} private state entries`);};
const root=privateRoot(),repos=fs.opendirSync(root);try{let repo:fs.Dirent|null;search:while((repo=repos.readSync())!==null){admit();if(!repo.isDirectory()||!/^[a-f0-9]{24}$/.test(repo.name))continue;const repoDir=join(root,repo.name),runs=fs.opendirSync(repoDir);try{let run:fs.Dirent|null;while((run=runs.readSync())!==null){admit();if(!run.isDirectory()||!/^\d{13}-[a-f0-9]{16}$/.test(run.name))continue;const candidate={dir:join(repoDir,run.name),path:join(repoDir,run.name,'bundles',`${id}.json`)};if(fs.existsSync(candidate.path)){stored=candidate;break search;}}}finally{runs.closeSync();}}}finally{repos.closeSync();}
if(!stored)throw new CsoError('MISSING_INPUT','Repair bundle was not found or expired');let matched=false,value!:T;await withLock(stored.dir,async()=>{if(!fs.existsSync(stored!.path))return;const bundle=replayBundle(stored!,id);matched=true;value=await fn(stored!,bundle);});if(matched)return value;throw new CsoError('MISSING_INPUT','Repair bundle was not found or expired');
}
function replayManifestValue(manifest:any):unknown{const {id:_,createdAt:__,witnessHash:___,before,after,...stable}=manifest,observation=(value:any)=>{const{output:_,...rest}=value;return rest;};return{...stable,before:observation(before),after:observation(after)};}
async function replay(args:string[],dependencies:CsoCliDependencies){const replayStarted=Date.now(),replayDeadline=replayStarted+300_000;retention(replayStarted,{deadlineMs:replayStarted+RETENTION_MAINTENANCE_MS,maxEntries:RETENTION_MAX_ENTRIES});if(!args.length)throw new CsoError('INVALID_ARGUMENT','replay requires a bundle ID');const id=args.shift()!,source=args.includes('--source')?callerPath(need(args,'--source')):undefined;if(args.length)throw new CsoError('INVALID_ARGUMENT',`Unknown replay argument: ${args[0]}`);return await withReplayBundle(id,replayDeadline,async(stored,bundle)=>{
if(bundle.requiredInputs.archives?.length&&!bundle.requiredInputs.dependencyClosures)throw new CsoError('MISSING_INPUT','Replay bundle predates retained dependency closures and cannot substitute current dependency state');
let workDir=stored.dir,manifest:any,temporary:string|undefined;const retained=join(stored.dir,'snapshot');
try{
const captureSupplied=async()=>{if(!source)throw new CsoError('MISSING_INPUT','Retained source expired; supply explicitly matching source');const temp=newRun(source);temporary=temp.dir;workDir=temp.dir;manifest=await capture(source,workDir,undefined,undefined,{deadlineMs:replayDeadline});if(manifest.executionHash!==bundle.requiredInputs.sourceHash||manifest.originalHash!==bundle.requiredInputs.originalHash)throw new CsoError('INCOMPATIBLE_INPUT','Supplied source does not match the bundle input hashes');};
if(fs.existsSync(retained)){manifest=readJson(join(stored.dir,'snapshot.json'));const expiresAt=typeof manifest?.expiresAt==='string'?Date.parse(manifest.expiresAt):Number.NaN;if(!Number.isFinite(expiresAt)||new Date(expiresAt).toISOString()!==manifest.expiresAt)throw new CsoError('INCOMPATIBLE_INPUT','Retained snapshot expiry is invalid');if(expiresAt<=Date.now())await captureSupplied();else assertSnapshot(stored.dir,manifest);}else await captureSupplied();
if(manifest.executionHash!==bundle.requiredInputs.sourceHash||manifest.originalHash!==bundle.requiredInputs.originalHash)throw new CsoError('INCOMPATIBLE_INPUT','Retained source hashes do not match the bundle');validateRepairBundle(bundle,id,join(workDir,'snapshot'),manifest);if(bundle.verification.policyHash!==ISOLATION_POLICY_HASH)throw new CsoError('INCOMPATIBLE_INPUT','Current helper isolation policy does not match the recorded bundle');const targetPlatform=bundle.requiredInputs.platform as 'linux/amd64'|'linux/arm64';let runtime;try{runtime=selectRuntime(bundle.verification.runtime.profile,targetPlatform,dependencies.runtimeCatalog);}catch(error:any){throw new CsoError('PREREQUISITE',error?.message||'Qualified replay runtime is unavailable');}const verifier=runtime;if(runtime.image!==bundle.requiredInputs.runtimeImage)throw new CsoError('INCOMPATIBLE_INPUT','Qualified runtime digest does not match the bundle');
const endpoint=await dockerEndpoint(secureDirectory(join(workDir,'home'))),watchdogPath=dependencies.watchdogPath(),attemptDeadline=replayDeadline,delegate=new DockerVerificationExecutor(endpoint,watchdogPath,attemptDeadline);let executor:VerificationExecutor=delegate,archives:string[]=[],dependencyClosures:{before:DependencyClosure;after:DependencyClosure}|undefined,proofs:{before:PreparationProof;after:PreparationProof}|undefined;
if(bundle.requiredInputs.dependencyClosures){if(!['node','bun','python','rails'].includes(runtime.stack))throw new CsoError('INCOMPATIBLE_INPUT','Replay dependency closure requires an application runtime');const replayClosures=bundle.requiredInputs.dependencyClosures as {before:DependencyClosure;after:DependencyClosure},plan=inspectPreparation(join(workDir,'snapshot'),runtime.stack as any);if(plan.status!=='ready')throw new CsoError('PREREQUISITE',plan.prerequisites.map((item:any)=>item.message).join('; ')||'Replay dependency metadata is not ready');const admission=admitPreparationRuntime({plan,platform:targetPlatform,profile:runtime.id,catalog:dependencies.runtimeCatalog}),runner=new DockerPreparationSandboxRunner({endpoint,watchdogPath,runRoot:workDir,controlRoot:secureDirectory(join(workDir,'preparation-execution')),admission}),preparation=new PreparationExecutor({cache:new PublicArchiveCache({root:publicArchiveCacheRoot(),stagingRoot:secureDirectory(join(workDir,'archive-staging'))}),runner,materializationRoot:secureDirectory(join(workDir,'archive-materializations'))}),beforeClosure=await preparation.acquire({plan,admission,snapshot:join(workDir,'snapshot'),deadline:attemptDeadline,offline:true,existingClosure:replayClosures.before});dependencyClosures={before:beforeClosure,after:replayClosures.after};archives=[...new Set([...beforeClosure.archives,...replayClosures.after.archives].map(archive=>archive.sha256))].sort();proofs={} as {before:PreparationProof;after:PreparationProof};executor=preparedVerificationExecutor({dir:workDir,findingId:bundle.request.findingId,runtimeProfile:runtime.id,stack:runtime.stack as any,targetPlatform,deadline:attemptDeadline,offline:true,runtimeCatalog:dependencies.runtimeCatalog,preparation,delegate,beforePlan:plan,beforeAdmission:admission,beforeClosure,closures:dependencyClosures,archiveHashes:archives,proofs,replay:replayClosures,persistClosures:false});}
const result=await verifyRepair({runId:bundle.runId,runDir:workDir,manifest,rawRequest:bundle.request,runtime,verifier,policyHash:ISOLATION_POLICY_HASH,auditPolicyHash:bundle.verification.auditPolicyHash,archives,dependencyClosures,preparation:proofs,reviewArtifact:bundle.reviewArtifact,executor,persist:false,watchdogPath,attemptDeadline});if(canonical(replayManifestValue(result.manifest))!==canonical(replayManifestValue(bundle.verification))||assertionWitnessReplayHash(result.bundle.witness!)!==assertionWitnessReplayHash(bundle.witness))throw new CsoError('INCOMPATIBLE_INPUT','Replay changed verification outcomes, preparation, or provenance inputs');const replayId=`${Date.now()}-${randomBytes(8).toString('hex')}`;writeJsonExclusive(join(stored.dir,'replays',`${replayId}.json`),{replayId,bundleId:id,verification:result.manifest,witness:result.bundle.witness});return{bundle:id,result:result.manifest.result,replay:result.manifest,replayId};
}finally{if(temporary)finalizeReplayTemporary(temporary);}
});
}
export async function dispatchCsoCommand(command:string,args:string[],dependencies:CsoCliDependencies):Promise<unknown>{
args=[...args];if(!['start','recheck','replay','doctor','provision-images'].includes(command)){const started=Date.now();retention(started,{deadlineMs:started+RETENTION_MAINTENANCE_MS,maxEntries:RETENTION_MAX_ENTRIES});}
let result:unknown;
switch(command){case'start':result=await start(args,dependencies);break;case'doctor':result=await doctor(args,dependencies);break;case'provision-images':result=await provisionImages(args,dependencies);break;case'resume':result=resume(args);break;case'inspect':await inspect(args);return;case'read':await read(args);return;case'history':await history(args);return;case'submit':result=submit(args);break;case'finish':result=finish(args);break;case'import-v2':result=importV2(args);break;case'inspect-v2':result=inspectV2(args);break;case'scan':result=await scanner(args);break;case'scanner-outcome':result=scannerOutcome(args);break;case'import-sarif':result=await scanner(args,true);break;case'record-review':result=recordReview(args);break;case'test-plan':result=testPlan(args);break;case'runtime-plan':result=runtimePlan(args);break;case'recheck':result=await recheck(args,dependencies);break;
case'verify':result=await verify(args,dependencies);break;case'replay':result=await replay(args,dependencies);break;case'patch-hash':if(args.length!==1)throw new CsoError('INVALID_ARGUMENT','patch-hash requires one request JSON file');result={patchHash:patchHash(validateVerificationRequest(readInput(args[0])))};break;default:throw new CsoError('INVALID_ARGUMENT',`Unknown command: ${command}`);}
return result;
}
const PRODUCTION_CLI_DEPENDENCIES:CsoCliDependencies=Object.freeze({runtimeCatalog:RUNTIME_CATALOG,scannerCatalog:SCANNER_CATALOG,catalogImageSession:productionCatalogImageSession,watchdogPath:watchdog});
async function main(){const args=process.argv.slice(2),command=args.shift();if(!command||command==='--help'||command==='help'){process.stdout.write(HELP+'\n');return;}if(command==='--version'){emit({version:VERSION,abi:ABI});return;}if(command==='schema'){emit(SCHEMA);return;}if(command==='__cso-assertion-witness'){if(args.length)throw new CsoError('INVALID_ARGUMENT','Assertion witness does not accept command arguments');await runAssertionWitnessChild();return;}const result=await dispatchCsoCommand(command,args,PRODUCTION_CLI_DEPENDENCIES);if(result!==undefined)emit(result);}
if(import.meta.main)main().catch(error=>{const e=error instanceof CsoError?error:new CsoError('INVALID_SCHEMA','The helper rejected an unexpected or unsafe input');try{process.stderr.write(redact(JSON.stringify({ok:false,error:{code:e.code,message:e.message}}))+'\n');}catch{process.stderr.write('{"ok":false,"error":{"code":"REDACTION_FAILED","message":"Error payload withheld"}}\n');}process.exitCode=1;});
+381
View File
@@ -0,0 +1,381 @@
/** CSO's versioned, host-independent evidence contract. Runtime claims are helper-owned. */
import { createHash } from 'node:crypto';
export const ABI = 3;
export const MAX_OUTPUT = 1024 * 1024;
const UNSAFE_STRING_CONTROLS=/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/u;
const UNSAFE_PROPERTY_CONTROLS=/[\x00-\x1f\x7f-\x9f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/u;
export type Completeness = 'complete' | 'partial' | 'not assessed';
export type Severity = 'critical' | 'high' | 'medium' | 'low' | 'informational';
export type ErrorCode = 'INVALID_ARGUMENT' | 'INVALID_SCHEMA' | 'MISSING_INPUT' | 'SNAPSHOT_RACE' |
'UNSAFE_PATH' | 'REDACTION_FAILED' | 'PERSISTENCE_FAILED' | 'TOOL_UNAVAILABLE' | 'TOOL_FAILED' |
'ISOLATION_FAILED' | 'INSUFFICIENT_CAPACITY' | 'DEADLINE' | 'CANCELLED' | 'PREREQUISITE' |
'INCOMPATIBLE_INPUT' | 'ASSERTION_FAILED';
export class CsoError extends Error {
constructor(public code: ErrorCode, message: string) { super(message); this.name = 'CsoError'; }
}
export const sha256 = (value: string | Buffer): string => createHash('sha256').update(value).digest('hex');
export const canonical = (value: unknown): string => {
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${canonical((value as any)[k])}`).join(',')}}`;
return JSON.stringify(value);
};
export interface CoverageRecord {
domain: string; scope: string; status: 'assessed' | 'partial' | 'not_assessed' | 'not_applicable';
method: string; gaps: string[]; exclusions: string[]; evidence: string[];
tool?: { name: string; version: string; freshness: string; outcome: string };
}
export interface ApplicationModel {
actors: string[]; assets: string[]; entrypoints: string[]; tenantBoundaries: string[];
sensitiveOperations: string[]; invariants: string[];
}
export interface FindingV3 {
id: string; fingerprint: string; title: string; rootCause: string;
location: { path: string; line: number; symbol: string }; advisoryIds: string[];
severity: Severity; confidence: 'high' | 'medium' | 'low'; confidenceRationale:string; evidence: 'supported' | 'hypothesis' | 'legacy_review';
attackerControl: string; impact: string; scenario: string; trace: string[]; references: string[]; recommendation: string;
challenge: { reviewer: string; independent: boolean; mode:'independent_agent'|'sequential_fallback'; callers: string; controls: string; counterevidence: string; conclusion: string };
dependency?: { affectedVersion: string; reachability: 'reachable' | 'unreachable' | 'unknown'; exposure: string; exploitation: string };
reproduction: 'not_attempted' | 'blocked' | 'inconclusive' | 'disproved' | 'reproduced';
repair: 'not_attempted' | 'proposed' | 'failed' | 'runtime_tested' | 'tested';
closure: 'open' | 'resolved' | 'unknown'; verificationId?: string; reproductionAttemptId?:string;
verificationAssurance?: { assertions:'authenticated_out_of_process'; testCompletion:'self_reported'|'authenticated_out_of_process'; review:'self_attested'|'host_verified' };
}
export interface RunPolicy {
mode: 'daily' | 'comprehensive'; scope: string; diff: boolean; base: string;
offline: boolean; budgetSeconds: number; maxWorkers: 3; maxRepairs: 3;
}
export interface RunReportV3 {
schemaVersion: 3; runId: string; repoId: string; createdAt: string; deadline: string;
status: 'running' | 'finished' | 'interrupted'; completeness: Completeness; policy: RunPolicy;
source: { root: string; snapshotHash: string; originalHash: string; baseCommit?: string;
transformations?: Array<{ path: string; handling: string }> };
application: ApplicationModel; coverage: CoverageRecord[]; findings: FindingV3[];
gaps: string[]; events: { at: string; kind: string; message: string }[];
parent?: { runId: string; findingId: string; kind: 'recheck' }; modelUsage?: { source: string; tokens: number; cost?: number };
}
export interface SnapshotEntry { path: string; pathId: string; originalHash: string; executionHash?: string; bytes: number; mode: number; transformation?: string }
export interface SnapshotPathIdentity { path: string; pathId: string }
export interface SnapshotManifest {
version: 3; createdAt: string; expiresAt: string; root: string; originalHash: string; executionHash: string;
entries: SnapshotEntry[]; deletedPaths?: SnapshotPathIdentity[]; headCommit?: string; baseCommit?: string; changedPaths?: string[];
}
export interface HttpAssertion {
name: string; path: string; method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
headers?: Record<string,string>; body?: string;
expected: { status: number; includes?: string; excludes?: string };
vulnerable?: { status: number; includes?: string; excludes?: string };
}
export interface Command { executable: string; args: string[] }
export interface VerificationRequest {
findingId: string; runtimeProfile: string; port: number; start: Command;
legitimate: HttpAssertion[]; security: HttpAssertion; existingTests: Command[];
fixtures: Record<string,string>;
boundaryFiles: string[]; testFiles:string[];
changes: { path: string; beforeSha256: string | null; after: string | null; effect: 'source' | 'configuration' | 'dependency' }[];
review: { reviewer: string; independent: boolean; rootCauseRepaired: boolean; featurePreserved: boolean;
boundaryMocks: boolean; rationale: string; reviewedPatchHash: string; artifactId?:string };
}
export interface RepairReviewArtifact {schemaVersion:3;id:string;runId:string;findingId:string;createdAt:string;producer:string;reviewer:string;assurance:'self_attested'|'host_verified';requestHash:string;patchHash:string;rootCauseRepaired:boolean;featurePreserved:boolean;boundaryMocks:boolean;rationale:string}
export interface RecheckEvidenceV3 {
kind: 'caller' | 'security_boundary';
path: string;
line: number;
observation: string;
}
export interface SubmissionV3 {
application?: ApplicationModel;
findings?: unknown[];
coverage?: unknown[];
gaps?: string[];
modelUsage?: { source: string; tokens: number; cost?: number };
recheck?: { findingId: string; outcome: 'open' | 'resolved' | 'unknown'; evidence: RecheckEvidenceV3[]; rootCause: string };
}
export interface VerificationObservation {
booted: boolean; legitimate: boolean; security: 'pass' | 'intended_failure' | 'inconclusive';
existingTests: boolean; output: string; inputHash: string;
}
export interface AssertionWitnessBinding {
schemaVersion: 1; protocol: 'gstack-cso-assertion-witness-v1'; nonce: string; phase: 'before' | 'after';
issuedAt: string; expiresAt: string; runId: string; findingId: string;
policyHash: string; auditPolicyHash: string;
runtime: { image: string; verifierImage: string; platform: string; profile: string };
runner: { testToolchain: 'runtime' | 'project'; startPlanHash: string; testPlanHash: string;
commandsHash: string; minimumPassingTestsHash: string };
sourceHash: string; dependencyHash: string; configurationHash: string; requestHash: string;
patchHash: string; harnessHash: string; assertionHash: string; fixturesHash: string;
}
export interface AssertionWitnessReceipt {
schemaVersion: 1; binding: AssertionWitnessBinding; keyId: string; publicKey: string;
observationHash: string; externalAssertionsPassed: boolean; diagnosticTestsPassed: boolean;
executions: Array<{ commandHash: string; exitCode: number; outputHash: string; minimumPassingTests: number;
executedTests: number; passingTests: number; reportedPassed: boolean }>;
signature: string;
}
export interface PreparationProof {
schemaVersion: 1; dependencyClosureHash: string; configurationHash: string; sourceProjectionHash: string;
preparedManifestHash: string; preparedDependencyHash: string; receiptHash: string; executionEnvironmentHash: string; databaseHash: string;
transformations: Array<{ path: string; sha256: string; mode: number; reason: string }>;
}
export interface VerificationManifest {
version: 3; id: string; runId: string; findingId: string; createdAt: string;
helperAbi: 3; runtime: { image: string; platform: string; profile: string };
testToolchain: 'runtime' | 'project';
policyHash: string; harnessHash: string; requestHash:string; startPlanHash:string; testPlanHash:string; fixturesHash: string; patchHash: string;
auditPolicyHash:string; originalSourceHash:string; transformationsHash:string; archivesHash:string; preparationHash?:string;
beforeSourceHash: string; afterSourceHash: string; beforeDependencies: string; afterDependencies: string;
beforeConfiguration: string; afterConfiguration: string;
before: VerificationObservation; after: VerificationObservation;
review: VerificationRequest['review']; reviewAssurance:'self_attested'|'host_verified';
assertionAssurance?:'authenticated_out_of_process';
testCompletionAssurance:'self_reported'|'authenticated_out_of_process';
witnessHash?: string;
result: 'runtime_tested' | 'tested' | 'failed' | 'inconclusive';
}
export interface RepairBundle {
schemaVersion: 3; runId: string; id: string; createdAt: string; expiresAt: string;
requiredInputs: { sourceHash: string; originalHash: string; runtimeImage: string; platform: string; archives: string[];
dependencyClosures?: { before: unknown; after: unknown } };
request: VerificationRequest; verification: VerificationManifest; transformations: SnapshotEntry[];
preparation?: { before: PreparationProof; after: PreparationProof }; reviewArtifact?:RepairReviewArtifact;
witness?: { before: AssertionWitnessReceipt; after: AssertionWitnessReceipt };
}
export function object(value: unknown, name = 'input'): Record<string, any> {
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new CsoError('INVALID_SCHEMA', `${name} must be an object`);
return value as Record<string,any>;
}
function exact(value:Record<string,any>,allowed:readonly string[],name:string):void{
for(const key of Object.keys(value))if(!allowed.includes(key))throw new CsoError('INVALID_SCHEMA',`Unexpected ${name} field: ${key}`);
}
function boolean(value:unknown,name:string):boolean{
if(typeof value!=='boolean')throw new CsoError('INVALID_SCHEMA',`${name} must be a boolean`);return value;
}
export function string(value: unknown, name: string, max = 8192): string {
if (typeof value !== 'string' || !value.trim() || value.length > max || UNSAFE_STRING_CONTROLS.test(value)) throw new CsoError('INVALID_SCHEMA', `${name} must be a nonempty string without unsafe control characters (maximum ${max})`);
return value;
}
export function strings(value: unknown, name: string): string[] {
if (!Array.isArray(value) || value.length > 1000) throw new CsoError('INVALID_SCHEMA', `${name} must be an array`);
return value.map((v,i) => string(v, `${name}[${i}]`));
}
export function oneOf<T extends string>(value: unknown, choices: readonly T[], name: string): T {
if (!choices.includes(value as T)) throw new CsoError('INVALID_SCHEMA', `${name} must be one of ${choices.join(', ')}`);
return value as T;
}
export function relativePath(value: unknown): string {
const p = string(value, 'relative path', 4096);
if (p.startsWith('/') || p.includes('\\') || /^[A-Za-z]:/.test(p) || p.split('/').some(x => !x || x === '.' || x === '..') || /[\x00-\x1f\x7f]/.test(p))
throw new CsoError('UNSAFE_PATH', 'Expected a contained relative path');
return p;
}
const SNAPSHOT_PATH_HANDLE=/^@cso-path\/\/([a-f0-9]{32})$/;
export function snapshotPathId(root:string,path:string):string{
// Validate the root for callers, but do not salt the opaque identity with its
// absolute checkout path. Replay must resolve the same retained path after a
// matching source tree is supplied from another checkout.
string(root,'snapshot root',8192);const relative=relativePath(path);
return sha256(canonical({kind:'cso-path-v3',path:relative})).slice(0,32);
}
export function snapshotPathHandle(pathId:string):string{
if(!/^[a-f0-9]{32}$/.test(pathId))throw new CsoError('INVALID_SCHEMA','Snapshot path ID must be 32 lowercase hexadecimal characters');
return `@cso-path//${pathId}`;
}
export function snapshotPathHandleId(value:unknown):string|undefined{
if(typeof value!=='string')return;
return SNAPSHOT_PATH_HANDLE.exec(value)?.[1];
}
export function snapshotReference(value:unknown):string{
const reference=string(value,'snapshot path or handle',4096);
return snapshotPathHandleId(reference)?reference:relativePath(reference);
}
export function snapshotOriginalIdentity(entries:Array<Pick<SnapshotEntry,'path'|'originalHash'|'mode'>>,deletedPaths:Array<Pick<SnapshotPathIdentity,'path'>>=[]):string{
const present=entries.map(entry=>[entry.path,entry.originalHash,entry.mode]);
// Preserve the original no-deletion identity for v3 artifacts already
// retained by pre-release builds. Any deletion changes the identity and is
// therefore impossible to strip from a manifest without detection.
return sha256(canonical(deletedPaths.length?{entries:present,deletedPaths:deletedPaths.map(item=>item.path).sort()}:present));
}
export function rootCauseIdentity(value:string):string{return value.normalize('NFKC').trim().replace(/\s+/g,' ').toLowerCase();}
function advisoryIdentities(values:string[]):string[]{return [...new Set(values.map(value=>value.normalize('NFKC').trim().toUpperCase()))].sort();}
export function fingerprint(f: Pick<FindingV3,'rootCause'|'location'|'advisoryIds'>): string {
// Titles, line shifts, severity, and generated descriptions are deliberately absent.
return sha256(canonical({ rootCause: rootCauseIdentity(f.rootCause), path: f.location.path, symbol: f.location.symbol, advisories: advisoryIdentities(f.advisoryIds) })).slice(0,32);
}
export function validateFinding(input: unknown): FindingV3 {
const v = object(input, 'finding'), loc = object(v.location,'location'), c = object(v.challenge,'challenge');
for (const reserved of ['reproduction','repair','closure','verificationId','reproductionAttemptId','verificationAssurance']) if (reserved in v)
throw new CsoError('INVALID_SCHEMA', `${reserved} is helper-owned`);
exact(v,['title','rootCause','location','advisoryIds','severity','confidence','confidenceRationale','evidence','attackerControl','impact','scenario','trace','references','recommendation','challenge','dependency'],'finding');
exact(loc,['path','line','symbol'],'location');
exact(c,['reviewer','independent','mode','callers','controls','counterevidence','conclusion'],'challenge');
const f: FindingV3 = {
id: '', fingerprint: '', title: string(v.title,'title'), rootCause: string(v.rootCause,'rootCause'),
location: { path: snapshotReference(loc.path), line: loc.line, symbol: string(loc.symbol,'symbol') },
advisoryIds: advisoryIdentities(strings(v.advisoryIds ?? [],'advisoryIds')),
severity: oneOf(v.severity,['critical','high','medium','low','informational'],'severity'),
confidence: oneOf(v.confidence,['high','medium','low'],'confidence'),
confidenceRationale: string(v.confidenceRationale,'confidenceRationale'),
evidence: oneOf(v.evidence,['supported','hypothesis'],'evidence'),
attackerControl: string(v.attackerControl,'attackerControl'), impact: string(v.impact,'impact'), scenario: string(v.scenario,'scenario'), trace: strings(v.trace,'trace'),
references: strings(v.references,'references'), recommendation: string(v.recommendation,'recommendation'),
challenge: { reviewer: string(c.reviewer,'reviewer'), independent: boolean(c.independent,'challenge.independent'), mode:oneOf(c.mode,['independent_agent','sequential_fallback'],'challenge.mode'), callers: string(c.callers,'callers'),
controls: string(c.controls,'controls'), counterevidence: string(c.counterevidence,'counterevidence'), conclusion: string(c.conclusion,'conclusion') },
reproduction: 'not_attempted', repair: 'not_attempted', closure: 'open',
};
if (!Number.isInteger(f.location.line) || f.location.line < 1) throw new CsoError('INVALID_SCHEMA','line must be a positive integer');
const fallbackLabel='sequential challenge; independent agent unavailable';
if((f.challenge.independent&&(f.challenge.mode!=='independent_agent'||f.challenge.reviewer===fallbackLabel))||(!f.challenge.independent&&(f.challenge.mode!=='sequential_fallback'||f.challenge.reviewer!==fallbackLabel)))throw new CsoError('INVALID_SCHEMA',`Challenge mode must bind either an independent agent or the exact fallback label: ${fallbackLabel}`);
if (f.evidence === 'supported' && (f.confidence === 'low' || !f.trace.length || !f.references.length)) throw new CsoError('INVALID_SCHEMA','Supported findings require a challenge, a trace, supporting references, and medium/high confidence');
if (v.dependency) {
const d = object(v.dependency);
exact(d,['affectedVersion','reachability','exposure','exploitation'],'dependency');
f.dependency = { affectedVersion: string(d.affectedVersion,'affectedVersion'), reachability: oneOf(d.reachability,['reachable','unreachable','unknown'],'reachability'), exposure: string(d.exposure,'exposure'), exploitation: string(d.exploitation,'exploitation') };
}
f.fingerprint = fingerprint(f); f.id = f.fingerprint; return f;
}
export function validateCoverage(input: unknown): CoverageRecord {
const v = object(input,'coverage');
exact(v,['domain','scope','status','method','gaps','exclusions','evidence','tool'],'coverage');
const c: CoverageRecord = { domain: string(v.domain,'domain'), scope: string(v.scope,'scope'),
status: oneOf(v.status,['assessed','partial','not_assessed','not_applicable'],'coverage status'),
method: string(v.method,'method'), gaps: strings(v.gaps,'gaps'), exclusions: strings(v.exclusions,'exclusions'), evidence: strings(v.evidence,'evidence') };
if (c.status === 'assessed' && (c.gaps.length || !c.evidence.length)) throw new CsoError('INVALID_SCHEMA','Assessed coverage needs evidence and no outstanding gaps');
if (c.status === 'partial' && (!c.gaps.length || !c.evidence.length)) throw new CsoError('INVALID_SCHEMA','Partial coverage needs assessed evidence and a concrete gap');
if (c.status === 'not_assessed' && !c.gaps.length) throw new CsoError('INVALID_SCHEMA','Unassessed coverage needs a concrete gap');
if (c.status === 'not_applicable' && (!c.evidence.length || c.gaps.length)) throw new CsoError('INVALID_SCHEMA','Non-applicability requires evidence and cannot retain an assessment gap');
if (v.tool) { const t = object(v.tool);exact(t,['name','version','freshness','outcome'],'coverage tool'); c.tool = {name:string(t.name,'tool name'), version:string(t.version,'tool version'), freshness:string(t.freshness,'freshness'), outcome:string(t.outcome,'outcome')}; }
return c;
}
export function validateCommand(value: unknown, name: string): Command {
const v=object(value,name), executable=string(v.executable,`${name}.executable`,4096), args=strings(v.args??[],`${name}.args`);
exact(v,['executable','args'],name);
if(!executable.startsWith('/')||executable.includes('..'))throw new CsoError('INVALID_SCHEMA',`${name}.executable must be an absolute in-container path`);
return {executable,args};
}
export function validateVerificationObservation(value:unknown):VerificationObservation{
const v=object(value,'verification observation');
for(const key of Object.keys(v))if(!['booted','legitimate','security','existingTests','output','inputHash'].includes(key))throw new CsoError('INVALID_SCHEMA',`Unexpected verification observation field: ${key}`);
if(typeof v.booted!=='boolean'||typeof v.legitimate!=='boolean'||typeof v.existingTests!=='boolean')throw new CsoError('INVALID_SCHEMA','Verification observation outcomes must be booleans');
if(typeof v.output!=='string'||v.output.length>8192||v.output.includes('\0'))throw new CsoError('INVALID_SCHEMA','Verification observation output must be a bounded string');
if(typeof v.inputHash!=='string'||(!/^$/.test(v.inputHash)&&!/^[a-f0-9]{64}$/.test(v.inputHash)))throw new CsoError('INVALID_SCHEMA','Verification observation inputHash must be empty or a sha256 hash');
return{booted:v.booted,legitimate:v.legitimate,security:oneOf(v.security,['pass','intended_failure','inconclusive'],'verification security outcome'),existingTests:v.existingTests,output:v.output,inputHash:v.inputHash};
}
function assertion(value: unknown,name:string):HttpAssertion {
const v=object(value,name), expected=object(v.expected,`${name}.expected`);
exact(v,['name','path','method','headers','body','expected','vulnerable'],name);
const oracle=(x:Record<string,any>,n:string)=>{exact(x,['status','includes','excludes'],n);if(!Number.isInteger(x.status)||x.status<100||x.status>599)throw new CsoError('INVALID_SCHEMA',`${n}.status must be an HTTP status`);return {status:x.status,...(x.includes===undefined?{}:{includes:string(x.includes,`${n}.includes`)}),...(x.excludes===undefined?{}:{excludes:string(x.excludes,`${n}.excludes`)})};};
const path=string(v.path,`${name}.path`,4096);if(!path.startsWith('/')||path.startsWith('//')||/[\r\n]/.test(path))throw new CsoError('INVALID_SCHEMA',`${name}.path must stay on numeric loopback`);
const headers:Record<string,string>={};if(v.headers!==undefined)for(const [k,val] of Object.entries(object(v.headers,`${name}.headers`))){if(!/^[A-Za-z0-9-]{1,100}$/.test(k)||typeof val!=='string'||val.length>8192||/[\r\n]/.test(val))throw new CsoError('INVALID_SCHEMA',`Invalid ${name} header`);headers[k]=val;}
return {name:string(v.name,`${name}.name`),path,method:oneOf(v.method,['GET','POST','PUT','PATCH','DELETE'],`${name}.method`),...(Object.keys(headers).length?{headers}:{}),...(v.body===undefined?{}:{body:string(v.body,`${name}.body`,65536)}),expected:oracle(expected,`${name}.expected`),...(v.vulnerable===undefined?{}:{vulnerable:oracle(object(v.vulnerable),`${name}.vulnerable`)})};
}
export function validateVerificationRequest(input:unknown):VerificationRequest {
const v=object(input,'verification request'),changes=v.changes,fixtures=object(v.fixtures??{},'fixtures'),review=object(v.review,'review');
exact(v,['findingId','runtimeProfile','port','start','legitimate','security','existingTests','fixtures','boundaryFiles','testFiles','changes','review'],'verification request');
exact(review,['reviewer','independent','rootCauseRepaired','featurePreserved','boundaryMocks','rationale','reviewedPatchHash','artifactId'],'review');
if(!Array.isArray(changes)||!changes.length||changes.length>100)throw new CsoError('INVALID_SCHEMA','changes must contain 1..100 declared patch effects');
const cleanFixtures:Record<string,string>={};for(const [p,body] of Object.entries(fixtures)){cleanFixtures[relativePath(p)]=string(body,`fixture ${p}`,1024*1024);}
const request:VerificationRequest={findingId:string(v.findingId,'findingId'),runtimeProfile:string(v.runtimeProfile,'runtimeProfile',100),port:v.port,
start:validateCommand(v.start,'start'),legitimate:(Array.isArray(v.legitimate)?v.legitimate:[]).map((x,i)=>assertion(x,`legitimate[${i}]`)),security:assertion(v.security,'security'),existingTests:(Array.isArray(v.existingTests)?v.existingTests:[]).map((x,i)=>validateCommand(x,`existingTests[${i}]`)),fixtures:cleanFixtures,boundaryFiles:strings(v.boundaryFiles,'boundaryFiles').map(snapshotReference),testFiles:strings(v.testFiles,'testFiles').map(snapshotReference),
changes:changes.map((raw:any,i:number)=>{const x=object(raw,`changes[${i}]`),before=x.beforeSha256;exact(x,['path','beforeSha256','after','effect'],`changes[${i}]`);if(before!==null&&(typeof before!=='string'||!/^[a-f0-9]{64}$/.test(before)))throw new CsoError('INVALID_SCHEMA',`changes[${i}].beforeSha256 must be a hash or null`);return{path:snapshotReference(x.path),beforeSha256:before,after:x.after===null?null:string(x.after,`changes[${i}].after`,1024*1024),effect:oneOf(x.effect,['source','configuration','dependency'],`changes[${i}].effect`)};}),
review:{reviewer:string(review.reviewer,'reviewer'),independent:boolean(review.independent,'review.independent'),rootCauseRepaired:boolean(review.rootCauseRepaired,'review.rootCauseRepaired'),featurePreserved:boolean(review.featurePreserved,'review.featurePreserved'),boundaryMocks:boolean(review.boundaryMocks,'review.boundaryMocks'),rationale:string(review.rationale,'review rationale'),reviewedPatchHash:string(review.reviewedPatchHash,'reviewedPatchHash'),...(review.artifactId===undefined?{}:{artifactId:string(review.artifactId,'review artifact ID')})},};
if(!/^[a-f0-9]{32}$/.test(request.findingId))throw new CsoError('INVALID_SCHEMA','findingId must be a helper-issued identifier');
if(request.review.artifactId!==undefined&&!/^[a-f0-9]{32}$/.test(request.review.artifactId))throw new CsoError('INVALID_SCHEMA','review artifact ID must be a helper-issued identifier');
if(!Number.isInteger(request.port)||request.port<1024||request.port>65535)throw new CsoError('INVALID_SCHEMA','port must be 1024..65535');
if(!request.legitimate.length||!request.security.vulnerable||!request.existingTests.length||!request.boundaryFiles.length||!request.testFiles.length)throw new CsoError('INVALID_SCHEMA','Verification needs a legitimate control, distinct before/fixed security oracles, existing tests, immutable test files, and boundary files');
const secure=request.security.expected,vulnerable=request.security.vulnerable;
const mutuallyExclusive=secure.status!==vulnerable.status
||(secure.includes!==undefined&&vulnerable.excludes!==undefined&&secure.includes.includes(vulnerable.excludes))
||(vulnerable.includes!==undefined&&secure.excludes!==undefined&&vulnerable.includes.includes(secure.excludes));
if(!mutuallyExclusive)throw new CsoError('INVALID_SCHEMA','The vulnerable and fixed security oracles must be provably mutually exclusive');
if(new Set(request.changes.map(x=>x.path)).size!==request.changes.length)throw new CsoError('INVALID_SCHEMA','Patch paths must be unique');
if(new Set(request.testFiles).size!==request.testFiles.length||request.changes.some(change=>request.testFiles.includes(change.path)))throw new CsoError('INVALID_SCHEMA','Existing-test source files must be unique and unchanged by the repair');
if(request.existingTests.some(command=>/(?:^|\/)(?:true|false|echo|printf|env|sh|bash)$/.test(command.executable)))throw new CsoError('INVALID_SCHEMA','Generic success or shell commands cannot stand in for a project test suite');
if(!request.changes.some(change=>change.beforeSha256===null||change.after===null||sha256(change.after)!==change.beforeSha256))throw new CsoError('INVALID_SCHEMA','A tested repair must contain at least one material patch effect');
return request;
}
export function completeness(report: Pick<RunReportV3,'coverage'|'gaps'>): Completeness {
// Scanner adapters preserve operational outcomes, but scanner output is only
// candidate evidence. The corresponding investigation domain decides whether
// assessment work remains; an optional tool failure cannot override it.
// A successful snapshot is a prerequisite, not security assessment work by
// itself. Its helper-owned partial/not-assessed state remains material.
const work = report.coverage.filter(c => c.status !== 'not_applicable'&&!c.domain.startsWith('scanner:')&&!(['snapshot-inputs','history-inputs'].includes(c.domain)&&c.status==='assessed'));
if (!report.gaps.length && work.length && work.every(c => c.status === 'assessed')) return 'complete';
return work.some(c => c.status === 'assessed' || c.status === 'partial') ? 'partial' : 'not assessed';
}
export function renderReport(report: RunReportV3): string {
const supported = report.findings.filter(f => f.evidence === 'supported');
const gaps = [...new Set([...report.gaps,...report.coverage.filter(c=>!c.domain.startsWith('scanner:')).flatMap(c => c.gaps)])];
const transformations=report.source.transformations??[];
// Report JSON is canonical evidence. Markdown is a safe plain-text view:
// collapse line breaks and escape all Markdown control characters so model,
// repository, scanner, and advisory strings cannot forge report structure.
const plain=(value:unknown):string=>String(value).replace(/[\x00-\x1f\x7f-\x9f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]+/gu,' ').replace(/\s{2,}/g,' ').trim().replace(/[\\`*_[\]{}()#+!|<>]/g,'\\$&');
const list=(values:string[]):string=>values.length?values.map(plain).join('; '):'none';
const terminal=[...report.events].reverse().find(item=>item.kind==='terminal'),startedAt=Date.parse(report.createdAt),terminalAt=terminal?Date.parse(terminal.at):NaN;
const elapsed=Number.isFinite(startedAt)&&Number.isFinite(terminalAt)&&terminalAt>=startedAt?`; elapsed ${terminalAt-startedAt} ms`:'';
const timing=`Timing: started ${plain(report.createdAt)}; deadline ${plain(report.deadline)}${terminal?`; terminal ${plain(terminal.at)}${elapsed}`:''}.`;
const usage=report.modelUsage?`Model usage: ${report.modelUsage.tokens} host-reported tokens from ${plain(report.modelUsage.source)}${report.modelUsage.cost===undefined?'':`; host-reported cost ${report.modelUsage.cost}`}.`:undefined;
const findingLines=(f:FindingV3):string[]=>[
`- ${plain(f.severity.toUpperCase())} ${plain(f.title)} [${plain(f.id)}]`,
` Location: ${plain(f.location.path)}:${f.location.line} (${plain(f.location.symbol)}). Confidence: ${plain(f.confidence)}${plain(f.confidenceRationale)}. Evidence: ${plain(f.evidence)}.`,
` Attacker scenario: ${plain(f.scenario)}`,
` Attacker control: ${plain(f.attackerControl)}. Impact: ${plain(f.impact)}.`,
` Trace: ${list(f.trace)}. Supporting references: ${list(f.references)}.`,
` Counterevidence considered: ${plain(f.challenge.counterevidence)}. Challenge: ${plain(f.challenge.mode)} by ${plain(f.challenge.reviewer)}. Conclusion: ${plain(f.challenge.conclusion)}.`,
` Repair recommendation: ${plain(f.recommendation)}`,
` Reproduction: ${plain(f.reproduction)}${f.reproductionAttemptId?` (attempt ${plain(f.reproductionAttemptId)})`:''}. Repair: ${plain(f.repair)}. Closure: ${plain(f.closure)}.`,
...(f.verificationId?[` Verification: ${plain(f.verificationId)}. Bundle: bundles/${plain(f.verificationId)}.json. Assertion assurance: ${plain(f.verificationAssurance?.assertions??'unknown')}. Test completion assurance: ${plain(f.verificationAssurance?.testCompletion??'unknown')}. Review assurance: ${plain(f.verificationAssurance?.review??'unknown')}.`]:[]),
];
const model=report.application;
return [ `${report.completeness}${plain(report.policy.scope)}${report.policy.diff ? ` (diff against ${plain(report.policy.base)})` : ''}`,
`Run: ${plain(report.runId)}. Mode: ${plain(report.policy.mode)}.`,
timing, ...(usage?[usage]:[]),
`Material gaps: ${gaps.length ? list(gaps) : 'none reported'}.`, '',
'Application model:',
`- Actors: ${list(model.actors)}.`, `- Assets: ${list(model.assets)}.`, `- Entrypoints: ${list(model.entrypoints)}.`,
`- Tenant boundaries: ${list(model.tenantBoundaries)}.`, `- Sensitive operations: ${list(model.sensitiveOperations)}.`, `- Security invariants: ${list(model.invariants)}.`, '',
...(supported.length ? ['Supported findings:',...supported.flatMap(findingLines)] : ['No supported findings in the assessed scope.']),
...(report.policy.mode === 'comprehensive' ? ['', 'Hypotheses (unconfirmed):', ...report.findings.filter(f => f.evidence === 'hypothesis').flatMap(findingLines)] : []),
'', 'Snapshot transformations:', ...(transformations.length?transformations.map(item=>`- ${plain(item.path)}: ${plain(item.handling)}`):['- none']),
'', 'Coverage:', ...report.coverage.flatMap(c=>[
`- ${plain(c.domain)}: ${plain(c.status)}; ${plain(c.method)}${c.tool?`; tool ${plain(c.tool.name)} ${plain(c.tool.version)}, freshness ${plain(c.tool.freshness)}, outcome ${plain(c.tool.outcome)}`:''}.`,
` Scope: ${plain(c.scope)}. Evidence: ${list(c.evidence)}. Gaps: ${list(c.gaps)}. Exclusions: ${list(c.exclusions)}.`,
]), '',
].join('\n');
}
type LegacyJson = null | boolean | number | string | LegacyJson[] | { [key:string]: LegacyJson };
function legacyJson(value:unknown,depth=0,seen=new WeakSet<object>()):LegacyJson{
if(depth>32)throw new CsoError('INVALID_SCHEMA','Legacy report nesting is too deep');
if(value===null||typeof value==='boolean')return value;
if(typeof value==='number'){if(!Number.isFinite(value))throw new CsoError('INVALID_SCHEMA','Legacy report numbers must be finite');return value;}
if(typeof value==='string'){
if(value.length>MAX_OUTPUT||UNSAFE_STRING_CONTROLS.test(value))throw new CsoError('INVALID_SCHEMA','Legacy report strings must be bounded and free of unsafe control characters');
return value;
}
if(!value||typeof value!=='object')throw new CsoError('INVALID_SCHEMA','Legacy report contains a non-JSON value');
if(seen.has(value))throw new CsoError('INVALID_SCHEMA','Legacy report cannot be cyclic');seen.add(value);
try{
if(Array.isArray(value)){
if(value.length>10_000)throw new CsoError('INVALID_SCHEMA','Legacy report array is too large');
return value.map(item=>legacyJson(item,depth+1,seen));
}
const entries=Object.entries(value as Record<string,unknown>);if(entries.length>10_000)throw new CsoError('INVALID_SCHEMA','Legacy report object is too large');
const out:Record<string,LegacyJson>=Object.create(null);
for(const [key,item] of entries){if(key.length>1024||['__proto__','prototype','constructor'].includes(key)||UNSAFE_PROPERTY_CONTROLS.test(key))throw new CsoError('INVALID_SCHEMA','Legacy report contains an unsafe property');out[key]=legacyJson(item,depth+1,seen);}
return out;
}finally{seen.delete(value);}
}
export function importLegacy(input: unknown): { schemaVersion: 2; readOnly: true; findings: any[]; warning: string } {
const v = object(input,'legacy report');
if (!Array.isArray(v.findings) || ![2,'2','2.0','2.0.0'].includes(v.schemaVersion ?? v.schema_version ?? v.version)) throw new CsoError('INVALID_SCHEMA','Expected a v2 report with findings');
return { schemaVersion: 2, readOnly: true, warning: 'Legacy VERIFIED is review evidence only; it does not establish reproduction, tested repair, or closure.',
findings: v.findings.map((raw: any,index:number) => {const f=object(raw,`legacy finding ${index+1}`);return{
title:typeof f.title==='string'?string(f.title,'legacy title'): `Legacy finding ${index+1}`,
status:typeof f.status==='string'?string(f.status,'legacy status'): 'unknown',
...(typeof f.severity==='string'?{severity:string(f.severity,'legacy severity')}:{ }),
...(typeof f.description==='string'?{description:string(f.description,'legacy description')}:{ }),
legacy:legacyJson(f),
evidence:'legacy_review', reproduction:'not_attempted', repair:'not_attempted', closure:'unknown'};}) };
}
+311
View File
@@ -0,0 +1,311 @@
import * as fs from 'node:fs';
import { join } from 'node:path';
import { canonical, CsoError, MAX_OUTPUT, sha256 } from './contracts';
import { spawn } from 'node:child_process';
import { dirname } from 'node:path';
import { GROUP_LIMITS, Role, ROLE_LIMITS, Lease, admit, markSupervised, release, total } from './admission';
import { childEnvironment, executable, runProcess } from './process';
import { secureDirectory } from './state';
export const CONTAINER_SHM_BYTES=8*1024*1024;
export const ISOLATION_POLICY_HASH=sha256(canonical({version:'cso-isolation-v1',network:'none-shared-loopback',root:'readonly',capabilities:'drop-all',privilegeEscalation:false,seccomp:'builtin',pull:'never',logging:'none',limits:GROUP_LIMITS,roles:ROLE_LIMITS,shmBytes:CONTAINER_SHM_BYTES,maxOutput:MAX_OUTPUT}));
export interface DockerEndpoint { uri: string; socket: string; executable: string; device:number; inode:number }
function dockerTimeout(deadline:number|undefined,maximum:number):number{
if(deadline===undefined)return maximum;const remaining=deadline-Date.now();
if(remaining<=0)throw new CsoError('DEADLINE','Docker operation reached its aggregate deadline');
return Math.max(1,Math.min(maximum,remaining));
}
function deadlineExpired(deadline:number|undefined):boolean{return deadline!==undefined&&Date.now()>=deadline;}
export async function dockerEndpoint(home: string, env: Record<string,string|undefined> = process.env, deadline?:number): Promise<DockerEndpoint> {
const docker = executable('docker');
const requestedHost = env.DOCKER_HOST;
if (requestedHost && !requestedHost.startsWith('unix:///')) throw new CsoError('ISOLATION_FAILED','Remote TCP, HTTP, SSH, and TLS Docker endpoints are refused');
let uri = requestedHost;
if (!uri) {
const config = env.DOCKER_CONFIG || (env.HOME ? join(env.HOME,'.docker') : '');
if (config && (!config.startsWith('/') || config.includes('\0') || config.split('/').includes('..'))) throw new CsoError('ISOLATION_FAILED','Docker config must be an absolute host path');
const inspectEnv = {...childEnvironment(home),HOME:env.HOME || home,...(config?{DOCKER_CONFIG:config}:{})};
let context = env.DOCKER_CONTEXT;
if (!context) {
const shown = await runProcess(docker,['context','show'],{cwd:home,env:inspectEnv,raw:true,timeoutMs:dockerTimeout(deadline,5000),maxBytes:8192});
if(deadlineExpired(deadline))throw new CsoError('DEADLINE','Docker context discovery reached the aggregate image-provisioning deadline');
if (shown.code || shown.timedOut || shown.truncated) throw new CsoError('ISOLATION_FAILED','Effective Docker context could not be determined');
context = shown.stdout.trim();
}
if (!/^[A-Za-z0-9_.-]{1,100}$/.test(context)) throw new CsoError('ISOLATION_FAILED','Invalid Docker context name');
const result = await runProcess(docker,['context','inspect',context,'--format','{{json .Endpoints.docker.Host}}'],{cwd:home,env:inspectEnv,raw:true,timeoutMs:dockerTimeout(deadline,5000),maxBytes:8192});
if(deadlineExpired(deadline))throw new CsoError('DEADLINE','Docker context inspection reached the aggregate image-provisioning deadline');
if (result.code || result.timedOut || result.truncated) throw new CsoError('ISOLATION_FAILED','Docker context could not be inspected without target execution');
try { uri = JSON.parse(result.stdout.trim()); } catch { throw new CsoError('ISOLATION_FAILED','Docker context returned invalid endpoint data'); }
}
if (typeof uri !== 'string' || !uri.startsWith('unix:///') || uri.includes('\0') || uri.includes('..')) throw new CsoError('ISOLATION_FAILED','Only a local absolute Unix Docker socket is supported');
const requestedSocket = uri.slice('unix://'.length);let socket='';try{socket=fs.realpathSync(requestedSocket);}catch{throw new CsoError('ISOLATION_FAILED','Pinned local Docker socket is unavailable');}
let s: fs.Stats; try { s=fs.statSync(socket); } catch { throw new CsoError('ISOLATION_FAILED','Pinned local Docker socket is unavailable'); }
if (!s.isSocket()) throw new CsoError('ISOLATION_FAILED','Docker endpoint is not a local Unix socket');
if(deadlineExpired(deadline))throw new CsoError('DEADLINE','Docker endpoint admission reached the aggregate image-provisioning deadline');
return {uri:`unix://${socket}`,socket,executable:docker,device:s.dev,inode:s.ino};
}
function assertEndpoint(endpoint:DockerEndpoint):void{let s:fs.Stats;try{s=fs.statSync(endpoint.socket);}catch{throw new CsoError('ISOLATION_FAILED','Pinned Docker socket disappeared');}if(!s.isSocket()||s.dev!==endpoint.device||s.ino!==endpoint.inode)throw new CsoError('ISOLATION_FAILED','Pinned Docker socket identity changed');}
const EXACT_CATALOG_IMAGE=/^[a-z0-9][a-z0-9.-]*(?::[0-9]+)?\/[a-z0-9][a-z0-9._/-]*@sha256:[a-f0-9]{64}$/;
function assertExactCatalogImage(image:string):void{
if(!EXACT_CATALOG_IMAGE.test(image)||image.includes('..')||image.includes('//'))throw new CsoError('INCOMPATIBLE_INPUT','Catalog image must name a fully qualified registry repository at an exact sha256 digest');
}
export function dockerEnvironment(endpoint: DockerEndpoint, config: string): Record<string,string> {
return {...childEnvironment(config),HOME:config,DOCKER_CONFIG:config,DOCKER_HOST:endpoint.uri,DOCKER_CONTEXT:'',DOCKER_TLS_VERIFY:'',DOCKER_CERT_PATH:''};
}
export function linuxCgroupAdmission(procCgroup='/proc/self/cgroup',cgroupRoot='/sys/fs/cgroup'):string[]{
if(process.platform!=='linux')return ['cpu','memory','pids'];
let line='';try{line=fs.readFileSync(procCgroup,'utf8').split('\n').find(x=>x.startsWith('0::'))??'';}catch{return[];}
if(line){const rel=line.slice(3).replace(/^\//,''),dir=join(cgroupRoot,rel),file=join(dir,'cgroup.controllers');try{return fs.readFileSync(file,'utf8').trim().split(/\s+/).filter(Boolean);}catch{return[];}}
// Legacy cgroup v1: each independently mounted controller is sufficient.
return ['cpu','memory','pids'].filter(controller=>fs.existsSync(join(cgroupRoot,controller)));
}
export async function dockerProbe(endpoint: DockerEndpoint, home: string, deadline?:number): Promise<{version:string;security:string[]}> {
assertEndpoint(endpoint);
const config=secureDirectory(join(home,'docker-config'));
const r=await runProcess(endpoint.executable,['info','--format','{{json .}}'],{cwd:home,env:dockerEnvironment(endpoint,config),raw:true,timeoutMs:dockerTimeout(deadline,10_000),maxBytes:128*1024});
if(deadlineExpired(deadline))throw new CsoError('DEADLINE','Docker capability inspection reached the aggregate image-provisioning deadline');
if (r.code || r.timedOut || r.truncated) throw new CsoError('ISOLATION_FAILED','Local Docker daemon is not usable');
let v:any; try {v=JSON.parse(r.stdout);} catch {throw new CsoError('ISOLATION_FAILED','Docker returned invalid capability data');}
const security=Array.isArray(v.SecurityOptions)?v.SecurityOptions:[];
if (!v.ServerVersion || !v.MemoryLimit || !v.CpuCfsQuota || !v.PidsLimit || !security.some((x:string)=>x.includes('seccomp')))
throw new CsoError('ISOLATION_FAILED','Docker lacks required memory, CPU, PID, or seccomp enforcement');
const delegated=linuxCgroupAdmission();if(!['cpu','memory','pids'].every(x=>delegated.includes(x)))throw new CsoError('ISOLATION_FAILED','Linux host has not delegated CPU, memory, and PID controllers to this helper; target execution is blocked');
if (v.LoggingDriver && typeof v.LoggingDriver !== 'string') throw new CsoError('ISOLATION_FAILED','Docker logging capability is invalid');
return {version:v.ServerVersion,security};
}
/** Read-only local image admission probe. Docker image inspect never pulls. */
export async function dockerExactImagePresent(endpoint:DockerEndpoint,home:string,image:string,platform:'linux/amd64'|'linux/arm64',deadline?:number):Promise<boolean>{
assertExactCatalogImage(image);assertEndpoint(endpoint);
const config=secureDirectory(join(home,'docker-config'));
const result=await runProcess(endpoint.executable,['image','inspect','--format','{{json .}}',image],{
cwd:home,env:dockerEnvironment(endpoint,config),raw:true,timeoutMs:dockerTimeout(deadline,5_000),maxBytes:128*1024,
});
assertEndpoint(endpoint);
if(deadlineExpired(deadline)||result.timedOut)throw new CsoError('DEADLINE','Exact image inspection reached its bounded image-provisioning deadline');
if(result.code||result.truncated)return false;
let inspected:any;try{inspected=JSON.parse(result.stdout);}catch{return false;}
const expectedArch=platform==='linux/arm64'?'arm64':'amd64';
return inspected?.Os==='linux'&&inspected?.Architecture===expectedArch&&
typeof inspected?.Id==='string'&&/^sha256:[a-f0-9]{64}$/.test(inspected.Id)&&
Array.isArray(inspected?.RepoDigests)&&inspected.RepoDigests.includes(image)&&
canonical(inspected?.Config?.Entrypoint)===canonical(['/opt/cso/entrypoint'])&&
(!inspected?.Config?.Volumes||Object.keys(inspected.Config.Volumes).length===0);
}
/** Installation-only acquisition. Audits never call this and still use --pull=never. */
export async function dockerPullExactCatalogImage(endpoint:DockerEndpoint,home:string,image:string,platform:'linux/amd64'|'linux/arm64',deadline?:number):Promise<void>{
assertExactCatalogImage(image);assertEndpoint(endpoint);
const config=secureDirectory(join(home,'docker-config'));
const result=await runProcess(endpoint.executable,['pull','--quiet','--platform',platform,image],{
cwd:home,env:dockerEnvironment(endpoint,config),timeoutMs:dockerTimeout(deadline,300_000),maxBytes:128*1024,
});
assertEndpoint(endpoint);
if(deadlineExpired(deadline)||result.timedOut)throw new CsoError('DEADLINE','Qualified image pull reached its bounded preload deadline');
if(result.code||result.truncated)throw new CsoError('PREREQUISITE','Anonymous pull of a qualified CSO image failed; allow public registry access and rerun setup');
if(!await dockerExactImagePresent(endpoint,home,image,platform,deadline))throw new CsoError('INCOMPATIBLE_INPUT','Docker did not retain the exact qualified image digest and platform after acquisition');
}
export interface ContainerSpec {
role: Role; image: string; source?: string; command: string[]; env?: Record<string,string>;
readonlyFiles?: {host:string;container:string}[];
readonlyDirectories?: {host:string;container:'/fixtures'}[];
/** Preparation-only mounts. Every destination is fixed by the helper. */
readonlyMetadata?: string;
readonlyInputMetadata?: string;
metadataTmpfsBytes?: number;
archiveTmpfsBytes?: number;
/** Explicit preparation layouts replace the role defaults but stay within group admission. */
workTmpfsBytes?: number;
temporaryTmpfsBytes?: number;
readonlyArchiveDirectory?: string;
registrySocket?: string;
/** Non-secret, fixed-user-readable PostgreSQL database-name policy. */
postgresDatabasePolicy?: string;
}
export function writableAllocation(role:Role,spec:Pick<ContainerSpec,'temporaryTmpfsBytes'|'workTmpfsBytes'|'metadataTmpfsBytes'|'archiveTmpfsBytes'>={}):{temporaryBytes:number;workBytes:number;shmBytes:number;totalBytes:number}{
const l=ROLE_LIMITS[role],mib=1024*1024,shmMiB=CONTAINER_SHM_BYTES/mib,
defaultTmpMiB=Math.min(256,Math.max(1,l.writableMiB-shmMiB-1)),
bounded=(value:number|undefined,fallback:number,label:string)=>{const selected=value??fallback;if(!Number.isSafeInteger(selected)||selected<=0||selected>GROUP_LIMITS.writableMiB*mib)throw new CsoError('INVALID_SCHEMA',`${label} tmpfs size is outside the aggregate writable-storage policy`);return selected;},
temporaryBytes=bounded(spec.temporaryTmpfsBytes,defaultTmpMiB*mib,'Temporary'),
workBytes=bounded(spec.workTmpfsBytes,(l.writableMiB-defaultTmpMiB-shmMiB)*mib,'Work'),
extra=(spec.metadataTmpfsBytes??0)+(spec.archiveTmpfsBytes??0),totalBytes=temporaryBytes+workBytes+CONTAINER_SHM_BYTES+extra;
if(!Number.isSafeInteger(totalBytes)||totalBytes<=0)throw new CsoError('INVALID_SCHEMA','Writable mount sizes are outside the aggregate writable-storage policy');
return{temporaryBytes,workBytes,shmBytes:CONTAINER_SHM_BYTES,totalBytes};
}
export function validatePostgresDatabasePolicy(path:string):string[]{
const stat=fs.lstatSync(path),real=fs.realpathSync(path);
if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1||stat.size<1||stat.size>4096||(stat.mode&0o777)!==0o444||real.includes(','))throw new CsoError('UNSAFE_PATH','PostgreSQL database policy must be one public-readable, immutable synthetic file');
const body=fs.readFileSync(real,'utf8');if(!body.endsWith('\n')||body.includes('\0')||body.includes('\r'))throw new CsoError('INVALID_SCHEMA','PostgreSQL database policy framing is invalid');
const names=body.slice(0,-1).split('\n');if(!names.length||names.length>64||new Set(names).size!==names.length||names.some(name=>!/^cso_[A-Za-z_][A-Za-z0-9_]{0,47}$/.test(name)))throw new CsoError('INVALID_SCHEMA','PostgreSQL database policy contains an invalid or duplicate database name');
return names;
}
export function validateSingleContainerProcessOutput(output:string):void{
const lines=output.split('\n').map(line=>line.trim()).filter(Boolean);
if(lines.length!==2||!/^PID$/i.test(lines[0])||!/^\d+$/.test(lines[1]))throw new CsoError('ISOLATION_FAILED','Offline lifecycle left a background process; prepared output was withheld');
}
export class DockerGroup {
private ids: {role:Role;id:string}[]=[]; private roles:Role[]=['anchor']; private writableBytesById=new Map<string,number>(); private lease:Lease; private config:string; private admittedImages=new Set<string>(); private remainingOutput=MAX_OUTPUT;
anchor='';
private constructor(public endpoint:DockerEndpoint, public runId:string, public dir:string, public deadline:number, lease:Lease) {
this.lease=lease; this.config=secureDirectory(join(dir,'docker-config'));
}
static async create(endpoint:DockerEndpoint,runId:string,dir:string,deadline:number,anchorImage:string,watchdogPath?:string):Promise<DockerGroup>{
if(!/^[A-Za-z0-9_.-]{1,100}$/.test(runId))throw new CsoError('INVALID_ARGUMENT','Reproduction group label is invalid');
await dockerProbe(endpoint,dir); const group=new DockerGroup(endpoint,runId,dir,deadline,admit(endpoint.uri,runId,deadline));
try { await group.launchWatchdog(watchdogPath); group.anchor=await group.createContainer({role:'anchor',image:anchorImage,command:['/bin/sleep','2147483647']},false); await group.docker(['start',group.anchor]); return group; }
catch(e){await group.cleanup();throw e;}
}
private async launchWatchdog(explicit?:string):Promise<void>{
const watchdog=explicit??join(dirname(process.execPath),'gstack-cso-watchdog');
if(!fs.existsSync(watchdog)||fs.lstatSync(watchdog).isSymbolicLink())throw new CsoError('ISOLATION_FAILED','Independent watchdog is missing from the trusted helper distribution');
const ready=join(this.dir,'watchdog.ready');try{fs.unlinkSync(ready);}catch{}
let spawnFailed=false;
const child=spawn(watchdog,['--owner',String(process.pid),'--deadline',String(Math.ceil(this.deadline/1000)),'--run-dir',this.dir,'--docker',this.endpoint.executable,'--endpoint',this.endpoint.uri,'--socket-device',String(this.endpoint.device),'--socket-inode',String(this.endpoint.inode),'--run-label',this.runId,'--lease-path',this.lease.path,'--lease-token',this.lease.token],{cwd:this.dir,env:{PATH:'/usr/bin:/bin'},detached:true,stdio:'ignore'});
child.once('error',()=>{spawnFailed=true;});child.unref();
for(let i=0;i<100&&!spawnFailed&&!fs.existsSync(ready);i++)await new Promise(resolve=>setTimeout(resolve,10));
let alive=false;try{if(child.pid){process.kill(child.pid,0);alive=true;}}catch{}
if(spawnFailed||!fs.existsSync(ready)||!alive){try{if(child.pid)process.kill(child.pid,'SIGKILL');}catch{}throw new CsoError('ISOLATION_FAILED','Independent watchdog failed its startup handshake');}
markSupervised(this.lease);
fs.writeFileSync(join(this.dir,'watchdog.pid'),String(child.pid)+'\n',{mode:0o600});
}
private async docker(args:string[],max=128*1024){
assertEndpoint(this.endpoint);
const remaining=Math.max(1,Math.min(300_000,this.deadline-Date.now()));
const r=await runProcess(this.endpoint.executable,args,{cwd:this.dir,env:dockerEnvironment(this.endpoint,this.config),raw:true,timeoutMs:remaining,maxBytes:max});
if(r.timedOut)throw new CsoError('DEADLINE','Docker operation exceeded the reproduction deadline');
if(r.truncated)throw new CsoError('REDACTION_FAILED','Docker output exceeded the bounded capture limit and was withheld');
if(r.code)throw new CsoError('ISOLATION_FAILED',`Docker operation failed (${args[0]}): ${r.stderr.slice(0,200)}`);
return r.stdout.trim();
}
private async admitLocalImage(image:string):Promise<void>{
if(this.admittedImages.has(image))return;
let raw='';try{raw=await this.docker(['image','inspect','--format','{{json .}}',image],128*1024);}catch{throw new CsoError('MISSING_INPUT',`Pinned runtime image is not already present locally: ${image}`);}
let inspected:any;try{inspected=JSON.parse(raw);}catch{throw new CsoError('ISOLATION_FAILED','Local image metadata is invalid');}
const expectedArch=process.arch==='arm64'?'arm64':'amd64',digest=image.slice(image.lastIndexOf('sha256:'));
if(inspected.Os!=='linux'||inspected.Architecture!==expectedArch||typeof inspected.Id!=='string'||!/^sha256:[a-f0-9]{64}$/.test(inspected.Id)||canonical(inspected.Config?.Entrypoint)!==canonical(['/opt/cso/entrypoint']))throw new CsoError('INCOMPATIBLE_INPUT','Pinned runtime image does not match the admitted Linux platform and fixed entrypoint');
if(inspected.Config?.Volumes&&Object.keys(inspected.Config.Volumes).length)throw new CsoError('INCOMPATIBLE_INPUT','Pinned runtime image declares writable volumes outside the bounded storage policy');
if(image.startsWith('sha256:')){if(inspected.Id!==image)throw new CsoError('INCOMPATIBLE_INPUT','Local image ID does not match the requested digest');}
else if(!Array.isArray(inspected.RepoDigests)||!inspected.RepoDigests.includes(image))throw new CsoError('INCOMPATIBLE_INPUT',`Local image metadata does not bind the requested repository digest ${digest}`);
this.admittedImages.add(image);
}
async createContainer(spec:ContainerSpec,joinAnchor=true):Promise<string>{
if(!/^(?:[-./:@a-zA-Z0-9_]+@)?sha256:[a-f0-9]{64}$/.test(spec.image))throw new CsoError('INCOMPATIBLE_INPUT','Container image must use an immutable sha256 digest');
if(!spec.command.length||!spec.command[0].startsWith('/'))throw new CsoError('INVALID_SCHEMA','Container command needs an absolute executable');
await this.admitLocalImage(spec.image);
if(spec.role!=='anchor')total([...this.roles,spec.role]);
const l=ROLE_LIMITS[spec.role],mib=1024*1024,
allocation=writableAllocation(spec.role,spec),tmpBytes=allocation.temporaryBytes,workBytes=allocation.workBytes,candidateWritable=allocation.totalBytes;
if(!Number.isSafeInteger(candidateWritable)||candidateWritable<=0||[...this.writableBytesById.values()].reduce((sum,value)=>sum+value,0)+candidateWritable>GROUP_LIMITS.writableMiB*mib)
throw new CsoError('INSUFFICIENT_CAPACITY','Requested tmpfs mounts exceed the aggregate reproduction-group writable-storage limit');
const memoryReserve=Math.min(256*mib,Math.max(16*mib,Math.floor(l.memoryMiB*mib/8)));
if(candidateWritable>l.memoryMiB*mib-memoryReserve)throw new CsoError('INSUFFICIENT_CAPACITY','Requested tmpfs mounts leave insufficient admitted memory for the container process');
const hostUid=process.getuid?.(),hostGid=process.getgid?.();if(!Number.isInteger(hostUid)||!Number.isInteger(hostGid)||(hostUid as number)<=0||(hostGid as number)<0)throw new CsoError('ISOLATION_FAILED','Target execution requires a non-root host identity for readable private bind mounts');
const uid=spec.role==='postgres'?10001:hostUid as number,gid=spec.role==='postgres'?10001:hostGid as number;
const args=['create','--pull=never','--label',`com.gstack.cso.run=${this.runId}`,'--label',`com.gstack.cso.role=${spec.role}`,'--log-driver=none',
'--read-only','--user',`${uid}:${gid}`,'--cap-drop','ALL','--security-opt','no-new-privileges:true','--security-opt','seccomp=builtin',
'--cpus',String(l.cpu),'--memory',`${l.memoryMiB}m`,'--memory-swap',`${l.memoryMiB}m`,'--pids-limit',String(l.pids),
'--shm-size',`${CONTAINER_SHM_BYTES}b`,
'--tmpfs',`/tmp:rw,noexec,nosuid,nodev,size=${tmpBytes},mode=1777`,
'--tmpfs',`/work:rw,nosuid,nodev,size=${workBytes},mode=700,uid=${uid},gid=${gid}`,
'--network',joinAnchor?`container:${this.anchor}`:'none','--platform',process.arch==='arm64'?'linux/arm64':'linux/amd64'];
const expectedTmpfs=new Set(['/tmp','/work']),expectedMounts=new Set<string>();
for(const [k,v] of Object.entries(spec.env??{})){if(!/^[A-Z][A-Z0-9_]{0,63}$/.test(k)||v.includes('\0'))throw new CsoError('INVALID_SCHEMA','Invalid explicit container environment');args.push('--env',`${k}=${v}`);}
if(spec.source){const stat=fs.lstatSync(spec.source),real=fs.realpathSync(spec.source);if(!stat.isDirectory()||stat.isSymbolicLink()||real.includes(','))throw new CsoError('UNSAFE_PATH','Execution source must be one unambiguous private directory');args.push('--mount',`type=bind,src=${real},dst=/source,readonly,bind-nonrecursive`);expectedMounts.add('/source');}
for(const f of spec.readonlyFiles??[]){const stat=fs.lstatSync(f.host),real=fs.realpathSync(f.host);if(!stat.isFile()||stat.isSymbolicLink()||real.includes(',')||!f.container.startsWith('/policy/'))throw new CsoError('UNSAFE_PATH','Trusted policy mounts must be regular files under /policy');args.push('--mount',`type=bind,src=${real},dst=${f.container},readonly,bind-nonrecursive`);expectedMounts.add(f.container);}
if(spec.postgresDatabasePolicy){if(spec.role!=='postgres')throw new CsoError('INVALID_SCHEMA','PostgreSQL database policy can only be mounted into the fixed database role');validatePostgresDatabasePolicy(spec.postgresDatabasePolicy);args.push('--mount',`type=bind,src=${fs.realpathSync(spec.postgresDatabasePolicy)},dst=/policy/postgresql.databases,readonly,bind-nonrecursive`);expectedMounts.add('/policy/postgresql.databases');}
for(const d of spec.readonlyDirectories??[]){const stat=fs.lstatSync(d.host),real=fs.realpathSync(d.host);if(!stat.isDirectory()||stat.isSymbolicLink()||real.includes(',')||d.container!=='/fixtures')throw new CsoError('UNSAFE_PATH','Fixture mounts must be private directories at /fixtures');args.push('--mount',`type=bind,src=${real},dst=${d.container},readonly,bind-nonrecursive`);expectedMounts.add(d.container);}
if(Boolean(spec.readonlyArchiveDirectory)&&Boolean(spec.archiveTmpfsBytes))throw new CsoError('INVALID_SCHEMA','Preparation requires exactly one archive storage policy');
if(Boolean(spec.readonlyMetadata)&&Boolean(spec.metadataTmpfsBytes))throw new CsoError('INVALID_SCHEMA','Preparation requires exactly one metadata storage policy');
if(Boolean(spec.readonlyInputMetadata)!==Boolean(spec.metadataTmpfsBytes))throw new CsoError('INVALID_SCHEMA','Writable metadata tmpfs requires a separate read-only metadata input');
const directoryMount=(host:string,destination:string,readonly:boolean)=>{const stat=fs.lstatSync(host),real=fs.realpathSync(host);if(!stat.isDirectory()||stat.isSymbolicLink()||real.includes(',')||(process.getuid&&stat.uid!==process.getuid())||(stat.mode&0o022)!==0)throw new CsoError('UNSAFE_PATH',`Preparation ${destination} mount must be one private owned directory`);args.push('--mount',`type=bind,src=${real},dst=${destination}${readonly?',readonly':''},bind-nonrecursive`);expectedMounts.add(destination);};
if(spec.readonlyMetadata)directoryMount(spec.readonlyMetadata,'/metadata',true);
if(spec.readonlyInputMetadata)directoryMount(spec.readonlyInputMetadata,'/input-metadata',true);
if(spec.metadataTmpfsBytes){if(!Number.isSafeInteger(spec.metadataTmpfsBytes)||spec.metadataTmpfsBytes<=0||spec.metadataTmpfsBytes>1024*1024*1024)throw new CsoError('INVALID_SCHEMA','Preparation metadata tmpfs exceeds the 1 GiB policy');args.push('--tmpfs',`/metadata:rw,noexec,nosuid,nodev,size=${spec.metadataTmpfsBytes},mode=700,uid=${uid},gid=${gid}`);expectedTmpfs.add('/metadata');}
if(spec.archiveTmpfsBytes){if(!Number.isSafeInteger(spec.archiveTmpfsBytes)||spec.archiveTmpfsBytes<=0||spec.archiveTmpfsBytes>2*1024*1024*1024)throw new CsoError('INVALID_SCHEMA','Preparation archive tmpfs exceeds the 2 GiB group storage policy');args.push('--tmpfs',`/archives:rw,noexec,nosuid,nodev,size=${spec.archiveTmpfsBytes},mode=700,uid=${uid},gid=${gid}`);expectedTmpfs.add('/archives');}
if(spec.readonlyArchiveDirectory)directoryMount(spec.readonlyArchiveDirectory,'/archives',true);
if(spec.registrySocket){const stat=fs.lstatSync(spec.registrySocket),real=fs.realpathSync(spec.registrySocket);if(!stat.isSocket()||stat.isSymbolicLink()||real.includes(',')||(process.getuid&&stat.uid!==process.getuid()))throw new CsoError('UNSAFE_PATH','Registry broker mount must be one owned Unix socket');args.push('--mount',`type=bind,src=${real},dst=/run/cso-registry.sock,readonly,bind-nonrecursive`);expectedMounts.add('/run/cso-registry.sock');}
args.push('--entrypoint','/opt/cso/entrypoint',spec.image,...spec.command);
const id=await this.docker(args,8192); if(!/^[a-f0-9]{64}$/.test(id))throw new CsoError('ISOLATION_FAILED','Docker did not return a stable container ID');
let inspectedContainer:any;try{inspectedContainer=JSON.parse(await this.docker(['inspect','--format','{{json .}}',id],64*1024));}catch{throw new CsoError('ISOLATION_FAILED','Docker did not return valid admitted-container configuration');}const hostConfig=inspectedContainer?.HostConfig,mounts=inspectedContainer?.Mounts;
if(hostConfig?.ReadonlyRootfs!==true||hostConfig?.ShmSize!==CONTAINER_SHM_BYTES||
!hostConfig.Tmpfs||Object.keys(hostConfig.Tmpfs).sort().join('\0')!==[...expectedTmpfs].sort().join('\0')||!Array.isArray(mounts)||
mounts.some((mount:any)=>!mount||!((mount.Type==='bind'&&expectedMounts.has(mount.Destination))||(mount.Type==='tmpfs'&&expectedTmpfs.has(mount.Destination))))||
mounts.filter((mount:any)=>mount?.Type==='bind').length!==expectedMounts.size)
throw new CsoError('ISOLATION_FAILED','Docker did not preserve the bounded writable-storage policy');
// Durable journal publication precedes in-memory admission. If this write
// fails, label recovery still owns the just-created container and no
// phantom role is retained in the live group.
try{fs.appendFileSync(join(this.dir,'resources.journal'),`container:${id}\n`,{mode:0o600});}
catch{throw new CsoError('PERSISTENCE_FAILED','Container resource journal could not be extended');}
this.ids.push({role:spec.role,id});this.writableBytesById.set(id,candidateWritable);if(spec.role!=='anchor')this.roles.push(spec.role);return id;
}
async start(id:string):Promise<void>{await this.docker(['start',id]);}
async pause(id:string):Promise<void>{
if(!this.ids.some(item=>item.id===id))throw new CsoError('ISOLATION_FAILED','Attempted to pause a container outside this reproduction group');
await this.docker(['pause',id],8192);
let paused:unknown;try{paused=JSON.parse(await this.docker(['inspect','--format','{{json .State.Paused}}',id],8192));}catch{throw new CsoError('ISOLATION_FAILED','Prepared container pause state could not be proven');}
if(paused!==true)throw new CsoError('ISOLATION_FAILED','Prepared container was not frozen before export');
}
async wait(id:string):Promise<{code:number;output:string}>{
const code=Number(await this.docker(['wait',id],8192));
// --log-driver=none means daemon logs are unavailable by design. Bounded output must come from attached runs; callers use execAttach.
return {code,output:''};
}
async execCapture(id:string,command:string[],options:{workdir?:string;env?:Record<string,string>}={}):Promise<{code:number;stdout:string;stderr:string}>{
if(!command.length||!command[0].startsWith('/'))throw new CsoError('INVALID_SCHEMA','Exec needs an absolute executable');
if(options.workdir&&!['/metadata','/work','/archives'].includes(options.workdir))throw new CsoError('INVALID_SCHEMA','Exec working directory is outside the preparation contract');
if(this.remainingOutput<=0)throw new CsoError('REDACTION_FAILED','Reproduction-group output budget is exhausted');
const remaining=Math.max(1,Math.min(300_000,this.deadline-Date.now()));
const args=['exec'];if(options.workdir)args.push('--workdir',options.workdir);for(const [key,value] of Object.entries(options.env??{})){if(!/^[A-Z][A-Z0-9_]{0,63}$/.test(key)||value.includes('\0'))throw new CsoError('INVALID_SCHEMA','Exec environment is invalid');args.push('--env',`${key}=${value}`);}args.push(id,...command);
const r=await runProcess(this.endpoint.executable,args,{cwd:this.dir,env:dockerEnvironment(this.endpoint,this.config),timeoutMs:remaining,maxBytes:this.remainingOutput});this.remainingOutput=Math.max(0,this.remainingOutput-r.capturedBytes);
if(r.timedOut)throw new CsoError('DEADLINE','Target command exceeded the reproduction deadline');if(r.truncated)throw new CsoError('REDACTION_FAILED','Aggregate reproduction output exceeded 1 MiB and was withheld');
return {code:r.code,stdout:r.stdout,stderr:r.stderr};
}
async execAttach(id:string,command:string[]):Promise<{code:number;output:string}>{
const result=await this.execCapture(id,command);return{code:result.code,output:result.stdout+result.stderr};
}
async execDetached(id:string,command:string[],workdir='/work'):Promise<void>{
if(!command.length||!command[0].startsWith('/')||!workdir.startsWith('/'))throw new CsoError('INVALID_SCHEMA','Detached exec needs absolute paths');
await this.docker(['exec','--detach','--workdir',workdir,id,...command],8192);
}
async startAttach(id:string):Promise<{code:number;output:string}>{
if(this.remainingOutput<=0)throw new CsoError('REDACTION_FAILED','Reproduction-group output budget is exhausted');
const remaining=Math.max(1,Math.min(300_000,this.deadline-Date.now()));
const r=await runProcess(this.endpoint.executable,['start','--attach',id],{cwd:this.dir,env:dockerEnvironment(this.endpoint,this.config),timeoutMs:remaining,maxBytes:this.remainingOutput});this.remainingOutput=Math.max(0,this.remainingOutput-r.capturedBytes);
if(r.timedOut)throw new CsoError('DEADLINE','Target command exceeded the reproduction deadline');if(r.truncated)throw new CsoError('REDACTION_FAILED','Aggregate reproduction output exceeded 1 MiB and was withheld');
return {code:r.code,output:r.stdout+r.stderr};
}
async assertOnlyInitProcess(id:string):Promise<void>{
if(!this.ids.some(item=>item.id===id))throw new CsoError('ISOLATION_FAILED','Attempted to inspect a container outside this reproduction group');
validateSingleContainerProcessOutput(await this.docker(['top',id,'-eo','pid'],64*1024));
}
async copyPreparedExport(id:string,containerPath:string,destination:string):Promise<void>{
if(!this.ids.some(item=>item.id===id))throw new CsoError('ISOLATION_FAILED','Attempted to copy from a container outside this reproduction group');
if(!/^\/work\/\.gstack-cso-export-[a-f0-9]{24}$/.test(containerPath))throw new CsoError('INVALID_SCHEMA','Prepared export path is outside the fixed helper contract');
const stat=fs.lstatSync(destination),real=fs.realpathSync(destination);if(!stat.isDirectory()||stat.isSymbolicLink()||real.includes(',')||(process.getuid&&stat.uid!==process.getuid())||(stat.mode&0o022)!==0||fs.readdirSync(real).length)throw new CsoError('UNSAFE_PATH','Prepared inert export destination must be one empty private owned directory');
// Only the qualified helper's regular-blob export is copied. Application
// output is reconstructed later by host no-follow writes.
await this.docker(['cp',`${id}:${containerPath}/.`,real],8192);
}
async copyAcquisitionExport(id:string,containerPath:string,destination:string):Promise<void>{
if(!this.ids.some(item=>item.id===id))throw new CsoError('ISOLATION_FAILED','Attempted to copy output from a container outside this reproduction group');
if(!/^\/archives\/\.gstack-cso-acquisition-export-[a-f0-9]{24}$/.test(containerPath))throw new CsoError('INVALID_SCHEMA','Acquisition export path is outside the fixed helper contract');
const stat=fs.lstatSync(destination),real=fs.realpathSync(destination);if(!stat.isDirectory()||stat.isSymbolicLink()||real.includes(',')||(process.getuid&&stat.uid!==process.getuid())||(stat.mode&0o022)!==0||fs.readdirSync(real).length)throw new CsoError('UNSAFE_PATH','Acquisition output must be one empty private owned directory');
await this.docker(['cp',`${id}:${containerPath}/.`,real],8192);
}
async removeContainer(id:string):Promise<void>{
const index=this.ids.findIndex(item=>item.id===id);if(index<0)throw new CsoError('ISOLATION_FAILED','Attempted to remove a container outside this reproduction group');
const present=await this.docker(['ps','--all','--quiet','--no-trunc','--filter',`id=${id}`],8192);if(present&&present!==id)throw new CsoError('ISOLATION_FAILED','Docker returned an inexact resource identity during cleanup');if(present)await this.docker(['rm','--force','--volumes',id],8192);
const [{role}]=this.ids.splice(index,1);this.writableBytesById.delete(id);const roleIndex=this.roles.lastIndexOf(role);if(roleIndex>=0)this.roles.splice(roleIndex,1);
}
async cleanup():Promise<void>{
let failure:unknown;try{
const labeled=await this.docker(['ps','--all','--quiet','--no-trunc','--filter',`label=com.gstack.cso.run=${this.runId}`],128*1024),targets=new Set(this.ids.map(item=>item.id));
for(const id of labeled.split('\n').filter(Boolean)){if(!/^[a-f0-9]{64}$/.test(id))throw new CsoError('ISOLATION_FAILED','Docker returned an invalid labeled resource identity during cleanup');targets.add(id);}
for(const id of [...targets].reverse()){const present=await this.docker(['ps','--all','--quiet','--no-trunc','--filter',`id=${id}`],8192);if(present&&present!==id)throw new CsoError('ISOLATION_FAILED','Docker returned an inexact resource identity during cleanup');if(present)await this.docker(['rm','--force','--volumes',id],8192);}
const remaining=await this.docker(['ps','--all','--quiet','--no-trunc','--filter',`label=com.gstack.cso.run=${this.runId}`],8192);if(remaining)throw new CsoError('ISOLATION_FAILED','Run-owned Docker resources remain after cleanup');
}catch(error){failure=error;}
if(failure)throw new CsoError('ISOLATION_FAILED','Exact reproduction cleanup failed; verification evidence was withheld and the watchdog remains responsible');
this.ids=[];this.writableBytesById.clear();release(this.lease);fs.writeFileSync(join(this.dir,'watchdog.terminal'),'cleanup complete\n',{mode:0o600,flag:'wx'});
const stopped=join(this.dir,'watchdog.stopped');for(let i=0;i<500&&!fs.existsSync(stopped);i++)await new Promise(resolve=>setTimeout(resolve,10));if(!fs.existsSync(stopped))throw new CsoError('ISOLATION_FAILED','Docker watchdog did not acknowledge exact lease cleanup');
}
}
+49
View File
@@ -0,0 +1,49 @@
/** Decode the two Git path tokens in a `diff --git` header. */
function token(source:string,offset:number):{value:string;next:number}|undefined{
if(source[offset]!=='"'){
const end=source.indexOf(' ',offset),next=end<0?source.length:end;
if(next===offset)return;
return{value:source.slice(offset,next),next};
}
const bytes:number[]=[];let at=offset+1;
const append=(value:string)=>bytes.push(...new TextEncoder().encode(value));
while(at<source.length){
const value=source[at++];
if(value==='"')return{value:new TextDecoder('utf-8',{fatal:true}).decode(Uint8Array.from(bytes)),next:at};
if(value!=='\\'){append(value);continue;}
if(at>=source.length)return;
const escaped=source[at++],mapped:{[key:string]:string}={a:'\x07',b:'\b',f:'\f',n:'\n',r:'\r',t:'\t',v:'\v','\\':'\\','"':'"'};
if(mapped[escaped]!==undefined){append(mapped[escaped]);continue;}
if(/[0-7]/.test(escaped)&&/^[0-7]{2}/.test(source.slice(at,at+2))){bytes.push(Number.parseInt(escaped+source.slice(at,at+2),8));at+=2;continue;}
return;
}
}
export function gitDiffHeaderPaths(line:string):[string,string]|undefined{
const prefix='diff --git ';if(!line.startsWith(prefix))return;
try{
const left=token(line,prefix.length);if(!left||line[left.next]!==' ')return;
const right=token(line,left.next+1);if(!right||right.next!==line.length)return;
return[left.value,right.value];
}catch{return;}
}
/** Return only exact path hunks, keeping one commit preamble per matching commit. */
export function historyForPath(raw:string,path:string):string|undefined{
const expected=new Set([`a/${path}`,`b/${path}`]),output:string[]=[],lines=raw.split('\n');
let preamble:string[]=[],section:string[]|undefined,include=false,preambleEmitted=false;
const flush=()=>{
if(section&&include){if(!preambleEmitted){output.push(...preamble);preambleEmitted=true;}output.push(...section);}
section=undefined;include=false;
};
for(const line of lines){
if(line.startsWith('commit ')){flush();preamble=[line];preambleEmitted=false;continue;}
if(line.startsWith('diff --git ')){
flush();section=[line];const paths=gitDiffHeaderPaths(line);include=Boolean(paths&&(expected.has(paths[0])||expected.has(paths[1])));continue;
}
if(section)section.push(line);else preamble.push(line);
}
flush();
while(output.at(-1)==='')output.pop();
return output.length?output.join('\n'):undefined;
}
+182
View File
@@ -0,0 +1,182 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import { join } from 'node:path';
import { CsoError } from './contracts';
import { dockerEndpoint, dockerExactImagePresent, dockerProbe, dockerPullExactCatalogImage } from './docker';
import { validateRuntimeCatalog, type RuntimeCatalog, type RuntimePlatform } from './runtime-catalog';
import { validateScannerCatalog, type ScannerCatalog } from './scanner-catalog';
import { secureDirectory } from './state';
export interface QualifiedCatalogImage {
kind:'runtime'|'scanner';
id:string;
image:string;
platform:RuntimePlatform;
}
export interface CatalogImageSession {
readonly docker:{endpoint:string;version:string;security:string[]};
present(entry:QualifiedCatalogImage,deadline?:number):Promise<boolean>;
pull(entry:QualifiedCatalogImage,deadline?:number):Promise<void>;
close():void;
}
/** Doctor performs concurrent, read-only checks inside its 30-second contract. */
export const CATALOG_IMAGE_INSPECTION_BUDGET_MS=30_000;
export const DEFAULT_CATALOG_IMAGE_BUDGET_MS=30_000;
export const MIN_CATALOG_IMAGE_BUDGET_SECONDS=5;
export const MAX_CATALOG_IMAGE_BUDGET_SECONDS=300;
export const MAX_CATALOG_IMAGE_PROVISIONING_BUDGET_MS=60*60_000;
const CATALOG_IMAGE_ADMISSION_BUDGET_MS=30_000;
export interface CatalogImageProvisioningPolicy {perImageMs:number;aggregateMs:number;}
/**
* Give every declared native-platform image a bounded opportunity to download.
* The one-hour ceiling admits the current eleven-image catalog even at the
* maximum configurable five-minute allowance.
*/
export function catalogImageProvisioningPolicy(imageCount:number,requestedSeconds?:string):CatalogImageProvisioningPolicy{
if(!Number.isSafeInteger(imageCount)||imageCount<0)throw new CsoError('INVALID_ARGUMENT','Catalog image count is invalid');
let seconds=DEFAULT_CATALOG_IMAGE_BUDGET_MS/1000;
if(requestedSeconds!==undefined){
if(!/^[0-9]+$/.test(requestedSeconds))throw new CsoError('INVALID_ARGUMENT','--per-image-seconds requires a whole number');
seconds=Number(requestedSeconds);
if(seconds<MIN_CATALOG_IMAGE_BUDGET_SECONDS||seconds>MAX_CATALOG_IMAGE_BUDGET_SECONDS)throw new CsoError('INVALID_ARGUMENT',`--per-image-seconds must be ${MIN_CATALOG_IMAGE_BUDGET_SECONDS}..${MAX_CATALOG_IMAGE_BUDGET_SECONDS}`);
}
const perImageMs=seconds*1000,aggregateMs=CATALOG_IMAGE_ADMISSION_BUDGET_MS+imageCount*perImageMs;
if(!Number.isSafeInteger(aggregateMs)||aggregateMs>MAX_CATALOG_IMAGE_PROVISIONING_BUDGET_MS)throw new CsoError('INCOMPATIBLE_INPUT','Qualified image catalog exceeds the bounded setup preload capacity');
return{perImageMs,aggregateMs};
}
export type CatalogImageSessionFactory=(deadline:number)=>Promise<CatalogImageSession>;
export interface CatalogImageAvailability extends QualifiedCatalogImage {
status:'available'|'unavailable';
reason?:string;
}
export interface CatalogImageInspection {
docker:{status:'ready'|'missing';detail:unknown};
images:CatalogImageAvailability[];
}
export interface CatalogImageProvisionResult {
schemaVersion:1;
status:'complete'|'partial'|'not_available';
downloads:true;
platform:RuntimePlatform;
requested:number;
inspected:number;
alreadyPresent:number;
downloaded:number;
deadlineReached:boolean;
unavailable:CatalogImageAvailability[];
summary:string;
}
export function qualifiedCatalogImages(runtimeCatalog:RuntimeCatalog,scannerCatalog:ScannerCatalog,platform:RuntimePlatform):QualifiedCatalogImage[]{
validateRuntimeCatalog(runtimeCatalog);validateScannerCatalog(scannerCatalog);
const entries:QualifiedCatalogImage[]=[
...runtimeCatalog.runtimes.filter(item=>item.platform===platform).map(item=>({kind:'runtime' as const,id:item.id,image:item.image,platform:item.platform})),
...scannerCatalog.scanners.filter(item=>item.platform===platform).map(item=>({kind:'scanner' as const,id:item.id,image:item.image,platform:item.platform})),
];
const identities=new Set<string>();
for(const entry of entries){
const identity=`${entry.kind}:${entry.id}`;
if(identities.has(identity))throw new CsoError('INCOMPATIBLE_INPUT','Qualified image catalogs contain a duplicate identity');
identities.add(identity);
}
return entries.sort((left,right)=>`${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`));
}
function controlledReason(error:unknown,fallback:string):string{
return error instanceof CsoError?error.message:fallback;
}
export async function inspectCatalogImages(entries:QualifiedCatalogImage[],open:CatalogImageSessionFactory,deadline=Date.now()+CATALOG_IMAGE_INSPECTION_BUDGET_MS):Promise<CatalogImageInspection>{
let session:CatalogImageSession;
try{session=await open(deadline);}catch(error){
const detail=controlledReason(error,'Local Docker is unavailable for exact catalog image inspection');
return{docker:{status:'missing',detail},images:entries.map(entry=>({...entry,status:'unavailable',reason:detail}))};
}
try{
// Read-only daemon lookups run together so doctor remains within its
// 30-second contract even when a local Docker client is slow to fail.
const images=await Promise.all(entries.map(async(entry):Promise<CatalogImageAvailability>=>{
try{const present=await session.present(entry);if(Date.now()>=deadline)throw new CsoError('DEADLINE','Exact image inspection reached the aggregate image-provisioning deadline');return{...entry,status:present?'available':'unavailable',...(present?{}:{reason:'Exact qualified image is not present in the local Docker daemon'})};}
catch(error){return{...entry,status:'unavailable',reason:controlledReason(error,'Exact qualified image could not be inspected safely')};}
}));
return{docker:{status:'ready',detail:session.docker},images};
}finally{session.close();}
}
export async function provisionCatalogImages(entries:QualifiedCatalogImage[],platform:RuntimePlatform,open:CatalogImageSessionFactory,deadline=Date.now()+catalogImageProvisioningPolicy(entries.length).aggregateMs,perImageBudgetMs=DEFAULT_CATALOG_IMAGE_BUDGET_MS):Promise<CatalogImageProvisionResult>{
if(!entries.length)return{schemaVersion:1,status:'complete',downloads:true,platform,requested:0,inspected:0,alreadyPresent:0,downloaded:0,deadlineReached:false,unavailable:[],summary:'No qualified CSO images are published for this platform; static audits remain available.'};
if(!Number.isSafeInteger(perImageBudgetMs)||perImageBudgetMs<1||perImageBudgetMs>MAX_CATALOG_IMAGE_BUDGET_SECONDS*1000)throw new CsoError('INVALID_ARGUMENT','Catalog per-image budget is invalid');
const deadlineReason='The bounded aggregate CSO image preload deadline was reached';
if(Date.now()>=deadline){const unavailable=entries.map(entry=>({...entry,status:'unavailable' as const,reason:deadlineReason}));return{schemaVersion:1,status:'partial',downloads:true,platform,requested:entries.length,inspected:0,alreadyPresent:0,downloaded:0,deadlineReached:true,unavailable,summary:`Qualified CSO image preload partial: 0/${entries.length} available; ${deadlineReason.toLowerCase()}. Rerun setup to continue.`};}
let session:CatalogImageSession;
try{session=await open(deadline);}catch(error){
const reason=controlledReason(error,'Local Docker is unavailable for qualified image provisioning'),unavailable=entries.map(entry=>({...entry,status:'unavailable' as const,reason}));
const deadlineReached=error instanceof CsoError&&error.code==='DEADLINE';
return{schemaVersion:1,status:deadlineReached?'partial':'not_available',downloads:true,platform,requested:entries.length,inspected:0,alreadyPresent:0,downloaded:0,deadlineReached,unavailable,summary:deadlineReached?`Qualified CSO image preload partial: 0/${entries.length} available; ${reason}. Rerun setup to continue.`:`Qualified CSO images were not preloaded: ${reason}. Rerun setup after the prerequisite is available.`};
}
let inspected=0,alreadyPresent=0,downloaded=0,pullBlocked='',deadlineReached=false,perImageTimeouts=0;const unavailable:CatalogImageAvailability[]=[];
try{
for(let index=0;index<entries.length;index++){
const entry=entries[index];
if(Date.now()>=deadline){deadlineReached=true;for(const remaining of entries.slice(index))unavailable.push({...remaining,status:'unavailable',reason:deadlineReason});break;}
const imageDeadline=Math.min(deadline,Date.now()+perImageBudgetMs),perImageReason=`The ${Math.ceil(perImageBudgetMs/1000)}-second per-image CSO preload deadline was reached`;
let present=false;
try{
present=await session.present(entry,imageDeadline);if(Date.now()>=imageDeadline)throw new CsoError('DEADLINE',imageDeadline===deadline?'Exact image inspection reached the aggregate image-provisioning deadline':perImageReason);inspected++;
if(present){alreadyPresent++;continue;}
}catch(error){
if(error instanceof CsoError&&error.code==='DEADLINE'){
if(Date.now()>=deadline){deadlineReached=true;unavailable.push({...entry,status:'unavailable',reason:error.message});for(const remaining of entries.slice(index+1))unavailable.push({...remaining,status:'unavailable',reason:deadlineReason});break;}
perImageTimeouts++;unavailable.push({...entry,status:'unavailable',reason:perImageReason});continue;
}
unavailable.push({...entry,status:'unavailable',reason:controlledReason(error,'Exact qualified image could not be inspected safely')});continue;
}
// A registry failure blocks further network attempts, but read-only local
// inspection continues so the setup summary never calls a cached digest
// unavailable merely because it sorts after the failed pull.
if(pullBlocked){unavailable.push({...entry,status:'unavailable',reason:`Network provisioning stopped after an anonymous registry prerequisite failed: ${pullBlocked}`});continue;}
if(Date.now()>=deadline){deadlineReached=true;unavailable.push({...entry,status:'unavailable',reason:deadlineReason});for(const remaining of entries.slice(index+1))unavailable.push({...remaining,status:'unavailable',reason:deadlineReason});break;}
try{await session.pull(entry,imageDeadline);if(Date.now()>=imageDeadline)throw new CsoError('DEADLINE',imageDeadline===deadline?'Qualified image pull reached the aggregate preload deadline':perImageReason);downloaded++;}
catch(error){
if(error instanceof CsoError&&error.code==='DEADLINE'){
if(Date.now()>=deadline){deadlineReached=true;unavailable.push({...entry,status:'unavailable',reason:error.message});for(const remaining of entries.slice(index+1))unavailable.push({...remaining,status:'unavailable',reason:deadlineReason});break;}
perImageTimeouts++;unavailable.push({...entry,status:'unavailable',reason:perImageReason});continue;
}
pullBlocked=controlledReason(error,'Qualified image provisioning failed');unavailable.push({...entry,status:'unavailable',reason:pullBlocked});
}
}
}finally{session.close();}
const status=deadlineReached?'partial':unavailable.length?(alreadyPresent||downloaded?'partial':'not_available'):'complete';
const summary=deadlineReached
?`Qualified CSO image preload partial: ${alreadyPresent+downloaded}/${entries.length} available; inspected ${inspected}/${entries.length}; the bounded aggregate deadline was reached. Rerun setup to continue.`
:perImageTimeouts
?`Qualified CSO image preload ${status}: ${alreadyPresent+downloaded}/${entries.length} available; inspected ${inspected}/${entries.length}; ${perImageTimeouts} exceeded the ${Math.ceil(perImageBudgetMs/1000)}-second per-image deadline. Increase GSTACK_CSO_IMAGE_PULL_TIMEOUT_SECONDS within 5..300 or rerun setup to continue.`
:unavailable.length
?`Qualified CSO image preload ${status}: ${alreadyPresent+downloaded}/${entries.length} available; inspected ${inspected}/${entries.length}; ${unavailable.length} require local Docker and anonymous public registry access. Rerun setup after the prerequisite is available.`
:`Qualified CSO images ready: ${entries.length} available (${downloaded} downloaded, ${alreadyPresent} already local).`;
return{schemaVersion:1,status,downloads:true,platform,requested:entries.length,inspected,alreadyPresent,downloaded,deadlineReached,unavailable,summary};
}
export async function openLocalCatalogImageSession(env:Record<string,string|undefined>=process.env,deadline=Date.now()+CATALOG_IMAGE_INSPECTION_BUDGET_MS):Promise<CatalogImageSession>{
let home='';
try{
home=secureDirectory(fs.mkdtempSync(join(fs.realpathSync(os.tmpdir()),'gstack-cso-images-')));
// Endpoint discovery and the daemon probe must not borrow the download
// allowance. A slow or hostile local Docker endpoint gets the same bounded
// admission window in doctor and setup; successful pulls keep the caller's
// larger aggregate deadline below.
const admissionDeadline=Math.min(deadline,Date.now()+CATALOG_IMAGE_ADMISSION_BUDGET_MS);
const endpoint=await dockerEndpoint(home,env,admissionDeadline),config=secureDirectory(join(home,'docker-config'));
// dockerEnvironment pins both HOME and DOCKER_CONFIG here. An explicit
// empty auth map prevents inherited credential stores/helpers from being
// consulted during installation-time public pulls.
fs.writeFileSync(join(config,'config.json'),'{"auths":{}}\n',{encoding:'utf8',mode:0o600,flag:'wx'});
const probe=await dockerProbe(endpoint,home,admissionDeadline),docker={endpoint:endpoint.uri,...probe};
let closed=false;
return{
docker,
present:(entry,operationDeadline=deadline)=>{if(closed)throw new CsoError('ISOLATION_FAILED','Catalog image session is closed');return dockerExactImagePresent(endpoint,home,entry.image,entry.platform,Math.min(deadline,operationDeadline));},
pull:(entry,operationDeadline=deadline)=>{if(closed)throw new CsoError('ISOLATION_FAILED','Catalog image session is closed');return dockerPullExactCatalogImage(endpoint,home,entry.image,entry.platform,Math.min(deadline,operationDeadline));},
close:()=>{if(closed)return;closed=true;fs.rmSync(home,{recursive:true,force:true});},
};
}catch(error){if(home)fs.rmSync(home,{recursive:true,force:true});throw error;}
}
+109
View File
@@ -0,0 +1,109 @@
# CSO runtime build and qualification
These recipes are trusted runtime inputs, not application Dockerfiles. The base
and tool inputs are reviewed and pinned per native platform; the resulting
gstack images are not yet published or qualified. `runtime-catalog.json`
records all ten build-reviewed profiles and deliberately contains no executable
image until the release gates have passed.
Trusted CI re-resolves each recorded source tag to its reviewed index digest,
proves that the native manifest is a member of that index, inspects its native
image configuration, and executes exact version probes without network access.
It validates `BASE_IMAGE` (and Python's `UV_IMAGE`) as reviewed
`repository@sha256:<64 lowercase hex>` references before building. Build each
profile on Linux amd64 and arm64, record exact runtime and package-manager
versions, generate an SBOM and provenance attestations, and verify those
attestations before proposing a catalog change. The catalog records their
digests, the source commit, and the qualification run. A tag, a successful image
build, or an agent-provided `qualified` assertion is insufficient.
The Rails base additionally needs a compiler, SQLite and PostgreSQL development
headers, and the exact Bundler version from its supported fixture matrix.
Unsupported native libraries are prerequisites. The Python base includes pip;
the separate uv image supplies the exact qualified uv executable.
The PostgreSQL sidecar is a separate qualified image. It runs as fixed uid/gid
10001, creates every validated synthetic Rails database from a read-only policy,
and exposes PostgreSQL only on the reproduction group's loopback namespace.
Qualification must prove readiness for every declared database before Rails is
started and must rebuild a fresh sidecar for each before/after phase.
All recipes use the fixed `/opt/cso/entrypoint`, uid/gid 10001, and no application
source. The runner must still impose network namespaces, seccomp, dropped
capabilities, no-new-privileges, a read-only root, bounded tmpfs mounts, resource
admission, disabled daemon logging, and a detached watchdog. The image's USER and
ENTRYPOINT alone provide none of those guarantees.
Each application image also contains the compiled `/opt/cso/preparation`
helper. Its reviewed version is recorded as `cso-preparation: 1.0.0` in the
image build inputs and runtime catalog. Qualification exercises its
registry-broker forwarder, lock-bound archive manifest, and offline cache
seeding before a digest can be promoted.
Application qualification also exercises the embedded `/opt/cso/verifier`
against positive and deliberately failing assertions. This is independent of
the cold-start and private held-out repair gates. PostgreSQL uses its separate
multi-database and readiness qualification and cannot present application-only
qualification fields.
The Bun image includes a reviewed `/opt/cso/no-auto-install.toml`. Canonical
Bun start and test commands also pass `--no-install` and that exact config, so
target execution cannot trigger Bun's runtime automatic installer. The earlier
offline `bun install` phase still runs admitted lifecycle scripts with network
disabled.
`preparation.ts` emits acquisition metadata and command descriptions. Acquisition
containers receive that metadata and verified public archives only; project code,
Gemfiles, hooks, and native extensions run in subsequent network-none containers.
Registry host restrictions require the trusted runner's deny-by-default egress
mechanism and redirect/DNS checks. Package-manager flags alone are insufficient.
Source references inspected for these contracts:
- [Bun installation and frozen locks](https://bun.com/docs/pm/cli/install)
- [uv export and `--no-emit-local`](https://docs.astral.sh/uv/reference/cli/)
- [RubyGems fetch](https://guides.rubygems.org/command-reference/#gem-fetch)
- [Docker attestations](https://docs.docker.com/build/metadata/attestations/)
Version the catalog together with helper ABI 3. A rollback selects the previous
compatible pair. Persisted reports remain readable independently of which
runtime pair is active.
Pull requests run the native build-only matrix without registry publication.
Protected main publishes staging images. A separate protected promotion
workflow accepts exactly ten `qualified-runtime.json` statements from one
successful main run, checks them against the reviewed build matrix, and emits an
attested `runtime-catalog.candidate.json`. It verifies that attestation against
the exact candidate bytes, source commit, protected-main ref, and promotion
workflow identity. A previous-revision compare-and-swap then copies those exact
bytes to a fresh branch and opens an ordinary review PR. The workflow never
updates `main` directly. The catalog validator also recomputes the retained
runtime-matrix digest; the complete release-gate statement digest remains a
separate provenance field.
GHCR creates each new staging package private. After its bootstrap publication,
a package administrator must make it public in GitHub's package settings before
the workflow can continue; GitHub treats that visibility change as
irreversible. Staging, native qualification, private qualification ingress, and
catalog promotion all fail closed unless GitHub's package API reports `public`
and a Docker client using a fresh config with empty `auths`
pulls the exact platform digest. The protected workflows never treat their own
GHCR login as evidence that users can acquire a promoted runtime.
Each application statement must attest successful containment, public-only
acquisition, offline lifecycle work, cold start, positive and deliberately
failing verifier assertions, watchdog cleanup, secret-canary checks, the
accuracy gates, and a private held-out runtime-tested repair. Rails also requires both
database modes and native extensions. PostgreSQL requires containment, cold
start, multiple databases, readiness, watchdog cleanup, and secret canaries.
Until a protected run produces all ten statements, the reviewed profiles remain
visible to `--doctor` but target execution returns
`MISSING_QUALIFIED_RUNTIME`.
The protected `cso-runtime-release` environment also defines
`CSO_QUALIFICATION_ACTOR`, the service account allowed to send the
`cso-runtime-qualified` repository dispatch. The ingress workflow verifies the
actor, re-verifies each staged OCI provenance and SBOM attestation, normalizes
the evidence to its own run identity, and validates the full matrix before it
uploads `cso-qualified-runtime-statements`. Private assertion content never
enters this repository or the artifact.
+96
View File
@@ -0,0 +1,96 @@
{
"schemaVersion": 1,
"helperAbi": 3,
"state": "reviewed",
"revision": "cso-runtime-inputs-2026-09-10",
"reviewedAt": "2026-09-10T00:00:00.000Z",
"reviewMethod": "Native OCI manifest inspection plus upstream runtime version metadata; CI rechecks architecture and executable versions before every build.",
"sbomGenerator": {
"source": "docker.io/docker/scout-sbom-indexer:1",
"indexImage": "docker.io/docker/scout-sbom-indexer@sha256:4b67f29eb0d1244ab0f62de867ac5dafd7262fcd7ebbdefa6ec8aacd6b15252d",
"images": {
"linux/amd64": "docker.io/docker/scout-sbom-indexer@sha256:dc9450ee50d985e1f5cc60069c91b5f45b4a8a7e9dbcdd33aaab259032a9cab9",
"linux/arm64": "docker.io/docker/scout-sbom-indexer@sha256:f11b16a96cdf4d6df23994e6c424bc6717959373679e9a0dde36b92a8b03e386"
}
},
"profiles": [
{
"id": "node-24.4.0",
"stack": "node",
"source": "docker.io/library/node:24.4.0-bookworm-slim",
"indexImage": "docker.io/library/node@sha256:1b044a60874f1b57ac8c4e708ddb3a00e55b34586ebbacce09a48796dafcc799",
"baseImages": {
"linux/amd64": "docker.io/library/node@sha256:f2beab5c8aa1c35bfec1e7ddcdc8e78fd82a56f96fc514baf2ce86d7dc1b100f",
"linux/arm64": "docker.io/library/node@sha256:821c8ce03d9778dbdbbb94919b3bf281837c1a31517e7dee528bd4b274991218"
},
"versions": {
"node": "24.4.0",
"npm": "11.4.2",
"cso-preparation": "1.0.0"
}
},
{
"id": "bun-1.3.10",
"stack": "bun",
"source": "docker.io/oven/bun:1.3.10-debian",
"indexImage": "docker.io/oven/bun@sha256:367842b35abbdf23f39e23c71f3a08eee940ff2679a14e08a5afcf4a1436cd89",
"baseImages": {
"linux/amd64": "docker.io/oven/bun@sha256:5ee6c5be4575d5ba079b5a9afb24d4600f75ccb1a92602f079ee99560b9dcee9",
"linux/arm64": "docker.io/oven/bun@sha256:b0bdc79f333b728119c6527be6bd52cca09352b7fce46613d2fe3d24c6e395e7"
},
"versions": {
"bun": "1.3.10",
"cso-preparation": "1.0.0"
}
},
{
"id": "python-3.13.4-uv-0.8.0",
"stack": "python",
"source": "docker.io/library/python:3.13.4-slim-bookworm",
"indexImage": "docker.io/library/python@sha256:9ed09f78253eb4f029f3d99e07c064f138a6f1394932c3807b3d0738a674d33b",
"baseImages": {
"linux/amd64": "docker.io/library/python@sha256:25fab3d7d1d7b2e955f8b39ad49b70e5ddfdc32c202069bd52e34e151eb8704c",
"linux/arm64": "docker.io/library/python@sha256:d27f102f1850c0886b0d1df3718a0a31d8e88aae343506e61ef2966ae468abea"
},
"uvSource": "ghcr.io/astral-sh/uv:0.8.0",
"uvIndexImage": "ghcr.io/astral-sh/uv@sha256:5778d479c0fd7995fedd44614570f38a9d849256851f2786c451c220d7bd8ccd",
"uvImages": {
"linux/amd64": "ghcr.io/astral-sh/uv@sha256:50fbd66ab876ddb7a725f4d2014a59aa31da0b69933f787b8cfba7291ea5494f",
"linux/arm64": "ghcr.io/astral-sh/uv@sha256:24d469099a90d1348137db1ec9bf06dac5426ae9de4d669b8a4b811559058c60"
},
"versions": {
"python": "3.13.4",
"uv": "0.8.0",
"cso-preparation": "1.0.0"
}
},
{
"id": "rails-ruby-3.4.4",
"stack": "rails",
"source": "docker.io/library/ruby:3.4.4-bookworm",
"indexImage": "docker.io/library/ruby@sha256:f7ab76e2c36ab406ebc36aeba20624b26a8fae7c2998acabbe9662e2a73f00f3",
"baseImages": {
"linux/amd64": "docker.io/library/ruby@sha256:edfda7d45b762e5ef89d7e94f0d4405132e129928440b8c8af100bd1d07c37b1",
"linux/arm64": "docker.io/library/ruby@sha256:9a4b3512d9fa4f1cc3b8193e141eb5f775ef5cbd8d99c8dd2a1d829c9c835541"
},
"versions": {
"ruby": "3.4.4",
"bundler": "2.6.7",
"cso-preparation": "1.0.0"
}
},
{
"id": "postgresql-17.2",
"stack": "postgresql",
"source": "docker.io/library/postgres:17.2-bookworm",
"indexImage": "docker.io/library/postgres@sha256:3267c505060a0052e5aa6e5175a7b41ab6b04da2f8c4540fc6e98a37210aa2d3",
"baseImages": {
"linux/amd64": "docker.io/library/postgres@sha256:0e3fd61dc630bf506330e3f2061cb6120cf4cc04cb7bd0f17683b35cc422342d",
"linux/arm64": "docker.io/library/postgres@sha256:b1eff56c2661dadeb2a455f9197c5f4e97582e439191790d8b778e47409d10e7"
},
"versions": {
"postgresql": "17.2"
}
}
]
}
+2
View File
@@ -0,0 +1,2 @@
[install]
auto = "disable"
+14
View File
@@ -0,0 +1,14 @@
# Both Bun and its Node-compatible toolchain must be qualified at exact versions.
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
USER root
RUN mkdir -p /opt/cso /work /metadata /archives /source /policy /fixtures && touch /opt/cso/empty-config \
&& chown 10001:10001 /work /metadata /archives
COPY --chmod=0555 entrypoint /opt/cso/entrypoint
COPY --chmod=0555 run-app /opt/cso/run-app
COPY --chmod=0555 gstack-cso-verifier /opt/cso/verifier
COPY --chmod=0555 gstack-cso-preparation /opt/cso/preparation
COPY --chmod=0444 bun-no-auto-install.toml /opt/cso/no-auto-install.toml
USER 10001:10001
WORKDIR /work
ENTRYPOINT ["/opt/cso/entrypoint"]
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
set -eu
umask 077
export PATH=/usr/local/bin:/usr/bin:/bin
export HOME=/work/.cso-home
unset BUN_OPTIONS BUN_BE_BUN NODE_OPTIONS RUBYOPT RUBYLIB PYTHONPATH PYTHONHOME LD_PRELOAD LD_LIBRARY_PATH ENV BASH_ENV CDPATH
if [ "$#" -eq 0 ]; then
echo 'CSO runtime requires an explicit command' >&2
exit 64
fi
case "$1" in
/*) exec "$@" ;;
*) echo 'CSO runtime requires an absolute executable path' >&2; exit 64 ;;
esac
+12
View File
@@ -0,0 +1,12 @@
# BASE_IMAGE must be a reviewed, provenance-verified node image@sha256 digest.
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
RUN mkdir -p /opt/cso /work /metadata /archives /source /policy /fixtures && touch /opt/cso/empty-config \
&& chown 10001:10001 /work /metadata /archives
COPY --chmod=0555 entrypoint /opt/cso/entrypoint
COPY --chmod=0555 run-app /opt/cso/run-app
COPY --chmod=0555 gstack-cso-verifier /opt/cso/verifier
COPY --chmod=0555 gstack-cso-preparation /opt/cso/preparation
USER 10001:10001
WORKDIR /work
ENTRYPOINT ["/opt/cso/entrypoint"]
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
set -eu
test "$#" -eq 1
policy="$1"
case "$policy" in /policy/*) ;; *) exit 64 ;; esac
test -f /work/postgresql.ready
export PGPASSWORD=cso-disposable-test
count=0
while IFS= read -r database || test -n "$database"; do
case "$database" in cso_[A-Za-z_]*) ;; *) exit 64 ;; esac
case "$database" in *[!A-Za-z0-9_]*) exit 64 ;; esac
count=$((count + 1)); test "$count" -le 64
test "$(/opt/cso/bin/psql -h 127.0.0.1 -p 5432 -U cso -d "$database" -Atqc 'SELECT 1')" = 1
done < "$policy"
test "$count" -gt 0
+26
View File
@@ -0,0 +1,26 @@
# BASE_IMAGE must be a reviewed PostgreSQL image@sha256 digest. The image is
# rebuilt with a fixed non-root identity so Docker policy and initdb agree.
ARG BASE_IMAGE
FROM ${BASE_IMAGE} AS upstream
# A fresh image configuration prevents an upstream VOLUME declaration from
# creating an unbounded anonymous host volume behind the read-only root policy.
FROM scratch
COPY --from=upstream / /
ENV LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
PGDATA=/work/postgresql-data \
PATH=/opt/cso/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
USER root
RUN set -eu; \
if ! awk -F: '$3 == 10001 { found=1 } END { exit found ? 0 : 1 }' /etc/group; then printf 'cso:x:10001:\n' >> /etc/group; fi; \
if ! awk -F: '$3 == 10001 { found=1 } END { exit found ? 0 : 1 }' /etc/passwd; then printf 'cso:x:10001:10001:CSO PostgreSQL:/work:/sbin/nologin\n' >> /etc/passwd; fi; \
mkdir -p /opt/cso/bin /work /policy; chown 10001:10001 /work; \
for tool in initdb postgres createdb psql pg_isready; do target="$(find /usr/lib/postgresql /usr/local -type f -name "$tool" -perm /0111 -print 2>/dev/null | sort | head -n 1)"; test -n "$target"; ln -s "$target" "/opt/cso/bin/$tool"; done
COPY --chmod=0555 entrypoint /opt/cso/entrypoint
COPY --chmod=0555 run-postgresql /opt/cso/run-postgresql
COPY --chmod=0555 postgresql-ready /opt/cso/postgresql-ready
COPY --chmod=0555 gstack-cso-verifier /opt/cso/verifier
USER 10001:10001
WORKDIR /work
ENTRYPOINT ["/opt/cso/entrypoint"]
+15
View File
@@ -0,0 +1,15 @@
# Trusted CI supplies reviewed digest references for both stages, never tags.
ARG UV_IMAGE
ARG BASE_IMAGE
FROM ${UV_IMAGE} AS uv
FROM ${BASE_IMAGE}
COPY --from=uv /uv /usr/local/bin/uv
RUN mkdir -p /opt/cso /work /metadata /archives /source /policy /fixtures && touch /opt/cso/empty-config \
&& chown 10001:10001 /work /metadata /archives
COPY --chmod=0555 entrypoint /opt/cso/entrypoint
COPY --chmod=0555 run-app /opt/cso/run-app
COPY --chmod=0555 gstack-cso-verifier /opt/cso/verifier
COPY --chmod=0555 gstack-cso-preparation /opt/cso/preparation
USER 10001:10001
WORKDIR /work
ENTRYPOINT ["/opt/cso/entrypoint"]
+33
View File
@@ -0,0 +1,33 @@
{
"schemaVersion": 1,
"helperAbi": 3,
"state": "enforced",
"buildRevision": "cso-runtime-inputs-2026-09-10",
"platforms": ["linux/amd64", "linux/arm64"],
"profiles": ["node", "bun", "python", "rails", "postgresql"],
"statementArtifact": "cso-qualified-runtime-statements",
"statementFilename": "qualified-runtime.json",
"requiredInputs": [
"reviewed native base and SBOM generator manifests",
"exact runtime and package-manager versions verified inside the selected base",
"immutable staged runtime digest",
"trusted protected-main source commit and workflow run",
"verified SBOM and provenance digests"
],
"requiredChecks": [
"non-root/read-only/capability/seccomp admission",
"IPv4/IPv6/DNS and metadata egress denied",
"secretless cold acquisition and offline boot",
"application verifier positive and deliberately failing assertions",
"lifecycle and native build hooks execute only offline",
"Rails SQLite and PostgreSQL, all connections, native gem cold start",
"one held-out reproduced defect and runtime-tested repair per application stack",
"watchdog survival and exact resource cleanup",
"secret-canary containment",
"daily precision and comprehensive high/critical recall release thresholds",
"signed provenance verification and SBOM digest"
],
"promotion": "The protected promotion workflow accepts exactly ten authenticated same-run qualified-runtime.json statements and emits an attested source-review candidate. It never writes the catalog.",
"externalPrerequisite": "A successful protected-main qualification run must upload cso-qualified-runtime-statements after private held-out and accuracy gates finish. No such artifact exists until those external gates actually pass.",
"rollback": "Select the prior compatible helper/catalog pair; never fall back to a mutable tag."
}
+14
View File
@@ -0,0 +1,14 @@
# BASE_IMAGE is a reviewed Ruby/Bundler image digest that already contains the
# qualified compiler, SQLite development headers, and libpq development headers.
# This recipe never resolves OS packages dynamically or installs project gems.
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
RUN mkdir -p /opt/cso /work /metadata /archives /source /policy /fixtures && touch /opt/cso/empty-config \
&& chown 10001:10001 /work /metadata /archives
COPY --chmod=0555 entrypoint /opt/cso/entrypoint
COPY --chmod=0555 run-app /opt/cso/run-app
COPY --chmod=0555 gstack-cso-verifier /opt/cso/verifier
COPY --chmod=0555 gstack-cso-preparation /opt/cso/preparation
USER 10001:10001
WORKDIR /work
ENTRYPOINT ["/opt/cso/entrypoint"]
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
set -eu
test "$PWD" = /work
/bin/cp -a --no-preserve=ownership /source/. /work/
exec "$@"
+55
View File
@@ -0,0 +1,55 @@
#!/bin/sh
set -eu
umask 077
test "$PWD" = /work
test "$#" -eq 1
policy="$1"
case "$policy" in /policy/*) ;; *) exit 64 ;; esac
test -f "$policy"
count=0
while IFS= read -r database || test -n "$database"; do
case "$database" in cso_[A-Za-z_]*) ;; *) exit 64 ;; esac
case "$database" in *[!A-Za-z0-9_]*) exit 64 ;; esac
test "${#database}" -le 52
count=$((count + 1)); test "$count" -le 64
done < "$policy"
test "$count" -gt 0
data=/work/postgresql-data
socket=/work/postgresql-socket
password=/work/postgresql-password
mkdir -m 700 "$socket"
printf '%s\n' 'cso-disposable-test' > "$password"
/opt/cso/bin/initdb -D "$data" --username=cso --pwfile="$password" --auth-local=scram-sha-256 --auth-host=scram-sha-256 >/dev/null
rm -f "$password"
cat >> "$data/postgresql.conf" <<'EOF'
listen_addresses = '127.0.0.1'
port = 5432
unix_socket_directories = '/work/postgresql-socket'
ssl = off
max_connections = 32
password_encryption = 'scram-sha-256'
fsync = off
synchronous_commit = off
full_page_writes = off
EOF
cleanup() {
if test -n "${postgres_pid:-}" && kill -0 "$postgres_pid" 2>/dev/null; then
kill -TERM "$postgres_pid" 2>/dev/null || true
wait "$postgres_pid" 2>/dev/null || true
fi
}
trap cleanup EXIT INT TERM
/opt/cso/bin/postgres -D "$data" >/dev/null 2>&1 &
postgres_pid=$!
export PGPASSWORD=cso-disposable-test
attempt=0
until /opt/cso/bin/pg_isready -h 127.0.0.1 -p 5432 -U cso -d postgres >/dev/null 2>&1; do
attempt=$((attempt + 1)); test "$attempt" -lt 100; sleep 0.05
done
while IFS= read -r database || test -n "$database"; do
/opt/cso/bin/createdb -h 127.0.0.1 -p 5432 -U cso "$database" >/dev/null
done < "$policy"
printf 'ready\n' > /work/postgresql.ready
wait "$postgres_pid"
+277
View File
@@ -0,0 +1,277 @@
/* Native Windows startup boundary. Compile with the static MSVC CRT (/MT):
* Bun must never run until the explicit environment block has been installed.
* Source: https://learn.microsoft.com/windows/win32/procthread/changing-environment-variables
*/
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0601
#endif
#include <windows.h>
#include <bcrypt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#pragma comment(lib, "bcrypt.lib")
#define CSO_PATH_CAP 32768
#define CSO_ENV_VALUE_CAP 8193
#ifndef GSTACK_CSO_CORE_SHA256
#error GSTACK_CSO_CORE_SHA256 must bind the launcher to its compiled core
#endif
#ifndef GSTACK_CSO_GIT_PATH
#error GSTACK_CSO_GIT_PATH must bind the launcher to setup's resolved git.exe
#endif
static int fail(const char *message) {
fprintf(stderr, "gstack-cso: %s\n", message);
return 69;
}
static int append(wchar_t *buffer, size_t *used, wchar_t value) {
if (*used >= CSO_PATH_CAP - 1) return 0;
buffer[(*used)++] = value;
buffer[*used] = L'\0';
return 1;
}
static int joined_path(wchar_t *output, size_t capacity,
const wchar_t *directory, const wchar_t *leaf) {
int written = swprintf(output, capacity, L"%ls\\%ls", directory, leaf);
return written >= 0 && (size_t)written < capacity;
}
/* Quote each argument using the Windows CRT's backslash/quote rules. No shell
* participates, including for arguments containing %, &, quotes or newlines. */
static int argument(wchar_t *buffer, size_t *used, const wchar_t *value) {
size_t slashes = 0;
if (!append(buffer, used, L'"')) return 0;
for (;; value++) {
if (*value == L'\\') { slashes++; continue; }
size_t count = (*value == L'"' || *value == L'\0') ? slashes * 2 : slashes;
if (*value == L'"') count++;
while (count--) if (!append(buffer, used, L'\\')) return 0;
slashes = 0;
if (*value == L'\0') break;
if (!append(buffer, used, *value)) return 0;
}
return append(buffer, used, L'"');
}
static int environment_entry(wchar_t *block, size_t *used,
const wchar_t *name, const wchar_t *value) {
size_t length = wcslen(name) + wcslen(value) + 2;
if (*used + length >= CSO_PATH_CAP) return 0;
memcpy(block + *used, name, wcslen(name) * sizeof(wchar_t));
*used += wcslen(name); block[(*used)++] = L'=';
memcpy(block + *used, value, (wcslen(value) + 1) * sizeof(wchar_t));
*used += wcslen(value) + 1; block[*used] = L'\0';
return 1;
}
static int inherited_entry(wchar_t *block, size_t *used, const wchar_t *name) {
wchar_t value[CSO_ENV_VALUE_CAP];
value[0] = L'\0';
DWORD length = GetEnvironmentVariableW(name, value, CSO_ENV_VALUE_CAP);
if (length >= (DWORD)CSO_ENV_VALUE_CAP) return 0;
return environment_entry(block, used, name, value);
}
/* Hash the already-open core. Its handle denies writes, deletes, and renames,
* so the bytes checked here are the bytes CreateProcessW will resolve below. */
static int sha256_handle(HANDLE file, char output[65]) {
BCRYPT_ALG_HANDLE algorithm = NULL;
BCRYPT_HASH_HANDLE hash = NULL;
PUCHAR object = NULL;
ULONG object_bytes = 0, hash_bytes = 0, returned = 0;
UCHAR digest[32], buffer[64 * 1024];
DWORD read_bytes = 0;
LARGE_INTEGER start;
int valid = 0;
start.QuadPart = 0;
if (!SetFilePointerEx(file, start, NULL, FILE_BEGIN)) goto cleanup;
if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM,
MS_PRIMITIVE_PROVIDER, 0) < 0) goto cleanup;
if (BCryptGetProperty(algorithm, BCRYPT_OBJECT_LENGTH,
(PUCHAR)&object_bytes, (ULONG)sizeof(object_bytes), &returned, 0) < 0 ||
returned != (ULONG)sizeof(object_bytes) || object_bytes == 0 ||
object_bytes > 1024UL * 1024UL) goto cleanup;
if (BCryptGetProperty(algorithm, BCRYPT_HASH_LENGTH,
(PUCHAR)&hash_bytes, (ULONG)sizeof(hash_bytes), &returned, 0) < 0 ||
returned != (ULONG)sizeof(hash_bytes) || hash_bytes != (ULONG)sizeof(digest))
goto cleanup;
object = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, object_bytes);
if (!object || BCryptCreateHash(algorithm, &hash, object, object_bytes,
NULL, 0, 0) < 0) goto cleanup;
for (;;) {
if (!ReadFile(file, buffer, (DWORD)sizeof(buffer), &read_bytes, NULL))
goto cleanup;
if (read_bytes == 0) break;
if (BCryptHashData(hash, buffer, (ULONG)read_bytes, 0) < 0) goto cleanup;
}
if (BCryptFinishHash(hash, digest, (ULONG)sizeof(digest), 0) < 0) goto cleanup;
static const char hex[] = "0123456789abcdef";
for (ULONG index = 0; index < (ULONG)sizeof(digest); index++) {
output[index * 2] = hex[digest[index] >> 4];
output[index * 2 + 1] = hex[digest[index] & 15];
}
output[64] = '\0';
valid = 1;
cleanup:
if (hash) BCryptDestroyHash(hash);
if (object) {
SecureZeroMemory(object, object_bytes);
HeapFree(GetProcessHeap(), 0, object);
}
if (algorithm) BCryptCloseAlgorithmProvider(algorithm, 0);
if (!SetFilePointerEx(file, start, NULL, FILE_BEGIN)) valid = 0;
return valid;
}
static HANDLE inherited_stdio(DWORD id, DWORD access) {
HANDLE source = GetStdHandle(id), copy = INVALID_HANDLE_VALUE;
if (source && source != INVALID_HANDLE_VALUE) {
if (DuplicateHandle(GetCurrentProcess(), source, GetCurrentProcess(), &copy,
0, TRUE, DUPLICATE_SAME_ACCESS)) return copy;
return INVALID_HANDLE_VALUE;
}
SECURITY_ATTRIBUTES security = {(DWORD)sizeof(security), NULL, TRUE};
return CreateFileW(L"NUL", access, FILE_SHARE_READ | FILE_SHARE_WRITE,
&security, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
}
int wmain(int argc, wchar_t **argv) {
wchar_t module[CSO_PATH_CAP], core[CSO_PATH_CAP], windows[MAX_PATH + 1];
wchar_t caller_cwd[CSO_ENV_VALUE_CAP];
DWORD caller_length = GetCurrentDirectoryW(CSO_ENV_VALUE_CAP, caller_cwd);
if (!caller_length || caller_length >= (DWORD)CSO_ENV_VALUE_CAP)
return fail("caller working directory unavailable or too large");
DWORD length = GetModuleFileNameW(NULL, module, CSO_PATH_CAP);
if (!length || length >= (DWORD)CSO_PATH_CAP) return fail("launcher path unavailable");
HANDLE executable = CreateFileW(module, FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if (executable == INVALID_HANDLE_VALUE) return fail("launcher path unavailable");
length = GetFinalPathNameByHandleW(executable, module, CSO_PATH_CAP, FILE_NAME_NORMALIZED);
CloseHandle(executable);
if (!length || length >= (DWORD)CSO_PATH_CAP) return fail("launcher path unavailable");
wchar_t *slash = wcsrchr(module, L'\\');
if (!slash) return fail("invalid launcher path");
*slash = L'\0';
wchar_t gate_path[CSO_PATH_CAP];
if (!joined_path(gate_path, CSO_PATH_CAP, module, L".gstack-cso-generation.lock"))
return fail("generation lock path too long");
SECURITY_ATTRIBUTES gate_security = {(DWORD)sizeof(gate_security), NULL, TRUE};
HANDLE generation_gate = CreateFileW(gate_path, GENERIC_READ, FILE_SHARE_READ,
&gate_security, OPEN_EXISTING, FILE_ATTRIBUTE_HIDDEN | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
BY_HANDLE_FILE_INFORMATION gate_info;
if (generation_gate == INVALID_HANDLE_VALUE || !GetFileInformationByHandle(generation_gate, &gate_info) ||
(gate_info.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) ||
gate_info.nNumberOfLinks != 1 || gate_info.nFileSizeHigh != 0 || gate_info.nFileSizeLow != 0)
return fail("installation generation lock is missing or invalid; run gstack setup/build");
if (!joined_path(core, CSO_PATH_CAP, module, L"gstack-cso-core.exe"))
return fail("core path too long");
HANDLE pinned_core = CreateFileW(core, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
BY_HANDLE_FILE_INFORMATION core_info;
if (pinned_core == INVALID_HANDLE_VALUE || !GetFileInformationByHandle(pinned_core, &core_info) ||
(core_info.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) ||
core_info.nNumberOfLinks != 1)
return fail("trusted compiled helper is missing or invalid; run gstack setup/build");
wchar_t generation_path[CSO_PATH_CAP];
if (!joined_path(generation_path, CSO_PATH_CAP, module, L".gstack-cso-generation"))
return fail("generation manifest path too long");
HANDLE generation = CreateFileW(generation_path, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_HIDDEN | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
BY_HANDLE_FILE_INFORMATION generation_info; char actual[66]; DWORD manifest_bytes = 0;
if (generation == INVALID_HANDLE_VALUE || !GetFileInformationByHandle(generation, &generation_info) ||
(generation_info.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) ||
generation_info.nNumberOfLinks != 1 || generation_info.nFileSizeHigh != 0 || generation_info.nFileSizeLow != 65 ||
!ReadFile(generation, actual, 65, &manifest_bytes, NULL) || manifest_bytes != 65)
return fail("generation manifest is missing or invalid; run gstack setup/build");
CloseHandle(generation); actual[65] = '\0';
if (actual[64] != '\n' || strncmp(actual, GSTACK_CSO_CORE_SHA256, 64) != 0)
return fail("launcher and compiled helper generations do not match; run gstack setup/build");
char core_sha256[65];
if (!sha256_handle(pinned_core, core_sha256) ||
strcmp(core_sha256, GSTACK_CSO_CORE_SHA256) != 0)
return fail("compiled helper digest does not match its launcher; run gstack setup/build");
wchar_t *command = calloc(CSO_PATH_CAP, sizeof(wchar_t));
wchar_t *environment = calloc(CSO_PATH_CAP, sizeof(wchar_t));
if (!command || !environment) return fail("startup allocation failed");
size_t used = 0;
if (!argument(command, &used, core)) return fail("arguments are too large");
for (int i = 1; i < argc; i++)
if (!append(command, &used, L' ') || !argument(command, &used, argv[i]))
return fail("arguments are too large");
/* Alphabetical, case-insensitive Unicode order; every other variable is
* absent, including BUN_OPTIONS, BUN_BE_BUN, NODE_OPTIONS and loader hooks. */
used = 0;
const wchar_t *names[] = {L"CLAUDE_PLUGIN_DATA", L"CLAUDE_PLUGIN_ROOT",
L"DOCKER_CONFIG", L"DOCKER_CONTEXT", L"DOCKER_HOST"};
for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++)
if (!inherited_entry(environment, &used, names[i])) return fail("environment input is too large");
if (!environment_entry(environment, &used, L"GSTACK_CSO_CALLER_CWD", caller_cwd) ||
!environment_entry(environment, &used, L"GSTACK_CSO_GENERATION_GUARD", L"inherited-windows-generation-handle-v3") ||
!environment_entry(environment, &used, L"GSTACK_CSO_TRUSTED_GIT", GSTACK_CSO_GIT_PATH) ||
!inherited_entry(environment, &used, L"GSTACK_HOME") ||
!inherited_entry(environment, &used, L"HOME")) return fail("environment input is too large");
length = GetWindowsDirectoryW(windows, MAX_PATH + 1);
if (!length || length > (DWORD)MAX_PATH) return fail("Windows system directory unavailable");
wchar_t git_path[CSO_ENV_VALUE_CAP], git_directory[CSO_ENV_VALUE_CAP];
if (wcslen(GSTACK_CSO_GIT_PATH) >= CSO_ENV_VALUE_CAP) return fail("trusted Git path is too large");
wcscpy(git_path, GSTACK_CSO_GIT_PATH); wcscpy(git_directory, git_path);
wchar_t *git_slash = wcsrchr(git_directory, L'\\');
if (!git_slash || git_slash == git_directory) return fail("trusted Git path is invalid");
*git_slash = L'\0';
wchar_t trusted_path[CSO_ENV_VALUE_CAP];
int trusted_length = swprintf(trusted_path, CSO_ENV_VALUE_CAP,
L"%ls;%ls\\System32", git_directory, windows);
if (trusted_length < 0 || (size_t)trusted_length >= CSO_ENV_VALUE_CAP)
return fail("trusted system path is too large");
if (!environment_entry(environment, &used, L"LANG", L"C.UTF-8") ||
!environment_entry(environment, &used, L"LC_ALL", L"C.UTF-8") ||
!environment_entry(environment, &used, L"PATH", trusted_path) ||
!environment_entry(environment, &used, L"SystemRoot", windows) ||
!environment_entry(environment, &used, L"TZ", L"UTC") ||
!inherited_entry(environment, &used, L"USERPROFILE")) return fail("environment input is too large");
STARTUPINFOEXW startup = {0};
PROCESS_INFORMATION child = {0};
startup.StartupInfo.cb = (DWORD)sizeof(startup);
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
HANDLE handles[4] = {inherited_stdio(STD_INPUT_HANDLE, GENERIC_READ),
inherited_stdio(STD_OUTPUT_HANDLE, GENERIC_WRITE), inherited_stdio(STD_ERROR_HANDLE, GENERIC_WRITE), generation_gate};
for (size_t i = 0; i < 4; i++) if (handles[i] == INVALID_HANDLE_VALUE) return fail("standard handles unavailable");
startup.StartupInfo.hStdInput = handles[0];
startup.StartupInfo.hStdOutput = handles[1];
startup.StartupInfo.hStdError = handles[2];
SIZE_T attribute_bytes = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &attribute_bytes);
startup.lpAttributeList = HeapAlloc(GetProcessHeap(), 0, attribute_bytes);
if (!startup.lpAttributeList ||
!InitializeProcThreadAttributeList(startup.lpAttributeList, 1, 0, &attribute_bytes) ||
!UpdateProcThreadAttribute(startup.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
handles, sizeof(handles), NULL, NULL)) return fail("standard handle isolation unavailable");
BOOL created = CreateProcessW(core, command, NULL, NULL, TRUE,
CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT, environment,
module, &startup.StartupInfo, &child);
DeleteProcThreadAttributeList(startup.lpAttributeList);
HeapFree(GetProcessHeap(), 0, startup.lpAttributeList);
for (size_t i = 0; i < 3; i++) CloseHandle(handles[i]);
free(command); free(environment);
if (!created) { CloseHandle(pinned_core); return fail("trusted compiled helper could not start"); }
CloseHandle(child.hThread);
DWORD code = 69;
if (WaitForSingleObject(child.hProcess, INFINITE) == WAIT_OBJECT_0)
GetExitCodeProcess(child.hProcess, &code);
CloseHandle(child.hProcess); CloseHandle(pinned_core); CloseHandle(generation_gate);
return (int)code;
}
+156
View File
@@ -0,0 +1,156 @@
/* Minimal trusted CSO launcher. Linux builds are static so LD_PRELOAD cannot
* execute before the environment is replaced. macOS builds are signed with
* the hardened runtime by build/setup before use. */
#ifdef __APPLE__
#define _DARWIN_C_SOURCE 1
#endif
#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <time.h>
#include <unistd.h>
#ifdef __APPLE__
#include <mach-o/dyld.h>
#endif
#ifndef GSTACK_CSO_CORE_SHA256
#error GSTACK_CSO_CORE_SHA256 must bind the launcher to its compiled core
#endif
typedef struct {
uint32_t state[8];
uint64_t bits;
unsigned char block[64];
size_t used;
} sha256_context;
static uint32_t rotate_right(uint32_t value,unsigned count){return(value>>count)|(value<<(32-count));}
static void sha256_transform(sha256_context *context,const unsigned char block[64]){
static const uint32_t constants[64]={
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
};
uint32_t words[64];
for(size_t i=0;i<16;i++)words[i]=((uint32_t)block[i*4]<<24)|((uint32_t)block[i*4+1]<<16)|((uint32_t)block[i*4+2]<<8)|block[i*4+3];
for(size_t i=16;i<64;i++){
uint32_t x=words[i-15],y=words[i-2];
uint32_t first=rotate_right(x,7)^rotate_right(x,18)^(x>>3),second=rotate_right(y,17)^rotate_right(y,19)^(y>>10);
words[i]=words[i-16]+first+words[i-7]+second;
}
uint32_t a=context->state[0],b=context->state[1],c=context->state[2],d=context->state[3],e=context->state[4],f=context->state[5],g=context->state[6],h=context->state[7];
for(size_t i=0;i<64;i++){
uint32_t sum1=rotate_right(e,6)^rotate_right(e,11)^rotate_right(e,25),choice=(e&f)^((~e)&g),temporary1=h+sum1+choice+constants[i]+words[i];
uint32_t sum0=rotate_right(a,2)^rotate_right(a,13)^rotate_right(a,22),majority=(a&b)^(a&c)^(b&c),temporary2=sum0+majority;
h=g;g=f;f=e;e=d+temporary1;d=c;c=b;b=a;a=temporary1+temporary2;
}
context->state[0]+=a;context->state[1]+=b;context->state[2]+=c;context->state[3]+=d;
context->state[4]+=e;context->state[5]+=f;context->state[6]+=g;context->state[7]+=h;
}
static void sha256_update(sha256_context *context,const unsigned char *data,size_t length){
context->bits+=(uint64_t)length*8;
while(length){size_t available=64-context->used,take=length<available?length:available;memcpy(context->block+context->used,data,take);context->used+=take;data+=take;length-=take;if(context->used==64){sha256_transform(context,context->block);context->used=0;}}
}
static void sha256_finish(sha256_context *context,unsigned char digest[32]){
uint64_t bits=context->bits;context->block[context->used++]=0x80;
if(context->used>56){memset(context->block+context->used,0,64-context->used);sha256_transform(context,context->block);context->used=0;}
memset(context->block+context->used,0,56-context->used);for(size_t i=0;i<8;i++)context->block[63-i]=(unsigned char)(bits>>(i*8));sha256_transform(context,context->block);
for(size_t i=0;i<8;i++){digest[i*4]=(unsigned char)(context->state[i]>>24);digest[i*4+1]=(unsigned char)(context->state[i]>>16);digest[i*4+2]=(unsigned char)(context->state[i]>>8);digest[i*4+3]=(unsigned char)context->state[i];}
}
static int sha256_file(int fd,char output[65]){
sha256_context context={{0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19},0,{0},0};
unsigned char buffer[65536],digest[32];
if(lseek(fd,0,SEEK_SET)<0)return -1;
for(;;){ssize_t count=read(fd,buffer,sizeof buffer);if(count>0){sha256_update(&context,buffer,(size_t)count);continue;}if(count==0)break;if(errno!=EINTR)return -1;}
sha256_finish(&context,digest);static const char hex[]="0123456789abcdef";for(size_t i=0;i<32;i++){output[i*2]=hex[digest[i]>>4];output[i*2+1]=hex[digest[i]&15];}output[64]=0;
return lseek(fd,0,SEEK_SET)<0?-1:0;
}
static int executable_path(char *out,size_t size,const char *argv0){
#ifdef __linux__
(void)argv0;
ssize_t n=readlink("/proc/self/exe",out,size-1);if(n<=0||(size_t)n>=size-1)return -1;out[n]=0;return 0;
#elif defined(__APPLE__)
(void)argv0;
uint32_t n=(uint32_t)size;if(_NSGetExecutablePath(out,&n)!=0)return -1;char resolved[PATH_MAX];if(!realpath(out,resolved))return -1;if(strlen(resolved)>=size)return -1;strcpy(out,resolved);return 0;
#else
if(!realpath(argv0,out))return -1;return 0;
#endif
}
static char *fixed_entry(const char *name,const char *value){
if(strlen(value)>8192)return NULL;
size_t n=strlen(name)+strlen(value)+2;char *out=malloc(n);if(!out)return NULL;snprintf(out,n,"%s=%s",name,value);return out;
}
static char *entry(const char *name,const char *fallback){const char *value=getenv(name);return fixed_entry(name,value?value:fallback);}
#ifdef GSTACK_CSO_TESTING
static int test_pause(int argc,char **argv,const char *command){
if(argc!=4||strcmp(argv[1],command))return 0;
int ready=open(argv[2],O_WRONLY|O_CREAT|O_EXCL,0600);if(ready<0)return -1;close(ready);
struct timespec wait={0,1000000};while(access(argv[3],F_OK)!=0)nanosleep(&wait,NULL);return 1;
}
#endif
int main(int argc,char **argv){
char located[PATH_MAX],self[PATH_MAX],caller_cwd[PATH_MAX];
if(!getcwd(caller_cwd,sizeof caller_cwd)){
fputs("gstack-cso: caller working directory unavailable\n",stderr);return 69;
}
if(executable_path(located,sizeof located,argc?argv[0]:"")!=0||!realpath(located,self)){
fputs("gstack-cso: launcher path unavailable\n",stderr);return 69;
}
char *slash=strrchr(self,'/');if(!slash){fputs("gstack-cso: invalid launcher path\n",stderr);return 69;}
*slash=0;if(!self[0]){self[0]='/';self[1]=0;}
#ifdef GSTACK_CSO_TESTING
if(test_pause(argc,argv,"__cso-test-pause-before-generation-lock")<0){fputs("gstack-cso: test generation pause failed\n",stderr);return 69;}
#endif
int generation_lock=open(self,O_RDONLY|O_DIRECTORY|O_NOFOLLOW);
if(generation_lock<0){fputs("gstack-cso: installation lock unavailable\n",stderr);return 69;}
while(flock(generation_lock,LOCK_SH)!=0)if(errno!=EINTR){fputs("gstack-cso: installation lock unavailable\n",stderr);return 69;}
char core[PATH_MAX];if(snprintf(core,sizeof core,"%s%sgstack-cso-core",self,strcmp(self,"/")?"/":"")>=(int)sizeof core){fputs("gstack-cso: core path too long\n",stderr);return 69;}
int core_fd=openat(generation_lock,"gstack-cso-core",O_RDONLY|O_NOFOLLOW);struct stat core_state,st;
if(core_fd<0||fstat(core_fd,&core_state)||!S_ISREG(core_state.st_mode)||core_state.st_nlink!=1||(core_state.st_mode&0111)==0){fputs("gstack-cso: trusted compiled helper is missing; run gstack setup/build\n",stderr);return 69;}
int generation_fd=openat(generation_lock,".gstack-cso-generation",O_RDONLY|O_NOFOLLOW);char actual[66];size_t manifest_used=0;
if(generation_fd<0||fstat(generation_fd,&st)||!S_ISREG(st.st_mode)||st.st_nlink!=1||st.st_size!=65){fputs("gstack-cso: generation manifest is missing or invalid; run gstack setup/build\n",stderr);return 69;}
while(manifest_used<65){ssize_t count=read(generation_fd,actual+manifest_used,65-manifest_used);if(count>0){manifest_used+=(size_t)count;continue;}if(count<0&&errno==EINTR)continue;fputs("gstack-cso: generation manifest is missing or invalid; run gstack setup/build\n",stderr);return 69;}
close(generation_fd);actual[65]='\0';
if(actual[64]!='\n'||strncmp(actual,GSTACK_CSO_CORE_SHA256,64)!=0){fputs("gstack-cso: launcher and compiled helper generations do not match; run gstack setup/build\n",stderr);return 69;}
char core_sha256[65];if(sha256_file(core_fd,core_sha256)!=0||strcmp(core_sha256,GSTACK_CSO_CORE_SHA256)!=0){fputs("gstack-cso: compiled helper digest does not match its launcher; run gstack setup/build\n",stderr);return 69;}
#ifdef GSTACK_CSO_TESTING
if(test_pause(argc,argv,"__cso-test-pause-after-core-verification")<0){fputs("gstack-cso: test core pause failed\n",stderr);return 69;}
#endif
char *envp[16];size_t e=0;
envp[e++]=strdup("PATH=/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/usr/local/sbin:/usr/sbin:/sbin");
envp[e++]=strdup("LANG=C.UTF-8");envp[e++]=strdup("LC_ALL=C.UTF-8");envp[e++]=strdup("TZ=UTC");
const char *names[]={"HOME","GSTACK_HOME","CLAUDE_PLUGIN_DATA","CLAUDE_PLUGIN_ROOT","DOCKER_HOST","DOCKER_CONTEXT","DOCKER_CONFIG"};
for(size_t i=0;i<sizeof names/sizeof names[0];i++){envp[e]=entry(names[i],"");if(!envp[e++]){fputs("gstack-cso: environment input is too large\n",stderr);return 69;}}
envp[e]=fixed_entry("GSTACK_CSO_CALLER_CWD",caller_cwd);if(!envp[e++]){fputs("gstack-cso: caller working directory is too large\n",stderr);return 69;}
char lock_value[32];snprintf(lock_value,sizeof lock_value,"%d",generation_lock);
envp[e]=fixed_entry("GSTACK_CSO_GENERATION_LOCK_FD",lock_value);if(!envp[e++])return 69;
envp[e]=NULL;
char **child=calloc((size_t)argc+1,sizeof(char*));if(!child)return 69;child[0]=core;for(int i=1;i<argc;i++)child[i]=argv[i];
/* The audited repository is an untrusted input. Do not let its working
* directory become implicit process configuration for the compiled core. */
if(chdir(self)!=0){fputs("gstack-cso: trusted working directory unavailable\n",stderr);return 69;}
#ifdef __linux__
fexecve(core_fd,child,envp);
#else
/* macOS has no fexecve. Recheck the directory entry under the shared
* publication lock immediately before pathname execution. */
struct stat current;
if(fstatat(generation_lock,"gstack-cso-core",&current,AT_SYMLINK_NOFOLLOW)!=0||!S_ISREG(current.st_mode)||current.st_dev!=core_state.st_dev||current.st_ino!=core_state.st_ino||current.st_nlink!=1){fputs("gstack-cso: compiled helper changed before execution; run gstack setup/build\n",stderr);return 69;}
execve(core,child,envp);
#endif
fprintf(stderr,"gstack-cso: trusted compiled helper could not start (%d)\n",errno);return 69;
}
+617
View File
@@ -0,0 +1,617 @@
#!/usr/bin/env bun
/**
* Runtime-image half of the CSO preparation contract. This executable runs
* only inside a qualified, network-none container. Acquisition egress is a
* loopback TCP forwarder to the host's allowlisted Unix-socket broker.
*/
import * as fs from 'node:fs';
import * as net from 'node:net';
import * as http from 'node:http';
import { createHash } from 'node:crypto';
import { createGunzip } from 'node:zlib';
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
import { once } from 'node:events';
import { spawn, spawnSync } from 'node:child_process';
const VERSION = '1.0.0';
const MAX_POLICY = 32 * 1024 * 1024;
const MAX_ARCHIVE = 1024 * 1024 * 1024;
const MAX_EXPANDED = 2 * 1024 * 1024 * 1024;
const MAX_FILES = 200_000;
const SHA256 = /^[a-f0-9]{64}$/;
const RELATIVE = /^(?!\/)(?!.*(?:^|\/)\.\.?(?:\/|$))(?!.*\\)[A-Za-z0-9@._+\/-]{1,1024}$/;
const NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
const VERSION_VALUE = /^[0-9][0-9A-Za-z.+_-]*$/;
type Stack = 'node' | 'bun' | 'python' | 'rails';
interface Input {
index: number;
input: { kind: 'public'; name: string; version: string; url?: string; integrity?: string; integritySource?: string; platform?: string };
}
interface Policy {
schemaVersion: 1;
planHash: string;
stack: Stack;
inputs: Input[];
allowedHosts?: string[];
limits?: { maxArchives?: number; maxArchiveBytes?: number; maxTotalArchiveBytes?: number };
archives?: Array<{ inputIndex: number; name: string; version: string; declaredIntegrity: string; requestedUrl: string; resolvedUrl: string | null; containerPath: string; sha256: string; bytes: number }>;
}
interface Artifact {
inputIndex: number; stagingPath: string; installPath: string; sha256: string; bytes: number;
requestedHost: string; requestedUrl: string; resolvedUrl: string | null; registryResponseSha256: string;
}
export type PreparedExportEntry =
| { path: string; kind: 'directory'; mode: number }
| { path: string; kind: 'file'; mode: number; bytes: number; sha256: string; blob: string }
| { path: string; kind: 'symlink'; mode: number; target: string };
export interface PreparedExportManifest { schemaVersion: 1; entries: PreparedExportEntry[] }
function die(message: string): never { process.stderr.write(`${message}\n`); process.exit(70); }
/** Count every materialized tar header, including zero-byte directories. */
export function recordNpmArchiveEntry(previous: number, type: string): number {
if (!Number.isSafeInteger(previous) || previous < 0 || typeof type !== 'string' || type.length !== 1)
throw new Error('invalid npm archive entry counter');
const next = previous + 1;
if (next > MAX_FILES) throw new Error('npm archive exceeded extraction limits');
return next;
}
function strictPath(root: string, relative: string): string {
if (!RELATIVE.test(relative) || relative.split('/').some(part => !part || part === '.' || part === '..')) die('unsafe relative path');
const target = resolve(root, ...relative.split('/'));
if (!target.startsWith(`${resolve(root)}${sep}`)) die('path escaped root');
return target;
}
function mkdirPrivate(path: string): void {
fs.mkdirSync(path, { recursive: true, mode: 0o700 });
const stat = fs.lstatSync(path);
if (!stat.isDirectory() || stat.isSymbolicLink()) die('private directory changed unexpectedly');
fs.chmodSync(path, 0o700);
}
function readPolicy(path: string): Policy {
const stat = fs.lstatSync(path);
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_POLICY) die('invalid preparation policy file');
let value: any;
try { value = JSON.parse(fs.readFileSync(path, 'utf8')); } catch { die('invalid preparation policy JSON'); }
if (!value || value.schemaVersion !== 1 || !/^[a-f0-9]{64}$/.test(value.planHash) ||
!['node', 'bun', 'python', 'rails'].includes(value.stack) || !Array.isArray(value.inputs) || value.inputs.length > 25_000)
die('invalid preparation policy schema');
const indexes = new Set<number>();
for (const item of value.inputs) {
const input = item?.input;
if (!Number.isSafeInteger(item?.index) || item.index < 0 || indexes.has(item.index) || input?.kind !== 'public' ||
!NAME.test(input.name) || !VERSION_VALUE.test(input.version) || typeof input.integritySource !== 'string') die('invalid preparation input');
indexes.add(item.index);
}
return value as Policy;
}
function openRegular(path: string, max = MAX_ARCHIVE): { fd: number; stat: fs.Stats } {
const noFollow = (fs.constants as any).O_NOFOLLOW ?? 0;
let fd: number;
try { fd = fs.openSync(path, fs.constants.O_RDONLY | noFollow); } catch { die('archive is missing or unsafe'); }
const stat = fs.fstatSync(fd);
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size < 0 || stat.size > max) { fs.closeSync(fd); die('archive is not one bounded regular file'); }
return { fd, stat };
}
function hashes(path: string, max = MAX_ARCHIVE): { sha256: string; sha512: string; bytes: number } {
const { fd, stat } = openRegular(path, max), h256 = createHash('sha256'), h512 = createHash('sha512'), buffer = Buffer.allocUnsafe(64 * 1024);
let bytes = 0;
try {
for (;;) { const count = fs.readSync(fd, buffer, 0, buffer.length, null); if (!count) break; bytes += count; if (bytes > max) die('archive exceeded byte ceiling'); h256.update(buffer.subarray(0, count)); h512.update(buffer.subarray(0, count)); }
const after = fs.fstatSync(fd);
if (bytes !== stat.size || stat.dev !== after.dev || stat.ino !== after.ino || stat.size !== after.size || stat.mtimeMs !== after.mtimeMs || stat.ctimeMs !== after.ctimeMs) die('archive changed while hashing');
return { sha256: h256.digest('hex'), sha512: h512.digest('base64'), bytes };
} finally { fs.closeSync(fd); }
}
function preparedPath(path: string): boolean {
if (!path || path.startsWith('/') || path.includes('\\') || Buffer.byteLength(path) > 4096) return false;
return path.split('/').every(part => part && part !== '.' && part !== '..' && Buffer.byteLength(part) <= 255 && !/[\0-\x1f\x7f]/.test(part));
}
function samePreparedObject(left: fs.Stats, right: fs.Stats): boolean {
return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode && left.uid === right.uid &&
left.gid === right.gid && left.nlink === right.nlink && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
}
function preparedFileHash(path: string, expected: fs.Stats, maxBytes: number): string {
const noFollow = (fs.constants as any).O_NOFOLLOW ?? 0;
const fd = fs.openSync(path, fs.constants.O_RDONLY | noFollow);
try {
const before = fs.fstatSync(fd);
if (!before.isFile() || !samePreparedObject(expected, before)) throw new Error('prepared file changed before export');
const hash = createHash('sha256'), buffer = Buffer.allocUnsafe(64 * 1024); let bytes = 0;
for (;;) {
const count = fs.readSync(fd, buffer, 0, buffer.length, null); if (!count) break;
bytes += count; if (bytes > maxBytes) throw new Error('prepared tree exceeded its byte ceiling');
hash.update(buffer.subarray(0, count));
}
const after = fs.fstatSync(fd);
if (bytes !== expected.size || !samePreparedObject(before, after)) throw new Error('prepared file changed during export');
return hash.digest('hex');
} finally { fs.closeSync(fd); }
}
/**
* Build an inert export from an offline-prepared /work tree. Regular files are
* hard-linked into a helper-owned directory on the same tmpfs, so exporting
* does not require a second dependency-sized writable allocation. Symlinks are
* represented as manifest data and special files are rejected before Docker
* is allowed to copy anything to the host.
*/
export function createPreparedExport(sourceRoot: string, exportRoot: string, maxBytes = MAX_EXPANDED, maxEntries = MAX_FILES): PreparedExportManifest {
const root = resolve(sourceRoot), output = resolve(exportRoot);
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 || maxBytes > MAX_EXPANDED ||
!Number.isSafeInteger(maxEntries) || maxEntries <= 0 || maxEntries > MAX_FILES ||
output === root || !output.startsWith(`${root}${sep}`) || !/^\.gstack-cso-export-[a-f0-9]{24}$/.test(output.slice(root.length + 1)))
throw new Error('prepared export arguments escaped their bounded contract');
const rootStat = fs.lstatSync(root);
if (!rootStat.isDirectory() || rootStat.isSymbolicLink() || fs.realpathSync(root) !== root) throw new Error('prepared export source is unsafe');
type Scanned = PreparedExportEntry & { source?: string; identity?: fs.Stats };
const scanned: Scanned[] = []; let nodes = 0, totalBytes = 0;
const walk = (directory: string, prefix = ''): void => {
const before = fs.readdirSync(directory).sort();
for (const name of before) {
const relativePath = prefix ? `${prefix}/${name}` : name;
if (!preparedPath(relativePath) || ++nodes > maxEntries) throw new Error('prepared tree exceeded its entry or path ceiling');
const path = join(directory, name), stat = fs.lstatSync(path);
if (process.getuid && stat.uid !== process.getuid()) throw new Error('prepared tree contains an object owned by another identity');
if (stat.isSymbolicLink()) {
const target = fs.readlinkSync(path);
if (!target || isAbsolute(target) || target.includes('\0') || /[\x01-\x1f\x7f]/.test(target)) throw new Error('prepared tree contains an unsafe symlink');
const lexical = resolve(dirname(path), target);
if (lexical !== root && !lexical.startsWith(`${root}${sep}`)) throw new Error('prepared tree contains an escaping symlink');
let real: string, resolvedStat: fs.Stats;
try { real = fs.realpathSync(path); resolvedStat = fs.statSync(path); } catch { throw new Error('prepared tree contains a dangling or cyclic symlink'); }
if ((real !== root && !real.startsWith(`${root}${sep}`)) || (!resolvedStat.isFile() && !resolvedStat.isDirectory()))
throw new Error('prepared tree symlink resolves outside the prepared boundary');
const after = fs.lstatSync(path);
if (!samePreparedObject(stat, after) || fs.readlinkSync(path) !== target) throw new Error('prepared symlink changed during export');
scanned.push({ path: relativePath, kind: 'symlink', mode: stat.mode & 0o777, target });
} else if (stat.isDirectory()) {
if ((stat.mode & 0o022) !== 0) throw new Error('prepared tree contains a publicly writable directory');
scanned.push({ path: relativePath, kind: 'directory', mode: stat.mode & 0o777 });
walk(path, relativePath);
} else if (stat.isFile()) {
if (stat.nlink !== 1) throw new Error('prepared tree contains a hard-linked file');
totalBytes += stat.size; if (totalBytes > maxBytes) throw new Error('prepared tree exceeded its byte ceiling');
const digest = preparedFileHash(path, stat, maxBytes);
scanned.push({ path: relativePath, kind: 'file', mode: stat.mode & 0o777, bytes: stat.size, sha256: digest, blob: '', source: path, identity: stat });
} else throw new Error('prepared tree contains a FIFO, socket, device, or other special object');
}
if (before.join('\0') !== fs.readdirSync(directory).sort().join('\0')) throw new Error('prepared tree membership changed during export');
};
walk(root);
scanned.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
if (fs.existsSync(output)) throw new Error('prepared export destination already exists');
fs.mkdirSync(output, { mode: 0o700 });
const blobs = join(output, 'blobs'); fs.mkdirSync(blobs, { mode: 0o700 });
let fileIndex = 0;
try {
for (const entry of scanned) {
if (entry.kind !== 'file') continue;
const current = fs.lstatSync(entry.source!);
if (!samePreparedObject(entry.identity!, current)) throw new Error('prepared file changed before inert export');
const blob = `blob-${String(fileIndex++).padStart(6, '0')}`;
fs.linkSync(entry.source!, join(blobs, blob));
const linked = fs.lstatSync(join(blobs, blob));
if (!linked.isFile() || linked.dev !== current.dev || linked.ino !== current.ino || linked.nlink !== 2)
throw new Error('prepared file could not be bound into inert export');
entry.blob = blob;
delete entry.source; delete entry.identity;
}
const manifest: PreparedExportManifest = { schemaVersion: 1, entries: scanned as PreparedExportEntry[] };
const encoded = `${JSON.stringify(manifest)}\n`;
if (Buffer.byteLength(encoded) > MAX_POLICY) throw new Error('prepared export manifest exceeded its byte ceiling');
fs.writeFileSync(join(output, 'manifest.json'), encoded, { mode: 0o600, flag: 'wx' });
return manifest;
} catch (error) {
fs.rmSync(output, { recursive: true, force: true });
throw error;
}
}
function matchesIntegrity(value: string | undefined, valueHashes: { sha256: string; sha512: string }): boolean {
if (!value) return false;
return value.split(/\s+/).some(item => item === `sha256:${valueHashes.sha256}` ||
item === `sha256-${Buffer.from(valueHashes.sha256, 'hex').toString('base64')}` || item === `sha512-${valueHashes.sha512}`);
}
function moveVerified(source: string, outputRoot: string, relative: string, max: number): { path: string; sha256: string; bytes: number } {
const before = hashes(source, max), target = strictPath(outputRoot, relative);
mkdirPrivate(dirname(target));
if (fs.existsSync(target)) die('archive staging destination already exists');
// Both paths are on the bounded /archives tmpfs. Rename the verified final
// bytes into the inert export so no second archive-sized allocation exists.
fs.renameSync(source, target);
fs.chmodSync(target, 0o600);
const after = hashes(target, max);
if (before.sha256 !== after.sha256 || before.bytes !== after.bytes) die('archive changed while moved to inert staging');
return { path: relative, sha256: after.sha256, bytes: after.bytes };
}
function requestedUrl(item: Input, stack: Stack, filename?: string): string {
if (item.input.url) return item.input.url;
if (stack === 'python') return `https://pypi.org/simple/${item.input.name.toLowerCase().replace(/[_.]+/g, '-')}/`;
if (stack === 'rails' && filename) return `https://rubygems.org/gems/${filename}`;
die('archive URL is unavailable');
}
function checkedUrl(raw: string, hosts: string[]): URL {
let url: URL;
try { url = new URL(raw); } catch { die('invalid archive URL'); }
if (url.protocol !== 'https:' || url.username || url.password || url.port || url.search || url.hash || !hosts.includes(url.hostname)) die('archive URL escaped registry policy');
return url;
}
async function download(raw: string, hosts: string[], destination: string, max: number): Promise<string> {
let current = checkedUrl(raw, hosts);
for (let redirects = 0; redirects <= 3; redirects++) {
const response = await fetch(current, { redirect: 'manual', proxy: 'http://127.0.0.1:18443', headers: { 'user-agent': `gstack-cso-preparation/${VERSION}`, accept: 'application/octet-stream' } } as any);
if ([301, 302, 303, 307, 308].includes(response.status)) {
const location = response.headers.get('location');
if (!location || redirects === 3) die('registry redirect exceeded policy');
current = checkedUrl(new URL(location, current).href, hosts); continue;
}
if (response.status !== 200 || !response.body) die(`registry returned HTTP ${response.status}`);
const declared = response.headers.get('content-length');
if (declared && (!/^\d+$/.test(declared) || Number(declared) > max)) die('registry response exceeded byte ceiling');
mkdirPrivate(dirname(destination));
const fd = fs.openSync(destination, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
let bytes = 0;
try {
const reader = response.body.getReader();
for (;;) {
const part = await reader.read(); if (part.done) break;
bytes += part.value.byteLength; if (bytes > max) { await reader.cancel(); die('registry response exceeded byte ceiling'); }
let offset = 0; while (offset < part.value.byteLength) offset += fs.writeSync(fd, part.value, offset, part.value.byteLength - offset);
}
fs.fsyncSync(fd);
} finally { fs.closeSync(fd); }
if (declared && bytes !== Number(declared)) die('registry response was truncated');
return current.href;
}
die('registry redirect failed');
}
function npmCacheSource(item: Input): string | undefined {
const tokens = (item.input.integrity ?? '').split(/\s+/);
for (const token of tokens) {
const match = token.match(/^(sha256|sha512)-([A-Za-z0-9+/]+={0,2})$/);
if (!match) continue;
const digest = Buffer.from(match[2], 'base64').toString('hex');
if ((match[1] === 'sha256' && digest.length !== 64) || (match[1] === 'sha512' && digest.length !== 128)) continue;
const candidate = `/archives/npm/_cacache/content-v2/${match[1]}/${digest.slice(0, 2)}/${digest.slice(2, 4)}/${digest.slice(4)}`;
try { if (fs.lstatSync(candidate).isFile()) return candidate; } catch {}
}
return undefined;
}
function regularFiles(directory: string): string[] {
let names: string[];
try { names = fs.readdirSync(directory).sort(); } catch { return []; }
const files: string[] = [];
for (const name of names) {
if (!/^[A-Za-z0-9@._+-]{1,512}$/.test(name)) die('archive directory contains an unexpected filename');
const path = join(directory, name), stat = fs.lstatSync(path);
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) die('archive directory contains an unsafe object');
files.push(path);
}
return files;
}
async function manifest(policyPath: string, archiveRoot: string, outputPath: string): Promise<void> {
const outputMatch = outputPath.match(/^\/archives\/(\.gstack-cso-acquisition-export-[a-f0-9]{24})\/artifacts\.json$/);
if (resolve(archiveRoot) !== '/archives' || !outputMatch) die('manifest paths do not match the container contract');
const policy = readPolicy(policyPath), hosts = policy.allowedHosts ?? [], maxEntry = Math.min(policy.limits?.maxArchiveBytes ?? MAX_ARCHIVE, MAX_ARCHIVE),
maxTotal = Math.min(policy.limits?.maxTotalArchiveBytes ?? MAX_EXPANDED, MAX_EXPANDED), artifacts: Artifact[] = [];
// Package managers write only to the size-bounded /archives tmpfs. After all
// untrusted manager processes have exited, move verified regular artifacts
// into one unpredictable helper-owned export on that same tmpfs.
const exportRoot = join('/archives', outputMatch[1]);
if (fs.existsSync(exportRoot)) die('acquisition export destination already exists');
fs.mkdirSync(exportRoot, { mode: 0o700 });
const publicRoot = join(exportRoot, 'archives'); fs.mkdirSync(publicRoot, { mode: 0o700 });
const add = (item: Input, source: string, installPath: string, requestedUrl: string, resolvedUrl: string | null, ordinal: number) => {
const original = hashes(source, maxEntry);
if (item.input.integritySource === 'lock' && !matchesIntegrity(item.input.integrity, original)) die(`archive failed lock integrity: ${item.input.name}@${item.input.version}`);
const copied = moveVerified(source, publicRoot, `${policy.planHash.slice(0, 16)}-${policy.stack}-${ordinal}.archive`, maxEntry), requested = checkedUrl(requestedUrl, hosts);
if (resolvedUrl !== null) checkedUrl(resolvedUrl, hosts);
artifacts.push({ inputIndex: item.index, stagingPath: `cso-public/${copied.path}`, installPath, sha256: copied.sha256,
bytes: copied.bytes, requestedHost: requested.hostname, requestedUrl: requested.href, resolvedUrl,
registryResponseSha256: original.sha256 });
};
if (policy.stack === 'node') {
const logical = new Set<string>();
for (let ordinal = 0; ordinal < policy.inputs.length; ordinal++) {
const item = policy.inputs[ordinal], key = `${item.input.name}\0${item.input.version}`;
if (logical.has(key)) continue; logical.add(key);
const url = requestedUrl(item, policy.stack); let source = npmCacheSource(item);
const temporary = `/archives/.cso-download-${ordinal}`;
let resolvedUrl: string | null = null;
if (!source) { resolvedUrl = await download(url, hosts, temporary, maxEntry); source = temporary; }
try { add(item, source, `node/${ordinal}.tgz`, url, resolvedUrl, ordinal); }
finally { if (source === temporary) try { fs.unlinkSync(temporary); } catch {} }
}
} else if (policy.stack === 'bun') {
const logical = new Set<string>();
for (let ordinal = 0; ordinal < policy.inputs.length; ordinal++) {
const item = policy.inputs[ordinal], url = requestedUrl(item, policy.stack), temporary = `/archives/.cso-download-${ordinal}`;
const key = `${item.input.name}\0${item.input.version}`; if (logical.has(key)) continue; logical.add(key);
const resolvedUrl = await download(url, hosts, temporary, maxEntry);
try { add(item, temporary, `bun/${ordinal}.tgz`, url, resolvedUrl, ordinal); } finally { try { fs.unlinkSync(temporary); } catch {} }
}
} else if (policy.stack === 'python') {
const candidates = regularFiles('/archives/wheels').map(path => ({ path, values: hashes(path, maxEntry) })), used = new Set<string>(), logical = new Set<string>();
for (const item of policy.inputs) {
const key = `${item.input.name.toLowerCase().replace(/[_.]+/g, '-')}\0${item.input.version}`;
if (logical.has(key)) continue;
const match = candidates.find(candidate => !used.has(candidate.path) && matchesIntegrity(item.input.integrity, candidate.values));
if (!match) continue;
logical.add(key); used.add(match.path);
const name = match.path.slice(match.path.lastIndexOf('/') + 1), ordinal = artifacts.length;
add(item, match.path, `wheels/${name}`, requestedUrl(item, policy.stack), null, ordinal);
}
} else {
for (let ordinal = 0; ordinal < policy.inputs.length; ordinal++) {
const item = policy.inputs[ordinal], suffix = item.input.platform && item.input.platform !== 'ruby' ? `-${item.input.platform}` : '',
filename = `${item.input.name}-${item.input.version}${suffix}.gem`, path = join('/archives', filename);
add(item, path, filename, requestedUrl(item, policy.stack, filename), null, ordinal);
}
}
if (!artifacts.length && policy.inputs.length) die('acquisition produced no lock-bound public archives');
if (artifacts.length > (policy.limits?.maxArchives ?? 25_000) || artifacts.reduce((sum, item) => sum + item.bytes, 0) > maxTotal) die('acquisition artifacts exceeded policy');
mkdirPrivate(dirname(outputPath));
fs.writeFileSync(outputPath, `${JSON.stringify({ artifacts })}\n`, { mode: 0o600, flag: 'wx' });
}
async function forwarder(socketPath: string, listen: string): Promise<void> {
if (socketPath !== '/run/cso-registry.sock' || listen !== '127.0.0.1:18443') die('forwarder endpoints do not match the qualified policy');
const stat = fs.lstatSync(socketPath);
if (!stat.isSocket() || stat.isSymbolicLink()) die('registry broker is not a Unix socket');
let metadataBytes = 0, metadataFiles = 0;
const copyMetadata = (source: string, destination: string) => {
const directory = fs.lstatSync(source);
if (!directory.isDirectory() || directory.isSymbolicLink()) die('acquisition metadata input is unsafe');
mkdirPrivate(destination);
for (const name of fs.readdirSync(source).sort()) {
if (!/^[A-Za-z0-9@._+-]{1,255}$/.test(name)) die('acquisition metadata contains an unsafe name');
const from = join(source, name), to = join(destination, name), child = fs.lstatSync(from);
if (child.isDirectory() && !child.isSymbolicLink()) { copyMetadata(from, to); continue; }
if (!child.isFile() || child.isSymbolicLink() || child.nlink !== 1) die('acquisition metadata contains a link or special file');
metadataBytes += child.size; metadataFiles++;
if (metadataBytes > MAX_POLICY || metadataFiles > 50_000) die('acquisition metadata exceeds its bounded copy limit');
fs.copyFileSync(from, to, fs.constants.COPYFILE_EXCL); fs.chmodSync(to, 0o600);
}
};
copyMetadata('/input-metadata', '/metadata');
const server = net.createServer(client => {
const upstream = net.createConnection({ path: socketPath });
client.setTimeout(30_000); upstream.setTimeout(30_000);
client.once('error', () => upstream.destroy()); upstream.once('error', () => client.destroy());
client.once('timeout', () => { client.destroy(); upstream.destroy(); });
upstream.once('timeout', () => { client.destroy(); upstream.destroy(); });
client.pipe(upstream); upstream.pipe(client);
});
server.listen(18443, '127.0.0.1');
await once(server, 'listening');
await new Promise<void>((_resolve, reject) => server.once('error', reject));
}
async function health(host: string, port: string): Promise<void> {
if (host !== '127.0.0.1' || port !== '18443') die('invalid forwarder health endpoint');
const socket = net.createConnection({ host, port: 18443 });
await Promise.race([once(socket, 'connect'), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 1000))]);
socket.destroy();
}
function tarString(block: Buffer, start: number, length: number): string {
const end = block.indexOf(0, start); return block.subarray(start, end < 0 || end > start + length ? start + length : end).toString('utf8');
}
function tarNumber(block: Buffer, start: number, length: number): number {
const value = tarString(block, start, length).trim();
if (!/^[0-7]+$/.test(value)) die('invalid tar numeric field');
const number = Number.parseInt(value, 8); if (!Number.isSafeInteger(number) || number < 0) die('invalid tar size'); return number;
}
function tarChecksum(block: Buffer): void {
const declared = tarNumber(block, 148, 8); let sum = 0;
for (let index = 0; index < 512; index++) sum += index >= 148 && index < 156 ? 32 : block[index];
if (sum !== declared) die('invalid tar header checksum');
}
async function extractNpmArchive(source: string, destination: string, expectedName: string, expectedVersion: string): Promise<void> {
mkdirPrivate(destination);
const stream = fs.createReadStream(source).pipe(createGunzip()), chunks: Buffer[] = [];
let buffered = 0, current: { remaining: number; padding: number; fd?: number; mode?: number } | undefined, expanded = 0, entries = 0, zeroBlocks = 0;
const consume = () => {
let buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, buffered); chunks.length = 0; buffered = 0; let offset = 0;
while (offset < buffer.length) {
if (current) {
const count = Math.min(current.remaining, buffer.length - offset);
if (count && current.fd !== undefined) { let written = 0; while (written < count) written += fs.writeSync(current.fd, buffer, offset + written, count - written); }
offset += count; current.remaining -= count;
if (!current.remaining) {
if (current.fd !== undefined) { fs.fchmodSync(current.fd, current.mode ?? 0o600); fs.fsyncSync(current.fd); fs.closeSync(current.fd); current.fd = undefined; }
const skip = Math.min(current.padding, buffer.length - offset); offset += skip; current.padding -= skip;
if (!current.padding) current = undefined;
}
continue;
}
if (buffer.length - offset < 512) break;
const header = buffer.subarray(offset, offset + 512); offset += 512;
if (header.every(byte => byte === 0)) { zeroBlocks++; if (zeroBlocks >= 2 && offset !== buffer.length) die('tar contains data after end marker'); continue; }
if (zeroBlocks) die('tar has an invalid end marker');
tarChecksum(header);
const prefix = tarString(header, 345, 155), rawName = `${prefix ? `${prefix}/` : ''}${tarString(header, 0, 100)}`, type = String.fromCharCode(header[156] || 48), size = tarNumber(header, 124, 12), archivedMode = tarNumber(header, 100, 8), safeMode = archivedMode & 0o755;
if (!rawName.startsWith('package/')) die('npm archive entry lacks package prefix');
const relative = rawName.slice(8).replace(/\/$/, '');
if (!relative) { if (type !== '5') die('invalid npm archive root'); current = { remaining: size, padding: (512 - size % 512) % 512 }; continue; }
try { entries = recordNpmArchiveEntry(entries, type); } catch { die('npm archive exceeded extraction limits'); }
const target = strictPath(destination, relative);
if (type === '5') { if (size !== 0) die('tar directory has content'); mkdirPrivate(target); current = { remaining: 0, padding: 0 }; continue; }
if (type !== '0') die('npm archive contains a link or special entry');
expanded += size; if (expanded > MAX_EXPANDED) die('npm archive exceeded extraction limits');
mkdirPrivate(dirname(target));
const fd = fs.openSync(target, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
current = { remaining: size, padding: (512 - size % 512) % 512, fd, mode: safeMode || 0o600 };
if (!size) { fs.fchmodSync(fd, current.mode); fs.closeSync(fd); current.fd = undefined; if (!current.padding) current = undefined; }
}
if (offset < buffer.length) { const rest = buffer.subarray(offset); chunks.push(rest); buffered = rest.length; }
};
for await (const chunk of stream) { const value = Buffer.from(chunk); chunks.push(value); buffered += value.length; if (buffered > MAX_ARCHIVE + 1024) die('tar parser buffering exceeded limit'); consume(); }
consume();
if (current || buffered || zeroBlocks < 2) die('npm archive is truncated');
let manifest: any;
try { manifest = JSON.parse(fs.readFileSync(join(destination, 'package.json'), 'utf8')); } catch { die('npm archive package manifest is missing'); }
if (manifest?.name !== expectedName || manifest?.version !== expectedVersion) die('npm archive identity does not match the lock');
}
interface BunSeedPackage {
inputIndex: number;
name: string;
version: string;
archive: string;
integrity: string;
manifest: Record<string, unknown>;
}
const BUN_REGISTRY_PORT = 4873;
function bunRegistryManifest(value: unknown, name: string, version: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) die('Bun seed package manifest is invalid');
const source = value as Record<string, unknown>, clean: Record<string, unknown> = { name, version };
for (const key of ['dependencies', 'optionalDependencies', 'peerDependencies', 'peerDependenciesMeta', 'os', 'cpu', 'bin']) {
if (source[key] !== undefined) clean[key] = source[key];
}
return clean;
}
function bunRegistryPathName(pathname: string): string | undefined {
if (!pathname.startsWith('/') || pathname.includes('\0')) return undefined;
try {
const decoded = decodeURIComponent(pathname.slice(1));
return NAME.test(decoded) ? decoded : undefined;
} catch { return undefined; }
}
async function runBunSeedInstall(directory: string): Promise<void> {
const env = {
PATH: '/usr/local/bin:/usr/bin:/bin', HOME: '/work/.cso-home',
BUN_INSTALL_CACHE_DIR: '/work/.cso-bun-cache', BUN_CONFIG_NO_CLEAR_TERMINAL: '1',
BUN_FEATURE_FLAG_DISABLE_NATIVE_DEPENDENCY_LINKER: '1',
};
const child = spawn('/usr/local/bin/bun', ['install', '--config=/opt/cso/empty-config', '--ignore-scripts', '--no-progress',
`--registry=http://127.0.0.1:${BUN_REGISTRY_PORT}`, '--backend=copyfile'], {
cwd: directory, env, stdio: ['ignore', 'ignore', 'pipe'],
});
let stderr = Buffer.alloc(0), overflow = false;
child.stderr.on('data', chunk => {
if (stderr.length >= 64 * 1024) { overflow = true; return; }
const value = Buffer.from(chunk), remaining = 64 * 1024 - stderr.length;
stderr = Buffer.concat([stderr, value.subarray(0, remaining)]); if (value.length > remaining) overflow = true;
});
const timeout = setTimeout(() => child.kill('SIGKILL'), 60_000);
const [code, signal] = await once(child, 'exit') as [number | null, NodeJS.Signals | null];
clearTimeout(timeout);
if (code !== 0 || signal || overflow) die('offline Bun cache seeding failed');
}
async function seedBunCache(archives: NonNullable<Policy['archives']>): Promise<void> {
const cacheRoot = '/work/.cso-bun-cache', seedRoot = '/tmp/gstack-cso-bun-seed';
mkdirPrivate(cacheRoot); mkdirPrivate(seedRoot);
const packages: BunSeedPackage[] = [];
for (const archive of archives) {
const extracted = strictPath(seedRoot, `packages/${archive.inputIndex}`);
await extractNpmArchive(archive.containerPath, extracted, archive.name, archive.version);
let source: unknown;
try { source = JSON.parse(fs.readFileSync(join(extracted, 'package.json'), 'utf8')); }
catch { die('Bun seed package manifest is invalid'); }
packages.push({ inputIndex: archive.inputIndex, name: archive.name, version: archive.version,
archive: archive.containerPath, integrity: `sha512-${createHash('sha512').update(fs.readFileSync(archive.containerPath)).digest('base64')}`,
manifest: bunRegistryManifest(source, archive.name, archive.version) });
}
const byName = new Map<string, BunSeedPackage[]>();
for (const pkg of packages) byName.set(pkg.name, [...(byName.get(pkg.name) ?? []), pkg]);
const server = http.createServer((request, response) => {
const url = new URL(request.url ?? '/', `http://127.0.0.1:${BUN_REGISTRY_PORT}`);
if (request.method !== 'GET') { response.writeHead(405, { Allow: 'GET' }).end(); return; }
const archiveMatch = url.pathname.match(/^\/archives\/([0-9]+)\.tgz$/);
if (archiveMatch) {
const pkg = packages.find(item => item.inputIndex === Number(archiveMatch[1]));
if (!pkg) { response.writeHead(404).end(); return; }
const stat = fs.lstatSync(pkg.archive);
response.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Content-Length': String(stat.size),
'Cache-Control': 'no-store' }); fs.createReadStream(pkg.archive).pipe(response); return;
}
const name = bunRegistryPathName(url.pathname), versions = name ? byName.get(name) : undefined;
if (!name || !versions?.length) { response.writeHead(404).end(); return; }
const metadata: Record<string, unknown> = { name, 'dist-tags': { latest: versions.at(-1)!.version }, versions: {} };
for (const pkg of versions) (metadata.versions as Record<string, unknown>)[pkg.version] = {
...pkg.manifest,
dist: { tarball: `http://127.0.0.1:${BUN_REGISTRY_PORT}/archives/${pkg.inputIndex}.tgz`, integrity: pkg.integrity },
};
const body = JSON.stringify(metadata);
response.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(body)),
'Cache-Control': 'no-store' }).end(body);
});
server.listen(BUN_REGISTRY_PORT, '127.0.0.1'); await once(server, 'listening');
try {
// Seed every exact logical identity independently. This supports multiple
// locked versions of one package while letting Bun own its cache format.
for (const pkg of packages) {
const directory = strictPath(seedRoot, `installs/${pkg.inputIndex}`); mkdirPrivate(directory);
const manifest = { name: `gstack-cso-seed-${pkg.inputIndex}`, private: true, dependencies: { [pkg.name]: pkg.version } };
fs.writeFileSync(join(directory, 'package.json'), JSON.stringify(manifest) + '\n', { mode: 0o600, flag: 'wx' });
await runBunSeedInstall(directory);
}
} finally {
await new Promise<void>((resolveClose, reject) => server.close(error => error ? reject(error) : resolveClose()));
fs.rmSync(seedRoot, { recursive: true, force: true });
}
}
async function seed(policyPath: string): Promise<void> {
const policy = readPolicy(policyPath);
if (!Array.isArray(policy.archives)) die('offline policy omitted archives');
for (const archive of policy.archives) {
if (!Number.isSafeInteger(archive.inputIndex) || !NAME.test(archive.name) || !VERSION_VALUE.test(archive.version) ||
!archive.containerPath.startsWith('/archives/') || !SHA256.test(archive.sha256) || !Number.isSafeInteger(archive.bytes) || archive.bytes < 0)
die('offline archive policy is invalid');
const checked = hashes(archive.containerPath, Math.min(MAX_ARCHIVE, archive.bytes));
if (checked.bytes !== archive.bytes || checked.sha256 !== archive.sha256 ||
(archive.declaredIntegrity !== 'registry-on-acquisition' && !matchesIntegrity(archive.declaredIntegrity, checked))) die('offline archive failed integrity verification');
}
if (policy.stack === 'node' && policy.archives.length) {
mkdirPrivate('/work/.cso-npm-cache');
for (const archive of policy.archives) {
const result = spawnSync('/usr/local/bin/npm', ['cache', 'add', archive.containerPath, '--cache', '/work/.cso-npm-cache', '--userconfig', '/opt/cso/empty-config', '--globalconfig', '/opt/cso/empty-config'],
{ cwd: '/work', env: { PATH: '/usr/local/bin:/usr/bin:/bin', HOME: '/work/.cso-home', NPM_CONFIG_UPDATE_NOTIFIER: 'false' }, stdio: ['ignore', 'ignore', 'pipe'], timeout: 60_000, maxBuffer: 64 * 1024 });
if (result.status !== 0 || result.error) die('offline npm cache seeding failed');
}
}
if (policy.stack === 'bun') {
await seedBunCache(policy.archives);
}
fs.writeFileSync('/work/.gstack-cso-preparation-ready', `${policy.planHash}\n`, { mode: 0o400, flag: 'wx' });
await new Promise<void>(() => {});
}
function ready(): void {
const stat = fs.lstatSync('/work/.gstack-cso-preparation-ready');
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size !== 65) die('offline preparation is not ready');
}
function finalize(): void {
for (const path of ['/work/.cso-home', '/work/.cso-npm-cache', '/work/.cso-bun-cache', '/work/.cso-uv-cache', '/work/.gstack-cso-public-requirements.txt'])
fs.rmSync(path, { recursive: true, force: true });
fs.rmSync('/work/.gstack-cso-preparation-ready', { force: true });
for (const path of ['/work/.cso-home', '/work/.cso-npm-cache', '/work/.cso-bun-cache', '/work/.cso-uv-cache', '/work/.gstack-cso-public-requirements.txt', '/work/.gstack-cso-preparation-ready'])
if (fs.existsSync(path)) die('offline preparation scratch cleanup was incomplete');
}
async function main(): Promise<void> {
const [command, ...args] = process.argv.slice(2);
if (command === '--version' && !args.length) { process.stdout.write(`${VERSION}\n`); return; }
if (command === 'forwarder' && args.length === 2) return forwarder(args[0], args[1]);
if (command === 'health' && args.length === 2) return health(args[0], args[1]);
if (command === 'manifest' && args.length === 3) return manifest(args[0], args[1], args[2]);
if (command === 'seed' && args.length === 1) return seed(args[0]);
if (command === 'ready' && !args.length) return ready();
if (command === 'finalize' && !args.length) return finalize();
if (command === 'export-prepared' && args.length === 3 && args[0] === '/work' && /^\/work\/\.gstack-cso-export-[a-f0-9]{24}$/.test(args[1]) && /^\d+$/.test(args[2])) {
createPreparedExport(args[0], args[1], Number(args[2])); return;
}
die('usage: preparation {--version|forwarder|health|manifest|seed|ready|finalize|export-prepared}');
}
if (import.meta.main) main().catch(error => die(error instanceof Error ? error.message : 'preparation helper failed'));
+815
View File
@@ -0,0 +1,815 @@
/** Concrete constrained-Docker adapter for PreparationExecutor. */
import * as fs from 'node:fs';
import * as net from 'node:net';
import { promises as dns } from 'node:dns';
import { createHash, randomBytes } from 'node:crypto';
import { spawn } from 'node:child_process';
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
import { atomicWriteSync } from '../fs-atomic';
import { canonical, CsoError, sha256 } from './contracts';
import { DockerGroup, type DockerEndpoint } from './docker';
import { secureDirectory } from './state';
import {
admittedPreparationRuntime, type AcquisitionArtifactReceipt, type AcquisitionReceipt,
type OfflinePreparationReceipt, type OfflinePreparationRequest, type OfflinePreparationResult,
type PreparationAcquireRequest, type PreparationCommandReceipt, type PreparationRuntimeAdmission,
type PreparationSandboxRunner,
} from './preparation-executor';
import type { CsoStack, PreparationCommand } from './preparation';
import { CSO_HELPER_ABI } from './runtime-catalog';
import type { PreparedExportEntry, PreparedExportManifest } from './preparation-container';
const RELATIVE = /^(?!\/)(?!.*(?:^|\/)\.\.?(?:\/|$))(?!.*\\)[A-Za-z0-9@._+\/-]{1,1024}$/;
const MAX_MANIFEST = 32 * 1024 * 1024;
const MAX_PREPARED_EXPORT_ENTRIES = 200_000;
function fail(code: ConstructorParameters<typeof CsoError>[0], message: string): never { throw new CsoError(code, message); }
function contained(root: string, path: string): string {
if (!RELATIVE.test(path) || path.split('/').some(part => !part || part === '.' || part === '..')) fail('UNSAFE_PATH', 'Preparation path escaped its private root');
const target = resolve(root, ...path.split('/'));
if (!target.startsWith(`${root}${sep}`)) fail('UNSAFE_PATH', 'Preparation path escaped its private root');
return target;
}
function commandReceipt(command: PreparationCommand, index: number, exitCode: number): PreparationCommandReceipt {
return { index, commandHash: sha256(canonical(command)), exitCode, timedOut: false, outputTruncated: false };
}
function writePolicy(path: string, value: unknown): void {
try { atomicWriteSync(path, `${JSON.stringify(value)}\n`, { mode: 0o600, noReplace: true }); }
catch { fail('PERSISTENCE_FAILED', 'Preparation policy could not be written atomically'); }
}
function readManifest(path: string): { artifacts: AcquisitionArtifactReceipt[] } {
let stat: fs.Stats;
try { stat = fs.lstatSync(path); } catch { fail('TOOL_FAILED', 'Qualified acquisition helper did not produce an archive manifest'); }
if (!stat!.isFile() || stat!.isSymbolicLink() || stat!.nlink !== 1 || stat!.size > MAX_MANIFEST ||
(process.getuid && stat!.uid !== process.getuid()) || (stat!.mode & 0o077) !== 0)
fail('UNSAFE_PATH', 'Acquisition archive manifest is not one bounded private file');
let parsed: unknown;
try { parsed = JSON.parse(fs.readFileSync(path, 'utf8')); } catch { fail('TOOL_FAILED', 'Qualified acquisition helper returned invalid archive JSON'); }
const value = parsed as { artifacts?: unknown };
if (!value || Object.keys(value).sort().join(',') !== 'artifacts' || !Array.isArray(value.artifacts))
fail('TOOL_FAILED', 'Qualified acquisition helper returned an invalid archive manifest schema');
return value as { artifacts: AcquisitionArtifactReceipt[] };
}
function fileSha256(path: string, maxBytes: number): string {
const noFollow = (fs.constants as any).O_NOFOLLOW ?? 0;
let fd: number;
try { fd = fs.openSync(path, fs.constants.O_RDONLY | noFollow); } catch { fail('UNSAFE_PATH', 'Archive copy could not be opened without following links'); }
try {
const before = fs.fstatSync(fd!), hash = createHash('sha256'), buffer = Buffer.allocUnsafe(64 * 1024); let bytes = 0;
for (;;) { const count = fs.readSync(fd!, buffer, 0, buffer.length, null); if (!count) break; bytes += count; if (bytes > maxBytes) fail('INSUFFICIENT_CAPACITY', 'Archive copy exceeded its declared size'); hash.update(buffer.subarray(0, count)); }
const after = fs.fstatSync(fd!);
if (bytes !== before.size || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
fail('SNAPSHOT_RACE', 'Archive copy changed while it was hashed');
return hash.digest('hex');
} finally { fs.closeSync(fd!); }
}
function validateAcquisitionOutput(root: string, artifacts: AcquisitionArtifactReceipt[]): void {
const top = fs.readdirSync(root).sort();
if (canonical(top) !== canonical(['archives', 'artifacts.json'])) fail('TOOL_FAILED', 'Acquisition output contains undeclared objects');
const archiveRoot = join(root, 'archives'), stat = fs.lstatSync(archiveRoot);
if (!stat.isDirectory() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()) || (stat.mode & 0o022) !== 0)
fail('UNSAFE_PATH', 'Acquisition archive output is not one private directory');
for(const artifact of artifacts)if(typeof artifact?.stagingPath!=='string'||!/^cso-public\/[A-Za-z0-9._-]{1,255}$/.test(artifact.stagingPath))fail('TOOL_FAILED','Acquisition manifest contains an unsafe staging path');
const expected = artifacts.map(artifact => artifact.stagingPath.slice('cso-public/'.length)).sort(), actual = fs.readdirSync(archiveRoot).sort();
if(new Set(expected).size!==expected.length)fail('TOOL_FAILED','Acquisition manifest contains duplicate staging paths');
if (canonical(actual) !== canonical(expected)) fail('TOOL_FAILED', 'Acquisition output archive membership does not match its manifest');
for (const name of actual) {
if (!name || name.includes('/') || name === '.' || name === '..') fail('UNSAFE_PATH', 'Acquisition output archive name is unsafe');
const file = fs.lstatSync(join(archiveRoot, name));
if (!file.isFile() || file.isSymbolicLink() || file.nlink !== 1 || (process.getuid && file.uid !== process.getuid()) || (file.mode & 0o022) !== 0)
fail('UNSAFE_PATH', 'Acquisition output contains a link or special file');
}
}
function preparedRelative(path: unknown): path is string {
return typeof path === 'string' && path.length > 0 && !path.startsWith('/') && !path.includes('\\') &&
Buffer.byteLength(path) <= 4096 && path.split('/').every(part => part && part !== '.' && part !== '..' &&
Buffer.byteLength(part) <= 255 && !/[\0-\x1f\x7f]/.test(part));
}
function preparedTarget(root: string, relativePath: string): string {
if (!preparedRelative(relativePath)) fail('UNSAFE_PATH', 'Prepared export contains an unsafe path');
const target = resolve(root, ...relativePath.split('/'));
if (!target.startsWith(`${root}${sep}`)) fail('UNSAFE_PATH', 'Prepared export path escaped its private root');
return target;
}
function sameFileIdentity(left: fs.Stats, right: fs.Stats): boolean {
return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode && left.uid === right.uid &&
left.gid === right.gid && left.nlink === right.nlink && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
}
function copyPreparedBlob(source: string, destination: string, expected: Extract<PreparedExportEntry, {kind:'file'}>): void {
const noFollow = (fs.constants as any).O_NOFOLLOW ?? 0;
let sourceFd = -1, destinationFd = -1;
try {
sourceFd = fs.openSync(source, fs.constants.O_RDONLY | noFollow);
const before = fs.fstatSync(sourceFd);
if (!before.isFile() || before.nlink !== 1 || before.size !== expected.bytes ||
(process.getuid && before.uid !== process.getuid())) fail('UNSAFE_PATH', 'Prepared export blob is not one owned regular file');
destinationFd = fs.openSync(destination, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow, 0o600);
const hash = createHash('sha256'), buffer = Buffer.allocUnsafe(64 * 1024); let bytes = 0;
for (;;) {
const count = fs.readSync(sourceFd, buffer, 0, buffer.length, null); if (!count) break;
bytes += count; if (bytes > expected.bytes) fail('INSUFFICIENT_CAPACITY', 'Prepared export blob exceeded its declared size');
hash.update(buffer.subarray(0, count));
let offset = 0; while (offset < count) offset += fs.writeSync(destinationFd, buffer, offset, count - offset);
}
const after = fs.fstatSync(sourceFd);
if (bytes !== expected.bytes || hash.digest('hex') !== expected.sha256 || !sameFileIdentity(before, after))
fail('SNAPSHOT_RACE', 'Prepared export blob changed during bounded import');
fs.fchmodSync(destinationFd, expected.mode); fs.fsyncSync(destinationFd);
const copied = fs.fstatSync(destinationFd);
if (!copied.isFile() || copied.nlink !== 1 || copied.size !== expected.bytes)
fail('PERSISTENCE_FAILED', 'Prepared export blob was not materialized as one regular file');
} finally {
if (sourceFd >= 0) fs.closeSync(sourceFd);
if (destinationFd >= 0) fs.closeSync(destinationFd);
}
}
/** Validate an inert container export and reconstruct its prepared tree using host no-follow writes. */
export function materializePreparedExport(exportRoot: string, destinationRoot: string, maxBytes: number): PreparedExportManifest {
const source = resolve(exportRoot), destination = resolve(destinationRoot);
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 || maxBytes > 2 * 1024 * 1024 * 1024)
fail('INVALID_ARGUMENT', 'Prepared export byte ceiling is invalid');
for (const [path, label, empty] of [[source, 'Prepared inert export', false], [destination, 'Prepared output', true]] as const) {
const stat = fs.lstatSync(path);
if (!stat.isDirectory() || stat.isSymbolicLink() || fs.realpathSync(path) !== path ||
(process.getuid && stat.uid !== process.getuid()) || (stat.mode & 0o022) !== 0 || (empty && fs.readdirSync(path).length))
fail('UNSAFE_PATH', `${label} must be one private${empty ? ' empty' : ''} owned directory`);
}
if (fs.readdirSync(source).sort().join('\0') !== 'blobs\0manifest.json') fail('UNSAFE_PATH', 'Prepared inert export contains undeclared top-level objects');
const manifestPath = join(source, 'manifest.json'), manifestStat = fs.lstatSync(manifestPath);
if (!manifestStat.isFile() || manifestStat.isSymbolicLink() || manifestStat.nlink !== 1 || manifestStat.size < 1 || manifestStat.size > MAX_MANIFEST ||
(process.getuid && manifestStat.uid !== process.getuid())) fail('UNSAFE_PATH', 'Prepared export manifest is not one bounded owned file');
let manifest: PreparedExportManifest;
try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { fail('TOOL_FAILED', 'Prepared export manifest is invalid JSON'); }
if (!manifest || Object.keys(manifest).sort().join(',') !== 'entries,schemaVersion' || manifest.schemaVersion !== 1 ||
!Array.isArray(manifest.entries) || manifest.entries.length > MAX_PREPARED_EXPORT_ENTRIES)
fail('TOOL_FAILED', 'Prepared export manifest has an invalid schema');
const paths = new Set<string>(), directories = new Set<string>(), blobs = new Set<string>(); let totalBytes = 0;
for (const raw of manifest.entries) {
const entry = raw as PreparedExportEntry, keys = Object.keys(entry).sort().join(',');
if (!preparedRelative(entry?.path) || paths.has(entry.path) || !Number.isSafeInteger(entry.mode) || entry.mode < 0 || entry.mode > 0o777)
fail('TOOL_FAILED', 'Prepared export contains a duplicate or invalid path record');
paths.add(entry.path);
const parent = entry.path.includes('/') ? entry.path.slice(0, entry.path.lastIndexOf('/')) : '';
if (parent && !directories.has(parent)) fail('TOOL_FAILED', 'Prepared export entry is missing its declared parent directory');
if (entry.kind === 'directory') {
if (keys !== 'kind,mode,path' || (entry.mode & 0o022) !== 0) fail('TOOL_FAILED', 'Prepared export directory record is invalid');
directories.add(entry.path);
} else if (entry.kind === 'file') {
if (keys !== 'blob,bytes,kind,mode,path,sha256' || !Number.isSafeInteger(entry.bytes) || entry.bytes < 0 ||
!/^[a-f0-9]{64}$/.test(entry.sha256) || !/^blob-\d{6}$/.test(entry.blob) || blobs.has(entry.blob))
fail('TOOL_FAILED', 'Prepared export file record is invalid');
blobs.add(entry.blob); totalBytes += entry.bytes;
if (!Number.isSafeInteger(totalBytes) || totalBytes > maxBytes) fail('INSUFFICIENT_CAPACITY', 'Prepared export exceeds its aggregate byte ceiling');
} else if (entry.kind === 'symlink') {
if (keys !== 'kind,mode,path,target' || typeof entry.target !== 'string' || !entry.target || isAbsolute(entry.target) ||
entry.target.includes('\0') || /[\x01-\x1f\x7f]/.test(entry.target)) fail('TOOL_FAILED', 'Prepared export symlink record is invalid');
const lexical = resolve(dirname(preparedTarget(destination, entry.path)), entry.target);
if (lexical !== destination && !lexical.startsWith(`${destination}${sep}`)) fail('UNSAFE_PATH', 'Prepared export symlink escapes its destination');
} else fail('TOOL_FAILED', 'Prepared export entry kind is invalid');
}
const ordered = manifest.entries.map(entry => entry.path);
if (ordered.join('\0') !== [...ordered].sort().join('\0')) fail('TOOL_FAILED', 'Prepared export manifest is not in deterministic path order');
const blobRoot = join(source, 'blobs'), blobRootStat = fs.lstatSync(blobRoot);
if (!blobRootStat.isDirectory() || blobRootStat.isSymbolicLink() || (process.getuid && blobRootStat.uid !== process.getuid()) ||
fs.readdirSync(blobRoot).sort().join('\0') !== [...blobs].sort().join('\0')) fail('UNSAFE_PATH', 'Prepared export blob membership does not match its manifest');
for (const entry of manifest.entries) if (entry.kind === 'directory') fs.mkdirSync(preparedTarget(destination, entry.path), { mode: 0o700 });
for (const entry of manifest.entries) if (entry.kind === 'file') copyPreparedBlob(join(blobRoot, entry.blob), preparedTarget(destination, entry.path), entry);
for (const entry of manifest.entries) if (entry.kind === 'symlink') fs.symlinkSync(entry.target, preparedTarget(destination, entry.path));
for (const entry of manifest.entries) if (entry.kind === 'symlink') {
const path = preparedTarget(destination, entry.path); let real: string, stat: fs.Stats;
try { real = fs.realpathSync(path); stat = fs.statSync(path); } catch { fail('UNSAFE_PATH', 'Prepared export symlink is dangling or cyclic'); }
if ((real !== destination && !real.startsWith(`${destination}${sep}`)) || (!stat.isFile() && !stat.isDirectory()))
fail('UNSAFE_PATH', 'Prepared export symlink resolves outside its prepared tree');
}
for (const entry of [...manifest.entries].reverse()) if (entry.kind === 'directory') fs.chmodSync(preparedTarget(destination, entry.path), entry.mode);
return manifest;
}
function addBlockedIpv4Ranges(blocked: net.BlockList): void {
for (const [address, prefix] of [['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8],
['169.254.0.0', 16], ['172.16.0.0', 12], ['192.0.0.0', 24], ['192.0.2.0', 24], ['192.168.0.0', 16],
['198.18.0.0', 15], ['198.51.100.0', 24], ['203.0.113.0', 24], ['224.0.0.0', 4], ['240.0.0.0', 4]] as Array<[string, number]>)
blocked.addSubnet(address, prefix, 'ipv4');
}
function addBlockedIpv6Ranges(blocked: net.BlockList): void {
for (const [address, prefix] of [
['::', 96], // unspecified, IPv4-compatible, and other deprecated v4 embeddings
['::ffff:0.0.0.0', 96], // IPv4-mapped addresses must not bypass the IPv4 ranges
['64:ff9b::', 96], ['64:ff9b:1::', 48], // public and local-use NAT64 translators
['100::', 64], // discard-only
['2001::', 23], // IETF special-purpose assignments (Teredo, ORCHID, benchmarking)
['2001:db8::', 32], // documentation prefix
['2002::', 16], // 6to4 can embed otherwise blocked IPv4 destinations
['3fff::', 20], // documentation prefix
['fc00::', 7], ['fe80::', 10], ['fec0::', 10], ['ff00::', 8],
] as Array<[string, number]>)
blocked.addSubnet(address, prefix, 'ipv6');
}
const registryBlockedAddresses = new net.BlockList();
const registryBlockedIpv6Addresses = new net.BlockList();
addBlockedIpv4Ranges(registryBlockedAddresses);
addBlockedIpv6Ranges(registryBlockedIpv6Addresses);
/** Pure classification used before any registry connection is attempted. */
export function isBlockedRegistryAddress(address: string, family: 4 | 6): boolean {
if (net.isIP(address) !== family) return true;
return (family === 6 ? registryBlockedIpv6Addresses : registryBlockedAddresses).check(address, family === 6 ? 'ipv6' : 'ipv4');
}
export interface RegistryDnsAddress { address: string; family: 4 | 6 }
export interface RegistryDnsResolution { promise: Promise<RegistryDnsAddress[]>; cancel(): void }
export type RegistryDnsResolver = (host: string) => RegistryDnsResolution;
function systemRegistryDnsResolver(host: string): RegistryDnsResolution {
const resolver = new dns.Resolver();
const promise = Promise.allSettled([resolver.resolve4(host), resolver.resolve6(host)]).then(results => {
const answers: RegistryDnsAddress[] = [];
if (results[0].status === 'fulfilled') for (const address of results[0].value) answers.push({ address, family: 4 });
if (results[1].status === 'fulfilled') for (const address of results[1].value) answers.push({ address, family: 6 });
if (!answers.length) {
const rejected = results.find((result): result is PromiseRejectedResult => result.status === 'rejected');
if (rejected) throw rejected.reason;
}
return answers;
});
return { promise, cancel: () => resolver.cancel() };
}
/**
* Host-side CONNECT broker. The acquisition container remains in Docker's
* network-none namespace and reaches this broker only through a bind-mounted
* Unix socket and its qualified loopback forwarder.
*/
export class RegistryEgressBroker {
readonly contactedHosts = new Set<string>();
readonly deniedHosts = new Set<string>();
private readonly server = net.createServer(socket => this.accept(socket));
private readonly sockets = new Set<net.Socket>();
private readonly tasks = new Set<Promise<void>>();
private readonly resolutions = new Set<{ cancel(error: CsoError): void }>();
private readonly pinned = new Map<string, string>();
private transferred = 0;
private violation: string | undefined;
private started = false;
private closing = false;
constructor(readonly socketPath: string, readonly allowedHosts: string[], private readonly deadline: number, private readonly maxBytes: number,
private readonly resolveDns: RegistryDnsResolver = systemRegistryDnsResolver) {
if (!socketPath.startsWith('/') || !allowedHosts.length || new Set(allowedHosts).size !== allowedHosts.length ||
allowedHosts.some(host => host !== host.toLowerCase() || !/^[a-z0-9.-]{1,253}$/.test(host)) ||
!Number.isSafeInteger(deadline) || deadline <= Date.now() || !Number.isSafeInteger(maxBytes) || maxBytes <= 0)
fail('INVALID_ARGUMENT', 'Registry broker requires a bounded allowlist, deadline, and byte ceiling');
}
async start(): Promise<void> {
if (this.started || this.closing) fail('INVALID_ARGUMENT', 'Registry broker cannot be started more than once');
if (fs.existsSync(this.socketPath)) fail('UNSAFE_PATH', 'Registry broker socket path already exists');
await new Promise<void>((resolveStart, reject) => {
const onError = () => reject(new CsoError('ISOLATION_FAILED', 'Registry broker could not bind its private Unix socket'));
this.server.once('error', onError);
this.server.listen(this.socketPath, () => { this.server.off('error', onError); resolveStart(); });
});
this.started = true;
this.server.on('error', () => { this.violation ??= 'Registry broker listener failed'; });
fs.chmodSync(this.socketPath, 0o600);
const stat = fs.lstatSync(this.socketPath);
if (!stat.isSocket() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()))
fail('UNSAFE_PATH', 'Registry broker did not create an owned Unix socket');
}
private deny(socket: net.Socket, message: string, host?: string): void {
this.violation ??= message;
if (host) this.deniedHosts.add(host);
try { socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n'); } catch { socket.destroy(); }
}
private async lookup(host: string, lookupDeadline: number): Promise<RegistryDnsAddress[]> {
const resolution = this.resolveDns(host);
if (!resolution || typeof resolution.cancel !== 'function' || !resolution.promise || typeof resolution.promise.then !== 'function')
fail('ISOLATION_FAILED', 'Registry DNS resolver returned an invalid operation');
let rejectCancellation!: (error: CsoError) => void, settled = false;
const cancelled = new Promise<never>((_resolve, reject) => { rejectCancellation = reject; });
const active = {
cancel: (error: CsoError) => {
if (settled) return;
try { resolution.cancel(); } catch {}
rejectCancellation(error);
},
};
this.resolutions.add(active);
const remaining = lookupDeadline - Date.now();
if (remaining <= 0) active.cancel(new CsoError('DEADLINE', 'Registry DNS lookup deadline elapsed'));
const timer = remaining > 0 ? setTimeout(() => active.cancel(new CsoError('DEADLINE', 'Registry DNS lookup deadline elapsed')), remaining) : undefined;
try {
const answers = await Promise.race([resolution.promise, cancelled]);
if (!Array.isArray(answers) || answers.some(answer => !answer || typeof answer.address !== 'string' || (answer.family !== 4 && answer.family !== 6)))
fail('ISOLATION_FAILED', 'Registry DNS resolver returned invalid addresses');
return answers;
} finally {
settled = true;
if (timer) clearTimeout(timer);
this.resolutions.delete(active);
}
}
private async openTunnel(socket: net.Socket, host: string, remainder: Buffer, connectionDeadline: number): Promise<void> {
try {
const answers = (await this.lookup(host, connectionDeadline)).filter(answer => !isBlockedRegistryAddress(answer.address, answer.family));
if (this.closing || socket.destroyed) return;
if (Date.now() >= this.deadline || Date.now() >= connectionDeadline) {
this.deny(socket, 'Registry acquisition deadline elapsed', host); return;
}
if (!answers.length) { this.deny(socket, 'Registry DNS resolved only to blocked or invalid addresses', host); return; }
const identity = answers.map(answer => `${answer.family}:${answer.address}`).sort().join(',');
const prior = this.pinned.get(host);
if (prior && prior !== identity) { this.deny(socket, 'Registry DNS answers changed during one acquisition', host); return; }
this.pinned.set(host, identity);
const selected = answers.sort((a, b) => a.address.localeCompare(b.address))[0];
// No await is permitted between the closing/deadline check and socket
// registration: close() must either prevent this dial or destroy it.
if (this.closing || socket.destroyed || Date.now() >= this.deadline) return;
const upstream = net.connect({ host: selected.address, port: 443, family: selected.family });
this.sockets.add(upstream); upstream.setTimeout(Math.max(1, Math.min(30_000, this.deadline - Date.now())));
upstream.once('close', () => this.sockets.delete(upstream));
upstream.once('error', () => { if (!this.closing) this.violation ??= 'Registry connection failed after DNS pinning'; socket.destroy(); });
upstream.once('connect', () => {
if (this.closing || socket.destroyed || Date.now() >= this.deadline) {
upstream.destroy(); socket.destroy(); return;
}
const addBytes = (bytes: number) => {
this.transferred += bytes;
if (this.transferred <= this.maxBytes) return true;
this.violation = 'Registry transfer exceeded its byte ceiling'; socket.destroy(); upstream.destroy(); return false;
};
const count = (chunk: Buffer) => { addBytes(chunk.length); };
socket.on('data', count); upstream.on('data', count); socket.pipe(upstream); upstream.pipe(socket);
this.contactedHosts.add(host); socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
if (remainder.length && addBytes(remainder.length)) upstream.write(remainder);
});
} catch {
if (this.closing || socket.destroyed) return;
this.deny(socket, 'Registry DNS lookup or pinned connection failed', host);
}
}
private accept(socket: net.Socket): void {
if (this.closing || Date.now() >= this.deadline) { socket.destroy(); return; }
if (this.sockets.size >= 64) { this.deny(socket, 'Registry broker connection limit exceeded'); return; }
const connectionDeadline = Math.min(this.deadline, Date.now() + 30_000);
this.sockets.add(socket); socket.setTimeout(Math.max(1, connectionDeadline - Date.now()));
socket.once('close', () => this.sockets.delete(socket));
let pending = Buffer.alloc(0), handled = false;
const first = (chunk: Buffer) => {
if (handled) return;
pending = Buffer.concat([pending, chunk]);
if (pending.length > 16 * 1024) { handled = true; this.deny(socket, 'Registry proxy request exceeded its header limit'); return; }
const end = pending.indexOf('\r\n\r\n'); if (end < 0) return;
handled = true; socket.off('data', first);
const header = pending.subarray(0, end + 4).toString('ascii'), remainder = pending.subarray(end + 4);
const line = header.slice(0, header.indexOf('\r\n'));
const match = line.match(/^CONNECT ([a-zA-Z0-9.-]+):443 HTTP\/1\.[01]$/);
const host = match?.[1].toLowerCase();
if (!host || header.toLowerCase().includes('\r\nproxy-authorization:') || !this.allowedHosts.includes(host)) {
this.deny(socket, 'Registry proxy rejected a non-allowlisted CONNECT request', host); return;
}
const task = this.openTunnel(socket, host, remainder, connectionDeadline);
this.tasks.add(task); void task.then(() => this.tasks.delete(task), () => this.tasks.delete(task));
};
socket.on('data', first); socket.once('timeout', () => this.deny(socket, 'Registry proxy connection timed out'));
socket.once('error', () => {});
}
assertClean(): void { if (this.violation) fail('ISOLATION_FAILED', this.violation); }
async close(): Promise<void> {
this.closing = true;
for (const resolution of this.resolutions) resolution.cancel(new CsoError('ISOLATION_FAILED', 'Registry broker closed during DNS resolution'));
for (const socket of this.sockets) socket.destroy();
if (this.started && this.server.listening) await new Promise<void>(resolveClose => this.server.close(() => resolveClose()));
await Promise.allSettled([...this.tasks]);
this.started = false;
try {
const stat = fs.lstatSync(this.socketPath);
if (!stat.isSocket() || stat.isSymbolicLink()) fail('UNSAFE_PATH', 'Registry broker socket changed before cleanup');
fs.unlinkSync(this.socketPath);
} catch (error: any) { if (error?.code !== 'ENOENT') throw error; }
}
}
export interface DockerPreparationRunnerOptions {
endpoint: DockerEndpoint;
watchdogPath: string;
controlRoot: string;
runRoot?: string;
admission: PreparationRuntimeAdmission;
}
export interface PreparedCallGuard { callRoot: string; controlRoot: string; dispose(): Promise<void> }
export interface SupervisedRegistrySocket { root: string; socketPath: string; dispose(): Promise<void> }
/** Darwin's sockaddr_un.sun_path is 104 bytes including its terminator. */
export const REGISTRY_SOCKET_PATH_MAX_BYTES = 90;
function sameDirectory(left:fs.Stats,right:fs.Stats):boolean{return left.dev===right.dev&&left.ino===right.ino&&left.uid===right.uid;}
function ownedDirectory(path:string,label:string):fs.Stats{
let stat:fs.Stats;try{stat=fs.lstatSync(path);}catch{fail('PERSISTENCE_FAILED',`${label} disappeared`);}
if(!stat!.isDirectory()||stat!.isSymbolicLink()||(process.getuid&&stat!.uid!==process.getuid()))fail('UNSAFE_PATH',`${label} is not one owned directory`);
return stat!;
}
/** Detached guard for a successful retained preparation copy. Exported for fault-injection qualification. */
export async function supervisePreparedCall(options:{watchdogPath:string;ownerPid:number;deadline:number;runRoot:string;callRoot:string;controlRoot:string}):Promise<PreparedCallGuard>{
const run=fs.realpathSync(options.runRoot),call=fs.realpathSync(options.callRoot),control=fs.realpathSync(options.controlRoot);
if(!Number.isSafeInteger(options.ownerPid)||options.ownerPid<=1||!Number.isSafeInteger(options.deadline)||options.deadline<=Date.now()||
!call.startsWith(`${run}${sep}`)||!control.startsWith(`${run}${sep}`)||call===control)fail('ISOLATION_FAILED','Prepared-copy supervision paths or deadline are invalid');
const callIdentity=ownedDirectory(call,'Prepared call root'),controlIdentity=ownedDirectory(control,'Prepared supervision control');
if(!fs.existsSync(options.watchdogPath)||fs.lstatSync(options.watchdogPath).isSymbolicLink())fail('ISOLATION_FAILED','Prepared-copy watchdog is missing from the trusted helper distribution');
const ready=join(control,'attempt.ready'),terminal=join(control,'attempt.terminal'),stopped=join(control,'attempt.stopped'),event=join(control,'attempt.event');
const child=spawn(options.watchdogPath,['--attempt-owner',String(options.ownerPid),'--deadline',String(Math.ceil(options.deadline/1000)),
'--control-dir',control,'--work-root',call,'--run-root',run],{cwd:control,env:{PATH:'/usr/bin:/bin'},detached:true,stdio:'ignore'});
let failed=false;child.once('error',()=>{failed=true;});child.unref();
for(let attempt=0;attempt<100&&!failed&&!fs.existsSync(ready);attempt++)await new Promise(resolveWait=>setTimeout(resolveWait,10));
let alive=false;try{if(child.pid){process.kill(child.pid,0);alive=true;}}catch{}
if(failed||!alive||!fs.existsSync(ready)){
try{if(child.pid)process.kill(child.pid,'SIGKILL');}catch{}
fail('ISOLATION_FAILED','Prepared-copy watchdog failed its startup handshake');
}
let disposed=false;
return {callRoot:call,controlRoot:control,dispose:async()=>{
if(disposed)fail('PERSISTENCE_FAILED','Prepared-copy guard was already disposed');
disposed=true;let supervised=false;
try{
const current=fs.lstatSync(call);
if(!sameDirectory(callIdentity,current)||!current.isDirectory()||current.isSymbolicLink())fail('SNAPSHOT_RACE','Prepared call root changed before cleanup');
fs.rmSync(call,{recursive:true,force:false});supervised=true;
}catch(error:any){
if(error instanceof CsoError)throw error;
if(error?.code!=='ENOENT'||!fs.existsSync(event))fail('PERSISTENCE_FAILED','Prepared execution copy could not be removed exactly');
}
if(supervised){fs.writeFileSync(terminal,'normal cleanup complete\n',{mode:0o600,flag:'wx'});for(let attempt=0;attempt<100&&!fs.existsSync(stopped);attempt++)await new Promise(resolveWait=>setTimeout(resolveWait,10));if(!fs.existsSync(stopped))fail('ISOLATION_FAILED','Prepared-copy watchdog did not acknowledge exact cleanup');}
const currentControl=ownedDirectory(control,'Prepared supervision control');if(!sameDirectory(controlIdentity,currentControl))fail('SNAPSHOT_RACE','Prepared supervision control changed before cleanup');
fs.rmSync(control,{recursive:true,force:false});
}};
}
function removeExactDirectory(path:string,identity:fs.Stats,label:string):void{
let current:fs.Stats;
try{current=fs.lstatSync(path);}catch(error:any){if(error?.code==='ENOENT')return;fail('PERSISTENCE_FAILED',`${label} disappeared before exact cleanup`);}
if(!current!.isDirectory()||current!.isSymbolicLink()||!sameDirectory(identity,current!))fail('SNAPSHOT_RACE',`${label} changed before exact cleanup`);
try{fs.rmSync(path,{recursive:true,force:false});}catch{fail('PERSISTENCE_FAILED',`${label} could not be removed exactly`);}
if(fs.existsSync(path))fail('PERSISTENCE_FAILED',`${label} cleanup could not be proven`);
}
function registrySocketBase():{path:string;uid:number}{
const getuid=process.getuid;
if(process.platform==='win32'||!getuid)fail('PREREQUISITE','Registry acquisition requires local Unix sockets');
const uid=getuid();
let temporary:string;
try{temporary=fs.realpathSync('/tmp');}catch{fail('PREREQUISITE','A canonical local temporary directory is required for registry acquisition');}
const stat=fs.lstatSync(temporary!);
if(temporary==='/'||!stat.isDirectory()||stat.isSymbolicLink()||(stat.uid!==0&&stat.uid!==uid)||
((stat.mode&0o022)!==0&&(stat.mode&0o1000)===0))fail('UNSAFE_PATH','The local temporary directory is not a trusted sticky directory');
return {path:temporary!,uid};
}
/**
* Create a collision-resistant, Darwin-safe registry socket path and place its
* entire per-call root under an independent owner/deadline watchdog. The
* watchdog control directory is nested deliberately, so normal termination,
* owner death, and deadline expiry all remove the same exact root without a
* caller-only cleanup interval.
*/
export async function superviseRegistrySocket(options:{watchdogPath:string;ownerPid:number;deadline:number}):Promise<SupervisedRegistrySocket>{
if(!Number.isSafeInteger(options.ownerPid)||options.ownerPid<=1||!Number.isSafeInteger(options.deadline)||options.deadline<=Date.now())
fail('ISOLATION_FAILED','Registry socket supervision owner or deadline is invalid');
let watchdog='',watchdogStat:fs.Stats;
try{watchdog=fs.realpathSync(options.watchdogPath);watchdogStat=fs.lstatSync(options.watchdogPath);}catch{fail('ISOLATION_FAILED','Registry socket watchdog is missing from the trusted helper distribution');}
if(!isAbsolute(options.watchdogPath)||watchdog!==options.watchdogPath||!watchdogStat!.isFile()||watchdogStat!.isSymbolicLink()||
(watchdogStat!.mode&0o111)===0||(process.getuid&&watchdogStat!.uid!==0&&watchdogStat!.uid!==process.getuid()))
fail('ISOLATION_FAILED','Registry socket watchdog must be one canonical owned executable');
const {path:base,uid}=registrySocketBase();let root='';
for(let attempt=0;attempt<4&&!root;attempt++){
const candidate=join(base,`gscso-${uid}-${randomBytes(16).toString('hex')}`);
try{fs.mkdirSync(candidate,{mode:0o700});root=candidate;}catch(error:any){if(error?.code!=='EEXIST')throw error;}
}
if(!root)fail('INSUFFICIENT_CAPACITY','A unique private registry socket directory could not be allocated');
fs.chmodSync(root,0o700);
const rootIdentity=ownedDirectory(root,'Registry socket root'),socketPath=join(root,'r.sock');
if(fs.realpathSync(root)!==root||Buffer.byteLength(socketPath)>REGISTRY_SOCKET_PATH_MAX_BYTES){
removeExactDirectory(root,rootIdentity,'Registry socket root');
fail('PREREQUISITE',`The canonical registry socket path exceeds ${REGISTRY_SOCKET_PATH_MAX_BYTES} bytes`);
}
let control='';let child:ReturnType<typeof spawn>|undefined;let failed=false;let exitCode:number|null|undefined;
try{
control=secureDirectory(join(root,'control'));
const ready=join(control,'attempt.ready'),terminal=join(control,'attempt.terminal');
child=spawn(watchdog,['--ephemeral-owner',String(options.ownerPid),'--deadline',String(Math.ceil(options.deadline/1000)),
'--control-dir',control,'--work-root',root,'--run-root',base],{cwd:control,env:{PATH:'/usr/bin:/bin'},detached:true,stdio:'ignore'});
const exited=new Promise<number|null>(resolveExit=>child!.once('close',code=>{exitCode=code;resolveExit(code);}));
child.once('error',()=>{failed=true;});child.unref();
for(let attempt=0;attempt<100&&!failed&&!fs.existsSync(ready);attempt++)await new Promise(resolveWait=>setTimeout(resolveWait,10));
let alive=false;try{if(child.pid){process.kill(child.pid,0);alive=true;}}catch{}
if(failed||!alive||!fs.existsSync(ready)){
try{if(child.pid)process.kill(child.pid,'SIGKILL');}catch{}
await Promise.race([exited,new Promise(resolveWait=>setTimeout(resolveWait,1000))]);
fail('ISOLATION_FAILED','Registry socket watchdog failed its startup handshake');
}
let disposed=false;
return {root,socketPath,dispose:async()=>{
if(disposed)fail('PERSISTENCE_FAILED','Registry socket guard was already disposed');
disposed=true;
let current:fs.Stats;
try{current=fs.lstatSync(root);}catch(error:any){
if(error?.code!=='ENOENT')fail('PERSISTENCE_FAILED','Registry socket root disappeared during cleanup');
const code=exitCode===undefined?await Promise.race([exited,new Promise<undefined>(resolveWait=>setTimeout(()=>resolveWait(undefined),5000))]):exitCode;
if(code!==0)fail('ISOLATION_FAILED','Registry socket watchdog did not complete abnormal exact cleanup');
return;
}
if(!current!.isDirectory()||current!.isSymbolicLink()||!sameDirectory(rootIdentity,current!))fail('SNAPSHOT_RACE','Registry socket root changed before cleanup');
try{fs.writeFileSync(terminal,'normal cleanup complete\n',{mode:0o600,flag:'wx'});}catch(error:any){
if(error?.code!=='ENOENT')throw error;
}
for(let attempt=0;attempt<500&&fs.existsSync(root);attempt++)await new Promise(resolveWait=>setTimeout(resolveWait,10));
if(fs.existsSync(root))fail('ISOLATION_FAILED','Registry socket watchdog did not remove the exact private root');
const code=exitCode===undefined?await Promise.race([exited,new Promise<undefined>(resolveWait=>setTimeout(()=>resolveWait(undefined),5000))]):exitCode;
if(code!==0)fail('ISOLATION_FAILED','Registry socket watchdog exited without completing exact cleanup');
}};
}catch(error){
try{if(child?.pid)process.kill(child.pid,'SIGKILL');}catch{}
let cleanupError:unknown;try{removeExactDirectory(root,rootIdentity,'Registry socket root');}catch(failure){cleanupError=failure;}
if(cleanupError)throw cleanupError;
throw error;
}
}
/**
* Requires qualified runtime images to contain the fixed, compiled
* /opt/cso/preparation@1.0.0 helper contract.
*/
export class DockerPreparationSandboxRunner implements PreparationSandboxRunner {
readonly qualification;
private readonly runtime;
private readonly controlRoot: string;
private readonly preparedRoots = new Map<string, { guard: PreparedCallGuard; callDir: string; callIdentity: fs.Stats }>();
constructor(private readonly options: DockerPreparationRunnerOptions) {
this.runtime = admittedPreparationRuntime(options.admission);
if (!['node', 'bun', 'python', 'rails'].includes(this.runtime.stack) || this.runtime.versions['cso-preparation'] !== '1.0.0')
fail('PREREQUISITE', 'Qualified runtime lacks the cso-preparation=1.0.0 container helper contract');
this.controlRoot = secureDirectory(resolve(options.controlRoot));
this.qualification = Object.freeze({ schemaVersion: 1 as const, helperAbi: CSO_HELPER_ABI,
runnerId: 'docker-registry-broker-v1', policyVersion: 'cso-preparation-v1' as const,
supportedStacks: [this.runtime.stack as CsoStack], registryRestrictionQualified: true as const,
dnsRebindingTestsPassed: true as const, acquisitionExcludesSource: true as const,
offlineContainmentQualified: true as const, immutableArchiveMounts: true as const, resourceLimitsEnforced: true as const });
}
private assertRuntime(request: PreparationAcquireRequest | OfflinePreparationRequest): void {
if (request.runtime.id !== this.runtime.id || request.runtime.image !== this.runtime.image || request.runtime.platform !== this.runtime.platform || request.stack !== this.runtime.stack)
fail('INCOMPATIBLE_INPUT', 'Docker preparation request does not match its admitted runtime');
}
private callDirectory(prefix: string): string {
return secureDirectory(join(this.controlRoot, `${prefix}-${Date.now()}-${randomBytes(8).toString('hex')}`));
}
private removeCallDirectory(path:string,identity:fs.Stats):void{
let current:fs.Stats;try{current=fs.lstatSync(path);}catch{fail('PERSISTENCE_FAILED','Preparation call directory disappeared before exact cleanup');}
if(!current!.isDirectory()||current!.isSymbolicLink()||current!.dev!==identity.dev||current!.ino!==identity.ino)fail('SNAPSHOT_RACE','Preparation call directory changed before cleanup');
try{fs.rmSync(path,{recursive:true,force:false});}catch{fail('PERSISTENCE_FAILED','Preparation call directory could not be removed');}
if(fs.existsSync(path))fail('PERSISTENCE_FAILED','Preparation call directory cleanup could not be proven');
}
private publishArtifacts(artifacts: AcquisitionArtifactReceipt[], output: string, stagingRoot: string, maxBytes: number): void {
const created: string[] = [];
try {
for (const artifact of artifacts) {
if (typeof artifact.stagingPath !== 'string' || !artifact.stagingPath.startsWith('cso-public/') ||
artifact.stagingPath.slice('cso-public/'.length).includes('/')) fail('TOOL_FAILED', 'Qualified helper returned an invalid staging artifact path');
const name = artifact.stagingPath.slice('cso-public/'.length), source = contained(join(output, 'archives'), name),
target = contained(stagingRoot, artifact.stagingPath);
const stat = fs.lstatSync(source);
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size !== artifact.bytes || stat.size > maxBytes ||
(process.getuid && stat.uid !== process.getuid()) || (stat.mode & 0o022) !== 0 || fileSha256(source, maxBytes) !== artifact.sha256)
fail('TOOL_FAILED', 'Qualified helper output did not match its archive receipt');
secureDirectory(resolve(target, '..'));
fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL); fs.chmodSync(target, 0o600); created.push(target);
const copied = fs.lstatSync(target);
if (!copied.isFile() || copied.isSymbolicLink() || copied.nlink !== 1 || copied.size !== artifact.bytes ||
fileSha256(target, maxBytes) !== artifact.sha256) fail('SNAPSHOT_RACE', 'Staged acquisition artifact changed during publication');
}
} catch (error) {
for (const path of created.reverse()) { try { const stat = fs.lstatSync(path); if (stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1) fs.unlinkSync(path); } catch {} }
throw error;
}
}
private execClean(group: DockerGroup, id: string, command: string[], options: { workdir?: string; env?: Record<string, string> } = {}) {
const forbidden = new Set(['BUN_OPTIONS', 'BUN_BE_BUN', 'NODE_OPTIONS', 'RUBYOPT', 'RUBYLIB', 'PYTHONPATH', 'PYTHONHOME',
'LD_PRELOAD', 'LD_LIBRARY_PATH', 'ENV', 'BASH_ENV', 'CDPATH']);
if (Object.keys(options.env ?? {}).some(key => forbidden.has(key))) fail('ISOLATION_FAILED', 'Preparation command attempted to restore a runtime injection variable');
const clean = { PATH: '/usr/local/bin:/usr/bin:/bin', HOME: '/work/.cso-home', ...(options.env ?? {}) };
const argv = ['/usr/bin/env', '-i', ...Object.entries(clean).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`), ...command];
return group.execCapture(id, argv, { workdir: options.workdir });
}
async acquire(request: PreparationAcquireRequest): Promise<AcquisitionReceipt> {
this.assertRuntime(request);
const dir = this.callDirectory('acquire'),callIdentity = fs.lstatSync(dir);
let group: DockerGroup | undefined, broker: RegistryEgressBroker | undefined,guard:PreparedCallGuard|undefined,
registrySocket:SupervisedRegistrySocket|undefined;
const commandResults: PreparationCommandReceipt[] = [];
try {
const executionCopies=secureDirectory(join(dir,'execution-copies')),metadata = secureDirectory(join(executionCopies, 'metadata')),
policyDir = secureDirectory(join(executionCopies, 'policy')),output = secureDirectory(join(executionCopies, 'output')),
runRoot=secureDirectory(resolve(this.options.runRoot??this.controlRoot)),supervision=secureDirectory(join(runRoot,'supervision')),
guardControl=secureDirectory(join(supervision,`acquisition-${randomBytes(12).toString('hex')}`));
guard=await supervisePreparedCall({watchdogPath:this.options.watchdogPath,ownerPid:process.pid,deadline:request.deadline,
runRoot,callRoot:executionCopies,controlRoot:guardControl});
for (const item of request.metadata) {
if (sha256(item.content) !== item.sha256) fail('INCOMPATIBLE_INPUT', `Acquisition metadata hash changed: ${item.path}`);
const file = contained(metadata, item.path); secureDirectory(resolve(file, '..')); fs.writeFileSync(file, item.content, { mode: 0o600, flag: 'wx' });
}
const policyFile = join(policyDir, 'acquisition.json');
writePolicy(policyFile, { schemaVersion: 1, planHash: request.planHash, stack: request.stack, inputs: request.inputs,
commands: request.commands, allowedHosts: request.network.allowedHosts, archiveRoot: '/archives', limits: request.limits });
group = await DockerGroup.create(this.options.endpoint, `prep-a-${randomBytes(10).toString('hex')}`, dir,
request.deadline, this.runtime.image, this.options.watchdogPath);
registrySocket=await superviseRegistrySocket({watchdogPath:this.options.watchdogPath,ownerPid:process.pid,deadline:request.deadline});
broker = new RegistryEgressBroker(registrySocket.socketPath, request.network.allowedHosts, request.deadline,
request.limits.maxTotalArchiveBytes + Math.min(64 * 1024 * 1024, request.limits.maxTotalArchiveBytes));
await broker.start();
const container = await group.createContainer({ role: 'app', image: this.runtime.image,
command: ['/opt/cso/preparation', 'forwarder', '/run/cso-registry.sock', '127.0.0.1:18443'],
readonlyFiles: [{ host: policyFile, container: '/policy/acquisition.json' }], readonlyInputMetadata: metadata,
workTmpfsBytes: 64 * 1024 * 1024, temporaryTmpfsBytes: 64 * 1024 * 1024,
metadataTmpfsBytes: (request.stack === 'node' || request.stack === 'bun' ? 1024 : 64) * 1024 * 1024,
archiveTmpfsBytes: request.limits.maxTotalArchiveBytes, registrySocket: broker.socketPath });
await group.start(container);
let ready = false;
for (let attempt = 0; attempt < 20 && !ready; attempt++) {
const health = await this.execClean(group, container, ['/opt/cso/preparation', 'health', '127.0.0.1', '18443']);
ready = health.code === 0; if (!ready) await new Promise(resolveWait => setTimeout(resolveWait, 25));
}
if (!ready) fail('ISOLATION_FAILED', 'Qualified registry forwarder did not become ready');
const proxy = { HTTP_PROXY: 'http://127.0.0.1:18443', HTTPS_PROXY: 'http://127.0.0.1:18443',
ALL_PROXY: 'http://127.0.0.1:18443', NO_PROXY: '' };
for (let index = 0; index < request.commands.length; index++) {
const command = request.commands[index], result = await this.execClean(group, container,
[command.executable, ...command.args], { workdir: command.cwd, env: { ...command.env, ...proxy } });
broker.assertClean(); commandResults.push(commandReceipt(command, index, result.code));
if (result.code !== 0) fail('TOOL_FAILED', `Dependency acquisition command ${index + 1} failed`);
}
await group.assertOnlyInitProcess(container);
const containerExport = `/archives/.gstack-cso-acquisition-export-${randomBytes(12).toString('hex')}`;
const manifest = await this.execClean(group, container,
['/opt/cso/preparation', 'manifest', '/policy/acquisition.json', '/archives', `${containerExport}/artifacts.json`], { workdir: '/archives', env: proxy });
if (manifest.code !== 0) fail('TOOL_FAILED', 'Qualified archive manifest helper failed');
broker.assertClean();
await group.assertOnlyInitProcess(container);
await group.pause(container);
await group.copyAcquisitionExport(container, containerExport, output);
const artifacts = readManifest(join(output, 'artifacts.json')).artifacts;
validateAcquisitionOutput(output, artifacts);
this.publishArtifacts(artifacts, output, request.stagingRoot, request.limits.maxArchiveBytes);
return { schemaVersion: 1, planHash: request.planHash, runtimeId: request.runtime.id, runtimeImage: request.runtime.image,
platform: request.runtime.platform, deadlineEnforced: true, network: { mode: 'registry-restricted',
allowedHosts: [...request.network.allowedHosts], contactedHosts: [...broker.contactedHosts].sort(),
redirectVisibility: 'opaque-tls',
dnsRebindingBlocked: true, credentialsMounted: false, sourceMounted: false, dockerSocketMounted: false },
lifecycleScriptsExecuted: false, targetCodeExecuted: false, commands: commandResults, artifacts };
} finally {
let cleanupError: unknown;
try { if (broker) await broker.close(); } catch (error) { cleanupError = error; }
try { if (group) await group.cleanup(); } catch (error) { cleanupError ??= error; }
try { if (registrySocket) await registrySocket.dispose(); } catch (error) { cleanupError ??= error; }
try { if (guard) await guard.dispose(); } catch (error) { cleanupError ??= error; }
if (cleanupError) throw cleanupError;
this.removeCallDirectory(dir,callIdentity);
}
}
private materializeArchives(request: OfflinePreparationRequest, root: string): void {
for (const archive of request.archives) {
if (!archive.containerPath.startsWith('/archives/')) fail('UNSAFE_PATH', 'Offline archive mount escaped /archives');
const relativePath = archive.containerPath.slice('/archives/'.length), target = contained(root, relativePath), source = resolve(archive.hostPath);
const stat = fs.lstatSync(source);
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size !== archive.bytes || (stat.mode & 0o222) !== 0 ||
(process.getuid && stat.uid !== process.getuid())) fail('UNSAFE_PATH', 'Offline archive is not an immutable cache file');
secureDirectory(resolve(target, '..')); fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL); fs.chmodSync(target, 0o400);
const hash = fileSha256(target, archive.bytes);
if (hash !== archive.sha256) fail('INCOMPATIBLE_INPUT', 'Offline archive changed while its execution view was materialized');
}
}
async prepareOffline(request: OfflinePreparationRequest): Promise<OfflinePreparationResult> {
this.assertRuntime(request);
const dir = this.callDirectory('offline'),callIdentity=fs.lstatSync(dir);let preparedRoot='',group: DockerGroup | undefined,success=false,guard:PreparedCallGuard|undefined;
try {
// Docker supervision owns `dir`; the retained-copy watchdog owns this
// child. An owner death can therefore remove every execution input without
// deleting the Docker watchdog's journal or control files.
const executionCopies=secureDirectory(join(dir,'execution-copies')),
source = secureDirectory(join(executionCopies, 'source')), metadata = secureDirectory(join(executionCopies, 'metadata')),
archives = secureDirectory(join(executionCopies, 'archives')), policyDir = secureDirectory(join(executionCopies, 'policy'));
preparedRoot = secureDirectory(join(executionCopies, 'prepared'));
const runRoot=secureDirectory(resolve(this.options.runRoot??this.controlRoot)),supervision=secureDirectory(join(runRoot,'supervision')),
guardControl=secureDirectory(join(supervision,`prepared-${randomBytes(12).toString('hex')}`));
// Supervision is acknowledged before the first potentially large source
// or dependency copy. It remains the retained-copy guard after Docker
// teardown, closing the owner-death handoff window.
guard=await supervisePreparedCall({watchdogPath:this.options.watchdogPath,ownerPid:process.pid,deadline:request.deadline,
runRoot,callRoot:executionCopies,controlRoot:guardControl});
fs.cpSync(request.sourceRoot, source, { recursive: true, force: false, errorOnExist: false, preserveTimestamps: false });
for (const transformation of request.transformations) {
if (sha256(transformation.content) !== transformation.sha256) fail('INCOMPATIBLE_INPUT', 'Offline transformation content hash changed');
const file = contained(source, transformation.path); secureDirectory(resolve(file, '..')); fs.writeFileSync(file, transformation.content, { mode: 0o600 });
}
for (const item of request.metadata) {
if (sha256(item.content) !== item.sha256) fail('INCOMPATIBLE_INPUT', `Offline metadata hash changed: ${item.path}`);
const file = contained(metadata, item.path); secureDirectory(resolve(file, '..')); fs.writeFileSync(file, item.content, { mode: 0o600, flag: 'wx' });
}
this.materializeArchives(request, archives);
const offlinePolicy = join(policyDir, 'offline.json');
writePolicy(offlinePolicy, { schemaVersion: 1, planHash: request.planHash, stack: request.stack,
inputs: request.archives.map(archive => ({ index: archive.inputIndex, input: { kind: 'public', name: archive.name,
version: archive.version, url: archive.requestedUrl, integrity: archive.declaredIntegrity,
integritySource: archive.declaredIntegrity === 'registry-on-acquisition' ? 'registry-on-acquisition' : 'lock' } })),
archives: request.archives.map(archive => ({ inputIndex: archive.inputIndex, name: archive.name, version: archive.version,
declaredIntegrity: archive.declaredIntegrity, requestedUrl: archive.requestedUrl, resolvedUrl: archive.resolvedUrl,
containerPath: archive.containerPath,
sha256: archive.sha256, bytes: archive.bytes })) });
const commands: PreparationCommandReceipt[] = [];
group = await DockerGroup.create(this.options.endpoint, `prep-o-${randomBytes(10).toString('hex')}`, dir,
request.deadline, this.runtime.image, this.options.watchdogPath);
const app = await group.createContainer({ role: 'app', image: this.runtime.image, source,
readonlyFiles: [{ host: offlinePolicy, container: '/policy/offline.json' }], readonlyMetadata: metadata, readonlyArchiveDirectory: archives,
command: ['/opt/cso/run-app', '/opt/cso/preparation', 'seed', '/policy/offline.json'] });
await group.start(app);
let ready = false;
for (let attempt = 0; attempt < 100 && !ready; attempt++) {
const result = await this.execClean(group, app, ['/opt/cso/preparation', 'ready']);
ready = result.code === 0; if (!ready) await new Promise(resolveWait => setTimeout(resolveWait, 25));
}
if (!ready) fail('TOOL_FAILED', 'Offline cache seeding did not become ready');
for (let index = 0; index < request.commands.length; index++) {
const command = request.commands[index], result = await this.execClean(group, app,
[command.executable, ...command.args], { workdir: command.cwd, env: command.env });
commands.push(commandReceipt(command, index, result.code));
if (result.code !== 0) fail('TOOL_FAILED', `Offline dependency preparation command ${index + 1} failed`);
}
const finalized = await this.execClean(group, app, ['/opt/cso/preparation', 'finalize']);
if (finalized.code !== 0) fail('TOOL_FAILED', 'Offline preparation scratch cleanup failed');
await group.assertOnlyInitProcess(app);
const containerExport = `/work/.gstack-cso-export-${randomBytes(12).toString('hex')}`;
const exported = await this.execClean(group, app,
['/opt/cso/preparation', 'export-prepared', '/work', containerExport, String(request.limits.writableBytes)]);
if (exported.code !== 0) fail('TOOL_FAILED', 'Qualified prepared-tree export rejected offline application output');
await group.assertOnlyInitProcess(app);
await group.pause(app);
const inertExport = secureDirectory(join(executionCopies, 'prepared-export')), inertIdentity = fs.lstatSync(inertExport);
await group.copyPreparedExport(app, containerExport, inertExport);
materializePreparedExport(inertExport, preparedRoot, request.limits.writableBytes);
this.removeCallDirectory(inertExport,inertIdentity);
const services: Array<'application' | 'postgresql'> = ['application'];
const receipt: OfflinePreparationReceipt = { schemaVersion: 1, planHash: request.planHash, runtimeId: request.runtime.id,
runtimeImage: request.runtime.image, platform: request.runtime.platform, sourceHash: request.sourceHash,
dependencyClosureHash: request.dependencyClosureHash, configurationHash: request.configurationHash,
databaseHash: request.databaseHash, deadlineEnforced: true,
network: { mode: 'none', namespaceAnchor: group.anchor, externalEgress: false, dnsAvailable: false, publishedPorts: false, services },
commands, inputSourceReadOnly: true, preparedCopySeparate: true, archivesReadOnly: true, applicationCodeExecutedOnlyOffline: true };
// The guard is live before Docker teardown starts. Remove redundant
// source, metadata, archives, and policy now; only the prepared tree is
// retained. Docker's independent watchdog keeps its parent control root.
for(const current of [source,metadata,archives,policyDir])this.removeCallDirectory(current,fs.lstatSync(current));
const completedGroup=group;await completedGroup.cleanup();group=undefined;
this.preparedRoots.set(resolve(preparedRoot),{guard,callDir:dir,callIdentity});
success=true;
return { preparedRoot, receipt };
} finally {
let cleanupError:unknown;
if (group) {
try { await group.cleanup(); }
catch (error) { cleanupError=error; }
}
if(cleanupError){if(preparedRoot)this.preparedRoots.delete(resolve(preparedRoot));if(guard)try{await guard.dispose();}catch{}throw cleanupError;}
if(!success){
if(preparedRoot)this.preparedRoots.delete(resolve(preparedRoot));
// Once the retained-copy watchdog has acknowledged supervision it is
// the only actor allowed to consume its call root. Stop and
// acknowledge it before removing the Docker call directory.
if(guard)await guard.dispose();
this.removeCallDirectory(dir,callIdentity);
}
}
}
async disposePrepared(preparedRoot: string): Promise<void> {
const root = resolve(preparedRoot), owned = this.preparedRoots.get(root),guard=owned?.guard,call=guard?.callRoot;
if (!guard || !call || !owned || root !== join(call, 'prepared') || !owned.callDir.startsWith(`${this.controlRoot}${sep}`))
fail('UNSAFE_PATH', 'Prepared execution copy is not owned by this runner');
this.preparedRoots.delete(root);
await guard.dispose();
this.removeCallDirectory(owned.callDir,owned.callIdentity);
}
}
+880
View File
@@ -0,0 +1,880 @@
/**
* Deterministic orchestration for dependency acquisition and offline target
* preparation. This module never opens a network connection or invokes Docker;
* a qualified helper adapter implements those effects and returns a receipt
* which is checked again here before any archive becomes executable input.
*/
import * as fs from 'node:fs';
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
import { PublicArchiveCache } from './cache';
import { canonical, CsoError, MAX_OUTPUT, sha256 } from './contracts';
import {
inspectPreparation, railsTestConfiguration, type CsoStack, type PreparationCommand,
type PreparationInput, type PreparationPlan,
} from './preparation';
import {
assertRuntimeCompatible, CSO_HELPER_ABI, RUNTIME_CATALOG, selectRuntime, validateRuntimeCatalog,
type QualifiedRuntime, type RuntimeCatalog, type RuntimePlatform,
} from './runtime-catalog';
const SHA256 = /^[a-f0-9]{64}$/;
const HOST = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
const RELATIVE_ARCHIVE = /^(?!\/)(?!.*(?:^|\/)\.\.?(?:\/|$))(?!.*\\)[A-Za-z0-9@._+\/-]{1,1024}$/;
const MAX_ARCHIVES = 25_000;
const MAX_TREE_FILES = 200_000;
const MAX_SOURCE_BYTES = 64 * 1024 * 1024;
// The smallest automatic execution role is the tests container: 768 MiB at
// /work after its fixed /tmp allocation. Reserve 32 MiB for filesystem and
// package-manager bookkeeping so a declared prepared tree is executable in
// every before/after/test phase rather than merely exportable.
const MAX_PREPARED_BYTES = 736 * 1024 * 1024;
// Anchor/application work consume 784 MiB and acquisition metadata is capped
// at 400 MiB. Archive and export tmpfs each use at most 384 MiB, keeping the
// aggregate below the 2 GiB group policy while avoiding writable host binds.
const MAX_ACQUISITION_ARCHIVE_BYTES = 384 * 1024 * 1024;
const admittedRuntimes = new WeakSet<object>();
function fail(code: ConstructorParameters<typeof CsoError>[0], message: string): never { throw new CsoError(code, message); }
function sameStrings(left: string[], right: string[]): boolean {
return canonical([...left].sort()) === canonical([...right].sort());
}
function deepFreeze<T>(value: T): T {
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
Object.freeze(value);
for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child);
}
return value;
}
function checkDeadline(deadline: number, signal?: AbortSignal): void {
if (signal?.aborted) fail('CANCELLED', 'Preparation was cancelled before the operation completed');
if (!Number.isSafeInteger(deadline) || deadline <= 0) fail('INVALID_ARGUMENT', 'Preparation deadline must be an absolute millisecond timestamp');
if (Date.now() >= deadline) fail('DEADLINE', 'Preparation deadline was reached before the operation completed');
}
function safeMessage(error: unknown): string { return error instanceof Error ? error.message.slice(0, 500) : 'Runtime catalog admission failed'; }
export interface PreparationRuntimeAdmission {
schemaVersion: 1;
catalogRevision: string;
runtime: QualifiedRuntime;
}
/** Select an application runtime from a reviewed catalog. The default empty catalog blocks execution. */
export function admitPreparationRuntime(options: {
plan: PreparationPlan;
platform: RuntimePlatform;
profile?: string;
catalog?: RuntimeCatalog;
}): PreparationRuntimeAdmission {
const catalog = options.catalog ?? RUNTIME_CATALOG;
try { validateRuntimeCatalog(catalog); }
catch (error) { fail('PREREQUISITE', `Runtime catalog is not qualified: ${safeMessage(error)}`); }
let runtime: QualifiedRuntime;
try { runtime = selectRuntime(options.profile ?? options.plan.runtimeProfile, options.platform, catalog); }
catch (error) { fail('PREREQUISITE', safeMessage(error)); }
assertRuntimeCompatible(options.plan, runtime!);
const admission = Object.freeze({ schemaVersion: 1 as const, catalogRevision: catalog.revision, runtime: runtime! });
admittedRuntimes.add(admission);
return admission;
}
/** PostgreSQL is admitted independently and must match the application's platform/catalog revision. */
export function admitPreparationSidecar(options: {
platform: RuntimePlatform;
profile?: string;
catalog?: RuntimeCatalog;
}): PreparationRuntimeAdmission {
const catalog = options.catalog ?? RUNTIME_CATALOG;
try { validateRuntimeCatalog(catalog); }
catch (error) { fail('PREREQUISITE', `Runtime catalog is not qualified: ${safeMessage(error)}`); }
let runtime: QualifiedRuntime;
try { runtime = selectRuntime(options.profile ?? 'postgresql', options.platform, catalog); }
catch (error) { fail('PREREQUISITE', safeMessage(error)); }
if (runtime!.stack !== 'postgresql') fail('INCOMPATIBLE_INPUT', 'Rails PostgreSQL preparation requires a qualified PostgreSQL runtime');
const admission = Object.freeze({ schemaVersion: 1 as const, catalogRevision: catalog.revision, runtime: runtime! });
admittedRuntimes.add(admission);
return admission;
}
export interface PreparationRunnerQualification {
schemaVersion: 1;
helperAbi: number;
runnerId: string;
policyVersion: 'cso-preparation-v1';
supportedStacks: CsoStack[];
registryRestrictionQualified: true;
dnsRebindingTestsPassed: true;
acquisitionExcludesSource: true;
offlineContainmentQualified: true;
immutableArchiveMounts: true;
resourceLimitsEnforced: true;
}
export interface PreparationCommandReceipt {
index: number;
commandHash: string;
exitCode: number;
timedOut: boolean;
outputTruncated: boolean;
}
export interface AcquisitionArtifactReceipt {
inputIndex: number;
stagingPath: string;
installPath: string;
sha256: string;
bytes: number;
requestedHost: string;
requestedUrl: string;
/** Final response URL only when the trusted helper performed the download directly. */
resolvedUrl: string | null;
/** Digest calculated over the registry response before it entered staging. */
registryResponseSha256: string;
}
export interface AcquisitionReceipt {
schemaVersion: 1;
planHash: string;
runtimeId: string;
runtimeImage: string;
platform: RuntimePlatform;
deadlineEnforced: true;
network: {
mode: 'registry-restricted';
allowedHosts: string[];
contactedHosts: string[];
/** CONNECT host allowlisting is enforced, but paths and redirects inside TLS are unobservable. */
redirectVisibility: 'opaque-tls';
dnsRebindingBlocked: true;
credentialsMounted: false;
sourceMounted: false;
dockerSocketMounted: false;
};
lifecycleScriptsExecuted: false;
targetCodeExecuted: false;
commands: PreparationCommandReceipt[];
artifacts: AcquisitionArtifactReceipt[];
}
export interface PreparationAcquireRequest {
schemaVersion: 1;
planHash: string;
stack: CsoStack;
runtime: { id: string; image: string; platform: RuntimePlatform };
metadata: PreparationPlan['metadata'];
inputs: Array<{ index: number; input: PreparationInput }>;
commands: PreparationCommand[];
stagingRoot: string;
deadline: number;
limits: { maxArchives: number; maxArchiveBytes: number; maxTotalArchiveBytes: number; maxOutputBytes: number };
network: { mode: 'registry-restricted'; allowedHosts: string[] };
sourceMounted: false;
}
export interface CachedArchiveMount {
inputIndex: number;
name: string;
version: string;
declaredIntegrity: string;
requestedUrl: string;
resolvedUrl: string | null;
hostPath: string;
containerPath: string;
sha256: string;
bytes: number;
}
export type RailsDatabaseSelection =
| { adapter: 'sqlite' }
| { adapter: 'postgresql'; sidecar: PreparationRuntimeAdmission };
export type PreparedDatabaseContract =
| { adapter: 'sqlite'; connections: string[] }
| { adapter: 'postgresql'; connections: string[]; sidecar: { id: string; image: string } };
export interface OfflinePreparationRequest {
schemaVersion: 1;
planHash: string;
stack: CsoStack;
runtime: { id: string; image: string; platform: RuntimePlatform };
sourceRoot: string;
sourceHash: string;
/** Sanitized inert files used only by offline package-manager metadata commands. */
metadata: PreparationPlan['metadata'];
dependencyClosureHash: string;
commands: PreparationCommand[];
archives: CachedArchiveMount[];
transformations: Array<{ path: string; content: string; sha256: string; reason: string }>;
configurationHash: string;
database?: PreparedDatabaseContract;
databaseHash: string;
deadline: number;
limits: { cpus: 2; memoryBytes: number; pids: 256; writableBytes: number; maxOutputBytes: number };
network: { mode: 'none'; sharedLoopbackNamespace: true; publishedPorts: false };
inputSourceReadOnly: true;
archivesReadOnly: true;
}
export interface OfflinePreparationReceipt {
schemaVersion: 1;
planHash: string;
runtimeId: string;
runtimeImage: string;
platform: RuntimePlatform;
sourceHash: string;
dependencyClosureHash: string;
configurationHash: string;
databaseHash: string;
deadlineEnforced: true;
network: {
mode: 'none';
namespaceAnchor: string;
externalEgress: false;
dnsAvailable: false;
publishedPorts: false;
services: Array<'application' | 'postgresql'>;
};
commands: PreparationCommandReceipt[];
inputSourceReadOnly: true;
preparedCopySeparate: true;
archivesReadOnly: true;
applicationCodeExecutedOnlyOffline: true;
}
export interface OfflinePreparationResult {
preparedRoot: string;
receipt: OfflinePreparationReceipt;
}
export interface PreparationSandboxRunner {
readonly qualification: PreparationRunnerQualification;
acquire(request: PreparationAcquireRequest): Promise<AcquisitionReceipt>;
prepareOffline(request: OfflinePreparationRequest): Promise<OfflinePreparationResult>;
disposePrepared(preparedRoot: string): Promise<void> | void;
}
export interface DependencyArchive {
inputIndex: number;
name: string;
version: string;
installPath: string;
sha256: string;
bytes: number;
requestedHost: string;
requestedUrl: string;
resolvedUrl: string | null;
declaredIntegrity: string;
}
export interface DependencyClosure {
schemaVersion: 1;
stack: CsoStack;
planHash: string;
catalogRevision: string;
runtimeId: string;
runtimeImage: string;
platform: RuntimePlatform;
archives: DependencyArchive[];
acquisitionReceiptHash: string | null;
closureHash: string;
}
export interface PreparedApplication {
schemaVersion: 1;
stack: CsoStack;
preparedRoot: string;
sourceHash: string;
preparedManifestHash: string;
preparedDependencyHash: string;
dependencyClosureHash: string;
configurationHash: string;
databaseHash: string;
database?: PreparedDatabaseContract;
sourceProjectionHash: string;
transformations: Array<{ path: string; sha256: string; mode: number; reason: string }>;
executionEnvironment: Record<string, string>;
receiptHash: string;
receipt: OfflinePreparationReceipt;
}
function runtimeFromAdmission(plan: PreparationPlan, admission: PreparationRuntimeAdmission): QualifiedRuntime {
if (!admission || !admittedRuntimes.has(admission)) fail('PREREQUISITE', 'Runtime must be selected through current-process catalog admission');
assertRuntimeCompatible(plan, admission.runtime);
return admission.runtime;
}
/** Trusted adapters use this to bind themselves to a current-process catalog admission. */
export function admittedPreparationRuntime(admission: PreparationRuntimeAdmission): QualifiedRuntime {
if (!admission || !admittedRuntimes.has(admission)) fail('PREREQUISITE', 'Runtime must be selected through current-process catalog admission');
return admission.runtime;
}
function planHash(plan: PreparationPlan): string { return sha256(canonical(plan)); }
function commandHash(command: PreparationCommand): string { return sha256(canonical(command)); }
function validateRunner(runner: PreparationSandboxRunner, stack: CsoStack): void {
const q = runner?.qualification;
if (!q || q.schemaVersion !== 1 || q.helperAbi !== CSO_HELPER_ABI || !/^[a-z0-9][a-z0-9._-]{0,100}$/.test(q.runnerId) ||
q.policyVersion !== 'cso-preparation-v1' || !Array.isArray(q.supportedStacks) || !q.supportedStacks.includes(stack) ||
new Set(q.supportedStacks).size !== q.supportedStacks.length || q.registryRestrictionQualified !== true ||
q.dnsRebindingTestsPassed !== true || q.acquisitionExcludesSource !== true || q.offlineContainmentQualified !== true ||
q.immutableArchiveMounts !== true || q.resourceLimitsEnforced !== true || typeof runner.disposePrepared !== 'function')
fail('ISOLATION_FAILED', `Preparation runner is not qualified for ${stack}`);
}
function validatePlan(plan: PreparationPlan, snapshot: string): string {
if (!plan || plan.schemaVersion !== 1 || plan.status !== 'ready') fail('PREREQUISITE', 'Dependency metadata is not ready for automatic preparation');
const current = inspectPreparation(snapshot, plan.stack);
if (current.status !== 'ready' || canonical(current) !== canonical(plan))
fail('INCOMPATIBLE_INPUT', 'Preparation plan does not match the current retained snapshot');
const hosts = plan.registryHosts;
if (!hosts.length || hosts.some(host => host !== host.toLowerCase() || !HOST.test(host)) || new Set(hosts).size !== hosts.length)
fail('INVALID_SCHEMA', 'Preparation plan contains invalid or duplicate registry hosts');
if (plan.inputs.filter(input => input.kind === 'public').length && !plan.acquisition.length)
fail('INVALID_SCHEMA', 'Public dependencies require a constrained acquisition command');
for (const command of [...plan.acquisition, ...plan.offline]) {
if (!command.executable.startsWith('/') || !['/metadata', '/work', '/archives'].includes(command.cwd) || !Array.isArray(command.args) ||
command.args.some(arg => typeof arg !== 'string' || arg.length > 4096 || /[\0\r\n]/.test(arg)) ||
Object.entries(command.env).some(([key, value]) => !/^[A-Z][A-Z0-9_]{0,63}$/.test(key) || /[\0\r\n]/.test(value)))
fail('INVALID_SCHEMA', 'Preparation command is not a bounded absolute argv/env description');
}
for (const command of plan.acquisition) {
if (command.cwd === '/work' || command.args.some(arg => arg === '/work' || arg.startsWith('/work/')))
fail('ISOLATION_FAILED', 'Acquisition commands must not receive application source');
}
if (plan.stack === 'node' && plan.acquisition.some(command => !command.args.includes('--ignore-scripts')))
fail('ISOLATION_FAILED', 'Node acquisition must disable lifecycle scripts');
if (plan.stack === 'bun' && plan.acquisition.some(command => !command.args.includes('--ignore-scripts')))
fail('ISOLATION_FAILED', 'Bun acquisition must disable lifecycle scripts');
if (plan.stack === 'python' && plan.acquisition.some(command => !['/usr/local/bin/uv', '/usr/local/bin/python'].includes(command.executable) || command.args.includes('install')))
fail('ISOLATION_FAILED', 'Python acquisition may only export metadata or download public wheels');
if (plan.stack === 'rails' && plan.acquisition.some(command => command.executable !== '/usr/local/bin/gem' || command.args[0] !== 'fetch' || !command.args.includes('--norc')))
fail('ISOLATION_FAILED', 'Rails acquisition may only fetch exact gems without evaluating application code');
return planHash(plan);
}
function validateCommandReceipts(receipts: PreparationCommandReceipt[], commands: PreparationCommand[]): void {
if (!Array.isArray(receipts) || receipts.length !== commands.length) fail('TOOL_FAILED', 'Preparation receipt omitted a command result');
const seen = new Set<number>();
for (const receipt of receipts) {
if (!Number.isInteger(receipt.index) || receipt.index < 0 || receipt.index >= commands.length || seen.has(receipt.index) ||
receipt.commandHash !== commandHash(commands[receipt.index]) || receipt.exitCode !== 0 || receipt.timedOut !== false || receipt.outputTruncated !== false)
fail('TOOL_FAILED', 'Preparation command failed or its exact argv receipt is invalid');
seen.add(receipt.index);
}
}
function validateUrl(value: string, host: string, allowed: string[]): void {
let url: URL;
try { url = new URL(value); } catch { fail('TOOL_FAILED', 'Acquisition receipt contains an invalid source URL'); }
if (url!.protocol !== 'https:' || url!.username || url!.password || url!.port || url!.search || url!.hash ||
url!.hostname !== host || !allowed.includes(host)) fail('ISOLATION_FAILED', 'Acquisition receipt escaped its public registry allowlist');
}
function relativeArchivePath(value: string, label: string): string {
if (typeof value !== 'string' || !RELATIVE_ARCHIVE.test(value) || value.split('/').some(part => !part || part === '.' || part === '..'))
fail('UNSAFE_PATH', `${label} must be a contained archive path`);
return value;
}
function stagedFileHashes(root: string, relativePath: string, expectedBytes: number, deadline: number,
signal?: AbortSignal): { sha256: string; sha512: string } {
checkDeadline(deadline, signal);
const parts = relativeArchivePath(relativePath, 'Staged archive').split('/');
let cursor = root;
for (const part of parts.slice(0, -1)) {
checkDeadline(deadline, signal);
cursor = join(cursor, part);
let stat: fs.Stats;
try { stat = fs.lstatSync(cursor); } catch { fail('MISSING_INPUT', 'Acquisition staging directory is missing'); }
if (!stat!.isDirectory() || stat!.isSymbolicLink() || (process.getuid && stat!.uid !== process.getuid()) || (stat!.mode & 0o022) !== 0)
fail('UNSAFE_PATH', 'Acquisition staging path has an unsafe ancestor');
}
const path = resolve(root, ...parts);
if (!path.startsWith(`${root}${sep}`)) fail('UNSAFE_PATH', 'Staged archive escaped acquisition storage');
const noFollow = (fs.constants as any).O_NOFOLLOW ?? 0;
let fd: number;
try { fd = fs.openSync(path, fs.constants.O_RDONLY | noFollow); }
catch { fail('UNSAFE_PATH', 'Staged archive could not be opened without following links'); }
try {
const before = fs.fstatSync(fd!);
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || (process.getuid && before.uid !== process.getuid()) ||
before.size !== expectedBytes) fail('UNSAFE_PATH', 'Staged archive is not the bounded regular file in its receipt');
const h256 = createHash('sha256'), h512 = createHash('sha512'), buffer = Buffer.allocUnsafe(64 * 1024);
let bytes = 0;
for (;;) {
checkDeadline(deadline, signal);
const count = fs.readSync(fd!, buffer, 0, buffer.length, null);
if (!count) break;
bytes += count;
if (bytes > expectedBytes) fail('SNAPSHOT_RACE', 'Staged archive grew while it was verified');
h256.update(buffer.subarray(0, count)); h512.update(buffer.subarray(0, count));
checkDeadline(deadline, signal);
}
checkDeadline(deadline, signal);
const after = fs.fstatSync(fd!);
if (bytes !== expectedBytes || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size ||
before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || before.mode !== after.mode || before.nlink !== after.nlink)
fail('SNAPSHOT_RACE', 'Staged archive changed while it was verified');
return { sha256: h256.digest('hex'), sha512: h512.digest('base64') };
} finally { fs.closeSync(fd!); }
}
function integrityMatches(integrity: string | undefined, hashes: { sha256: string; sha512: string }): boolean {
if (!integrity) return false;
return integrity.split(/\s+/).some(value => value === `sha256:${hashes.sha256}` ||
value === `sha256-${Buffer.from(hashes.sha256, 'hex').toString('base64')}` || value === `sha512-${hashes.sha512}`);
}
function closureIdentity(closure: Omit<DependencyClosure, 'closureHash'>): string { return sha256(canonical(closure)); }
function logicalInput(input: PreparationInput): string { return `${input.name}\0${input.version}`; }
function validateClosure(plan: PreparationPlan, admission: PreparationRuntimeAdmission, closure: DependencyClosure,
cache: PublicArchiveCache, deadline: number, signal?: AbortSignal,
materializationBase?: string): { mounts: CachedArchiveMount[]; materializedRoot?: string } {
checkDeadline(deadline, signal);
const runtime = admission.runtime, expectedPlanHash = planHash(plan);
const { closureHash, ...closureBody } = closure ?? ({} as DependencyClosure);
if (!closure || closure.schemaVersion !== 1 || closure.stack !== plan.stack || closure.planHash !== expectedPlanHash ||
closure.catalogRevision !== admission.catalogRevision || closure.runtimeId !== runtime.id || closure.runtimeImage !== runtime.image ||
closure.platform !== runtime.platform || !Array.isArray(closure.archives) || closure.archives.length > MAX_ARCHIVES || !SHA256.test(closureHash) ||
closureHash !== closureIdentity(closureBody))
fail('INCOMPATIBLE_INPUT', 'Dependency closure does not match the admitted plan and runtime');
const publicInputs = plan.inputs.map((input, index) => ({ input, index })).filter(item => item.input.kind === 'public');
if (publicInputs.length > MAX_ARCHIVES) fail('INSUFFICIENT_CAPACITY', 'Dependency closure exceeds the archive-count limit');
const publicInputsByIndex = new Map(publicInputs.map(item => [item.index, item]));
const covered = new Set<string>(), paths = new Set<string>(), pending: Array<{ archive: DependencyArchive; input: PreparationInput }> = [];
for (const archive of closure.archives) {
checkDeadline(deadline, signal);
const selected = publicInputsByIndex.get(archive.inputIndex);
if (!selected || archive.name !== selected.input.name || archive.version !== selected.input.version ||
archive.declaredIntegrity !== (selected.input.integrity ?? 'registry-on-acquisition') || !SHA256.test(archive.sha256) ||
!Number.isSafeInteger(archive.bytes) || archive.bytes < 0 || !plan.registryHosts.includes(archive.requestedHost))
fail('INCOMPATIBLE_INPUT', 'Dependency closure contains an invalid public archive');
relativeArchivePath(archive.installPath, 'Dependency install path');
validateUrl(archive.requestedUrl, archive.requestedHost, plan.registryHosts);
if (archive.resolvedUrl !== null) validateUrl(archive.resolvedUrl, new URL(archive.resolvedUrl).hostname, plan.registryHosts);
if (paths.has(archive.installPath)) fail('INCOMPATIBLE_INPUT', 'Dependency closure contains colliding archive install paths');
paths.add(archive.installPath); covered.add(logicalInput(selected.input));
pending.push({ archive, input: selected.input });
}
for (const { input } of publicInputs) {
checkDeadline(deadline, signal);
if (!covered.has(logicalInput(input)))
fail('MISSING_INPUT', `Dependency closure does not contain a compatible public archive for ${input.name}@${input.version}`);
}
if (!pending.length) return { mounts: [] };
let base: string;
if (materializationBase) {
base = resolve(materializationBase);
const stat = fs.lstatSync(base), real = fs.realpathSync(base);
if (!stat.isDirectory() || stat.isSymbolicLink() || real !== base || (process.getuid && stat.uid !== process.getuid()) || (stat.mode & 0o022) !== 0)
fail('UNSAFE_PATH', 'Archive materialization root must be one private owned directory');
} else base = tmpdir();
const materializedRoot = fs.mkdtempSync(join(base, 'gstack-cso-archives-'));
fs.chmodSync(materializedRoot, 0o700);
try {
checkDeadline(deadline, signal);
const copies = cache.materialize(pending.map(item => item.archive.sha256), materializedRoot, { deadline, signal }),
byDigest = new Map(copies.map(copy => [copy.sha256, copy]));
const mounts = pending.map(({ archive }) => {
checkDeadline(deadline, signal);
const copy = byDigest.get(archive.sha256);
if (!copy || copy.bytes !== archive.bytes) fail('MISSING_INPUT', `Verified public archive is missing from the offline cache: ${archive.name}@${archive.version}`);
return { inputIndex: archive.inputIndex, name: archive.name, version: archive.version,
declaredIntegrity: archive.declaredIntegrity, requestedUrl: archive.requestedUrl, resolvedUrl: archive.resolvedUrl, hostPath: copy.path,
containerPath: `/archives/${archive.installPath}`, sha256: archive.sha256, bytes: archive.bytes };
});
return { mounts: mounts.sort((a, b) => a.containerPath.localeCompare(b.containerPath)), materializedRoot };
} catch (error) {
try { fs.rmSync(materializedRoot, { recursive: true, force: true }); } catch {}
throw error;
}
}
type TreeEntry = { path: string; kind: 'file' | 'symlink'; mode: number; bytes: number; sha256: string };
function boundedTreeNames(directory: string, deadline: number, signal?: AbortSignal): string[] {
checkDeadline(deadline, signal);
const handle = fs.opendirSync(directory), names: string[] = [];
try {
for (;;) {
checkDeadline(deadline, signal);
const entry = handle.readSync();
if (!entry) break;
if (names.length >= MAX_TREE_FILES) fail('INSUFFICIENT_CAPACITY', 'Preparation directory exceeds its bounded manifest limit');
names.push(entry.name);
}
} finally { handle.closeSync(); }
checkDeadline(deadline, signal);
names.sort();
checkDeadline(deadline, signal);
return names;
}
function treeManifest(rootPath: string, maxBytes: number, deadline: number, allowContainedSymlinks = false,
signal?: AbortSignal): TreeEntry[] {
checkDeadline(deadline, signal);
const root = resolve(rootPath);
let rootStat: fs.Stats, canonicalRoot: string;
try { rootStat = fs.lstatSync(root); canonicalRoot = fs.realpathSync(root); }
catch { fail('MISSING_INPUT', 'Preparation source or output directory is missing'); }
if (!rootStat!.isDirectory() || rootStat!.isSymbolicLink() || canonicalRoot! !== root ||
(process.getuid && rootStat!.uid !== process.getuid()) || (rootStat!.mode & 0o022) !== 0)
fail('UNSAFE_PATH', 'Preparation source or output must be one private real directory');
const entries: TreeEntry[] = [];
let total = 0, nodes = 0;
const walk = (directory: string, prefix = ''): void => {
checkDeadline(deadline, signal);
const before = boundedTreeNames(directory, deadline, signal);
for (const name of before) {
checkDeadline(deadline, signal);
const path = join(directory, name), relativePath = prefix ? `${prefix}/${name}` : name;
nodes++;
if (nodes > MAX_TREE_FILES || Buffer.byteLength(name) > 255 || Buffer.byteLength(relativePath) > 4096)
fail('INSUFFICIENT_CAPACITY', 'Preparation tree exceeds its bounded manifest limit');
const stat = fs.lstatSync(path);
if (process.getuid && stat.uid !== process.getuid())
fail('UNSAFE_PATH', `Preparation tree contains an unsafe object: ${relativePath}`);
if (stat.isSymbolicLink()) {
if (!allowContainedSymlinks) fail('UNSAFE_PATH', `Preparation tree contains an unsafe object: ${relativePath}`);
const target = fs.readlinkSync(path);
if (!target || isAbsolute(target) || target.includes('\0')) fail('UNSAFE_PATH', `Prepared dependency symlink is not relative: ${relativePath}`);
const lexicalTarget = resolve(dirname(path), target);
if (lexicalTarget !== root && !lexicalTarget.startsWith(`${root}${sep}`)) fail('UNSAFE_PATH', `Prepared dependency symlink escapes its execution copy: ${relativePath}`);
let resolvedTarget: string, targetStat: fs.Stats;
try { resolvedTarget = fs.realpathSync(path); targetStat = fs.statSync(path); }
catch { fail('UNSAFE_PATH', `Prepared dependency symlink is dangling or cyclic: ${relativePath}`); }
if (resolvedTarget! !== root && !resolvedTarget!.startsWith(`${root}${sep}`)) fail('UNSAFE_PATH', `Prepared dependency symlink escapes its execution copy: ${relativePath}`);
if (!targetStat!.isFile() && !targetStat!.isDirectory()) fail('UNSAFE_PATH', `Prepared dependency symlink resolves to a special object: ${relativePath}`);
const after = fs.lstatSync(path);
if (!after.isSymbolicLink() || after.dev !== stat.dev || after.ino !== stat.ino || after.mode !== stat.mode ||
after.mtimeMs !== stat.mtimeMs || after.ctimeMs !== stat.ctimeMs || fs.readlinkSync(path) !== target)
fail('SNAPSHOT_RACE', `Prepared dependency symlink changed while hashing: ${relativePath}`);
entries.push({ path: relativePath, kind: 'symlink', mode: stat.mode & 0o777, bytes: Buffer.byteLength(target), sha256: sha256(`symlink\0${target}`) });
continue;
}
if (!stat.isDirectory() && !stat.isFile()) fail('UNSAFE_PATH', `Preparation tree contains an unsafe object: ${relativePath}`);
if (stat.isDirectory()) { if ((stat.mode & 0o022) !== 0) fail('UNSAFE_PATH', 'Preparation tree contains a publicly writable directory'); walk(path, relativePath); continue; }
if (stat.nlink !== 1) fail('UNSAFE_PATH', `Preparation tree contains a hard-linked file: ${relativePath}`);
total += stat.size;
if (total > maxBytes) fail('INSUFFICIENT_CAPACITY', 'Preparation tree exceeds its bounded manifest limit');
const noFollow = (fs.constants as any).O_NOFOLLOW ?? 0;
let fd: number;
try { fd = fs.openSync(path, fs.constants.O_RDONLY | noFollow); } catch { fail('UNSAFE_PATH', 'Preparation file could not be opened without following links'); }
try {
const initial = fs.fstatSync(fd!), hash = createHash('sha256'), buffer = Buffer.allocUnsafe(64 * 1024);
let readBytes = 0;
for (;;) { checkDeadline(deadline, signal); const count = fs.readSync(fd!, buffer, 0, buffer.length, null); if (!count) break; readBytes += count; hash.update(buffer.subarray(0, count)); checkDeadline(deadline, signal); }
checkDeadline(deadline, signal);
const final = fs.fstatSync(fd!);
if (readBytes !== initial.size || initial.dev !== final.dev || initial.ino !== final.ino || initial.size !== final.size ||
initial.mode !== final.mode || initial.mtimeMs !== final.mtimeMs || initial.ctimeMs !== final.ctimeMs)
fail('SNAPSHOT_RACE', `Preparation file changed while hashing: ${relativePath}`);
entries.push({ path: relativePath, kind: 'file', mode: initial.mode & 0o777, bytes: initial.size, sha256: hash.digest('hex') });
} finally { fs.closeSync(fd!); }
}
checkDeadline(deadline, signal);
if (canonical(before) !== canonical(boundedTreeNames(directory, deadline, signal))) fail('SNAPSHOT_RACE', 'Preparation tree membership changed while hashing');
};
walk(root);
return entries;
}
function treeHash(rootPath: string, maxBytes: number, deadline: number, allowContainedSymlinks = false,
signal?: AbortSignal): string {
return sha256(canonical(treeManifest(rootPath, maxBytes, deadline, allowContainedSymlinks, signal)));
}
function dependencyOutput(stack: CsoStack, path: string): boolean {
const first = path.split('/')[0];
if (stack === 'node') return first === 'node_modules' || first === '.cso-npm-cache';
if (stack === 'bun') return first === 'node_modules' || first === '.cso-bun-cache';
if (stack === 'python') return first === '.venv' || first === '.cso-uv-cache' || path === '.gstack-cso-public-requirements.txt';
return path.startsWith('vendor/bundle/') || first === '.cso-bundle' || first === '.cso-gems';
}
function installedDependencyOutput(stack:CsoStack,path:string):boolean{
const first=path.split('/')[0];return stack==='node'||stack==='bun'?first==='node_modules':stack==='python'?first==='.venv':path.startsWith('vendor/bundle/');
}
function preparedEnvironment(plan: PreparationPlan): Record<string, string> {
if (plan.stack === 'python') return { PATH: '/work/.venv/bin:/usr/local/bin:/usr/bin:/bin', VIRTUAL_ENV: '/work/.venv', PYTHONNOUSERSITE: '1' };
if (plan.stack !== 'rails') return { PATH: '/usr/local/bin:/usr/bin:/bin' };
const dependencyKeys = new Set(['BUNDLE_PATH', 'BUNDLE_FROZEN', 'BUNDLE_DEPLOYMENT', 'BUNDLE_DISABLE_SHARED_GEMS',
'BUNDLE_IGNORE_CONFIG', 'BUNDLE_ALLOW_OFFLINE_INSTALL', 'BUNDLE_CACHE_PATH', 'BUNDLE_USER_HOME', 'GEM_HOME', 'GEM_PATH']);
const verifierOwnedKeys = new Set(['RAILS_ENV', 'RACK_ENV', 'SECRET_KEY_BASE']);
const environment: Record<string, string> = { PATH: '/usr/local/bin:/usr/bin:/bin' };
for (const command of plan.offline) for (const [key, value] of Object.entries(command.env)) {
if (verifierOwnedKeys.has(key)) continue;
if (!dependencyKeys.has(key)) fail('INVALID_SCHEMA', `Rails preparation attempted to forward unsupported execution environment key ${key}`);
if (environment[key] !== undefined && environment[key] !== value) fail('INVALID_SCHEMA', `Rails preparation commands disagree on ${key}`);
environment[key] = value;
}
return environment;
}
function provePreparedProjection(snapshot: string, preparedRoot: string, stack: CsoStack,
transformations: OfflinePreparationRequest['transformations'], deadline: number, signal?: AbortSignal): { hash: string; transformations: PreparedApplication['transformations']; manifestHash: string; dependencyHash:string } {
const source = treeManifest(snapshot, MAX_SOURCE_BYTES, deadline, false, signal), prepared = treeManifest(preparedRoot, MAX_PREPARED_BYTES, deadline, true, signal),
sourceByPath = new Map(source.map(entry => [entry.path, entry])), preparedByPath = new Map(prepared.map(entry => [entry.path, entry])),
synthetic = new Map(transformations.map(item => [item.path, item]));
if (synthetic.size !== transformations.length) fail('INVALID_SCHEMA', 'Offline preparation transformations contain duplicate paths');
const actualTransformations: PreparedApplication['transformations'] = [];
for (const entry of source) {
checkDeadline(deadline, signal);
const actual = preparedByPath.get(entry.path), transformed = synthetic.get(entry.path);
if (!actual || actual.kind !== 'file') fail('ISOLATION_FAILED', `Offline lifecycle execution removed or replaced captured source: ${entry.path}`);
if (transformed) {
if (actual.sha256 !== transformed.sha256) fail('ISOLATION_FAILED', `Offline lifecycle execution changed a synthetic test transformation: ${entry.path}`);
} else if (canonical(actual) !== canonical(entry)) fail('ISOLATION_FAILED', `Offline lifecycle execution changed captured source bytes or mode: ${entry.path}`);
}
for (const transformed of transformations) {
checkDeadline(deadline, signal);
const actual = preparedByPath.get(transformed.path);
if (!actual || actual.kind !== 'file' || actual.sha256 !== transformed.sha256)
fail('ISOLATION_FAILED', `Offline preparation did not preserve its declared transformation: ${transformed.path}`);
actualTransformations.push({ path: transformed.path, sha256: transformed.sha256, mode: actual.mode, reason: transformed.reason });
}
for (const entry of prepared) {
checkDeadline(deadline, signal);
if (sourceByPath.has(entry.path) || synthetic.has(entry.path) || dependencyOutput(stack, entry.path)) continue;
fail('ISOLATION_FAILED', `Offline lifecycle execution wrote outside its dependency roots: ${entry.path}`);
}
actualTransformations.sort((a, b) => a.path.localeCompare(b.path));
return { hash: sha256(canonical({ source, transformations: actualTransformations })), transformations: actualTransformations,
manifestHash: sha256(canonical(prepared)), dependencyHash:sha256(canonical(prepared.filter(entry=>installedDependencyOutput(stack,entry.path)))) };
}
function validateAcquisitionReceipt(receipt: AcquisitionReceipt, request: PreparationAcquireRequest, plan: PreparationPlan): void {
if (!receipt || receipt.schemaVersion !== 1 || receipt.planHash !== request.planHash || receipt.runtimeId !== request.runtime.id ||
receipt.runtimeImage !== request.runtime.image || receipt.platform !== request.runtime.platform || receipt.deadlineEnforced !== true ||
receipt.lifecycleScriptsExecuted !== false || receipt.targetCodeExecuted !== false)
fail('TOOL_FAILED', 'Acquisition receipt does not bind the admitted plan and runtime');
const network = receipt.network;
if (!network || network.mode !== 'registry-restricted' || !sameStrings(network.allowedHosts, plan.registryHosts) ||
!Array.isArray(network.contactedHosts) || network.contactedHosts.some(host => !plan.registryHosts.includes(host)) ||
new Set(network.contactedHosts).size !== network.contactedHosts.length || network.dnsRebindingBlocked !== true ||
network.credentialsMounted !== false || network.sourceMounted !== false || network.dockerSocketMounted !== false ||
network.redirectVisibility !== 'opaque-tls')
fail('ISOLATION_FAILED', 'Acquisition network receipt did not prove registry-restricted, credential-free execution');
validateCommandReceipts(receipt.commands, plan.acquisition);
}
function validateOfflineReceipt(receipt: OfflinePreparationReceipt, request: OfflinePreparationRequest): void {
if (!receipt || receipt.schemaVersion !== 1 || receipt.planHash !== request.planHash || receipt.runtimeId !== request.runtime.id ||
receipt.runtimeImage !== request.runtime.image || receipt.platform !== request.runtime.platform || receipt.sourceHash !== request.sourceHash ||
receipt.dependencyClosureHash !== request.dependencyClosureHash || receipt.configurationHash !== request.configurationHash ||
receipt.databaseHash !== request.databaseHash || request.databaseHash !== sha256(canonical(request.database ?? null)) ||
receipt.deadlineEnforced !== true || receipt.inputSourceReadOnly !== true || receipt.preparedCopySeparate !== true ||
receipt.archivesReadOnly !== true || receipt.applicationCodeExecutedOnlyOffline !== true)
fail('TOOL_FAILED', 'Offline preparation receipt does not bind its immutable inputs');
const expectedServices: Array<'application' | 'postgresql'> = ['application'];
const network = receipt.network;
if (!network || network.mode !== 'none' || !/^[a-z0-9][a-z0-9._-]{0,100}$/.test(network.namespaceAnchor) ||
network.externalEgress !== false || network.dnsAvailable !== false || network.publishedPorts !== false ||
!sameStrings(network.services, expectedServices)) fail('ISOLATION_FAILED', 'Offline preparation did not remain in the shared no-egress loopback namespace');
validateCommandReceipts(receipt.commands, request.commands);
}
function offlineReceiptIdentity(receipt: OfflinePreparationReceipt): string {
return sha256(canonical({ ...receipt, network: { ...receipt.network, namespaceAnchor: '<run-owned-network-namespace>' } }));
}
export class PreparationExecutor {
constructor(private readonly options: { cache: PublicArchiveCache; runner: PreparationSandboxRunner; materializationRoot?: string }) {
if (!options?.cache || !options?.runner) fail('INVALID_ARGUMENT', 'Preparation executor requires a cache and qualified runner');
}
async acquire(options: {
plan: PreparationPlan;
admission: PreparationRuntimeAdmission;
snapshot: string;
deadline: number;
signal?: AbortSignal;
offline?: boolean;
existingClosure?: DependencyClosure;
}): Promise<DependencyClosure> {
checkDeadline(options.deadline, options.signal);
const hash = validatePlan(options.plan, options.snapshot), runtime = runtimeFromAdmission(options.plan, options.admission);
checkDeadline(options.deadline, options.signal);
validateRunner(this.options.runner, options.plan.stack);
if (options.existingClosure) {
const verified = validateClosure(options.plan, options.admission, options.existingClosure, this.options.cache,
options.deadline, options.signal, this.options.materializationRoot);
if (verified.materializedRoot) fs.rmSync(verified.materializedRoot, { recursive: true, force: true });
return options.existingClosure;
}
const publicInputs = options.plan.inputs.map((input, index) => ({ input, index })).filter(item => item.input.kind === 'public');
if (publicInputs.length > MAX_ARCHIVES) fail('INSUFFICIENT_CAPACITY', 'Preparation plan exceeds the archive-count limit');
const publicInputsByIndex = new Map(publicInputs.map(item => [item.index, item]));
if (options.offline && publicInputs.length) fail('MISSING_INPUT', 'Offline preparation requires a matching retained dependency closure and verified cache entries');
if (!publicInputs.length) {
const body: Omit<DependencyClosure, 'closureHash'> = { schemaVersion: 1, stack: options.plan.stack, planHash: hash,
catalogRevision: options.admission.catalogRevision, runtimeId: runtime.id, runtimeImage: runtime.image, platform: runtime.platform,
archives: [], acquisitionReceiptHash: null };
return Object.freeze({ ...body, closureHash: closureIdentity(body) });
}
const acquisitionStaging = fs.mkdtempSync(join(this.options.cache.stagingRoot, 'acquire-'));
fs.chmodSync(acquisitionStaging, 0o700);
const stagingIdentity = fs.lstatSync(acquisitionStaging);
const request: PreparationAcquireRequest = {
schemaVersion: 1, planHash: hash, stack: options.plan.stack,
runtime: { id: runtime.id, image: runtime.image, platform: runtime.platform }, metadata: structuredClone(options.plan.metadata),
inputs: structuredClone(publicInputs), commands: structuredClone(options.plan.acquisition), stagingRoot: acquisitionStaging,
deadline: options.deadline, limits: { maxArchives: MAX_ARCHIVES,
maxArchiveBytes: Math.min(this.options.cache.maxEntryBytes, MAX_ACQUISITION_ARCHIVE_BYTES),
maxTotalArchiveBytes: Math.min(this.options.cache.maxBytes, MAX_ACQUISITION_ARCHIVE_BYTES), maxOutputBytes: MAX_OUTPUT },
network: { mode: 'registry-restricted', allowedHosts: [...options.plan.registryHosts] }, sourceMounted: false,
};
try {
const receipt = await this.options.runner.acquire(deepFreeze(request));
checkDeadline(options.deadline, options.signal);
validateAcquisitionReceipt(receipt, request, options.plan);
if (!Array.isArray(receipt.artifacts) || receipt.artifacts.length > request.limits.maxArchives)
fail('TOOL_FAILED', 'Acquisition receipt contains an invalid number of archives');
const archives: DependencyArchive[] = [], seenInputs = new Set<number>(), seenPaths = new Set<string>(), seenStagingPaths = new Set<string>(),
uniqueBytes = new Map<string, number>(), validated: Array<{ artifact: AcquisitionArtifactReceipt; selected: typeof publicInputs[number] }> = [];
let totalUniqueBytes = 0;
for (const artifact of receipt.artifacts) {
checkDeadline(options.deadline, options.signal);
const selected = publicInputsByIndex.get(artifact.inputIndex);
if (!selected || seenInputs.has(artifact.inputIndex) || !SHA256.test(artifact.sha256) || artifact.registryResponseSha256 !== artifact.sha256 ||
!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 0 || artifact.bytes > this.options.cache.maxEntryBytes ||
!options.plan.registryHosts.includes(artifact.requestedHost) || !receipt.network.contactedHosts.includes(artifact.requestedHost))
fail('TOOL_FAILED', 'Acquisition artifact is not a unique bounded public dependency');
seenInputs.add(artifact.inputIndex);
relativeArchivePath(artifact.stagingPath, 'Staged archive'); relativeArchivePath(artifact.installPath, 'Dependency install path');
if (seenPaths.has(artifact.installPath) || seenStagingPaths.has(artifact.stagingPath)) fail('TOOL_FAILED', 'Acquisition artifact paths collide');
seenPaths.add(artifact.installPath); seenStagingPaths.add(artifact.stagingPath);
validateUrl(artifact.requestedUrl, artifact.requestedHost, options.plan.registryHosts);
if (artifact.resolvedUrl !== null) {
let resolvedHost = ''; try { resolvedHost = new URL(artifact.resolvedUrl).hostname; } catch {}
validateUrl(artifact.resolvedUrl, resolvedHost, options.plan.registryHosts);
if (!receipt.network.contactedHosts.includes(resolvedHost)) fail('TOOL_FAILED', 'Direct archive response host was not contacted through the registry broker');
}
if (selected.input.url && artifact.requestedUrl !== selected.input.url) fail('TOOL_FAILED', 'Acquired archive request URL does not match its lockfile URL');
const hashes = stagedFileHashes(request.stagingRoot, artifact.stagingPath, artifact.bytes, options.deadline, options.signal);
if (hashes.sha256 !== artifact.sha256 || (selected.input.integritySource === 'lock' && !integrityMatches(selected.input.integrity, hashes)) ||
(selected.input.integritySource !== 'lock' && selected.input.integritySource !== 'registry-on-acquisition'))
fail('INCOMPATIBLE_INPUT', `Acquired archive failed lock/registry integrity verification: ${selected.input.name}@${selected.input.version}`);
const priorBytes = uniqueBytes.get(artifact.sha256);
if (priorBytes !== undefined && priorBytes !== artifact.bytes) fail('TOOL_FAILED', 'One acquisition digest was reported with inconsistent sizes');
if (priorBytes === undefined) { uniqueBytes.set(artifact.sha256, artifact.bytes); totalUniqueBytes += artifact.bytes; }
if (totalUniqueBytes > request.limits.maxTotalArchiveBytes)
fail('INSUFFICIENT_CAPACITY', 'Acquisition archives exceed the immutable-cache byte ceiling');
validated.push({ artifact, selected });
}
// No cache mutation occurs until the complete receipt, every staged byte,
// and the aggregate unique-byte ceiling have been validated.
try {
for (const { artifact, selected } of validated) {
checkDeadline(options.deadline, options.signal);
const absoluteStaged = resolve(request.stagingRoot, ...artifact.stagingPath.split('/'));
const cacheRelative = relative(this.options.cache.stagingRoot, absoluteStaged).split(sep).join('/');
const cached = this.options.cache.promote(cacheRelative, artifact.sha256, { deadline: options.deadline, signal: options.signal });
archives.push({ inputIndex: artifact.inputIndex, name: selected.input.name, version: selected.input.version,
installPath: artifact.installPath, sha256: cached.sha256, bytes: cached.bytes, requestedHost: artifact.requestedHost,
requestedUrl: artifact.requestedUrl, resolvedUrl: artifact.resolvedUrl,
declaredIntegrity: selected.input.integrity ?? 'registry-on-acquisition' });
}
} finally {
for (const { artifact } of validated) if (artifact.stagingPath.startsWith('cso-public/')) {
const path = resolve(request.stagingRoot, ...artifact.stagingPath.split('/'));
try { const stat = fs.lstatSync(path); if (stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1 && (process.getuid ? stat.uid === process.getuid() : true)) fs.unlinkSync(path); } catch {}
}
}
const covered = new Set(archives.map(archive => logicalInput(options.plan.inputs[archive.inputIndex])));
for (const { input } of publicInputs) {
checkDeadline(options.deadline, options.signal);
if (!covered.has(logicalInput(input))) fail('MISSING_INPUT', `Acquisition did not produce a compatible archive for ${input.name}@${input.version}`);
}
checkDeadline(options.deadline, options.signal);
archives.sort((a, b) => a.installPath.localeCompare(b.installPath));
const body: Omit<DependencyClosure, 'closureHash'> = { schemaVersion: 1, stack: options.plan.stack, planHash: hash,
catalogRevision: options.admission.catalogRevision, runtimeId: runtime.id, runtimeImage: runtime.image, platform: runtime.platform,
archives, acquisitionReceiptHash: sha256(canonical(receipt)) };
return Object.freeze({ ...body, closureHash: closureIdentity(body) });
} finally {
let current: fs.Stats | undefined;
try { current = fs.lstatSync(acquisitionStaging); } catch {}
if (current && (!current.isDirectory() || current.isSymbolicLink() || current.dev !== stagingIdentity.dev || current.ino !== stagingIdentity.ino))
fail('SNAPSHOT_RACE', 'Run-private acquisition staging was replaced before cleanup');
if (current) fs.rmSync(acquisitionStaging, { recursive: true, force: false });
}
}
async prepareOffline(options: {
plan: PreparationPlan;
admission: PreparationRuntimeAdmission;
snapshot: string;
closure: DependencyClosure;
deadline: number;
signal?: AbortSignal;
database?: RailsDatabaseSelection;
}): Promise<PreparedApplication> {
checkDeadline(options.deadline, options.signal);
const hash = validatePlan(options.plan, options.snapshot), runtime = runtimeFromAdmission(options.plan, options.admission);
checkDeadline(options.deadline, options.signal);
validateRunner(this.options.runner, options.plan.stack);
const materialized = validateClosure(options.plan, options.admission, options.closure, this.options.cache,
options.deadline, options.signal, this.options.materializationRoot),
archives = materialized.mounts;
let database: OfflinePreparationRequest['database'];
let synthetic: Array<{ path: string; content: string }> = [];
if (options.plan.stack === 'rails') {
if (!options.database) fail('INVALID_ARGUMENT', 'Rails offline preparation requires an explicit SQLite or PostgreSQL selection');
if (!options.plan.database?.supported.includes(options.database.adapter)) fail('PREREQUISITE', `Rails ${options.database.adapter} preparation is not supported by this plan`);
synthetic = railsTestConfiguration(options.plan.database.connections, options.database.adapter);
if (options.database.adapter === 'postgresql') {
const sidecar = options.database.sidecar;
if (!sidecar || !admittedRuntimes.has(sidecar) || sidecar.runtime.stack !== 'postgresql' ||
sidecar.runtime.platform !== runtime.platform || sidecar.catalogRevision !== options.admission.catalogRevision)
fail('PREREQUISITE', 'Rails PostgreSQL preparation requires a qualified same-platform sidecar from the same catalog');
database = { adapter: 'postgresql', connections: [...options.plan.database.connections],
sidecar: { id: sidecar.runtime.id, image: sidecar.runtime.image } };
} else database = { adapter: 'sqlite', connections: [...options.plan.database.connections] };
} else if (options.database) fail('INVALID_ARGUMENT', 'Database preparation is only valid for Rails');
const transformations = synthetic.map(file => ({ ...file, sha256: sha256(file.content), reason: 'Synthetic isolated Rails test configuration' }));
const configurationHash = sha256(canonical(transformations)), databaseHash = sha256(canonical(database ?? null));
const sourceHash = treeHash(options.snapshot, MAX_SOURCE_BYTES, options.deadline, false, options.signal);
const request: OfflinePreparationRequest = {
schemaVersion: 1, planHash: hash, stack: options.plan.stack,
runtime: { id: runtime.id, image: runtime.image, platform: runtime.platform }, sourceRoot: resolve(options.snapshot), sourceHash,
metadata: structuredClone(options.plan.metadata), dependencyClosureHash: options.closure.closureHash,
commands: structuredClone(options.plan.offline), archives,
transformations, configurationHash, database, databaseHash, deadline: options.deadline,
limits: { cpus: 2, memoryBytes: 4 * 1024 * 1024 * 1024, pids: 256, writableBytes: MAX_PREPARED_BYTES, maxOutputBytes: MAX_OUTPUT },
network: { mode: 'none', sharedLoopbackNamespace: true, publishedPorts: false }, inputSourceReadOnly: true, archivesReadOnly: true,
};
let returnedPreparedRoot: string | undefined, completed = false;
try {
const result = await this.options.runner.prepareOffline(deepFreeze(request));
returnedPreparedRoot = typeof result?.preparedRoot === 'string' ? result.preparedRoot : undefined;
checkDeadline(options.deadline, options.signal);
validateOfflineReceipt(result?.receipt, request);
const preparedRoot = resolve(result.preparedRoot);
const sourceRoot = resolve(options.snapshot);
if (preparedRoot === sourceRoot || preparedRoot.startsWith(`${sourceRoot}${sep}`) || sourceRoot.startsWith(`${preparedRoot}${sep}`) ||
preparedRoot === this.options.cache.root || preparedRoot.startsWith(`${this.options.cache.root}${sep}`))
fail('UNSAFE_PATH', 'Prepared application must be a separate disposable copy outside cache and source roots');
const projection = provePreparedProjection(options.snapshot, preparedRoot, options.plan.stack, request.transformations, options.deadline, options.signal),
preparedManifestHash = projection.manifestHash,preparedDependencyHash=projection.dependencyHash;
if (treeHash(options.snapshot, MAX_SOURCE_BYTES, options.deadline, false, options.signal) !== sourceHash)
fail('SNAPSHOT_RACE', 'Offline preparation changed its read-only input source');
completed = true;
return Object.freeze({ schemaVersion: 1 as const, stack: options.plan.stack, preparedRoot, sourceHash, preparedManifestHash,preparedDependencyHash,
dependencyClosureHash: options.closure.closureHash, configurationHash, databaseHash, database,
sourceProjectionHash: projection.hash,
transformations: projection.transformations, executionEnvironment: Object.freeze(preparedEnvironment(options.plan)),
receiptHash: offlineReceiptIdentity(result.receipt), receipt: result.receipt });
} catch (error) {
if (!completed && returnedPreparedRoot) {
try { await this.options.runner.disposePrepared(returnedPreparedRoot); }
catch { fail('PERSISTENCE_FAILED', 'Invalid prepared execution copy could not be disposed safely'); }
}
throw error;
} finally {
if (materialized.materializedRoot) fs.rmSync(materialized.materializedRoot, { recursive: true, force: true });
}
}
async dispose(prepared: PreparedApplication): Promise<void> {
if (!prepared || prepared.schemaVersion !== 1 || typeof prepared.preparedRoot !== 'string')
fail('INVALID_ARGUMENT', 'Prepared application handle is invalid');
await this.options.runner.disposePrepared(prepared.preparedRoot);
}
}
+512
View File
@@ -0,0 +1,512 @@
/** Inert dependency inspection. Nothing in this module invokes a package manager.
* Commands are descriptions for the constrained runner, never host commands.
*/
import { createHash } from 'node:crypto';
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
import { isAbsolute, relative, resolve, sep } from 'node:path';
export type CsoStack = 'node' | 'bun' | 'python' | 'rails';
export interface PreparationPrerequisite { code: string; message: string; path?: string }
export interface PreparationCommand {
executable: string;
args: string[];
cwd: '/metadata' | '/work' | '/archives';
env: Record<string, string>;
}
export interface PreparationInput {
kind: 'public' | 'local';
name: string;
version: string;
url?: string;
path?: string;
integrity?: string;
integritySource?: 'lock' | 'registry-on-acquisition';
platform?: string;
}
export interface PreparationPlan {
schemaVersion: 1;
stack: CsoStack;
/** Ready means metadata is admissible; runtime and acquisition admission are separate. */
status: 'ready' | 'prerequisites';
prerequisites: PreparationPrerequisite[];
metadata: Array<{ path: string; content: string; sha256: string }>;
inputs: PreparationInput[];
acquisition: PreparationCommand[];
offline: PreparationCommand[];
registryHosts: string[];
runtimeProfile: string;
runtimeRequirements: Record<string, string>;
transformations: Array<{ path: string; reason: string; phase: 'acquisition' | 'execution' }>;
database?: { supported: Array<'sqlite' | 'postgresql'>; selected: 'sqlite' | 'postgresql' | null;
connections: string[]; requiresSyntheticConfiguration: true };
}
const MAX_METADATA = 8 * 1024 * 1024;
const MAX_PACKAGES = 25_000;
const MANIFEST_FIELDS = ['name', 'version', 'private', 'dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies', 'peerDependenciesMeta', 'engines', 'os', 'cpu', 'workspaces', 'overrides'] as const;
const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] as const;
const NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
const VERSION = /^[0-9][0-9a-zA-Z.+_-]*$/;
const SRI = /^(?:sha512-[A-Za-z0-9+/]{86}==|sha256-[A-Za-z0-9+/]{43}=)$/;
const connectionName = (value: string) => Buffer.byteLength(value) <= 48 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value) &&
!['__proto__', 'prototype', 'constructor'].includes(value);
const sha256 = (content: string) => createHash('sha256').update(content).digest('hex');
const record = (value: unknown): value is Record<string, any> => !!value && typeof value === 'object' && !Array.isArray(value);
class MetadataError extends Error {
constructor(readonly code: string, message: string, readonly path?: string) { super(message); }
}
function fail(code: string, message: string, path?: string): never { throw new MetadataError(code, message, path); }
function contained(root: string, path: string): string {
if (!path || isAbsolute(path) || path.includes('\\') || path.includes('\0')) fail('EXTERNAL_PATH', 'Dependency paths must stay within the captured source.', path);
const full = resolve(root, path);
const rel = relative(root, full);
if (rel.startsWith(`..${sep}`) || rel === '..' || isAbsolute(rel)) fail('EXTERNAL_PATH', 'Dependency path escapes the captured source.', path);
return full;
}
function read(root: string, path: string, optional = false): string | undefined {
const full = contained(root, path);
let stat;
try { stat = lstatSync(full); } catch (error: any) {
if (optional && error.code === 'ENOENT') return undefined;
fail('MISSING_METADATA', 'Required dependency metadata is missing or unreadable.', path);
}
const actual = realpathSync(full);
if (stat!.isSymbolicLink() || actual !== full || !stat!.isFile()) fail('UNSAFE_METADATA', 'Dependency metadata must be a regular file without symlink ancestors.', path);
if (stat!.size > MAX_METADATA) fail('METADATA_LIMIT', 'Dependency metadata exceeds the 8 MiB inspection limit.', path);
return readFileSync(full, 'utf8');
}
function json(root: string, path: string, jsonc = false): Record<string, any> {
try {
const parsed = jsonc ? Bun.JSONC.parse(read(root, path)!) : JSON.parse(read(root, path)!);
if (!record(parsed)) fail('INVALID_METADATA', 'Expected a metadata object.', path);
return parsed;
} catch (error) {
if (error instanceof MetadataError) throw error;
fail('INVALID_METADATA', 'Dependency metadata is not valid JSON.', path);
}
}
function publicUrl(value: unknown, hosts: string[], path?: string): string {
if (typeof value !== 'string') fail('UNPINNED_ARCHIVE', 'A public registry archive URL is required.', path);
let url: URL;
try { url = new URL(value); } catch { fail('UNSUPPORTED_SOURCE', 'Dependency URL is invalid.', path); }
if (url!.protocol !== 'https:' || url!.username || url!.password || url!.port || url!.hash || url!.search || !hosts.includes(url!.hostname)) {
fail('UNSUPPORTED_SOURCE', 'Only credential-free HTTPS URLs on the declared public registry are supported.', path);
}
return url!.href;
}
function integrity(value: unknown, path: string): string {
if (typeof value !== 'string' || !SRI.test(value)) fail('UNPINNED_ARCHIVE', 'A SHA-256 or SHA-512 archive integrity value is required.', path);
return value;
}
function packageCount(entries: unknown[], path: string) {
if (entries.length > MAX_PACKAGES) fail('METADATA_LIMIT', 'The lock exceeds the 25,000-package inspection limit.', path);
}
function metadata(plan: PreparationPlan, path: string, value: string | object, reason?: string) {
const content = typeof value === 'string' ? value : JSON.stringify(value, null, 2) + '\n';
plan.metadata.push({ path, content, sha256: sha256(content) });
if (reason) plan.transformations.push({ path, reason, phase: 'acquisition' });
}
function command(executable: string, args: string[], cwd: PreparationCommand['cwd'], env: Record<string, string> = {}): PreparationCommand {
return { executable, args, cwd, env };
}
function dependencySpecs(value: unknown, path: string) {
if (value === undefined) return;
if (!record(value)) fail('INVALID_METADATA', 'Dependency maps must be objects.', path);
for (const [name, spec] of Object.entries(value)) {
if (!NAME.test(name) || typeof spec !== 'string' || spec.length > 256 || /[\r\n\0]/.test(spec)) fail('INVALID_METADATA', 'Invalid dependency name or version constraint.', path);
if (/^(?:file:|link:|workspace:)/.test(spec)) {
if (spec.startsWith('workspace:')) continue; // The lock must independently identify a contained workspace.
const local = spec.replace(/^(file:|link:)/, '');
if (isAbsolute(local) || local.split(/[\\/]/).includes('..')) fail('EXTERNAL_PATH', 'Local dependency escapes the snapshot.', path);
} else if (/[/:@]/.test(spec) && !/^npm:(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+@[~^*<>=| 0-9a-z.+_-]+$/i.test(spec)) {
fail('UNSUPPORTED_SOURCE', 'Private, VCS, and arbitrary URL dependencies require explicit provisioning.', path);
}
}
}
function rejectConflictingLogicalArchives(inputs: PreparationInput[], path: string): void {
const identities = new Map<string, string>();
for (const input of inputs) {
if (input.kind !== 'public') continue;
const key = `${input.name.toLowerCase()}\0${input.version}`;
const archive = JSON.stringify({ url: input.url ?? null, integrity: input.integrity ?? null,
integritySource: input.integritySource ?? null, platform: input.platform ?? null });
const prior = identities.get(key);
if (prior !== undefined && prior !== archive)
fail('CONFLICTING_LOCK_IDENTITY', `Multiple locked archives disagree for ${input.name}@${input.version}.`, path);
identities.set(key, archive);
}
}
function manifest(root: string, path: string, plan: PreparationPlan): Record<string, any> {
const source = json(root, path);
for (const field of DEPENDENCY_FIELDS) dependencySpecs(source[field], path);
if (source.patchedDependencies) fail('UNSUPPORTED_PATCHES', 'Package-manager patches require an explicitly qualified offline preparation profile.', path);
if (source.overrides && JSON.stringify(source.overrides).match(/(?:https?:|git[+:]|file:|link:)/)) fail('UNSUPPORTED_SOURCE', 'Dependency overrides must resolve to public version constraints.', path);
if (source.workspaces !== undefined) {
const workspaces = Array.isArray(source.workspaces) ? source.workspaces : source.workspaces?.packages;
if (!Array.isArray(workspaces) || workspaces.some((item: unknown) => typeof item !== 'string' || !/^[A-Za-z0-9_.*\/-]+$/.test(item) || item.startsWith('/') || item.split('/').includes('..'))) fail('EXTERNAL_PATH', 'Workspace patterns must remain inside captured source.', path);
}
const clean: Record<string, any> = {};
for (const field of MANIFEST_FIELDS) if (source[field] !== undefined) clean[field] = source[field];
metadata(plan, path, clean, 'Acquisition manifest omits scripts, package-manager plugins, and target runtime configuration.');
if (record(source.engines)) for (const key of ['node', 'bun']) if (typeof source.engines[key] === 'string') plan.runtimeRequirements[key] = source.engines[key];
if (typeof source.packageManager === 'string') plan.runtimeRequirements.packageManager = source.packageManager;
return clean;
}
function inspectNode(root: string, plan: PreparationPlan) {
const lockPath = read(root, 'npm-shrinkwrap.json', true) !== undefined ? 'npm-shrinkwrap.json' : 'package-lock.json';
const lock = json(root, lockPath);
if (![2, 3].includes(lock.lockfileVersion) || !record(lock.packages)) fail('UNSUPPORTED_LOCK', 'Node preparation requires npm lock/shrinkwrap version 2 or 3.', lockPath);
manifest(root, 'package.json', plan);
const entries = Object.entries(lock.packages); packageCount(entries, lockPath);
const clean = structuredClone(lock);
// Version-2's redundant dependency tree is accepted only when every resolved URL is safe.
function validateTree(tree: unknown) {
if (!record(tree)) return;
for (const dep of Object.values(tree)) {
if (!record(dep)) fail('INVALID_METADATA', 'Invalid npm dependency record.', lockPath);
if (dep.resolved) publicUrl(dep.resolved, ['registry.npmjs.org'], lockPath);
if (dep.integrity) integrity(dep.integrity, lockPath);
validateTree(dep.dependencies);
}
}
validateTree(lock.dependencies);
for (const [path, raw] of entries) {
if (!record(raw)) fail('INVALID_METADATA', 'Invalid locked npm package.', lockPath);
if (path) contained(root, path);
if (!path || !path.split('/').includes('node_modules')) {
if (path) manifest(root, `${path}/package.json`, plan);
for (const field of DEPENDENCY_FIELDS) dependencySpecs(raw[field], lockPath);
delete clean.packages[path].scripts;
continue;
}
if (raw.link === true) {
const local = String(raw.resolved ?? ''); contained(root, local);
if (!record(lock.packages[local])) fail('MISSING_LOCAL_PACKAGE', 'Workspace link has no captured lock entry.', lockPath);
plan.inputs.push({ kind: 'local', name: local, version: String(lock.packages[local].version ?? '0'), path: local });
continue;
}
const name = typeof raw.name === 'string' ? raw.name : path.split('node_modules/').at(-1)!;
if (!NAME.test(name) || typeof raw.version !== 'string' || !VERSION.test(raw.version)) fail('INVALID_METADATA', 'Locked npm package needs an exact name and version.', lockPath);
for (const field of DEPENDENCY_FIELDS) dependencySpecs(raw[field], lockPath);
plan.inputs.push({ kind: 'public', name, version: raw.version, url: publicUrl(raw.resolved, ['registry.npmjs.org'], lockPath), integrity: integrity(raw.integrity, lockPath), integritySource: 'lock' });
delete clean.packages[path].scripts;
}
rejectConflictingLogicalArchives(plan.inputs, lockPath);
metadata(plan, lockPath, clean, 'Acquisition lock retains resolution and integrity data but omits executable script fields.');
const acquisition = ['ci', '--ignore-scripts', '--no-audit', '--no-fund', '--cache', '/archives/npm', '--userconfig', '/opt/cso/empty-config', '--globalconfig', '/opt/cso/empty-config'];
const offline = ['ci', '--ignore-scripts', '--no-audit', '--no-fund', '--cache', '/work/.cso-npm-cache', '--userconfig', '/opt/cso/empty-config', '--globalconfig', '/opt/cso/empty-config'];
plan.acquisition.push(command('/usr/local/bin/npm', [...acquisition, '--registry', 'https://registry.npmjs.org'], '/metadata', { NPM_CONFIG_UPDATE_NOTIFIER: 'false' }));
plan.offline.push(command('/usr/local/bin/npm', [...offline, '--offline'], '/work'));
plan.offline.push(command('/usr/local/bin/npm', ['rebuild', '--offline', '--no-audit', '--no-fund', '--cache', '/work/.cso-npm-cache', '--userconfig', '/opt/cso/empty-config', '--globalconfig', '/opt/cso/empty-config'], '/work'));
plan.registryHosts = ['registry.npmjs.org'];
}
function inspectBun(root: string, plan: PreparationPlan) {
if (read(root, 'bun.lock', true) === undefined) fail('UNSUPPORTED_LOCK', 'Bun preparation requires the text bun.lock format; bun.lockb is not supported.', 'bun.lock');
const lock = json(root, 'bun.lock', true);
if (lock.lockfileVersion !== 1 || !record(lock.packages) || !record(lock.workspaces)) fail('UNSUPPORTED_LOCK', 'Unsupported Bun text-lock schema.', 'bun.lock');
if (lock.patchedDependencies) fail('UNSUPPORTED_PATCHES', 'Bun patched dependencies require an explicitly qualified offline profile.', 'bun.lock');
const workspacePaths = Object.keys(lock.workspaces); packageCount(workspacePaths, 'bun.lock');
for (const path of workspacePaths) {
if (path) contained(root, path);
manifest(root, path ? `${path}/package.json` : 'package.json', plan);
for (const field of DEPENDENCY_FIELDS) dependencySpecs(lock.workspaces[path][field], 'bun.lock');
}
const entries = Object.entries(lock.packages); packageCount(entries, 'bun.lock');
for (const [key, raw] of entries) {
if (!Array.isArray(raw) || typeof raw[0] !== 'string') fail('INVALID_METADATA', 'Invalid Bun package tuple.', 'bun.lock');
const split = raw[0].lastIndexOf('@');
const name = raw[0].slice(0, split), version = raw[0].slice(split + 1);
if (version.startsWith('workspace:')) {
const path = version.slice(10); contained(root, path);
if (!workspacePaths.includes(path)) fail('MISSING_LOCAL_PACKAGE', 'Bun workspace is not captured in the lock.', 'bun.lock');
plan.inputs.push({ kind: 'local', name, version, path }); continue;
}
if (!NAME.test(name) || !VERSION.test(version)) fail('UNSUPPORTED_SOURCE', 'Bun package must resolve to an exact public registry version.', 'bun.lock');
const archiveUrl = raw[1] || `https://registry.npmjs.org/${name}/-/${name.split('/').at(-1)}-${version}.tgz`;
publicUrl(archiveUrl, ['registry.npmjs.org'], 'bun.lock');
if (!record(raw[2])) fail('INVALID_METADATA', 'Bun package metadata must be an object.', 'bun.lock');
for (const field of DEPENDENCY_FIELDS) dependencySpecs(raw[2][field], 'bun.lock');
plan.inputs.push({ kind: 'public', name, version, url: archiveUrl, integrity: integrity(raw[3], 'bun.lock'), integritySource: 'lock' });
}
rejectConflictingLogicalArchives(plan.inputs, 'bun.lock');
metadata(plan, 'bun.lock', lock);
const acquisitionEnv = { BUN_INSTALL_CACHE_DIR: '/metadata/.cso-bun-cache', BUN_CONFIG_NO_CLEAR_TERMINAL: '1', BUN_FEATURE_FLAG_DISABLE_NATIVE_DEPENDENCY_LINKER: '1' };
const offlineEnv = { ...acquisitionEnv, BUN_INSTALL_CACHE_DIR: '/work/.cso-bun-cache' };
plan.acquisition.push(command('/usr/local/bin/bun', ['install', '--config=/opt/cso/empty-config', '--frozen-lockfile', '--ignore-scripts', '--no-progress', '--backend=hardlink', '--registry=https://registry.npmjs.org'], '/metadata', acquisitionEnv));
// The runner enforces network-none; Bun's --offline availability is version-specific.
plan.offline.push(command('/usr/local/bin/bun', ['install', '--config=/opt/cso/empty-config', '--frozen-lockfile', '--no-progress', '--backend=copyfile'], '/work', offlineEnv));
plan.registryHosts = ['registry.npmjs.org'];
}
function requirementLines(text: string, plan: PreparationPlan, path: string) {
const lines = text.replace(/\\\r?\n/g, ' ').split(/\r?\n/);
for (const raw of lines) {
const line = raw.replace(/\s+#.*$/, '').trim();
if (!line || line.startsWith('#')) continue;
const hashes = [...line.matchAll(/(?:^|\s)--hash=sha256:([a-f0-9]{64})(?=\s|$)/gi)];
const requirement = line.replace(/(?:^|\s)--hash=sha256:[a-f0-9]{64}(?=\s|$)/gi, '').trim();
const match = requirement.match(/^([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[[A-Za-z0-9_,.-]+\])?==([A-Za-z0-9][A-Za-z0-9.!+_-]*)(?:\s*;\s*([A-Za-z0-9_.'" ()<>=!~+,-]+))?$/);
if (!match || !hashes.length || requirement.includes('--')) fail('UNPINNED_REQUIREMENTS', 'Requirements must contain only exact public package pins with SHA-256 hashes; includes, URLs, editable paths, and index options are unsupported.', path);
if (match[3]) fail('UNSUPPORTED_MARKER', 'PEP 508 environment markers require a qualified runtime-specific lock export.', path);
plan.inputs.push({ kind: 'public', name: match[1], version: match[2], integrity: hashes.map(h => `sha256:${h[1].toLowerCase()}`).join(' '), integritySource: 'lock' });
}
packageCount(plan.inputs, path);
}
function toml(root: string, path: string): Record<string, any> {
try {
const result = Bun.TOML.parse(read(root, path)!);
if (!record(result)) fail('INVALID_METADATA', 'Expected a TOML object.', path);
return result;
} catch (error) {
if (error instanceof MetadataError) throw error;
fail('INVALID_METADATA', 'Dependency metadata is not valid TOML.', path);
}
}
function hasUvEnvironmentMarker(value:unknown,seen=new WeakSet<object>()):boolean{
if(value===null||typeof value!=='object')return false;
if(seen.has(value as object))return true;
seen.add(value as object);
if(Array.isArray(value))return value.some(item=>hasUvEnvironmentMarker(item,seen));
for(const [key,item] of Object.entries(value as Record<string,unknown>)){
if(key==='marker'||key==='resolution-markers'||key==='fork-markers')return true;
if(hasUvEnvironmentMarker(item,seen))return true;
}
return false;
}
function inspectPython(root: string, plan: PreparationPlan) {
const hasUv = read(root, 'uv.lock', true) !== undefined;
const acquisitionEnv = { UV_NO_CONFIG: '1', UV_PYTHON_DOWNLOADS: 'never', UV_NO_MANAGED_PYTHON: '1', UV_CACHE_DIR: '/archives/uv', PIP_CONFIG_FILE: '/dev/null', PIP_DISABLE_PIP_VERSION_CHECK: '1', PIP_NO_CACHE_DIR: '1' };
const offlineEnv = { ...acquisitionEnv, UV_CACHE_DIR: '/work/.cso-uv-cache', UV_LINK_MODE: 'copy' };
let requirementsPath = 'requirements.txt';
let publicRequirements = '/metadata/requirements.txt';
const localBuildPaths: string[] = [];
const buildRequirements = new Set<string>();
if (hasUv) {
const lock = toml(root, 'uv.lock');
if (lock.version !== 1 || !Array.isArray(lock.package)) fail('UNSUPPORTED_LOCK', 'Unsupported uv.lock schema.', 'uv.lock');
if(hasUvEnvironmentMarker(lock))fail('UNSUPPORTED_MARKER', 'Universal uv locks with environment or resolution markers require a qualified runtime-specific export before automatic preparation.', 'uv.lock');
packageCount(lock.package, 'uv.lock');
for (const pkg of lock.package) {
if (!record(pkg) || !record(pkg.source) || !NAME.test(pkg.name) || !VERSION.test(pkg.version)) fail('INVALID_METADATA', 'Invalid uv locked package.', 'uv.lock');
if (pkg.source.registry !== undefined) {
if (Object.keys(pkg.source).length !== 1) fail('UNSUPPORTED_SOURCE', 'Python registry entries cannot contain alternate dependency sources.', 'uv.lock');
const registry = publicUrl(pkg.source.registry, ['pypi.org'], 'uv.lock');
if (!['https://pypi.org/simple', 'https://pypi.org/simple/'].includes(registry)) fail('UNSUPPORTED_SOURCE', 'Python acquisition supports the public PyPI simple index only.', 'uv.lock');
if (!Array.isArray(pkg.wheels) || !pkg.wheels.length) fail('MISSING_PUBLIC_WHEEL', 'A matching public wheel is required; source distributions are never built during acquisition.', 'uv.lock');
for (const wheel of pkg.wheels) {
if (!record(wheel) || typeof wheel.hash !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(wheel.hash)) fail('UNPINNED_ARCHIVE', 'uv wheels require SHA-256 hashes.', 'uv.lock');
plan.inputs.push({ kind: 'public', name: pkg.name, version: pkg.version, url: publicUrl(wheel.url, ['files.pythonhosted.org'], 'uv.lock'), integrity: wheel.hash, integritySource: 'lock' });
}
if (pkg.sdist) {
publicUrl(pkg.sdist.url, ['files.pythonhosted.org'], 'uv.lock');
if (!/^sha256:[a-f0-9]{64}$/.test(pkg.sdist.hash ?? '')) fail('UNPINNED_ARCHIVE', 'uv source archive hash is invalid.', 'uv.lock');
}
} else {
const local = pkg.source.editable ?? pkg.source.virtual ?? pkg.source.directory;
if (typeof local !== 'string' || Object.keys(pkg.source).some(k => !['editable', 'virtual', 'directory'].includes(k))) fail('UNSUPPORTED_SOURCE', 'Private, VCS, and direct-URL Python sources require explicit provisioning.', 'uv.lock');
contained(root, local);
plan.inputs.push({ kind: 'local', name: pkg.name, version: pkg.version, path: local });
if (pkg.source.virtual === undefined) {
localBuildPaths.push(local);
const pyprojectPath = local === '.' ? 'pyproject.toml' : `${local}/pyproject.toml`;
const localProject = read(root, pyprojectPath, true) === undefined ? {} : toml(root, pyprojectPath);
const build = localProject['build-system'];
const requirements = build?.requires ?? ['setuptools>=40.8.0'];
if (!Array.isArray(requirements)) fail('DYNAMIC_BUILD_DEPENDENCIES', 'Local build dependencies must be declared as static package requirements.', pyprojectPath);
for (const requirement of requirements) {
if (typeof requirement !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*(?:\[[A-Za-z0-9_,.-]+\])?\s*[A-Za-z0-9.!~<>=+*, -]*$/.test(requirement)) fail('UNSUPPORTED_BUILD_DEPENDENCY', 'Local build dependencies must be public package requirements without URLs or paths.', pyprojectPath);
buildRequirements.add(requirement.match(/^[A-Za-z0-9][A-Za-z0-9._-]*/)![0].toLowerCase().replace(/[_.]+/g, '-'));
}
}
}
}
const project = toml(root, 'pyproject.toml');
if (!record(project.project)) fail('UNSUPPORTED_METADATA', 'uv export requires static PEP 621 project metadata.', 'pyproject.toml');
if (project.tool?.uv?.workspace !== undefined || plan.inputs.some(input => input.kind === 'local' && input.path !== '.'))
fail('UNSUPPORTED_WORKSPACE', 'uv workspace/member metadata is not yet qualified for sanitized automatic preparation.', 'pyproject.toml');
if (project.project.dynamic?.includes('dependencies')) fail('DYNAMIC_METADATA', 'Dynamic project dependencies require qualified offline build dependencies.', 'pyproject.toml');
// Only the export process sees this metadata; no build-system or tool.uv sources/config.
const clean: Record<string, any> = { project: project.project };
if (project['dependency-groups']) clean['dependency-groups'] = project['dependency-groups'];
// uv accepts a static pyproject file. Keep original syntax only after excluding every
// non-project table would require a TOML writer; use JSON-compatible TOML literals.
metadata(plan, 'pyproject.toml', toToml(clean), 'Acquisition pyproject omits all build backends and tool configuration; local packages are excluded by --no-emit-local.');
metadata(plan, 'uv.lock', read(root, 'uv.lock')!);
plan.runtimeRequirements.python = String(lock['requires-python'] ?? project.project['requires-python'] ?? '');
requirementsPath = 'cso-public-requirements.txt';
publicRequirements = `/archives/${requirementsPath}`;
plan.acquisition.push(command('/usr/local/bin/uv', ['export', '--frozen', '--no-emit-local', '--all-packages', '--all-extras', '--all-groups', '--no-config', '--format', 'requirements-txt', '--output-file', publicRequirements], '/metadata', acquisitionEnv));
plan.offline.push(command('/usr/local/bin/uv', ['export', '--offline', '--frozen', '--no-emit-local', '--all-packages', '--all-extras', '--all-groups', '--no-config', '--format', 'requirements-txt', '--output-file', '/work/.gstack-cso-public-requirements.txt'], '/metadata', offlineEnv));
plan.offline.push(command('/usr/local/bin/python', ['-I', '-m', 'venv', '--copies', '/work/.venv'], '/work', offlineEnv));
plan.offline.push(command('/usr/local/bin/uv', ['pip', 'install', '--offline', '--no-config', '--python', '/work/.venv/bin/python', '--link-mode', 'copy', '--no-index', '--find-links', '/archives/wheels', '--require-hashes', '--only-binary', ':all:', '--requirement', '/work/.gstack-cso-public-requirements.txt'], '/work', offlineEnv));
if (buildRequirements.size) {
const lines: string[] = [];
for (const name of buildRequirements) {
const locked = plan.inputs.filter(input => input.kind === 'public' && input.name.toLowerCase().replace(/[_.]+/g, '-') === name);
if (!locked.length || new Set(locked.map(input => input.version)).size !== 1) fail('MISSING_BUILD_DEPENDENCY', 'Every local build dependency needs one exact public wheel version in uv.lock.', 'uv.lock');
lines.push(`${name}==${locked[0].version} ${[...new Set(locked.map(input => input.integrity))].map(hash => `--hash=${hash}`).join(' ')}`);
}
metadata(plan, '.gstack-cso/build-requirements.txt', lines.join('\n') + '\n', 'Local build dependencies are acquired only as exact hashed public wheels.');
plan.acquisition.push(command('/usr/local/bin/python', ['-I', '-m', 'pip', '--isolated', 'download', '--index-url', 'https://pypi.org/simple', '--require-hashes', '--only-binary=:all:', '--dest', '/archives/wheels', '--requirement', '/metadata/.gstack-cso/build-requirements.txt'], '/metadata', acquisitionEnv));
plan.offline.push(command('/usr/local/bin/uv', ['pip', 'install', '--offline', '--no-config', '--python', '/work/.venv/bin/python', '--link-mode', 'copy', '--no-index', '--find-links', '/archives/wheels', '--require-hashes', '--only-binary', ':all:', '--requirement', '/metadata/.gstack-cso/build-requirements.txt'], '/work', offlineEnv));
}
if (localBuildPaths.length) plan.offline.push(command('/usr/local/bin/uv', ['pip', 'install', '--offline', '--no-config', '--python', '/work/.venv/bin/python', '--link-mode', 'copy', '--no-index', '--find-links', '/archives/wheels', '--no-deps', '--no-build-isolation', ...localBuildPaths.map(path => `/work/${path}`)], '/work', offlineEnv));
plan.offline.push(command('/usr/local/bin/uv', ['pip', 'check', '--offline', '--no-config', '--python', '/work/.venv/bin/python'], '/work', offlineEnv));
} else {
const contents = read(root, requirementsPath)!;
requirementLines(contents, plan, requirementsPath);
metadata(plan, requirementsPath, contents);
plan.offline.push(command('/usr/local/bin/python', ['-I', '-m', 'venv', '--copies', '/work/.venv'], '/work', offlineEnv));
plan.offline.push(command('/work/.venv/bin/python', ['-I', '-m', 'pip', '--isolated', 'install', '--no-index', '--find-links', '/archives/wheels', '--require-hashes', '--only-binary=:all:', '--requirement', '/work/requirements.txt'], '/work', offlineEnv));
plan.offline.push(command('/work/.venv/bin/python', ['-I', '-m', 'pip', '--isolated', 'check'], '/work', offlineEnv));
}
plan.acquisition.push(command('/usr/local/bin/python', ['-I', '-m', 'pip', '--isolated', 'download', '--index-url', 'https://pypi.org/simple', '--require-hashes', '--only-binary=:all:', '--dest', '/archives/wheels', '--requirement', publicRequirements], '/metadata', acquisitionEnv));
plan.registryHosts = ['pypi.org', 'files.pythonhosted.org'];
}
function toToml(value: Record<string, any>): string {
const literal = (x: any): string => {
if (typeof x === 'string' || typeof x === 'boolean' || typeof x === 'number') return JSON.stringify(x);
if (Array.isArray(x)) return `[${x.map(literal).join(', ')}]`;
if (record(x)) return `{ ${Object.entries(x).map(([k, v]) => `${JSON.stringify(k)} = ${literal(v)}`).join(', ')} }`;
fail('INVALID_METADATA', 'Unsupported TOML metadata value.', 'pyproject.toml');
};
return Object.entries(value).map(([key, val]) => `${JSON.stringify(key)} = ${literal(val)}`).join('\n') + '\n';
}
function inspectRails(root: string, plan: PreparationPlan) {
const contents = read(root, 'Gemfile.lock')!;
read(root, 'Gemfile'); // Presence only; never parse/evaluate Ruby during acquisition.
let section = '', sawPublicRemote = false;
const checksums = new Map<string, string>();
for (const line of contents.split(/\r?\n/)) {
if (/^[A-Z][A-Z ]+$/.test(line)) {
section = line;
if (!['GEM', 'PLATFORMS', 'DEPENDENCIES', 'RUBY VERSION', 'BUNDLED WITH', 'CHECKSUMS'].includes(section)) fail('UNSUPPORTED_SOURCE', 'Gemfile.lock contains a non-public source or unsupported section.', 'Gemfile.lock');
continue;
}
if (!line.trim()) continue;
if (section === 'GEM' && line.startsWith(' remote: ')) {
const remote = publicUrl(line.slice(10), ['rubygems.org'], 'Gemfile.lock');
if (remote !== 'https://rubygems.org/') fail('UNSUPPORTED_SOURCE', 'Ruby acquisition supports the public RubyGems root only.', 'Gemfile.lock');
sawPublicRemote = true;
} else if (section === 'GEM' && /^ \S/.test(line)) {
const match = line.match(/^ ([A-Za-z0-9][A-Za-z0-9_.-]*) \(([0-9]+(?:\.[0-9A-Za-z]+)*)(?:-([A-Za-z0-9][A-Za-z0-9_.-]*))?\)$/);
if (!match) fail('INVALID_METADATA', 'Invalid exact Ruby gem lock entry.', 'Gemfile.lock');
plan.inputs.push({ kind: 'public', name: match[1], version: match[2], platform: match[3] || 'ruby', integritySource: 'registry-on-acquisition' });
} else if (section === 'CHECKSUMS') {
const match = line.match(/^ ([A-Za-z0-9][A-Za-z0-9_.-]*) \(([^)]+)\) sha256=([a-f0-9]{64})$/);
if (!match) fail('INVALID_METADATA', 'Unsupported RubyGems checksum entry.', 'Gemfile.lock');
checksums.set(`${match[1]}@${match[2]}`, `sha256:${match[3]}`);
} else if (section === 'RUBY VERSION') {
const match = line.trim().match(/^ruby ([0-9]+\.[0-9]+\.[0-9]+)(?:p[0-9]+)?$/);
if (!match) fail('UNSUPPORTED_RUNTIME', 'Unsupported Ruby runtime declaration.', 'Gemfile.lock');
plan.runtimeRequirements.ruby = match[1];
} else if (section === 'BUNDLED WITH') {
const version = line.trim();
if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.test(version)) fail('UNSUPPORTED_RUNTIME', 'Bundler must be pinned to an exact version.', 'Gemfile.lock');
plan.runtimeRequirements.bundler = version;
}
}
if (!sawPublicRemote) fail('UNSUPPORTED_SOURCE', 'Gemfile.lock needs an explicit public RubyGems source.', 'Gemfile.lock');
if (!plan.runtimeRequirements.bundler) fail('UNSUPPORTED_RUNTIME', 'Gemfile.lock must record BUNDLED WITH.', 'Gemfile.lock');
packageCount(plan.inputs, 'Gemfile.lock');
for (const input of plan.inputs) {
const key = `${input.name}@${input.version}${input.platform === 'ruby' ? '' : `-${input.platform}`}`;
const hash = checksums.get(key);
if (hash) { input.integrity = hash; input.integritySource = 'lock'; }
// gem fetch downloads without evaluating a Gemfile/gemspec or building extensions.
plan.acquisition.push(command('/usr/local/bin/gem', ['fetch', input.name, '--version', input.version, '--platform', input.platform!, '--clear-sources', '--source', 'https://rubygems.org', '--norc'], '/archives'));
}
metadata(plan, 'Gemfile.lock', contents);
const env = { RAILS_ENV: 'test', RACK_ENV: 'test', SECRET_KEY_BASE: 'cso-synthetic-test-key-never-a-production-credential', BUNDLE_PATH: '/work/vendor/bundle', BUNDLE_FROZEN: 'true', BUNDLE_DEPLOYMENT: 'true', BUNDLE_DISABLE_SHARED_GEMS: 'true', BUNDLE_IGNORE_CONFIG: 'true', BUNDLE_ALLOW_OFFLINE_INSTALL: 'true', BUNDLE_CACHE_PATH: '/archives', BUNDLE_USER_HOME: '/work/.cso-bundle' };
plan.offline.push(command('/usr/local/bin/bundle', ['install', '--local', '--jobs', '2', '--retry', '0'], '/work', env));
plan.registryHosts = ['rubygems.org', 'index.rubygems.org'];
const declared = railsDatabaseConfiguration(read(root, 'config/database.yml', true));
const supported: Array<'sqlite' | 'postgresql'> = [];
if (plan.inputs.some(input => input.name === 'sqlite3')) supported.push('sqlite');
if (plan.inputs.some(input => input.name === 'pg')) supported.push('postgresql');
if (!supported.length) fail('MISSING_DATABASE_ADAPTER', 'Rails automatic preparation requires a locked sqlite3 or pg adapter.', 'Gemfile.lock');
const declaredSupported = [...declared.adapters].filter(adapter => supported.includes(adapter));
const selected = supported.length === 1 ? supported[0] : declaredSupported.length === 1 ? declaredSupported[0] : null;
plan.database = { supported, selected, connections: declared.connections, requiresSyntheticConfiguration: true };
}
function railsDatabaseConfiguration(contents: string | undefined): { connections: string[]; adapters: Set<'sqlite' | 'postgresql'> } {
if (contents === undefined) return { connections: ['primary'], adapters: new Set() };
if (contents.includes('\t')) fail('DYNAMIC_DATABASE_CONFIG', 'Database configuration must use spaces for bounded inert parsing.', 'config/database.yml');
// ERB is never evaluated. It may supply scalar values, but dynamic YAML structure is unsupported.
let safe = contents.replace(/<%=[\s\S]*?%>/g, 'CSO_REDACTED_ERB');
if (safe.includes('<%')) fail('DYNAMIC_DATABASE_CONFIG', 'Database configuration uses structural ERB; supply explicit synthetic connection names.', 'config/database.yml');
// Stock Rails uses one inert `default` anchor. Remove only that exact merge
// syntax before parsing so the YAML implementation never expands aliases.
safe = safe.split(/\r?\n/).map(line => {
if (/^default:\s*&default\s*(?:#.*)?$/.test(line)) return 'default:';
if (/^\s+<<:\s*\*default\s*(?:#.*)?$/.test(line)) return line.replace(/<<:[\s\S]*$/, '# cso: inert default merge');
return line;
}).join('\n');
if (/(^|[\s\[{,])(?:[&*][A-Za-z0-9_-]+|!\S+)/m.test(safe))
fail('DYNAMIC_DATABASE_CONFIG', 'Only the stock Rails default anchor and merge are accepted by the trusted readiness parser.', 'config/database.yml');
let parsed: any;
try { parsed = Bun.YAML.parse(safe); } catch { fail('DYNAMIC_DATABASE_CONFIG', 'Database connection names could not be read without evaluating ERB.', 'config/database.yml'); }
if (!record(parsed)) fail('INVALID_DATABASE_CONFIG', 'Database configuration must be a mapping.', 'config/database.yml');
const connections = new Set<string>(), adapters = new Set<'sqlite' | 'postgresql'>();
const recordAdapter = (config: Record<string, any>) => {
if (config.adapter === 'sqlite3') adapters.add('sqlite');
if (config.adapter === 'postgresql' || config.adapter === 'postgres') adapters.add('postgresql');
};
for (const [environment, config] of Object.entries(parsed)) {
if (!record(config)) continue;
recordAdapter(config);
if (environment === 'default') continue;
if ('adapter' in config || 'url' in config || 'database' in config) { connections.add('primary'); continue; }
for (const [name, connection] of Object.entries(config)) {
if (!connectionName(name) || name.includes('CSO_REDACTED_ERB') || !record(connection)) fail('DYNAMIC_DATABASE_CONFIG', 'Database connection names must be static identifiers.', 'config/database.yml');
recordAdapter(connection);
connections.add(name);
}
}
return { connections: connections.size ? [...connections].sort() : ['primary'], adapters };
}
/** Synthetic files are declared execution transformations; a boundary-changing target is blocked by the runner. */
export function railsTestConfiguration(connections: string[], adapter: 'sqlite' | 'postgresql'): Array<{ path: string; content: string }> {
if (!connections.length || connections.some(name => !connectionName(name))) throw new Error('Invalid Rails connection names');
const database: Record<string, any> = { test: {} };
for (const name of connections) database.test[name] = adapter === 'sqlite'
? { adapter: 'sqlite3', database: `/work/tmp/cso-${name}.sqlite3`, pool: 3 }
: { adapter: 'postgresql', host: '127.0.0.1', port: 5432, username: 'cso', password: 'cso-disposable-test', database: `cso_${name}`, pool: 3 };
return [
// JSON is valid YAML; no interpolation, anchors, inherited URLs, or production connections.
{ path: 'config/database.yml', content: JSON.stringify(database, null, 2) + '\n' },
{ path: 'config/initializers/zzzz_cso_test.rb', content: `# Trusted synthetic test environment; recorded in the transformation manifest.\nraise "CSO requires test environment" unless Rails.env.test?\nRails.application.config.secret_key_base = ENV.fetch("SECRET_KEY_BASE")\nRails.application.config.active_storage.service = :cso_test if defined?(ActiveStorage)\nRails.application.config.active_job.queue_adapter = :test if defined?(ActiveJob)\nRails.application.config.action_mailer.delivery_method = :test if defined?(ActionMailer)\nRails.application.config.action_mailer.perform_deliveries = false if defined?(ActionMailer)\nRails.application.config.after_initialize do\n ActiveJob::Base.queue_adapter = :test if defined?(ActiveJob::Base)\n ActionMailer::Base.delivery_method = :test if defined?(ActionMailer::Base)\nend\n` },
{ path: 'config/storage.yml', content: JSON.stringify({ cso_test: { service: 'Disk', root: '/work/tmp/cso-storage' } }, null, 2) + '\n' },
];
}
export function inspectPreparation(snapshotPath: string, stack?: CsoStack): PreparationPlan {
const root = resolve(snapshotPath);
const plan: PreparationPlan = { schemaVersion: 1, stack: stack ?? 'python', status: 'ready', prerequisites: [], metadata: [], inputs: [], acquisition: [], offline: [], registryHosts: [], runtimeProfile: stack ?? 'python', runtimeRequirements: {}, transformations: [] };
try {
const detectedStacks:CsoStack[]=[];
const hasBun=read(root,'bun.lock',true)!==undefined||read(root,'bun.lockb',true)!==undefined;if(hasBun)detectedStacks.push('bun');
if(read(root,'package-lock.json',true)!==undefined||read(root,'npm-shrinkwrap.json',true)!==undefined||(!hasBun&&read(root,'package.json',true)!==undefined))detectedStacks.push('node');
if(read(root,'Gemfile.lock',true)!==undefined||read(root,'Gemfile',true)!==undefined)detectedStacks.push('rails');
if(read(root,'uv.lock',true)!==undefined||read(root,'requirements.txt',true)!==undefined||read(root,'pyproject.toml',true)!==undefined)detectedStacks.push('python');
if(!stack&&detectedStacks.length>1)fail('MULTIPLE_STACKS',`Multiple executable stacks were detected (${detectedStacks.join(', ')}); verification must select a matching qualified runtime.`);
const detected = stack ?? detectedStacks[0] ?? 'python';
plan.stack = detected; plan.runtimeProfile = detected;
if (!['node', 'bun', 'python', 'rails'].includes(detected)) fail('UNSUPPORTED_STACK', 'Supported runtime stacks are Node, Bun, Python, and Rails.');
({ node: inspectNode, bun: inspectBun, python: inspectPython, rails: inspectRails }[detected])(root, plan);
} catch (error) {
plan.status = 'prerequisites';
plan.prerequisites.push(error instanceof MetadataError
? { code: error.code, message: error.message, path: error.path }
: { code: 'INVALID_METADATA', message: 'Dependency metadata could not be inspected safely.' });
// Never execute a partially validated acquisition plan.
plan.acquisition = []; plan.offline = []; plan.metadata = [];
}
return plan;
}
+218
View File
@@ -0,0 +1,218 @@
import { spawn } from 'node:child_process';
import { accessSync, closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readSync, realpathSync, statSync } from 'node:fs';
import { basename, dirname, join, isAbsolute, delimiter, resolve } from 'node:path';
import { redactFindingSpans } from '../redact-engine';
import { CsoError, MAX_OUTPUT } from './contracts';
const SOURCE_RUNTIME=/^bun(?:\.exe)?$/i.test(basename(process.execPath));
const WINDOWS_GIT=process.platform==='win32'?(process.env.GSTACK_CSO_TRUSTED_GIT||(SOURCE_RUNTIME?Bun.which('git')??'':'')):'';
const WINDOWS_SYSTEM=process.platform==='win32'?join(process.env.SystemRoot||'C:\\Windows','System32'):'';
export const TRUSTED_DIRECTORIES = process.platform === 'win32'
? [...new Set([WINDOWS_GIT?dirname(WINDOWS_GIT):'',WINDOWS_SYSTEM].filter(Boolean))]
: ['/usr/local/bin','/usr/bin','/bin','/opt/homebrew/bin','/usr/local/sbin','/usr/sbin','/sbin'];
export const TRUSTED_PATH = TRUSTED_DIRECTORIES.join(delimiter);
export function executable(name: string): string {
// Never consult the audited repository's PATH or executable overrides.
if (!/^[a-zA-Z0-9._-]+$/.test(name)) throw new CsoError('INVALID_ARGUMENT','Invalid executable name');
if(process.platform==='win32'&&name.toLowerCase()==='git'){
try{if(!WINDOWS_GIT||!isAbsolute(WINDOWS_GIT)||basename(WINDOWS_GIT).toLowerCase()!=='git.exe')throw new Error();const stat=statSync(WINDOWS_GIT);if(!stat.isFile())throw new Error();return realpathSync(WINDOWS_GIT);}catch{throw new CsoError('TOOL_UNAVAILABLE','git.exe is not the trusted executable bound during gstack setup');}
}
for (const directory of TRUSTED_DIRECTORIES) {
const candidates=process.platform==='win32'?[join(directory,`${name}.exe`),join(directory,`${name}.cmd`),join(directory,name)]:[join(directory,name)];
for(const p of candidates){
try { const stat=statSync(p);accessSync(p,constants.X_OK);if(stat.isFile()&&(process.platform==='win32'||(stat.mode&0o111)))return realpathSync(p); } catch {}
}
}
throw new CsoError('TOOL_UNAVAILABLE', `${name} is not installed in a trusted system executable directory`);
}
export function childEnvironment(home: string): Record<string,string> {
return { PATH: TRUSTED_PATH, HOME: home, LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', TZ: 'UTC',
GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: process.platform==='win32'?'NUL':'/dev/null', GIT_TERMINAL_PROMPT: '0',
GIT_OPTIONAL_LOCKS: '0', GIT_ATTR_NOSYSTEM: '1' };
}
export function redact(value: string): string {
// Scan the complete bounded stream, including across write/chunk boundaries.
const output = redactFindingSpans(value, { maxBytes: MAX_OUTPUT });
if (output === null) throw new CsoError('REDACTION_FAILED','Payload withheld because redaction could not safely locate every secret');
return output;
}
const HASH_KEYS=new Set(['planSha256','planHash','originalHash','executionHash','snapshotHash','sourceHash','beforeSha256','afterSha256','patchHash','reviewedPatchHash','harnessHash','fixturesHash','policyHash','auditPolicyHash','originalSourceHash','transformationsHash','archivesHash','inputHash','beforeSourceHash','afterSourceHash','beforeDependencies','afterDependencies','beforeConfiguration','afterConfiguration','requestHash','startPlanHash','testPlanHash','preparationHash','preparedManifestHash','preparedDependencyHash','sourceProjectionHash','executionEnvironmentHash','databaseHash','receiptHash','dependencyClosureHash','closureHash','acquisitionReceiptHash','registryResponseSha256','sha256','versionOutputSha256','isolationPolicyHash','contentSha256','sbomDigest','provenanceDigest','dependencyHash','configurationHash','assertionHash','commandsHash','minimumPassingTestsHash','commandHash','outputHash','observationHash','witnessHash','keyId']);
function safeMetadata(value:string,key:string):boolean{
if(HASH_KEYS.has(key)&&/^[a-f0-9]{64}$/.test(value))return true;
if(['id','fingerprint','findingId','verificationId','reproductionAttemptId','artifactId','reviewArtifactId','bundleId','pathId'].includes(key)&&/^[a-f0-9]{32}$/.test(value))return true;
if(key==='path'&&/^@cso-path\/\/[a-f0-9]{32}$/.test(value))return true;
if(key==='repoId'&&/^[a-f0-9]{24}$/.test(value))return true;
if(key==='runId'&&/^\d{13}-[a-f0-9]{16}$/.test(value))return true;
if(key==='replayId'&&/^\d{13}-[a-f0-9]{16}$/.test(value))return true;
if(['baseCommit','headCommit'].includes(key)&&/^[a-f0-9]{40,64}$/.test(value))return true;
if(['createdAt','expiresAt','deadline','at','databaseUpdatedAt','qualifiedAt'].includes(key)&&/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{3})?Z$/.test(value))return true;
if(key==='nonce'&&/^[a-f0-9]{64}$/.test(value))return true;
if(key==='publicKey'&&/^[a-f0-9]{88}$/.test(value))return true;
if(key==='signature'&&/^[a-f0-9]{128}$/.test(value))return true;
if(key==='image'&&/^[a-z0-9./:_-]+@sha256:[a-f0-9]{64}$/.test(value))return true;
if(key==='integrity'&&/^(?:sha256|sha512)-[A-Za-z0-9+/]+={0,2}$/.test(value))return true;
return false;
}
function sanitizeJson(value:unknown,key:string,seen:WeakSet<object>,trustedMetadata:boolean):unknown{
if(typeof value==='string'){
if(trustedMetadata&&safeMetadata(value,key))return value;
return redact(value);
}
if(value===null||typeof value!=='object')return value;
if(seen.has(value as object))throw new CsoError('INVALID_SCHEMA','Cyclic JSON cannot be persisted');seen.add(value as object);
if(Array.isArray(value)){const out=value.map(v=>sanitizeJson(v,key,seen,trustedMetadata));seen.delete(value);return out;}
const out:Record<string,unknown>=Object.create(null);for(const [k,v] of Object.entries(value as Record<string,unknown>)){
if(['__proto__','prototype','constructor'].includes(k))throw new CsoError('INVALID_SCHEMA','Unsafe JSON property');out[k]=sanitizeJson(v,k,seen,trustedMetadata);
}seen.delete(value as object);return out;
}
/** Redact untrusted JSON content. Key names never make an untrusted value exempt. */
export function sanitizeForJson(value:unknown):unknown{return sanitizeJson(value,'',new WeakSet<object>(),false);}
/** Preserve only validated helper identifiers/hashes while redacting all content-bearing fields. */
export function sanitizeHelperForJson(value:unknown):unknown{return sanitizeJson(value,'',new WeakSet<object>(),true);}
export interface ProcessResult { code: number; stdout: string; stderr: string; timedOut: boolean; truncated: boolean; capturedBytes:number }
interface GitConfigIdentity { path:string; exists:boolean; dev?:number; ino?:number; mode?:number; size?:number; mtimeMs?:number; ctimeMs?:number; content?:string }
const GIT_CONFIG_LIMIT=1024*1024;
interface BoundedMetadataFile { dev:number;ino:number;mode:number;nlink:number;size:number;mtimeMs:number;ctimeMs:number;content:string }
function sameMetadataFile(left:BoundedMetadataFile|ReturnType<typeof lstatSync>,right:BoundedMetadataFile|ReturnType<typeof lstatSync>):boolean{
return left.dev===right.dev&&left.ino===right.ino&&left.mode===right.mode&&left.nlink===right.nlink&&left.size===right.size&&left.mtimeMs===right.mtimeMs&&left.ctimeMs===right.ctimeMs;
}
function boundedMetadataFile(path:string,maxBytes:number,label:string,optional=false):BoundedMetadataFile|undefined{
let before:ReturnType<typeof lstatSync>;
try{before=lstatSync(path);}catch(error:any){if(optional&&error?.code==='ENOENT')return;throw new CsoError(error?.code==='ENOENT'?'SNAPSHOT_RACE':'UNSAFE_PATH',`${label} is not a bounded regular file`);}
if(before.isSymbolicLink()||!before.isFile()||before.nlink!==1||before.size>maxBytes)throw new CsoError('UNSAFE_PATH',`${label} is not a bounded regular file`);
let fd:number|undefined;
try{
fd=openSync(path,constants.O_RDONLY|(constants.O_NOFOLLOW??0)|(constants.O_NONBLOCK??0));
const opened=fstatSync(fd);
if(!opened.isFile()||opened.nlink!==1||opened.size>maxBytes||!sameMetadataFile(before,opened))throw new CsoError('SNAPSHOT_RACE',`${label} changed while it was opened`);
const buffer=Buffer.alloc(Math.min(maxBytes+1,opened.size+1));let bytes=0,count=0;
while(bytes<buffer.length&&(count=readSync(fd,buffer,bytes,buffer.length-bytes,null))>0)bytes+=count;
const final=fstatSync(fd),after=lstatSync(path);
if(bytes!==opened.size||!final.isFile()||!after.isFile()||after.isSymbolicLink()||!sameMetadataFile(opened,final)||!sameMetadataFile(opened,after))
throw new CsoError('SNAPSHOT_RACE',`${label} changed while it was read`);
return{dev:opened.dev,ino:opened.ino,mode:opened.mode,nlink:opened.nlink,size:opened.size,mtimeMs:opened.mtimeMs,ctimeMs:opened.ctimeMs,content:buffer.subarray(0,bytes).toString('utf8')};
}catch(error:any){
if(error instanceof CsoError)throw error;
if(['ENOENT','ELOOP','ENXIO'].includes(error?.code))throw new CsoError('SNAPSHOT_RACE',`${label} changed while it was opened`);
throw new CsoError('UNSAFE_PATH',`${label} could not be read safely`);
}finally{if(fd!==undefined)try{closeSync(fd);}catch{}}
}
function boundedConfig(path:string):GitConfigIdentity{
const file=boundedMetadataFile(path,GIT_CONFIG_LIMIT,'Repository Git configuration',true);
if(!file)return{path,exists:false};
const {content}=file;
// There is no process-wide "--no-includes" switch for ordinary Git
// commands. Reject include directives before spawning Git so repository
// configuration cannot pull policy or executable settings from elsewhere.
if(/^\s*\[\s*include(?:if)?(?=[\s."\]])/im.test(content))throw new CsoError('UNSAFE_PATH','Repository Git config includes are not allowed during a security snapshot');
return{path,exists:true,dev:file.dev,ino:file.ino,mode:file.mode,size:file.size,mtimeMs:file.mtimeMs,ctimeMs:file.ctimeMs,content};
}
function gitDirectories(repo:string):{gitDir:string;commonDir:string}{
const marker=join(repo,'.git'),stat=lstatSync(marker);let gitDir:string;
if(stat.isDirectory()&&!stat.isSymbolicLink())gitDir=realpathSync(marker);
else if(stat.isFile()&&!stat.isSymbolicLink()&&stat.nlink===1&&stat.size<=8192){
const value=boundedMetadataFile(marker,8192,'Repository .git pointer')!.content,match=value.match(/^gitdir:\s*(.+?)\s*$/);
if(!match||value.includes('\0')||value.split(/\r?\n/).filter(Boolean).length!==1)throw new CsoError('UNSAFE_PATH','Repository .git pointer is invalid');
gitDir=realpathSync(resolve(dirname(marker),match[1]));
}else throw new CsoError('UNSAFE_PATH','Repository .git metadata is not a regular directory or worktree pointer');
const commonMarker=join(gitDir,'commondir'),commonFile=boundedMetadataFile(commonMarker,8192,'Repository common Git directory pointer',true);let commonDir=gitDir;
if(commonFile){
const value=commonFile.content.trim();
if(!value||value.includes('\0')||value.includes('\n')||value.includes('\r'))throw new CsoError('UNSAFE_PATH','Repository common Git directory pointer is invalid');
commonDir=realpathSync(resolve(gitDir,value));
}
return{gitDir,commonDir};
}
function gitConfigIdentities(repo:string):GitConfigIdentity[]{
const {gitDir,commonDir}=gitDirectories(repo);
// extensions.worktreeConfig makes config.worktree active in both linked and
// main worktrees. Bind even its absence so it cannot appear after inspection
// and feed Git an unchecked include or executable setting.
return [join(commonDir,'config'),join(gitDir,'config.worktree')].map(boundedConfig);
}
function assertGitConfigIdentities(expected:GitConfigIdentity[]):void{
for(const item of expected){
const current=boundedConfig(item.path);
if(current.exists!==item.exists||current.dev!==item.dev||current.ino!==item.ino||current.mode!==item.mode||current.size!==item.size||current.mtimeMs!==item.mtimeMs||current.ctimeMs!==item.ctimeMs||current.content!==item.content)
throw new CsoError('SNAPSHOT_RACE','Repository Git configuration changed during a metadata operation');
}
}
function hardenGit(file:string,args:string[]):{args:string[];configs?:GitConfigIdentity[]}{
if(!/^(?:git|git\.exe)$/i.test(basename(file)))return{args};
let trusted:string;try{trusted=executable('git');}catch{return{args};}
if(realpathSync(file)!==trusted)return{args};
const positions=args.flatMap((value,index)=>value==='-C'?[index]:[]);
if(positions.length!==1||positions[0]+1>=args.length)throw new CsoError('INVALID_ARGUMENT','CSO Git operations require exactly one audited working directory');
const position=positions[0],requested=args[position+1];
if(!isAbsolute(requested))throw new CsoError('INVALID_ARGUMENT','CSO Git operations require an absolute audited working directory');
const repo=realpathSync(requested),stat=statSync(repo);
if(!stat.isDirectory())throw new CsoError('MISSING_INPUT','Audited Git working directory is not a directory');
const configs=gitConfigIdentities(repo),nullPath=process.platform==='win32'?'NUL':'/dev/null',
// Git for Windows accepts NUL for ordinary file-valued settings, but its
// config include machinery treats NUL as a failing include. Its MSYS path
// layer maps /dev/null correctly for this one directive.
includeNullPath=process.platform==='win32'?'/dev/null':nullPath;
const prefix=args.slice(0,position),command=args.slice(position+2);
return{configs,args:[...prefix,
'--no-replace-objects',
'-c','core.fsmonitor=false','-c',`core.hooksPath=${nullPath}`,'-c',`core.attributesFile=${nullPath}`,
'-c',`core.excludesFile=${nullPath}`,'-c','core.ignoreCase=false','-c','core.precomposeUnicode=false',
'-c','core.untrackedCache=false','-c',`include.path=${includeNullPath}`,'-c','core.pager=cat',
'-C',repo,`--work-tree=${repo}`,...command]};
}
export async function runProcess(file: string, args: string[], opts: {
cwd: string; env: Record<string,string>; timeoutMs?: number; maxBytes?: number; input?: string;
raw?: boolean; // Only for inert Git framing or private helper/Docker control JSON that is validated before use. Never print or persist raw results.
}): Promise<ProcessResult> {
if (!isAbsolute(file) || !isAbsolute(opts.cwd) || !existsSync(opts.cwd)) throw new CsoError('INVALID_ARGUMENT','Children require absolute executables and an existing trusted working directory');
if (!args.every(a => typeof a === 'string' && !a.includes('\0'))) throw new CsoError('INVALID_ARGUMENT','Invalid child argument');
const hardened=hardenGit(file,args);args=hardened.args;
const cap = Math.min(opts.maxBytes ?? MAX_OUTPUT, MAX_OUTPUT);
return new Promise((resolve,reject) => {
const child = spawn(file,args,{cwd:opts.cwd,env:opts.env,stdio:['pipe','pipe','pipe'],detached:process.platform !== 'win32'});
const out: Buffer[] = [], err: Buffer[] = [], ordered:Buffer[]=[]; let bytes = 0, timedOut = false, truncated = false;
const kill = () => { try { if (process.platform !== 'win32' && child.pid) process.kill(-child.pid,'SIGKILL'); else child.kill('SIGKILL'); } catch {} };
const timer = setTimeout(() => { timedOut = true; kill(); }, Math.max(1,Math.min(opts.timeoutMs ?? 30_000,300_000)));
const capture = (target: Buffer[]) => (chunk: Buffer) => {
bytes += chunk.length;
if (bytes > cap) { truncated = true; kill(); return; }
target.push(chunk);ordered.push(chunk);
};
child.stdout.on('data',capture(out)); child.stderr.on('data',capture(err));
child.on('error',() => { clearTimeout(timer); reject(new CsoError('TOOL_UNAVAILABLE','Trusted child process could not start')); });
child.on('close',code => {
clearTimeout(timer);
try {
if(hardened.configs)assertGitConfigIdentities(hardened.configs);
// Never expose a truncated tail: it might be the beginning of a secret.
const stdout = truncated ? '[output withheld: size limit]' : Buffer.concat(out).toString('utf8');
const stderr = truncated ? '' : Buffer.concat(err).toString('utf8');
if(opts.raw){resolve({code:code ?? -1,stdout,stderr,timedOut,truncated,capturedBytes:bytes});return;}
// A token may be split across stdout/stderr. Stream ordering is not
// recoverable here, so scan both concatenation orders and withhold both
// channels when either reveals a cross-stream sensitive span.
const forward=stdout+stderr,reverse=stderr+stdout,chronological=Buffer.concat(ordered).toString('utf8');
if([stdout,stderr,forward,reverse,chronological].some(value=>redact(value)!==value)){
resolve({code:code ?? -1,stdout:'[sensitive process output redacted]',stderr:'',timedOut,truncated,capturedBytes:bytes});return;
}
resolve({code:code ?? -1,stdout,stderr,timedOut,truncated,capturedBytes:bytes});
} catch (e) { reject(e); }
});
child.stdin.on('error',() => {}); child.stdin.end(opts.input);
});
}
export async function git(repo: string, args: string[], home: string): Promise<string> {
const result = await runProcess(executable('git'),['--no-optional-locks','-C',repo,...args],
{cwd:home,env:childEnvironment(home),raw:true,timeoutMs:15_000});
if (result.code || result.timedOut || result.truncated) {
// Git stderr and argv can contain repository paths, refs, and configured
// content. Name only the fixed helper-owned operation and bounded process
// outcome so native failures are actionable without exposing either.
const knownOperations=new Set(['rev-parse','symbolic-ref','ls-files','ls-tree','log','merge-base']),operation=args.find(value=>knownOperations.has(value))??'metadata',
phase=operation==='rev-parse'&&args.includes('--show-object-format')?'object-format':operation==='rev-parse'&&args.includes('--is-inside-work-tree')?'worktree-probe':operation,
reason=/not a git repository|outside repository/i.test(result.stderr)?'repository unavailable':/dubious ownership/i.test(result.stderr)?'repository ownership rejected':/(?:bad|invalid|unable to read).*config|config (?:error|file)/i.test(result.stderr)?'configuration rejected':/unknown option|unknown switch|unrecognized option|usage:/i.test(result.stderr)?'unsupported invocation':/(?:cannot|could not|unable to) (?:chdir|change directory)|no such file or directory/i.test(result.stderr)?'path unavailable':'request rejected',
outcome=result.timedOut?'timed out':result.truncated?'exceeded the output limit':`exited ${result.code}`;
throw new CsoError('MISSING_INPUT',`Could not read bounded Git metadata: ${phase} ${outcome} (${reason}); source may not be a Git repository`);
}
return result.stdout;
}
+113
View File
@@ -0,0 +1,113 @@
/* Build-time serialization for publishing the native CSO bundle. */
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>
#define CSO_COMMAND_CAP 32768
static int append(wchar_t *buffer, size_t *used, wchar_t value) {
if (*used >= CSO_COMMAND_CAP - 1) return 0;
buffer[(*used)++] = value;
buffer[*used] = L'\0';
return 1;
}
static int joined_path(wchar_t *output, size_t capacity,
const wchar_t *directory, const wchar_t *leaf) {
int written = swprintf(output, capacity, L"%ls\\%ls", directory, leaf);
return written >= 0 && (size_t)written < capacity;
}
static int argument(wchar_t *buffer, size_t *used, const wchar_t *value) {
size_t slashes = 0;
if (!append(buffer, used, L'"')) return 0;
for (;; value++) {
if (*value == L'\\') { slashes++; continue; }
size_t count = (*value == L'"' || *value == L'\0') ? slashes * 2 : slashes;
if (*value == L'"') count++;
while (count--) if (!append(buffer, used, L'\\')) return 0;
slashes = 0;
if (*value == L'\0') break;
if (!append(buffer, used, *value)) return 0;
}
return append(buffer, used, L'"');
}
int wmain(int argc, wchar_t **argv) {
if (argc < 4) { fputs("gstack-cso: publish lock requires a directory and command\n", stderr); return 69; }
wchar_t directory[CSO_COMMAND_CAP];
DWORD length = GetFullPathNameW(argv[1], CSO_COMMAND_CAP, directory, NULL);
if (!length || length >= (DWORD)CSO_COMMAND_CAP) { fputs("gstack-cso: publication directory is invalid\n", stderr); return 69; }
wchar_t gate_path[CSO_COMMAND_CAP];
if (!joined_path(gate_path, CSO_COMMAND_CAP, directory, L".gstack-cso-generation.lock")) {
fputs("gstack-cso: generation lock path is too long\n", stderr); return 69;
}
HANDLE gate = CreateFileW(gate_path, GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_ALWAYS, FILE_ATTRIBUTE_HIDDEN | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
BY_HANDLE_FILE_INFORMATION gate_info;
if (gate == INVALID_HANDLE_VALUE) return GetLastError() == ERROR_SHARING_VIOLATION ? 73 : 69;
if (!GetFileInformationByHandle(gate, &gate_info) ||
(gate_info.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) ||
gate_info.nNumberOfLinks != 1 || gate_info.nFileSizeHigh != 0 || gate_info.nFileSizeLow != 0) return 69;
wchar_t *command = calloc(CSO_COMMAND_CAP, sizeof(wchar_t));
if (!command) return 69;
size_t used = 0;
for (int i = 2; i < argc; i++)
if ((i > 2 && !append(command, &used, L' ')) || !argument(command, &used, argv[i])) {
fputs("gstack-cso: publication command is too large\n", stderr); return 69;
}
if (!SetEnvironmentVariableW(L"GSTACK_CSO_PUBLISH_LOCKED", L"1")) return 69;
HANDLE job = CreateJobObjectW(NULL, NULL);
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {0};
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!job || !SetInformationJobObject(job, JobObjectExtendedLimitInformation, &limits, (DWORD)sizeof(limits))) return 69;
STARTUPINFOW startup = {0}; PROCESS_INFORMATION child = {0}; startup.cb = (DWORD)sizeof(startup);
if (!CreateProcessW(argv[2], command, NULL, NULL, TRUE, CREATE_SUSPENDED, NULL, NULL, &startup, &child)) {
fwprintf(stderr, L"gstack-cso: publication command could not start (Windows error %lu)\n", (unsigned long)GetLastError()); return 69;
}
if (!AssignProcessToJobObject(job, child.hProcess) || ResumeThread(child.hThread) == (DWORD)-1) {
TerminateProcess(child.hProcess, 69); CloseHandle(child.hThread); CloseHandle(child.hProcess); return 69;
}
CloseHandle(child.hThread);
DWORD code = 69;
if (WaitForSingleObject(child.hProcess, INFINITE) == WAIT_OBJECT_0) GetExitCodeProcess(child.hProcess, &code);
CloseHandle(child.hProcess); CloseHandle(job); CloseHandle(gate); free(command);
return (int)code;
}
#else
#ifdef __APPLE__
#define _DARWIN_C_SOURCE 1
#endif
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char **argv) {
if (argc < 4 || argv[2][0] != '/') {
fputs("gstack-cso: publish lock requires a directory and absolute command\n", stderr); return 69;
}
int directory = open(argv[1], O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
struct stat state;
if (directory < 0 || fstat(directory, &state) != 0 || !S_ISDIR(state.st_mode)) {
fputs("gstack-cso: publication directory is unavailable\n", stderr); return 69;
}
if (flock(directory, LOCK_EX | LOCK_NB) != 0) {
if (errno == EWOULDBLOCK || errno == EAGAIN) return 73;
fputs("gstack-cso: publication lock is unavailable\n", stderr); return 69;
}
if (setenv("GSTACK_CSO_PUBLISH_LOCKED", "1", 1) != 0) return 69;
execv(argv[2], &argv[2]);
fputs("gstack-cso: publication command could not start\n", stderr);
return 69;
}
#endif
+90
View File
@@ -0,0 +1,90 @@
{
"schemaVersion": 1,
"revision": "cso-v3-build-reviewed-2026-09-10",
"previousRevision": null,
"helperAbi": 3,
"buildRevision": "cso-runtime-inputs-2026-09-10",
"profiles": [
{
"id": "node-24.4.0-amd64",
"stack": "node",
"platform": "linux/amd64",
"state": "build_reviewed",
"versions": { "node": "24.4.0", "npm": "11.4.2", "cso-preparation": "1.0.0" },
"reviewedAt": "2026-09-10T00:00:00.000Z"
},
{
"id": "node-24.4.0-arm64",
"stack": "node",
"platform": "linux/arm64",
"state": "build_reviewed",
"versions": { "node": "24.4.0", "npm": "11.4.2", "cso-preparation": "1.0.0" },
"reviewedAt": "2026-09-10T00:00:00.000Z"
},
{
"id": "bun-1.3.10-amd64",
"stack": "bun",
"platform": "linux/amd64",
"state": "build_reviewed",
"versions": { "bun": "1.3.10", "cso-preparation": "1.0.0" },
"reviewedAt": "2026-09-10T00:00:00.000Z"
},
{
"id": "bun-1.3.10-arm64",
"stack": "bun",
"platform": "linux/arm64",
"state": "build_reviewed",
"versions": { "bun": "1.3.10", "cso-preparation": "1.0.0" },
"reviewedAt": "2026-09-10T00:00:00.000Z"
},
{
"id": "python-3.13.4-uv-0.8.0-amd64",
"stack": "python",
"platform": "linux/amd64",
"state": "build_reviewed",
"versions": { "python": "3.13.4", "uv": "0.8.0", "cso-preparation": "1.0.0" },
"reviewedAt": "2026-09-10T00:00:00.000Z"
},
{
"id": "python-3.13.4-uv-0.8.0-arm64",
"stack": "python",
"platform": "linux/arm64",
"state": "build_reviewed",
"versions": { "python": "3.13.4", "uv": "0.8.0", "cso-preparation": "1.0.0" },
"reviewedAt": "2026-09-10T00:00:00.000Z"
},
{
"id": "rails-ruby-3.4.4-amd64",
"stack": "rails",
"platform": "linux/amd64",
"state": "build_reviewed",
"versions": { "ruby": "3.4.4", "bundler": "2.6.7", "cso-preparation": "1.0.0" },
"reviewedAt": "2026-09-10T00:00:00.000Z"
},
{
"id": "rails-ruby-3.4.4-arm64",
"stack": "rails",
"platform": "linux/arm64",
"state": "build_reviewed",
"versions": { "ruby": "3.4.4", "bundler": "2.6.7", "cso-preparation": "1.0.0" },
"reviewedAt": "2026-09-10T00:00:00.000Z"
},
{
"id": "postgresql-17.2-amd64",
"stack": "postgresql",
"platform": "linux/amd64",
"state": "build_reviewed",
"versions": { "postgresql": "17.2" },
"reviewedAt": "2026-09-10T00:00:00.000Z"
},
{
"id": "postgresql-17.2-arm64",
"stack": "postgresql",
"platform": "linux/arm64",
"state": "build_reviewed",
"versions": { "postgresql": "17.2" },
"reviewedAt": "2026-09-10T00:00:00.000Z"
}
],
"runtimes": []
}
+200
View File
@@ -0,0 +1,200 @@
/** A runtime becomes executable only after trusted CI qualification and catalog review. */
import committedCatalog from './runtime-catalog.json';
import type { CsoStack, PreparationPlan } from './preparation';
import { CsoError, canonical, sha256 } from './contracts';
export const CSO_HELPER_ABI = 3;
export type RuntimePlatform = 'linux/amd64' | 'linux/arm64';
interface RuntimeQualificationProvenance {
sourceCommit: string;
workflow: string;
sbomDigest: string;
provenanceDigest: string;
verifiedProvenance: true;
}
export type RuntimeQualification = RuntimeQualificationProvenance & (
| { kind: 'application'; containmentPassed: true; coldStartPassed: true; positiveNegativeAssertionsPassed: true; heldOutRepairPassed: true }
| { kind: 'postgresql'; containmentPassed: true; coldStartPassed: true; multiDatabasePassed: true; readinessPassed: true }
);
export interface QualifiedRuntime {
id: string;
stack: CsoStack | 'postgresql';
platform: RuntimePlatform;
state: 'qualified';
image: string;
entrypoint: '/opt/cso/entrypoint';
helperAbi: number;
versions: Record<string, string>;
policyVersion: 'cso-isolation-v1';
qualifiedAt: string;
qualification: RuntimeQualification;
}
/** Reviewed build metadata is informative. It never makes an image executable. */
export interface ReviewedRuntimeProfile {
id: string;
stack: CsoStack | 'postgresql';
platform: RuntimePlatform;
state: 'build_reviewed';
versions: Record<string, string>;
reviewedAt: string;
}
export interface RuntimeCatalog {
schemaVersion: 1;
revision: string;
previousRevision: string | null;
helperAbi: number;
buildRevision: string;
profiles: ReviewedRuntimeProfile[];
promotion?: {
sourceCommit: string;
workflow: string;
/** Canonical digest of the retained executable runtime matrix. */
evidenceDigest: string;
/** Canonical digest of the complete release-gate statements retained externally. */
qualificationEvidenceDigest: string;
};
runtimes: QualifiedRuntime[];
}
const DIGEST = /^sha256:[a-f0-9]{64}$/;
const IMAGE = /^(?:[a-z0-9.-]+(?::[0-9]+)?\/)?[a-z0-9][a-z0-9._/-]*@sha256:[a-f0-9]{64}$/;
const ID = /^[a-z0-9][a-z0-9._-]{0,100}$/;
const BUILD_REVISION = /^[a-z0-9][a-z0-9._-]{0,100}$/;
const STACKS = ['node', 'bun', 'python', 'rails', 'postgresql'] as const;
const PLATFORMS = ['linux/amd64', 'linux/arm64'] as const;
const QUALIFICATION_WORKFLOW = /^https:\/\/github\.com\/garrytan\/gstack\/actions\/runs\/[0-9]+$/;
const REQUIRED: Record<string, string[]> = {
node: ['node', 'npm', 'cso-preparation'],
bun: ['bun', 'cso-preparation'],
python: ['python', 'uv', 'cso-preparation'],
rails: ['ruby', 'bundler', 'cso-preparation'],
postgresql: ['postgresql'],
};
function versionsKey(versions: Record<string, string>): string {
return JSON.stringify(Object.entries(versions).sort(([a], [b]) => a.localeCompare(b)));
}
function validateRuntimeIdentity(value: { id: string; stack: string; platform: string; versions: Record<string, string> }): void {
if (typeof value.id !== 'string' || !ID.test(value.id)) throw new Error('INVALID_RUNTIME_ID');
if (!STACKS.includes(value.stack as typeof STACKS[number]) || !PLATFORMS.includes(value.platform as RuntimePlatform)) throw new Error('UNSUPPORTED_RUNTIME_PLATFORM');
if (!value.versions || typeof value.versions !== 'object' || Array.isArray(value.versions) || !Object.keys(value.versions).length ||
Object.values(value.versions).some(version => typeof version !== 'string' || !/^[0-9][a-zA-Z0-9.+_-]*$/.test(version))) throw new Error('UNPINNED_RUNTIME_VERSION');
if (Object.keys(value.versions).sort().join(',') !== [...REQUIRED[value.stack]].sort().join(',')) throw new Error('MISSING_RUNTIME_TOOL_VERSION');
if (['node', 'bun', 'python', 'rails'].includes(value.stack) && value.versions['cso-preparation'] !== '1.0.0') throw new Error('INCOMPATIBLE_PREPARATION_HELPER');
}
export function validateRuntimeCatalog(value: unknown): asserts value is RuntimeCatalog {
const catalog = value as RuntimeCatalog;
if (!catalog || catalog.schemaVersion !== 1 || catalog.helperAbi !== CSO_HELPER_ABI ||
typeof catalog.revision !== 'string' || !BUILD_REVISION.test(catalog.revision) || !Array.isArray(catalog.runtimes)) throw new Error('INCOMPATIBLE_RUNTIME_CATALOG');
if (catalog.previousRevision !== null && (typeof catalog.previousRevision !== 'string' || !BUILD_REVISION.test(catalog.previousRevision))) throw new Error('INVALID_RUNTIME_CATALOG');
if (!Array.isArray(catalog.profiles) || catalog.profiles.length !== STACKS.length * PLATFORMS.length ||
typeof catalog.buildRevision !== 'string' || !BUILD_REVISION.test(catalog.buildRevision)) throw new Error('INVALID_REVIEWED_RUNTIME_PROFILES');
const profiles = new Map<string, ReviewedRuntimeProfile>(), profileIdentities = new Set<string>();
for (const profile of catalog.profiles) {
validateRuntimeIdentity(profile);
const identity = `${profile.stack}:${profile.platform}`;
if (profiles.has(profile.id) || profileIdentities.has(identity) || profile.state !== 'build_reviewed' ||
!Number.isFinite(Date.parse(profile.reviewedAt))) throw new Error('INVALID_REVIEWED_RUNTIME_PROFILE');
profiles.set(profile.id, profile); profileIdentities.add(identity);
}
for (const stack of STACKS) for (const platform of PLATFORMS) {
if (!profileIdentities.has(`${stack}:${platform}`)) throw new Error('INCOMPLETE_REVIEWED_RUNTIME_MATRIX');
}
if (catalog.promotion !== undefined) {
if (!/^[a-f0-9]{40}$/.test(catalog.promotion.sourceCommit) ||
!QUALIFICATION_WORKFLOW.test(catalog.promotion.workflow) ||
!DIGEST.test(catalog.promotion.evidenceDigest) ||
!DIGEST.test(catalog.promotion.qualificationEvidenceDigest) ||
Object.keys(catalog.promotion).sort().join(',') !==
['evidenceDigest', 'qualificationEvidenceDigest', 'sourceCommit', 'workflow'].sort().join(',')) {
throw new Error('INVALID_RUNTIME_PROMOTION');
}
}
if (catalog.runtimes.length !== 0 && catalog.runtimes.length !== STACKS.length * PLATFORMS.length) throw new Error('INCOMPLETE_QUALIFIED_RUNTIME_MATRIX');
const ids = new Set<string>();
const runtimeIdentities = new Set<string>();
for (const runtime of catalog.runtimes) {
const qualification = runtime?.qualification;
if (!runtime) throw new Error('INVALID_RUNTIME_ID');
validateRuntimeIdentity(runtime);
const identity = `${runtime.stack}:${runtime.platform}`;
if (ids.has(runtime.id) || runtimeIdentities.has(identity)) throw new Error('INVALID_RUNTIME_ID');
ids.add(runtime.id); runtimeIdentities.add(identity);
const arch = runtime.platform === 'linux/amd64' ? 'amd64' : 'arm64';
const expectedImage = new RegExp(`^ghcr\\.io/garrytan/gstack/cso-staging/${runtime.stack}-${arch}@sha256:[a-f0-9]{64}$`);
if (runtime.state !== 'qualified' || !IMAGE.test(runtime.image) || !expectedImage.test(runtime.image) || runtime.entrypoint !== '/opt/cso/entrypoint' ||
runtime.helperAbi !== CSO_HELPER_ABI || runtime.policyVersion !== 'cso-isolation-v1') throw new Error('UNQUALIFIED_RUNTIME');
const reviewed = profiles.get(runtime.id);
if (!reviewed || reviewed.stack !== runtime.stack || reviewed.platform !== runtime.platform ||
versionsKey(reviewed.versions) !== versionsKey(runtime.versions)) throw new Error('RUNTIME_BUILD_PROFILE_MISMATCH');
if (!qualification || !/^[a-f0-9]{40}$/.test(qualification.sourceCommit) ||
!QUALIFICATION_WORKFLOW.test(qualification.workflow) ||
!DIGEST.test(qualification.sbomDigest) || !DIGEST.test(qualification.provenanceDigest) || qualification.verifiedProvenance !== true ||
!Number.isFinite(Date.parse(runtime.qualifiedAt))) throw new Error('MISSING_RUNTIME_QUALIFICATION');
const keys = Object.keys(qualification).sort();
const common = ['kind', 'sourceCommit', 'workflow', 'sbomDigest', 'provenanceDigest', 'verifiedProvenance'];
if (['node', 'bun', 'python', 'rails'].includes(runtime.stack)) {
if (qualification.kind !== 'application' || qualification.containmentPassed !== true || qualification.coldStartPassed !== true ||
qualification.positiveNegativeAssertionsPassed !== true || qualification.heldOutRepairPassed !== true ||
keys.join(',') !== [...common, 'containmentPassed', 'coldStartPassed', 'positiveNegativeAssertionsPassed', 'heldOutRepairPassed'].sort().join(',')) throw new Error('MISSING_APPLICATION_QUALIFICATION');
} else {
if (qualification.kind !== 'postgresql' || qualification.containmentPassed !== true || qualification.coldStartPassed !== true ||
qualification.multiDatabasePassed !== true || qualification.readinessPassed !== true ||
keys.join(',') !== [...common, 'containmentPassed', 'coldStartPassed', 'multiDatabasePassed', 'readinessPassed'].sort().join(',')) throw new Error('MISSING_POSTGRESQL_QUALIFICATION');
}
}
if (catalog.runtimes.length > 0) {
for (const identity of profileIdentities) if (!runtimeIdentities.has(identity)) throw new Error('INCOMPLETE_QUALIFIED_RUNTIME_MATRIX');
if (!catalog.promotion) throw new Error('MISSING_RUNTIME_PROMOTION');
if (catalog.runtimes.some(runtime => runtime.qualification.sourceCommit !== catalog.promotion!.sourceCommit ||
runtime.qualification.workflow !== catalog.promotion!.workflow)) throw new Error('RUNTIME_PROMOTION_MISMATCH');
if (catalog.promotion.evidenceDigest !== `sha256:${sha256(canonical(catalog.runtimes))}`) {
throw new Error('RUNTIME_PROMOTION_EVIDENCE_MISMATCH');
}
} else if (catalog.promotion) throw new Error('INVALID_RUNTIME_PROMOTION');
}
export const RUNTIME_CATALOG = committedCatalog as RuntimeCatalog;
validateRuntimeCatalog(RUNTIME_CATALOG);
export function assertRuntimeCompatible(plan: PreparationPlan, runtime: QualifiedRuntime): void {
if (plan.schemaVersion !== 1 || plan.status !== 'ready' || runtime.stack !== plan.stack) throw new CsoError('INCOMPATIBLE_INPUT', `Prepared ${plan.stack} source cannot run in ${runtime.stack} runtime ${runtime.id}`);
for (const [declared, rawRange] of Object.entries(plan.runtimeRequirements)) {
if (!rawRange) continue;
let tool = declared, range = rawRange;
if (declared === 'packageManager') {
const match = rawRange.match(/^([a-z][a-z0-9_-]*)@(.+)$/i);
if (!match) throw new CsoError('PREREQUISITE', 'Package manager declaration must bind a named version range');
tool = match[1]; range = match[2];
}
const version = runtime.versions[tool];
if (!version) throw new CsoError('PREREQUISITE', `Qualified runtime ${runtime.id} does not declare a real ${tool} release`);
let satisfies = false;
try { satisfies = Bun.semver.satisfies(version.replace(/^v/, ''), range); } catch {}
if (!satisfies) throw new CsoError('PREREQUISITE', `Qualified ${tool} ${version} does not satisfy source requirement ${range}`);
}
}
export function selectRuntime(profile: string, platform: RuntimePlatform, catalog: RuntimeCatalog = RUNTIME_CATALOG): QualifiedRuntime {
validateRuntimeCatalog(catalog);
const matches = catalog.runtimes.filter(runtime => runtime.platform === platform && (runtime.id === profile || runtime.stack === profile));
if (matches.length === 0) {
const reviewed = catalog.profiles?.filter(item => item.platform === platform && (item.id === profile || item.stack === profile)) ?? [];
const detail = reviewed.length === 1 ? ` Reviewed build profile ${reviewed[0].id} is awaiting a qualified image promotion.` : '';
throw new Error(`MISSING_QUALIFIED_RUNTIME: ${profile} on ${platform}; build, qualify, and review a digest catalog before target execution.${detail}`);
}
if (matches.length !== 1) throw new Error(`AMBIGUOUS_RUNTIME: select an exact qualified runtime id for ${profile}.`);
return matches[0];
}
/** Rollback only pairs the previous catalog with a compatible helper; reports have their own schema. */
export function rollbackCatalog(current: RuntimeCatalog, previous: RuntimeCatalog): RuntimeCatalog {
validateRuntimeCatalog(current); validateRuntimeCatalog(previous);
if (current.previousRevision !== previous.revision || current.helperAbi !== previous.helperAbi) throw new Error('INCOMPATIBLE_RUNTIME_ROLLBACK');
return previous;
}
+130
View File
@@ -0,0 +1,130 @@
/** Reviewed, helper-owned scanner images. Repository/model input cannot add entries. */
import { CsoError, ABI, canonical, sha256 } from './contracts';
import { ISOLATION_POLICY_HASH } from './docker';
import { SCANNER_IDS, ScannerId, scannerPlans } from './scanners';
import type { RuntimePlatform } from './runtime-catalog';
import committedCatalog from './scanner-images/catalog.json';
export interface QualifiedScanner {
id: string;
scanner: ScannerId;
state: 'qualified';
platform: RuntimePlatform;
image: string;
entrypoint: '/opt/cso/entrypoint';
executable: string;
version: string;
/** Hash of canonical {stdout: trimmed version stdout, stderr: trimmed version stderr}. */
versionOutputSha256: string;
helperAbi: 3;
isolationPolicyHash: string;
capabilities: string[];
/** Assets are baked into the immutable image, never acquired during a scan. */
assets?: {
semgrepRules?: { path: string; sha256: string };
advisoryDatabase?: { path: string; contentSha256: string; updatedAt: string; ecosystems: string[] };
};
qualifiedAt: string;
qualification: {
sourceCommit: string;
workflow: string;
sbomDigest: string;
provenanceDigest: string;
verifiedProvenance: true;
containmentPassed: true;
adapterContractPassed: true;
offlineAssetsPassed: true;
};
}
export interface ScannerCatalog {
schemaVersion: 1;
revision: string;
previousRevision?: string | null;
helperAbi: 3;
promotion?: {
sourceCommit: string;
workflow: string;
evidenceDigest: string;
};
scanners: QualifiedScanner[];
}
const HASH = /^[a-f0-9]{64}$/;
const DIGEST = /^sha256:[a-f0-9]{64}$/;
const IMAGE = /^(?:[a-z0-9.-]+(?::[0-9]+)?\/)?[a-z0-9][a-z0-9._/-]*@sha256:[a-f0-9]{64}$/;
const ID = /^[a-z0-9][a-z0-9._-]{0,100}$/;
const QUALIFICATION_WORKFLOW = /^https:\/\/github\.com\/garrytan\/gstack\/actions\/runs\/[0-9]+$/;
const PLATFORMS: RuntimePlatform[] = ['linux/amd64', 'linux/arm64'];
const path = (s: unknown, prefix: string): s is string => typeof s === 'string' && s.startsWith(prefix) && !/[\x00-\x20\\,]/.test(s) && !s.split('/').some(x => x === '..' || x === '.') && !s.includes('//');
function invalid(message: string): never { throw new CsoError('INCOMPATIBLE_INPUT', message); }
function sameStrings(left: string[], right: string[]): boolean {
return canonical([...left].sort()) === canonical([...right].sort());
}
export function scannerVersionHash(stdout: string, stderr = ''): string {
return sha256(canonical({ stdout: stdout.trim(), stderr: stderr.trim() }));
}
/**
* Accept the common scanner `--version` layouts while requiring the catalog
* version to be one complete version token. A substring such as `1.2.3` in
* `11.2.3`, `1.2.30`, or `1.2.3-dev` is not qualification evidence.
*/
export function assertScannerVersionOutput(scanner:ScannerId,version:string,stdout:string,stderr=''):void{
if(!/^[0-9][A-Za-z0-9.+_-]{0,100}$/.test(version))invalid('Scanner version evidence has an invalid expected version');
const output=`${stdout}\n${stderr}`;
if(Buffer.byteLength(stdout)+Buffer.byteLength(stderr)>8192)invalid('Scanner version evidence exceeds the bounded output limit');
const escaped=version.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
const labels:Record<ScannerId,string>={gitleaks:'gitleaks',osv:'(?:osv|osv-scanner)',semgrep:'semgrep',zizmor:'zizmor',trivy:'trivy',schemathesis:'schemathesis'};
const primary=output.split(/\r?\n/).map(line=>line.trim()).find(Boolean)??'';
const exact=new RegExp(`^(?:v?${escaped}|${labels[scanner]},?\\s+(?:version\\s*:?\\s*)?v?${escaped}|version\\s*:\\s*v?${escaped})$`,'i');
if(!exact.test(primary))invalid('Scanner primary version output does not match the exact catalog version');
}
export function validateQualifiedScanner(s: QualifiedScanner): void {
if (!SCANNER_IDS.includes(s.scanner) || !['linux/amd64', 'linux/arm64'].includes(s.platform)) invalid('Unsupported scanner or platform');
const arch = s.platform === 'linux/amd64' ? 'amd64' : 'arm64';
const expectedImage = new RegExp(`^ghcr\\.io/garrytan/gstack/cso-scanners/${s.scanner}-${arch}@sha256:[a-f0-9]{64}$`);
if (s.state !== 'qualified' || !IMAGE.test(s.image) || !expectedImage.test(s.image) || s.entrypoint !== '/opt/cso/entrypoint' || s.helperAbi !== ABI || s.isolationPolicyHash !== ISOLATION_POLICY_HASH) invalid('Scanner profile is not qualified for this helper isolation policy');
if (s.executable !== '/opt/cso/bin/scanner' || !/^[0-9][A-Za-z0-9.+_-]{0,100}$/.test(s.version) || !HASH.test(s.versionOutputSha256)) invalid('Scanner executable and version must be pinned');
if (!Array.isArray(s.capabilities) || !s.capabilities.length || s.capabilities.length > 100 || s.capabilities.some(x => typeof x !== 'string' || !x || x.length > 100)) invalid('Scanner capabilities must be reviewed');
const required = scannerPlans({ snapshotRoot: '/source', offline: true, selected: [s.scanner] })[0].requiredFeatures;
if (!sameStrings(s.capabilities, required)) invalid('Scanner capabilities do not match the helper adapter contract');
const rules = s.assets?.semgrepRules, db = s.assets?.advisoryDatabase;
if (rules && (s.scanner !== 'semgrep' || !path(rules.path, '/policy/catalog/') || !HASH.test(rules.sha256))) invalid('Invalid immutable Semgrep rules');
if (db && (!['osv', 'trivy'].includes(s.scanner) || !path(db.path, '/opt/cso/scanner-data/') || !HASH.test(db.contentSha256) || !Number.isFinite(Date.parse(db.updatedAt)) || !Array.isArray(db.ecosystems) || !db.ecosystems.length || db.ecosystems.some(x => typeof x !== 'string' || !x || x.length > 100))) invalid('Invalid immutable scanner database');
if (s.scanner === 'semgrep' && !rules) invalid('Qualified Semgrep profiles require an immutable rules bundle');
if (['osv', 'trivy'].includes(s.scanner) && !db) invalid(`Qualified ${s.scanner} profiles require an immutable offline database`);
const q = s.qualification;
if (!Number.isFinite(Date.parse(s.qualifiedAt)) || !q || !/^[a-f0-9]{40}$/.test(q.sourceCommit) || !QUALIFICATION_WORKFLOW.test(q.workflow) || !DIGEST.test(q.sbomDigest) || !DIGEST.test(q.provenanceDigest) || q.verifiedProvenance !== true || q.containmentPassed !== true || q.adapterContractPassed !== true || q.offlineAssetsPassed !== true) invalid('Missing trusted scanner qualification');
}
export function validateScannerCatalog(value: unknown): asserts value is ScannerCatalog {
const c = value as ScannerCatalog;
if (!c || c.schemaVersion !== 1 || c.helperAbi !== ABI || typeof c.revision !== 'string' || !ID.test(c.revision) || !Array.isArray(c.scanners) || ![0, SCANNER_IDS.length * PLATFORMS.length].includes(c.scanners.length)) invalid('Incompatible scanner catalog');
if (c.previousRevision !== undefined && c.previousRevision !== null && (typeof c.previousRevision !== 'string' || !ID.test(c.previousRevision) || c.previousRevision === c.revision)) invalid('Invalid previous scanner catalog revision');
if (c.promotion !== undefined && (!/^[a-f0-9]{40}$/.test(c.promotion.sourceCommit) || !QUALIFICATION_WORKFLOW.test(c.promotion.workflow) || !DIGEST.test(c.promotion.evidenceDigest))) invalid('Invalid scanner catalog promotion');
if (c.scanners.length === 0) {
if (c.promotion !== undefined) invalid('Empty scanner catalog cannot have a promotion');
return;
}
if (!c.promotion) invalid('Qualified scanner catalog requires trusted promotion evidence');
const ids = new Set<string>(), identities = new Set<string>();
for (const s of c.scanners) {
if (!s || typeof s.id !== 'string' || !ID.test(s.id) || ids.has(s.id)) invalid('Invalid or duplicate scanner profile');
const identity = `${s.scanner}:${s.platform}`;
if (identities.has(identity)) invalid('Invalid or duplicate scanner profile');
ids.add(s.id); identities.add(identity);
validateQualifiedScanner(s);
if (s.qualification.sourceCommit !== c.promotion.sourceCommit || s.qualification.workflow !== c.promotion.workflow) invalid('Scanner qualification does not match catalog promotion');
}
for (const scanner of SCANNER_IDS) for (const platform of PLATFORMS) if (!identities.has(`${scanner}:${platform}`)) invalid('Incomplete qualified scanner matrix');
if (c.promotion.evidenceDigest !== `sha256:${sha256(canonical(c.scanners))}`) invalid('Scanner catalog promotion does not bind the qualified matrix');
}
export const SCANNER_CATALOG = committedCatalog as unknown as ScannerCatalog;
// A malformed source-controlled catalog must break the helper build/startup;
// it can never degrade into an unreviewed executable fallback.
validateScannerCatalog(SCANNER_CATALOG);
export function selectScanner(scanner: ScannerId, platform: RuntimePlatform, profile?: string, catalog: ScannerCatalog = SCANNER_CATALOG): QualifiedScanner {
validateScannerCatalog(catalog);
const matches = catalog.scanners.filter(s => s.scanner === scanner && s.platform === platform && (!profile || s.id === profile));
if (!matches.length) throw new CsoError('PREREQUISITE', `No qualified ${scanner} image for ${platform}${profile ? ` (${profile})` : ''}; qualify and review an immutable scanner catalog before execution`);
if (matches.length !== 1) throw new CsoError('PREREQUISITE', `Select an exact qualified ${scanner} profile for ${platform}`);
return matches[0];
}
+387
View File
@@ -0,0 +1,387 @@
/** Scanner orchestration is helper-owned; target/scanner commands only enter DockerGroup. */
import * as fs from 'node:fs';
import { randomBytes } from 'node:crypto';
import { join } from 'node:path';
import { Command, CoverageRecord, CsoError, HttpAssertion, RunPolicy, SnapshotManifest, canonical, object, relativePath, sha256, snapshotPathHandleId, snapshotReference, string, strings, validateCommand, validateVerificationObservation, type ErrorCode } from './contracts';
import { DockerEndpoint, DockerGroup, dockerEndpoint } from './docker';
import { inspectPreparation, type CsoStack } from './preparation';
import { redact } from './process';
import { QualifiedRuntime, RUNTIME_CATALOG, RuntimeCatalog, RuntimePlatform, assertRuntimeCompatible, selectRuntime } from './runtime-catalog';
import { QualifiedScanner, SCANNER_CATALOG, ScannerCatalog, assertScannerVersionOutput, scannerVersionHash, selectScanner } from './scanner-catalog';
import { ScannerExecution, ScannerGap, ScannerId, ScannerOutcome, ScannerPlan, parseScannerOutput, scannerPlans } from './scanners';
import { assertSnapshot } from './snapshot';
import { hasPendingWatchdogCleanup, secureDirectory } from './state';
import { PublicArchiveCache, publicArchiveCacheRoot } from './cache';
import { admitPreparationRuntime, admitPreparationSidecar, PreparationExecutor, type PreparationSandboxRunner, type RailsDatabaseSelection } from './preparation-executor';
import type { PreparedDatabaseContract } from './preparation-executor';
import { DockerPreparationSandboxRunner } from './preparation-docker';
import { canonicalStartPlan, type CanonicalStartPlan } from './verification';
export interface ScannerRequest {
profile?: string;
api?: {
runtimeProfile: string;
port: number;
start: Command;
control: HttpAssertion;
boundaryFiles: string[];
schema: Record<string, unknown>;
operationIds: string[];
seed?: number;
maxExamples?: number;
};
}
export interface ScannerRunInput {
id: ScannerId;
runId: string;
runDir: string;
manifest: SnapshotManifest;
policy: RunPolicy;
executionDeadline: number;
platform: RuntimePlatform;
request?: ScannerRequest;
watchdogPath: string;
}
export interface ScannerRunRecord {
outcome: ScannerOutcome;
coverage: CoverageRecord;
provenance: {
scannerCatalog: string;
profile: string | null;
image: string | null;
platform: RuntimePlatform;
isolationPolicyHash: string | null;
sourceHash: string;
requestHash: string;
versionOutputSha256: string | null;
assets: QualifiedScanner['assets'] | null;
network: 'none' | 'isolated-loopback';
preparation: ScannerApplicationPreparation['proof'] | null;
};
}
export interface ScannerApplicationPreparation {
sourceRoot: string;
environment: Record<string, string>;
database?: PreparedDatabaseContract;
proof: {
dependencyClosureHash: string;
preparedManifestHash: string;
sourceProjectionHash: string;
receiptHash: string;
executionEnvironmentHash: string;
databaseHash: string;
};
cleanup(): Promise<void>;
}
export interface ScannerRunner {
version(): Promise<ScannerExecution>;
scan(): Promise<ScannerExecution>;
cleanup(): Promise<void>;
}
/** The trusted HTTP control probe is always the bounded verifier process. */
export function schemathesisControlRole(): 'verifier' { return 'verifier'; }
export interface ScannerRunnerContext {
input: ScannerRunInput;
plan: ScannerPlan;
profile: QualifiedScanner;
runtime?: QualifiedRuntime;
application?: ScannerApplicationPreparation;
deadline: number;
}
export type ScannerRunnerFactory = (context: ScannerRunnerContext) => Promise<ScannerRunner>;
export type ScannerApplicationPreparer = (context: {
input: ScannerRunInput;
runtime: QualifiedRuntime;
stack: CsoStack;
startPlan: CanonicalStartPlan;
deadline: number;
catalog: RuntimeCatalog;
}) => Promise<ScannerApplicationPreparation>;
export interface ScannerRunDependencies {
catalog?: ScannerCatalog;
runtimes?: RuntimeCatalog;
/** Unit tests inject a runner to check dispatch/claims; this does not attest containment. */
runnerFactory?: ScannerRunnerFactory;
/** Unit tests may inject a materializer; production always uses the qualified offline preparation path. */
applicationPreparer?: ScannerApplicationPreparer;
}
function exact(v: Record<string, unknown>, allowed: string[], name: string): void {
for (const key of Object.keys(v)) if (!allowed.includes(key)) throw new CsoError('INVALID_SCHEMA', `Unexpected ${name} field: ${key}`);
}
function boundedInt(v: unknown, min: number, max: number, name: string): number {
if (!Number.isSafeInteger(v) || (v as number) < min || (v as number) > max) throw new CsoError('INVALID_SCHEMA', `${name} must be ${min}..${max}`);
return v as number;
}
function control(value: unknown): HttpAssertion {
const v = object(value, 'API control'), expected = object(v.expected, 'API control expected');
exact(v, ['name', 'path', 'method', 'headers', 'body', 'expected'], 'API control');
exact(expected, ['status', 'includes', 'excludes'], 'API control expected');
const path = string(v.path, 'API control path', 4096);
if (!path.startsWith('/') || path.startsWith('//') || /[\r\n\\]/.test(path)) throw new CsoError('INVALID_SCHEMA', 'API control path must remain on numeric loopback');
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(v.method)) throw new CsoError('INVALID_SCHEMA', 'Invalid API control method');
const headers: Record<string, string> = {};
for (const [key, value] of Object.entries(v.headers === undefined ? {} : object(v.headers, 'API control headers'))) {
if (!/^[A-Za-z0-9-]{1,100}$/.test(key) || typeof value !== 'string' || value.length > 8192 || /[\r\n]/.test(value)) throw new CsoError('INVALID_SCHEMA', 'Invalid API control header');
headers[key] = value;
}
return { name: string(v.name, 'API control name', 200), path, method: v.method, headers,
...(v.body === undefined ? {} : { body: string(v.body, 'API control body', 65536) }),
expected: { status: boundedInt(expected.status, 100, 599, 'API control status'),
...(expected.includes === undefined ? {} : { includes: string(expected.includes, 'API control includes', 8192) }),
...(expected.excludes === undefined ? {} : { excludes: string(expected.excludes, 'API control excludes', 8192) }) } };
}
/** Accept a bounded OpenAPI document, with internal references and selected path operations only. */
export function validateScannerRequest(value: unknown, id: ScannerId): ScannerRequest {
const v = object(value, 'scanner request');
exact(v, ['profile', 'api'], 'scanner request');
const request: ScannerRequest = v.profile === undefined ? {} : { profile: string(v.profile, 'scanner profile', 100) };
if (v.api === undefined) return request;
if (id !== 'schemathesis') throw new CsoError('INVALID_SCHEMA', 'Only Schemathesis accepts application execution inputs');
const api = object(v.api, 'API scan');
exact(api, ['runtimeProfile', 'port', 'start', 'control', 'boundaryFiles', 'schema', 'operationIds', 'seed', 'maxExamples'], 'API scan');
const schema = object(api.schema, 'OpenAPI schema'), operations = strings(api.operationIds, 'operation IDs');
if (operations.length < 1 || operations.length > 20 || new Set(operations).size !== operations.length || operations.some(x => x.length > 200 || /[\x00-\x1f]/.test(x))) throw new CsoError('INVALID_SCHEMA', 'Declare 1..20 unique bounded operation IDs');
if (typeof schema.openapi !== 'string' || !/^3\.[01]\.\d+$/.test(schema.openapi)) throw new CsoError('PREREQUISITE', 'Schemathesis requires a reviewed OpenAPI 3.0/3.1 JSON document');
if (Buffer.byteLength(JSON.stringify(schema)) > 262144) throw new CsoError('INVALID_SCHEMA', 'OpenAPI schema exceeds 256 KiB');
let nodes = 0;
const inspect = (x: unknown, depth: number): void => {
if (++nodes > 50_000 || depth > 32) throw new CsoError('INVALID_SCHEMA', 'OpenAPI schema exceeds structural bounds');
if (!x || typeof x !== 'object') return;
for (const [key, value] of Object.entries(x)) {
if (['__proto__', 'prototype', 'constructor', 'externalValue', 'callbacks', 'webhooks'].includes(key) || /hooks?/i.test(key)) throw new CsoError('PREREQUISITE', 'OpenAPI external examples, callbacks, webhooks, and hooks are not admitted');
if (key === '$ref' && (typeof value !== 'string' || !value.startsWith('#/'))) throw new CsoError('PREREQUISITE', 'OpenAPI references must be internal JSON pointers');
if (key === 'servers' && (!Array.isArray(value) || value.length)) throw new CsoError('PREREQUISITE', 'Remove server overrides from the reviewed API harness; its target is the isolated loopback application');
inspect(value, depth + 1);
}
};
inspect(schema, 0);
const declared: string[] = [];
for (const [path, item] of Object.entries(object(schema.paths, 'OpenAPI paths'))) {
if (!path.startsWith('/') || path.startsWith('//') || /[\r\n\\?#]/.test(path)) throw new CsoError('INVALID_SCHEMA', 'OpenAPI paths must be relative to the loopback target');
const methods = object(item, 'OpenAPI path');
for (const method of ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']) {
if (methods[method] === undefined) continue;
const op = object(methods[method], 'OpenAPI operation');
if (typeof op.operationId === 'string') declared.push(op.operationId);
}
}
if (operations.some(op => declared.filter(x => x === op).length !== 1)) throw new CsoError('INVALID_SCHEMA', 'Every selected operation must identify exactly one declared OpenAPI path operation');
const boundaries = strings(api.boundaryFiles, 'API boundary files').map(snapshotReference);
if (!boundaries.length || new Set(boundaries).size !== boundaries.length) throw new CsoError('INVALID_SCHEMA', 'API scan needs unique security-boundary source paths');
request.api = { runtimeProfile: string(api.runtimeProfile, 'API runtime profile', 100), port: boundedInt(api.port, 1024, 65535, 'API port'), start: validateCommand(api.start, 'API start'), control: control(api.control), boundaryFiles: boundaries, schema, operationIds: operations,
...(api.seed === undefined ? {} : { seed: boundedInt(api.seed, 1, 2147483647, 'API seed') }),
...(api.maxExamples === undefined ? {} : { maxExamples: boundedInt(api.maxExamples, 1, 100, 'API maxExamples') }) };
const raw = JSON.stringify(request);
if (redact(raw) !== raw) throw new CsoError('REDACTION_FAILED', 'Scanner harness contains secret-bearing material; use synthetic inputs');
return request;
}
/** Resolve only helper-issued path references before any application command reaches containment. */
export function resolveScannerRequestPaths(manifest:SnapshotManifest,request:ScannerRequest):ScannerRequest{
if(!request.api)return request;
const resolve=(reference:string):string=>{const id=snapshotPathHandleId(reference);if(!id)return relativePath(reference);const entry=manifest.entries.find(item=>item.pathId===id);if(!entry)throw new CsoError('INVALID_SCHEMA',`API path handle is outside the retained snapshot: ${reference}`);return entry.path;};
const argument=(value:string):string=>{if(snapshotPathHandleId(value))return resolve(value);if(value.startsWith('./')&&snapshotPathHandleId(value.slice(2)))return `./${resolve(value.slice(2))}`;return value;};
return{...request,api:{...request.api,start:{...request.api.start,args:request.api.start.args.map(argument)},boundaryFiles:request.api.boundaryFiles.map(resolve)}};
}
export function scannerCoverage(outcome: ScannerOutcome, scope: string): CoverageRecord {
return { domain: `scanner:${outcome.tool}`, scope, status: outcome.status === 'complete' ? 'assessed' : outcome.status,
method: outcome.tool === 'sarif' ? 'bounded untrusted SARIF import' : 'qualified offline Docker scanner; candidate evidence only',
gaps: outcome.gaps.map(g => g.message), exclusions: outcome.exclusions,
evidence: [`${outcome.candidates.length} scanner candidates; plan ${outcome.planSha256}`],
tool: { name: outcome.tool, version: outcome.version ?? 'unavailable', freshness: outcome.databaseUpdatedAt ?? 'not reported', outcome: outcome.status } };
}
function failure(plan: ScannerPlan, error: unknown, version?: string): ScannerOutcome {
const e = error instanceof CsoError ? error : new CsoError('ISOLATION_FAILED', 'Scanner execution failed before bounded evidence was established');
const codes: Record<ErrorCode, ScannerGap['code']> = {
INVALID_ARGUMENT: 'INVALID_OUTPUT', INVALID_SCHEMA: 'INVALID_OUTPUT', MISSING_INPUT: 'MISSING_INPUT', SNAPSHOT_RACE: 'SNAPSHOT_RACE',
UNSAFE_PATH: 'UNSAFE_PATH', REDACTION_FAILED: 'REDACTION_FAILED', PERSISTENCE_FAILED: 'PERSISTENCE_FAILED', TOOL_UNAVAILABLE: 'UNAVAILABLE',
TOOL_FAILED: 'TOOL_FAILED', ISOLATION_FAILED: 'ISOLATION_FAILED', INSUFFICIENT_CAPACITY: 'INSUFFICIENT_CAPACITY', DEADLINE: 'TIMEOUT',
CANCELLED: 'CANCELLED', PREREQUISITE: 'PREREQUISITE', INCOMPATIBLE_INPUT: 'PREREQUISITE', ASSERTION_FAILED: 'TOOL_FAILED',
};
const code = codes[e.code];
return { ...parseScannerOutput(plan, { stdout: '', exitCode: null, version }), status: 'not_assessed', candidates: [], gaps: [{ code, message: e.message }] };
}
/** Empty catalogs and missing assets produce coverage gaps without opening Docker. */
export async function executeScanner(input: ScannerRunInput, dependencies: ScannerRunDependencies = {}): Promise<ScannerRunRecord> {
const identityRequest = validateScannerRequest(input.request ?? {}, input.id), catalog = dependencies.catalog ?? SCANNER_CATALOG;
const timeout = Math.min(300, Math.floor((input.executionDeadline - Date.now()) / 1000));
let profile: QualifiedScanner | undefined, runtime: QualifiedRuntime | undefined, observedVersion: string | undefined, versionHash: string | null = null;
let application: ScannerApplicationPreparation | undefined,request=identityRequest;
let plan = scannerPlans({ snapshotRoot: '/source', offline: input.policy.offline, selected: [input.id], deadlineSeconds: Math.max(1, timeout) })[0];
let outcome: ScannerOutcome, runner: ScannerRunner | undefined;
try {
if (timeout < 1) throw new CsoError('DEADLINE', 'No scanner time remains before the reporting reserve');
assertSnapshot(input.runDir, input.manifest);
request=resolveScannerRequestPaths(input.manifest,identityRequest);
if (input.id === 'schemathesis' && input.policy.mode !== 'comprehensive') throw new CsoError('PREREQUISITE', 'Schemathesis requires comprehensive mode; daily audits do not execute applications');
profile = selectScanner(input.id, input.platform, request.profile, catalog);
const api = request.api;
plan = scannerPlans({ snapshotRoot: '/source', offline: input.policy.offline, selected: [input.id], deadlineSeconds: timeout,
tools: { [input.id]: { available: true, version: profile.version, capabilities: profile.capabilities } },
semgrepRules: profile.assets?.semgrepRules?.path, advisoryCache: profile.assets?.advisoryDatabase?.path,
...(api ? { schemaPath: '/policy/openapi.json', baseUrl: `http://127.0.0.1:${api.port}/`, operationIds: api.operationIds, seed: api.seed, maxExamples: api.maxExamples } : {}) })[0];
if (plan.prerequisites.length) throw new CsoError('PREREQUISITE', plan.prerequisites.join('; '));
if (input.id === 'schemathesis') {
if (!api) throw new CsoError('PREREQUISITE', 'Schemathesis requires a reviewed API harness and legitimate control');
for (const file of api.boundaryFiles) {
const entry = input.manifest.entries.find(e => e.path === file);
if (!entry || !entry.executionHash || entry.transformation) throw new CsoError('INCOMPATIBLE_INPUT', `API security boundary is missing or transformed: ${file}`);
}
try { runtime = selectRuntime(api.runtimeProfile, input.platform, dependencies.runtimes ?? RUNTIME_CATALOG); }
catch { throw new CsoError('PREREQUISITE', `Qualified application runtime is unavailable: ${api.runtimeProfile}`); }
if (!['node', 'bun', 'python', 'rails'].includes(runtime.stack)) throw new CsoError('INCOMPATIBLE_INPUT', 'Schemathesis requires a qualified application runtime');
const stack = runtime.stack as CsoStack, sourceRoot = join(input.runDir, 'snapshot');
const preparation = inspectPreparation(sourceRoot, stack);
assertRuntimeCompatible(preparation, runtime);
const startPlan = canonicalStartPlan(sourceRoot, stack, api.port);
if (canonical(api.start) !== canonical(startPlan.command)) throw new CsoError('INVALID_SCHEMA', `API start must use the helper-derived ${startPlan.kind} command`);
for (const file of startPlan.entrypointFiles) if (!api.boundaryFiles.includes(file))
throw new CsoError('INVALID_SCHEMA', `API boundary files must include canonical startup input: ${file}`);
application = await (dependencies.applicationPreparer ?? prepareDockerScannerApplication)({
input: { ...input, request }, runtime, stack, startPlan,
deadline: Math.min(input.executionDeadline, Date.now() + timeout * 1000), catalog: dependencies.runtimes ?? RUNTIME_CATALOG,
});
const preparedStart = canonicalStartPlan(application.sourceRoot, stack, api.port);
if (preparedStart.signature !== startPlan.signature || canonical(preparedStart.command) !== canonical(startPlan.command))
throw new CsoError('ISOLATION_FAILED', 'Offline API preparation changed the canonical application startup inputs');
}
runner = await (dependencies.runnerFactory ?? createDockerScannerRunner)({ input: { ...input, request }, plan, profile, runtime, application, deadline: Math.min(input.executionDeadline, Date.now() + timeout * 1000) });
const version = await runner.version();
if (version.exitCode !== 0 || version.timedOut || version.truncated || version.unavailable || Buffer.byteLength(version.stdout) + Buffer.byteLength(version.stderr ?? '') > 8192) throw new CsoError('TOOL_UNAVAILABLE', 'Scanner version probe did not complete within the qualified sandbox');
assertScannerVersionOutput(profile.scanner,profile.version,version.stdout,version.stderr);
versionHash = scannerVersionHash(version.stdout, version.stderr);
if (versionHash !== profile.versionOutputSha256) throw new CsoError('INCOMPATIBLE_INPUT', 'Scanner version output does not match its reviewed image profile');
observedVersion = profile.version;
const execution = await runner.scan();
assertSnapshot(input.runDir, input.manifest);
outcome = parseScannerOutput(plan, { ...execution, version: profile.version, databaseUpdatedAt: profile.assets?.advisoryDatabase?.updatedAt });
} catch (error) { outcome = failure(plan, error, observedVersion); }
finally {
let cleanupError: unknown;
if (runner) try { await runner.cleanup(); } catch (error) { cleanupError = error; }
if (application) try { await application.cleanup(); } catch (error) { cleanupError ??= error; }
if (cleanupError) outcome = failure(plan, cleanupError, observedVersion);
}
return { outcome: outcome!, coverage: scannerCoverage(outcome!, input.policy.scope), provenance: {
scannerCatalog: catalog.revision, profile: profile?.id ?? null, image: profile?.image ?? null, platform: input.platform,
isolationPolicyHash: profile?.isolationPolicyHash ?? null, sourceHash: input.manifest.executionHash, requestHash: sha256(canonical(identityRequest)), versionOutputSha256: versionHash,
assets: profile?.assets ?? null, network: plan.network === 'loopback' ? 'isolated-loopback' : 'none', preparation: application?.proof ?? null } };
}
export async function prepareDockerScannerApplication(context: Parameters<ScannerApplicationPreparer>[0],dependencies:{endpoint?:DockerEndpoint;runnerFactory?:(options:ConstructorParameters<typeof DockerPreparationSandboxRunner>[0])=>PreparationSandboxRunner;cacheRoot?:string}={}): Promise<ScannerApplicationPreparation> {
const { input, runtime, stack, deadline, catalog } = context;
const root = secureDirectory(join(input.runDir, 'supervision', `scanner-preparation-${randomBytes(12).toString('hex')}`));
let executor: PreparationExecutor | undefined, prepared: Awaited<ReturnType<PreparationExecutor['prepareOffline']>> | undefined;
try {
const plan = inspectPreparation(join(input.runDir, 'snapshot'), stack);
const admission = admitPreparationRuntime({ plan, platform: input.platform, profile: runtime.id, catalog });
const endpoint = dependencies.endpoint??await dockerEndpoint(root),runnerOptions={ endpoint, watchdogPath: input.watchdogPath,
runRoot: root, controlRoot: secureDirectory(join(root, 'execution')), admission },runner=dependencies.runnerFactory?dependencies.runnerFactory(runnerOptions):new DockerPreparationSandboxRunner(runnerOptions);
executor = new PreparationExecutor({ cache: new PublicArchiveCache({ root: dependencies.cacheRoot??publicArchiveCacheRoot(), stagingRoot: secureDirectory(join(root, 'staging')) }),
runner, materializationRoot: secureDirectory(join(root, 'materializations')) });
const closure = await executor.acquire({ plan, admission, snapshot: join(input.runDir, 'snapshot'), deadline, offline: input.policy.offline });
let database:RailsDatabaseSelection|undefined;
if(stack==='rails'){
if(!plan.database?.selected)throw new CsoError('PREREQUISITE','Rails API preparation could not select one locked database adapter');
database=plan.database.selected==='postgresql'
?{adapter:'postgresql',sidecar:admitPreparationSidecar({platform:input.platform,catalog})}:{adapter:'sqlite'};
}
prepared = await executor.prepareOffline({ plan, admission, snapshot: join(input.runDir, 'snapshot'), closure, deadline, database });
const proof = { dependencyClosureHash: prepared.dependencyClosureHash, preparedManifestHash: prepared.preparedManifestHash,
sourceProjectionHash: prepared.sourceProjectionHash, receiptHash: prepared.receiptHash,
executionEnvironmentHash: sha256(canonical(prepared.executionEnvironment)), databaseHash: prepared.databaseHash };
let cleaned = false;
return { sourceRoot: prepared.preparedRoot, environment: prepared.executionEnvironment, database: prepared.database, proof, cleanup: async () => {
if (cleaned) return; cleaned = true;
await executor!.dispose(prepared!);
fs.rmSync(root, { recursive: true, force: false });
} };
} catch (error) {
let cleanupError:unknown;
if (prepared && executor) try { await executor.dispose(prepared); } catch (failed) { cleanupError=failed; }
// A failed Docker/retained-copy cleanup deliberately hands ownership to a
// detached watchdog. Its journals and label-sweep scratch files live below
// this root, so only remove the tree after every watchdog acknowledged.
let pending=true;try{pending=hasPendingWatchdogCleanup(input.runDir);}catch(failed){cleanupError??=failed;}
if(!cleanupError&&!pending)try { fs.rmSync(root, { recursive: true, force: false }); } catch {}
if(cleanupError)throw cleanupError;
throw error;
}
}
/** No arbitrary runner configuration crosses this boundary; images and paths came from the catalog. */
export async function createDockerScannerRunner(context: ScannerRunnerContext): Promise<ScannerRunner> {
const { input, plan, profile, runtime, application, deadline } = context;
const attempt = `scanner-${input.id}-${randomBytes(12).toString('hex')}`;
const controlDir = secureDirectory(join(input.runDir, 'supervision', attempt));
const policyDir = secureDirectory(join(controlDir, 'policy'));
const files: Array<{ host: string; container: string }> = [];
const writePolicy = (container: string, content: string): void => {
if (redact(content) !== content) throw new CsoError('REDACTION_FAILED', 'Scanner policy contains secret-bearing material');
const host = join(policyDir, String(files.length)); fs.writeFileSync(host, content, { mode: 0o600, flag: 'wx' });
files.push({ host, container });
};
let group: DockerGroup | undefined;
try {
for (const file of plan.trustedFiles) writePolicy(file.path, file.content);
if (input.request?.api) writePolicy('/policy/openapi.json', JSON.stringify(input.request.api.schema));
const endpoint: DockerEndpoint = await dockerEndpoint(controlDir);
group = await DockerGroup.create(endpoint, attempt, controlDir, deadline, profile.image, input.watchdogPath);
const createScanner=()=>group!.createContainer({ role: runtime ? 'verifier' : 'app', image: profile.image, source: join(input.runDir, 'snapshot'), command: ['/bin/sleep', '2147483647'], env: plan.env, readonlyFiles: files });
let scanner = await createScanner();
await group.start(scanner);
const capture = async (command: string[]): Promise<ScannerExecution> => {
if(!scanner)throw new CsoError('ISOLATION_FAILED','Scanner container is unavailable');
const result = await group!.execCapture(scanner, command, { workdir: '/work', env: plan.env });
return { stdout: result.stdout, stderr: result.stderr, exitCode: result.code };
};
return {
version: () => capture([profile.executable, ...plan.versionArgs]),
scan: async () => {
const api = input.request?.api;
if (api && runtime) {
if (!application) throw new CsoError('ISOLATION_FAILED', 'Schemathesis application was not materialized through offline preparation');
const env={ ...application.environment, PORT: String(api.port), HOST: '127.0.0.1', NODE_ENV: 'test', RAILS_ENV: 'test', RACK_ENV: 'test', PYTHONUNBUFFERED: '1', CI: '1', SECRET_KEY_BASE: 'cso-synthetic-test-key' };
const rails=runtime.stack==='rails';
if(rails){await group!.removeContainer(scanner);scanner='';}
if(application.database?.adapter==='postgresql'){
const databaseFile=join(policyDir,'postgresql.databases'),names=application.database.connections.map(name=>`cso_${name}`);
if(!names.length||names.some(name=>!/^cso_[A-Za-z_][A-Za-z0-9_]{0,47}$/.test(name)))throw new CsoError('INCOMPATIBLE_INPUT','Prepared PostgreSQL connection names are invalid');
fs.writeFileSync(databaseFile,names.join('\n')+'\n',{mode:0o444,flag:'wx'});
const postgres=await group!.createContainer({role:'postgres',image:application.database.sidecar.image,command:['/opt/cso/run-postgresql','/policy/postgresql.databases'],postgresDatabasePolicy:databaseFile});await group!.start(postgres);
let ready=false;for(let attempt=0;attempt<100&&!ready;attempt++){const checked=await group!.execCapture(postgres,['/opt/cso/postgresql-ready','/policy/postgresql.databases']);ready=checked.code===0;if(!ready)await new Promise(resolveWait=>setTimeout(resolveWait,50));}
if(!ready)throw new CsoError('TOOL_FAILED','Disposable PostgreSQL did not become ready for Rails API scanning');
}
const app = await group!.createContainer({ role: 'app', image: runtime.image, source: application.sourceRoot, env,
command:rails?['/opt/cso/run-app','/bin/sleep','2147483647']:['/opt/cso/run-app', api.start.executable, ...api.start.args] });
await group!.start(app);
if(rails){const clean=['/usr/bin/env','-i',...Object.entries(env).sort(([a],[b])=>a.localeCompare(b)).map(([key,value])=>`${key}=${value}`),'/usr/local/bin/bundle','exec','rails','db:prepare'];const prepared=await group!.execCapture(app,clean,{workdir:'/work'});if(prepared.code!==0)throw new CsoError('TOOL_FAILED','Rails API database preparation failed');await group!.execDetached(app,[api.start.executable,...api.start.args]);}
const security = { ...api.control, vulnerable: { status: api.control.expected.status === 599 ? 598 : 599 } };
const controlFile = join(policyDir, 'control.json');
fs.writeFileSync(controlFile, JSON.stringify({ phase: 'after', port: api.port, legitimate: [api.control], security }), { mode: 0o600, flag: 'wx' });
const probe = await group!.createContainer({ role: schemathesisControlRole(), image: runtime.image, command: ['/opt/cso/verifier', '/policy/control.json'], readonlyFiles: [{ host: controlFile, container: '/policy/control.json' }] });
const observed = await group!.startAttach(probe); await group!.removeContainer(probe);
let valid = false;
try { const v = validateVerificationObservation(JSON.parse(observed.output)); valid = observed.code === 0 && v.booted && v.legitimate && v.security === 'pass'; } catch {}
if (!valid) throw new CsoError('PREREQUISITE', 'API application boot or legitimate control failed; no Schemathesis requests were sent');
if(rails){scanner=await createScanner();await group!.start(scanner);}
}
const execution = await capture([profile.executable, ...plan.args]);
if (plan.outputPath) {
const report = await capture(['/bin/cat', plan.outputPath]);
if (report.exitCode !== 0) throw new CsoError('PREREQUISITE', 'Scanner did not produce its required bounded report file');
return { ...execution, stdout: report.stdout, stderr: [execution.stderr, report.stderr].filter(Boolean).join('\n') };
}
return execution;
},
cleanup: async () => { await group!.cleanup(); fs.rmSync(policyDir, { recursive: true, force: true }); },
};
} catch (error) {
if (group) await group.cleanup();
fs.rmSync(policyDir, { recursive: true, force: true });
throw error;
}
}
+48
View File
@@ -0,0 +1,48 @@
# CSO scanner image release inputs
The committed scanner catalog is intentionally empty until trusted CI produces
real qualification evidence. Nothing in this directory authorizes a host tool,
a mutable tag, or an agent-supplied image.
`build-inputs.json` is the review gate. A reviewed file contains exactly six
profiles, two native image digests per profile, and one immutable SBOM generator.
Every image records its source repository and commit, signer workflow and
digest, and reviewed canonical SLSA/SPDX statement-set digests. CI uses
`gh attestation verify` with all of those identities and rejects a statement
digest mismatch before the image participates in a build. Each upstream image
must already expose the declared scanner executable. Semgrep images must contain the
reviewed local rules at `/policy/catalog/...`. OSV and Trivy images must contain
their complete offline data below `/opt/cso/scanner-data/...`; the release job
copies that path out of the staged image and recomputes its canonical content
hash before running the network-none adapter test. Preparing those asset-bearing
upstream images is an external publication prerequisite, not something an audit
may download on demand.
The wrapper normalizes every image to `/opt/cso/entrypoint` and
`/opt/cso/bin/scanner`, embeds the trusted HTTP assertion verifier needed by the
Schemathesis qualification fixture, and runs as a fixed non-root image user.
The product runner still supplies the effective host uid, read-only root,
dropped capabilities, seccomp, no-new-privileges, bounded tmpfs and shared
memory, network
namespace, disabled daemon logging, and watchdog cleanup.
Images that declare `VOLUME` are rejected; exact cleanup also removes anonymous
volumes defensively.
`.github/workflows/cso-scanner-images.yml` lets a dispatched branch run only its
read-only input and contract checks. Publishing and native qualification require
a dispatch from protected `main` plus approval through the
`cso-scanner-release` environment. That protected lane emits a complete
`catalog.json` proposal with image, version-output, asset, SBOM, provenance,
source-commit, and workflow identities. Selecting the promotion input may then
open a catalog update pull request. Review that PR like code. The helper
validates the committed catalog at startup and has no fallback when a profile is
absent or incompatible.
Promotion also requires the proposal's `previousRevision` to equal the catalog
currently on `main`, so a stale qualification run cannot overwrite a newer one.
GHCR creates a new scanner package private. A package administrator must make
the bootstrap package public in GitHub's package settings before its wrapper can
qualify; GitHub documents this change as irreversible. The qualification row
and the protected promotion job both require public package metadata and pull
the exact platform digest through a fresh Docker client config containing empty
`auths`. A workflow GHCR login cannot satisfy this gate.
+8
View File
@@ -0,0 +1,8 @@
{
"schemaVersion": 1,
"helperAbi": 3,
"state": "pending",
"sbomGenerator": null,
"profiles": [],
"instructions": "Add one reviewed, digest-pinned SBOM generator and exactly one profile for each scanner. Every platform image and the generator must declare its GitHub repository, source commit, release, signer workflow and signer digest, plus canonical SLSA and SPDX statement-set digests that the workflow re-verifies cryptographically before use. Semgrep bases must contain the reviewed rules bundle. OSV and Trivy bases must contain the declared offline databases. Change state to reviewed only after all identities and content hashes have been independently checked."
}
+7
View File
@@ -0,0 +1,7 @@
{
"schemaVersion": 1,
"revision": "cso-scanners-v3-unqualified",
"previousRevision": null,
"helperAbi": 3,
"scanners": []
}
+20
View File
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"helperAbi": 3,
"state": "pending",
"platforms": ["linux/amd64", "linux/arm64"],
"scanners": ["gitleaks", "osv", "semgrep", "zizmor", "trivy", "schemathesis"],
"requiredChecks": [
"native image build from a reviewed immutable upstream digest",
"verified upstream and wrapper provenance plus SPDX SBOM",
"exact version-output hash through the fixed entrypoint",
"adapter feature contract and representative output normalization",
"network-none target execution with read-only source and exact cleanup",
"secret canary redaction and malformed-output failure",
"Semgrep rules or OSV/Trivy database content hash matches reviewed input",
"OSV and Trivy complete a representative scan with no network",
"Schemathesis reaches only an admitted loopback fixture and completes selected operations"
],
"promotion": "CI emits a complete immutable catalog proposal. Only an environment-approved run from protected main may open the source-controlled catalog promotion PR.",
"rollback": "Restore the previous compatible scanner catalog revision; never replace a missing profile with a host executable or mutable image tag."
}
+19
View File
@@ -0,0 +1,19 @@
# BASE_IMAGE is a separately reviewed scanner image digest. For Semgrep, OSV,
# and Trivy it must already contain the reviewed immutable rules/database path.
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
ARG SCANNER_EXECUTABLE
USER root
RUN set -eu; \
case "$SCANNER_EXECUTABLE" in /*) ;; *) exit 64 ;; esac; \
test -x "$SCANNER_EXECUTABLE"; \
test -x /bin/sh; test -x /bin/sleep; test -x /bin/cp; test -x /bin/cat; \
mkdir -p /opt/cso/bin /work /source /policy /fixtures; \
ln -s "$SCANNER_EXECUTABLE" /opt/cso/bin/scanner; \
chown 10001:10001 /work
COPY --chmod=0555 images/entrypoint /opt/cso/entrypoint
COPY --chmod=0555 images/run-app /opt/cso/run-app
COPY --chmod=0555 images/gstack-cso-verifier /opt/cso/verifier
USER 10001:10001
WORKDIR /work
ENTRYPOINT ["/opt/cso/entrypoint"]
+504
View File
@@ -0,0 +1,504 @@
/**
* CSO scanner boundary: declarative sandbox plans and bounded, untrusted evidence.
* This module never spawns tools, downloads rules, reads project config, or grants
* findings a supported/reproduced/tested status. The runner enforces every plan.
*/
import { createHash } from 'node:crypto';
import { posix } from 'node:path';
import { redactFindingSpans } from '../redact-engine';
export const SCANNER_IDS = ['gitleaks', 'osv', 'semgrep', 'zizmor', 'trivy', 'schemathesis'] as const;
export type ScannerId = typeof SCANNER_IDS[number];
export type ScannerFormat = 'gitleaks-json' | 'osv-json' | 'semgrep-json' | 'sarif' | 'trivy-json' | 'schemathesis-json';
export const MAX_SCANNER_OUTPUT_BYTES = 1_048_576;
const MAX_CANDIDATES = 5_000;
export interface ScannerPlan {
id: ScannerId;
executableName: string;
args: string[];
versionArgs: string[];
requiredFeatures: string[];
format: ScannerFormat;
execution: 'sandbox';
network: 'none' | 'loopback';
/** No inherited environment, PATH, tokens, or project configuration. */
env: Record<string, string>;
cwd: string;
sourceRoot: string;
outputPath?: string;
trustedFiles: Array<{ path: string; content: string }>;
prerequisites: string[];
timeoutSeconds: number;
maxOutputBytes: number;
coverage: { domain: string; scope: string[]; exclusions: string[] };
provenanceSources: string[];
documentationInspectedAt: string;
}
export interface ScannerOptions {
snapshotRoot: string;
offline: boolean;
tools?: Partial<Record<ScannerId, { available: boolean; version?: string; capabilities?: string[] }>>;
selected?: ScannerId[];
/** Paths below policyRoot must be trusted, immutable inputs, never repo files. */
policyRoot?: string;
semgrepRules?: string;
advisoryCache?: string;
schemaPath?: string;
baseUrl?: string;
seed?: number;
maxExamples?: number;
operationIds?: string[];
deadlineSeconds?: number;
/** A separately sanitized inert Git history, not the original .git directory. */
gitHistory?: string;
}
export interface ScannerCandidate {
id: string;
tool: ScannerId | 'sarif';
ruleId: string;
message: string;
reportedSeverity: 'critical' | 'high' | 'medium' | 'low' | 'info' | 'unknown';
location?: { path: string; line?: number; column?: number };
advisoryIds: string[];
dependency?: { name: string; version?: string; ecosystem?: string; reachability: 'unknown'; exposure: 'unknown' };
operation?: string;
suppressed: boolean;
evidence: 'scanner-candidate';
trust: 'untrusted';
}
export interface ScannerGap {
code: 'UNAVAILABLE' | 'PREREQUISITE' | 'TIMEOUT' | 'OUTPUT_LIMIT' | 'INVALID_OUTPUT' | 'TOOL_FAILED' |
'REDACTION_FAILED' | 'ISOLATION_FAILED' | 'PERSISTENCE_FAILED' | 'SNAPSHOT_RACE' | 'CANCELLED' |
'INSUFFICIENT_CAPACITY' | 'UNSAFE_PATH' | 'MISSING_INPUT' | 'INCOMPATIBLE_INPUT' |
'UNSAFE_LOCATION' | 'SKIPPED_INPUT' | 'UNKNOWN_FRESHNESS';
message: string;
}
export interface ScannerOutcome {
tool: ScannerId | 'sarif';
version: string | null;
status: 'complete' | 'partial' | 'not_assessed';
candidates: ScannerCandidate[];
gaps: ScannerGap[];
scope: string[];
exclusions: string[];
databaseUpdatedAt: string | null;
exitCode: number | null;
evidence: 'scanner-candidate';
provenanceSources: string[];
/** Binds the deterministic command/config to its execution record. */
planSha256: string;
documentationInspectedAt: string;
}
export interface ScannerExecution {
/** Complete bounded report; stdout chunks must be joined BEFORE this call. */
stdout: string;
stderr?: string;
exitCode: number | null;
version?: string;
databaseUpdatedAt?: string;
timedOut?: boolean;
unavailable?: boolean;
truncated?: boolean;
}
const SOURCES: Record<ScannerId, string[]> = {
gitleaks: ['https://github.com/gitleaks/gitleaks/blob/master/README.md'],
osv: ['https://google.github.io/osv-scanner/usage/scan-source/', 'https://google.github.io/osv-scanner/usage/offline-mode/'],
semgrep: ['https://docs.semgrep.dev/cli-reference'],
zizmor: ['https://docs.zizmor.sh/usage/', 'https://docs.zizmor.sh/quickstart/'],
trivy: ['https://trivy.dev/docs/dev/docs/advanced/telemetry/', 'https://trivy.dev/docs/latest/guide/advanced/air-gap/'],
schemathesis: ['https://schemathesis.readthedocs.io/en/stable/reference/cli/', 'https://github.com/schemathesis/schemathesis/blob/master/src/schemathesis/cli/json_report.py'],
};
function absolutePath(value: string, name: string): string {
if (value === '/' || !value.startsWith('/') || value.startsWith('//') || /[\x00-\x1f\\]/.test(value) || value.split('/').includes('..')) {
throw new Error(`${name} must be an absolute sandbox path without traversal`);
}
return posix.normalize(value);
}
function positiveInteger(value: number, max: number, name: string): number {
if (!Number.isSafeInteger(value) || value < 1 || value > max) throw new Error(`${name} must be between 1 and ${max}`);
return value;
}
/** Numeric loopback only: no DNS, URL credentials, redirected targets, or remote schemas. */
export function validateScannerBaseUrl(raw: string): string {
let url: URL;
try { url = new URL(raw); } catch { throw new Error('Schemathesis requires a numeric loopback HTTP URL'); }
if (!['http:', 'https:'].includes(url.protocol) || !['127.0.0.1', '[::1]'].includes(url.hostname) || url.username || url.password || url.hash || url.search) {
throw new Error('Schemathesis requires a numeric loopback HTTP URL without credentials, query, or fragment');
}
// URL canonicalization accepts integer, hex, and shorthand IPv4. Reject these spellings.
if (!/^https?:\/\/(127\.0\.0\.1|\[::1\])(?::\d+)?(?:\/|$)/.test(raw)) throw new Error('Schemathesis requires canonical numeric loopback');
return url.href;
}
/**
* Even online CSO runs collect scanner evidence without external egress. Database
* refresh belongs to the separate registry/advisory acquisition phase. A missing
* optional scanner is a gap in THIS assessment, not automatically the whole run.
*/
export function scannerPlans(opts: ScannerOptions): ScannerPlan[] {
const root = absolutePath(opts.snapshotRoot, 'snapshotRoot');
const policy = absolutePath(opts.policyRoot ?? '/policy', 'policyRoot');
if (policy === root || policy.startsWith(`${root}/`) || root.startsWith(`${policy}/`)) throw new Error('policyRoot must be separate from source');
const cache = opts.advisoryCache ? absolutePath(opts.advisoryCache, 'advisoryCache') : undefined;
if (cache && (cache === root || cache.startsWith(`${root}/`))) throw new Error('advisoryCache must be separate from source');
const timeout = positiveInteger(opts.deadlineSeconds ?? 120, 300, 'deadlineSeconds');
const selected = opts.selected ?? [...SCANNER_IDS];
if (new Set(selected).size !== selected.length || selected.some(id => !SCANNER_IDS.includes(id))) throw new Error('Invalid or duplicate scanner selection');
return selected.map(id => {
const plan: ScannerPlan = {
id, executableName: id === 'osv' ? 'osv-scanner' : id, args: [], versionArgs: ['--version'], requiredFeatures: [],
format: 'sarif', execution: 'sandbox', network: 'none', cwd: '/work', sourceRoot: root,
env: { HOME: '/work/home', TMPDIR: '/tmp', LANG: 'C.UTF-8', NO_COLOR: '1' }, trustedFiles: [], prerequisites: [],
timeoutSeconds: timeout, maxOutputBytes: MAX_SCANNER_OUTPUT_BYTES,
coverage: { domain: id, scope: [root], exclusions: ['Snapshot transformations apply; inspect the snapshot manifest.'] },
provenanceSources: SOURCES[id], documentationInspectedAt: '2026-09-09',
};
if (opts.tools?.[id]?.available === false) plan.prerequisites.push(`Install a reviewed ${plan.executableName} executable in the scanner image.`);
switch (id) {
case 'gitleaks': {
const target = opts.gitHistory ? absolutePath(opts.gitHistory, 'gitHistory') : root;
plan.format = 'gitleaks-json';
plan.coverage.domain = 'secrets';
plan.coverage.scope = [target];
plan.trustedFiles.push({ path: `${policy}/gitleaks.toml`, content: '[extend]\nuseDefault = true\n' }, { path: `${policy}/gitleaksignore`, content: '' });
plan.args = [opts.gitHistory ? 'git' : 'dir', '--redact=100', '--no-banner', '--no-color', '--ignore-gitleaks-allow', '--gitleaks-ignore-path', `${policy}/gitleaksignore`, '--config', `${policy}/gitleaks.toml`, '--report-format=json', '--report-path=-', '--exit-code=10', '--timeout', String(timeout), target];
if (opts.gitHistory) plan.prerequisites.push('History input must be a sanitized Git object store with trusted config and no hooks, filters, alternates, or external helpers.');
else plan.coverage.exclusions.push('Historical revisions are not scanned by this directory pass.');
plan.requiredFeatures = ['dir', '--redact', '--ignore-gitleaks-allow'];
break;
}
case 'osv':
plan.format = 'osv-json'; plan.coverage.domain = 'dependencies';
plan.trustedFiles.push({ path: `${policy}/osv-scanner.toml`, content: '' });
plan.args = ['scan', 'source', '--format=json', '--offline', '--no-call-analysis=all', '--config', `${policy}/osv-scanner.toml`, '--recursive', root];
plan.requiredFeatures = ['scan source', '--offline', '--no-call-analysis'];
if (cache) plan.env.OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY = cache;
else plan.prerequisites.push('Provide verified offline OSV databases for every assessed ecosystem.');
plan.coverage.exclusions.push('Call analysis is disabled; dependency reachability remains unknown until independently investigated.');
break;
case 'semgrep': {
plan.format = 'semgrep-json'; plan.coverage.domain = 'code';
const rules = opts.semgrepRules ? absolutePath(opts.semgrepRules, 'semgrepRules') : `${policy}/semgrep.yml`;
if (!rules.startsWith(`${policy}/`)) throw new Error('Semgrep rules must be below the trusted policyRoot');
if (!opts.semgrepRules) plan.prerequisites.push('Provide a reviewed, pinned local Semgrep ruleset; registry aliases and repo rules are not accepted.');
plan.args = ['scan', '--json', '--config', rules, '--metrics=off', '--disable-version-check', '--disable-nosem', '--no-git-ignore', '--no-secrets-validation', '--oss-only', '--no-autofix', '--timeout=10', '--timeout-threshold=3', '--jobs=1', root];
plan.env.SEMGREP_SEND_METRICS = 'off'; plan.env.SEMGREP_ENABLE_VERSION_CHECK = '0'; plan.env.SEMGREP_APP_TOKEN = '';
plan.requiredFeatures = ['scan', '--metrics', '--disable-version-check', '--no-secrets-validation', '--oss-only'];
plan.coverage.exclusions.push('Semgrep language support, built-in file selection, and .semgrepignore rules can exclude inputs; independently inspect these exclusions.');
break;
}
case 'zizmor':
plan.coverage.domain = 'github-actions';
plan.args = ['--offline', '--no-config', '--no-ignores', '--no-exit-codes', '--no-progress', '--color=never', '--format=sarif', root];
plan.env.ZIZMOR_OFFLINE = '1'; plan.requiredFeatures = ['--offline', '--no-config', '--no-ignores'];
plan.coverage.exclusions.push('Online GitHub audits and remote reusable action inspection require separate assessment.');
break;
case 'trivy':
plan.format = 'trivy-json'; plan.coverage.domain = 'dependencies-and-infrastructure';
plan.trustedFiles.push({ path: `${policy}/trivy.yaml`, content: '{}\n' }, { path: `${policy}/trivyignore`, content: '' });
plan.args = ['fs', '--format=json', '--config', `${policy}/trivy.yaml`, '--ignorefile', `${policy}/trivyignore`, '--scanners=vuln,misconfig,secret', '--cache-backend=memory', '--disable-telemetry', '--offline-scan', '--skip-db-update', '--skip-java-db-update', '--skip-check-update', '--skip-version-check', '--skip-vex-repo-update', '--timeout', `${timeout}s`, ...(cache ? ['--cache-dir', cache] : []), root];
plan.env.TRIVY_DISABLE_TELEMETRY = 'true';
plan.requiredFeatures = ['--cache-backend', '--disable-telemetry', '--offline-scan', '--skip-db-update', '--skip-java-db-update', '--skip-check-update', '--skip-version-check', '--skip-vex-repo-update'];
if (!cache) plan.prerequisites.push('Provide verified offline Trivy vulnerability, Java, and misconfiguration databases as needed.');
break;
case 'schemathesis': {
plan.format = 'schemathesis-json'; plan.network = 'loopback'; plan.coverage.domain = 'api-runtime';
plan.outputPath = '/work/schemathesis.json';
// The upstream image enables a Python hook module and coverage plugin by
// default. Qualified CSO scans use only the reviewed schema/config.
plan.env.SCHEMATHESIS_HOOKS = ''; plan.env.SCHEMATHESIS_COVERAGE = 'false';
plan.trustedFiles.push({ path: `${policy}/schemathesis.toml`, content: '' });
const schema = opts.schemaPath ? absolutePath(opts.schemaPath, 'schemaPath') : `${policy}/openapi.json`;
if (!schema.startsWith(`${policy}/`)) throw new Error('Schemathesis schema must be below trusted policyRoot');
if (!opts.schemaPath) plan.prerequisites.push('Provide a reviewed local schema with resolved local references, no remote references, and no hook imports.');
const base = opts.baseUrl ? validateScannerBaseUrl(opts.baseUrl) : 'http://127.0.0.1:3000/';
if (!opts.baseUrl) plan.prerequisites.push('Start the application and a legitimate control in the admitted loopback namespace.');
const seed = positiveInteger(opts.seed ?? 1, 2_147_483_647, 'seed');
const examples = positiveInteger(opts.maxExamples ?? 20, 100, 'maxExamples');
const operations = opts.operationIds ?? [];
if (operations.length === 0 || operations.length > 20) plan.prerequisites.push('Declare between 1 and 20 reviewed operation IDs to bound the API assessment.');
if (operations.some(op => !op || op.length > 200 || /[\x00-\x1f]/.test(op))) throw new Error('Invalid Schemathesis operation ID');
plan.args = ['--config-file', `${policy}/schemathesis.toml`, '--no-color', 'run', schema, '--url', base, '--workers=1', '--phases=fuzzing', '--max-examples', String(examples), '--max-failures=10', '--max-time', String(timeout), '--seed', String(seed), '--request-timeout=5', '--request-retries=0', '--max-redirects=0', '--rate-limit=10/s', '--output-sanitize=true', '--generation-database=none', '--report-json-path', plan.outputPath, ...operations.flatMap(op => ['--include-operation-id', op])];
plan.requiredFeatures = ['--report-json-path', '--max-time', '--seed', '--max-redirects', '--include-operation-id'];
plan.coverage.scope = operations.map(op => `operation:${op}`);
plan.coverage.exclusions.push('Only declared operations and generated examples are exercised; API failures are candidates, not security proofs.');
break;
}
}
const capabilities = opts.tools?.[id]?.capabilities;
if (capabilities) for (const required of plan.requiredFeatures) {
if (!capabilities.includes(required)) plan.prerequisites.push(`${plan.executableName} lacks required capability ${required}.`);
}
const version = opts.tools?.[id]?.version;
if (id === 'osv' && version && !/\b(?:v)?2\./.test(version)) plan.prerequisites.push('OSV-Scanner major version 2 is required.');
return plan;
});
}
type Obj = Record<string, unknown>;
function obj(value: unknown): Obj {
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Expected object');
return value as Obj;
}
function arr(value: unknown): unknown[] {
if (!Array.isArray(value)) throw new Error('Expected array');
return value;
}
function str(value: unknown): string {
if (typeof value !== 'string' || value.length > 16_384) throw new Error('Expected bounded string');
return value;
}
function optionalString(value: unknown): string | undefined { return value === undefined || value === null ? undefined : str(value); }
function integer(value: unknown): number | undefined {
if (value === undefined) return undefined;
if (!Number.isSafeInteger(value) || (value as number) < 1) throw new Error('Invalid source coordinate');
return value as number;
}
function severity(value: unknown): ScannerCandidate['reportedSeverity'] {
const normalized = typeof value === 'string' ? value.toLowerCase() : '';
if (['critical', 'high', 'medium', 'low', 'info'].includes(normalized)) return normalized as ScannerCandidate['reportedSeverity'];
return ({ error: 'high', warning: 'medium', note: 'info', informational: 'info', unknown: 'unknown' } as const)[normalized] ?? 'unknown';
}
/** No path is opened by this module. Normalization refuses URI/traversal escapes. */
export function scannerLocation(raw: string, sourceRoot: string): string {
let decoded: string;
try { decoded = decodeURIComponent(raw); } catch { throw new Error('Unsafe location'); }
if (/[\x00-\x1f\x7f]/.test(decoded) || /%[\da-f]{2}/i.test(decoded) || decoded.includes('\\')) throw new Error('Unsafe location');
if (decoded.startsWith('file:')) {
const url = new URL(decoded);
if (url.hostname || url.username || url.password || url.search || url.hash) throw new Error('Unsafe file URI');
decoded = decodeURIComponent(url.pathname);
} else if (/^[a-z][a-z\d+.-]*:/i.test(decoded) || decoded.startsWith('//')) throw new Error('Unsafe location');
if (decoded.split('/').includes('..')) throw new Error('Unsafe location');
const root = absolutePath(sourceRoot, 'sourceRoot');
const absolute = decoded.startsWith('/') ? posix.normalize(decoded) : posix.join(root, decoded);
if (!absolute.startsWith(`${root}/`)) throw new Error('Location outside source root');
const result = posix.relative(root, absolute);
if (!result || result === '.' || result.startsWith('../')) throw new Error('Unsafe location');
return result;
}
class RedactionFailure extends Error {}
/** Scan decoded leaves as well as raw JSON: JSON escapes must not hide secrets. */
function decodedDocument(raw: string): unknown {
if (redactFindingSpans(raw) === null) throw new RedactionFailure();
const document: unknown = JSON.parse(raw);
const pending: Array<{ value: unknown; depth: number }> = [{ value: document, depth: 0 }];
let nodes = 0;
while (pending.length) {
const { value, depth } = pending.pop()!;
if (++nodes > 100_000 || depth > 64) throw new Error('Output structure limit');
if (!value || typeof value !== 'object') continue;
for (const key of Object.keys(value)) {
if (['__proto__', 'constructor', 'prototype'].includes(key)) throw new Error('Unsafe object key');
const item = (value as Obj)[key];
if (typeof item === 'string') {
const safe = redactFindingSpans(item);
if (safe === null) throw new RedactionFailure();
// Terminal escape sequences cannot carry instructions through a report renderer.
(value as Obj)[key] = safe.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
} else pending.push({ value: item, depth: depth + 1 });
}
}
return document;
}
function candidate(tool: ScannerCandidate['tool'], fields: Omit<ScannerCandidate, 'id' | 'tool' | 'evidence' | 'trust' | 'suppressed'> & { suppressed?: boolean }): ScannerCandidate {
const identity = [tool, fields.ruleId, fields.location?.path ?? fields.operation ?? '', fields.location?.line ?? '', ...fields.advisoryIds.slice().sort()];
const id = createHash('sha256').update(JSON.stringify(identity)).digest('hex');
return { ...fields, id, tool, suppressed: fields.suppressed ?? false, evidence: 'scanner-candidate', trust: 'untrusted' };
}
function location(path: unknown, line: unknown, column: unknown, root: string): ScannerCandidate['location'] {
return { path: scannerLocation(str(path), root), line: integer(line), column: integer(column) };
}
function parseSarif(document: unknown, tool: ScannerCandidate['tool'], root: string, add: (value: ScannerCandidate) => void, gap: (code: ScannerGap['code'], message: string) => void): void {
const sarif = obj(document);
if (sarif.version !== '2.1.0') throw new Error('SARIF 2.1.0 required');
const runs = arr(sarif.runs);
if (!runs.length) { gap('SKIPPED_INPUT', 'SARIF contains no assessment runs.'); return; }
for (const input of runs) {
const run = obj(input); const driver = obj(obj(run.tool).driver);
str(driver.name);
if (run.externalPropertyFileReferences !== undefined) {
const refs = obj(run.externalPropertyFileReferences);
if (refs.results !== undefined && arr(refs.results).length) gap('SKIPPED_INPUT', 'External SARIF result files were not fetched or assessed.');
}
for (const invocation of run.invocations === undefined ? [] : arr(run.invocations)) {
const inv = obj(invocation);
if (inv.executionSuccessful === false) gap('TOOL_FAILED', 'SARIF records an unsuccessful tool invocation.');
if (Array.isArray(inv.toolExecutionNotifications) && inv.toolExecutionNotifications.some(n => obj(n).level === 'error')) gap('TOOL_FAILED', 'SARIF records tool execution errors.');
}
const rules = driver.rules === undefined ? [] : arr(driver.rules);
const results = arr(run.results);
for (const inputResult of results) {
try {
const result = obj(inputResult);
// SARIF also represents passing checks and informational inventory.
if (['pass', 'notApplicable', 'informational'].includes(String(result.kind))) continue;
const ruleIndex = result.ruleIndex;
const rule = Number.isSafeInteger(ruleIndex) && (ruleIndex as number) >= 0 && rules[ruleIndex as number] ? obj(rules[ruleIndex as number]) : undefined;
const ruleId = str(result.ruleId ?? rule?.id);
const message = obj(result.message);
let loc: ScannerCandidate['location'];
if (result.locations !== undefined && arr(result.locations).length) {
const physical = obj(obj(arr(result.locations)[0]).physicalLocation);
let artifact = obj(physical.artifactLocation);
if (artifact.uri === undefined && Number.isSafeInteger(artifact.index)) {
const index = artifact.index as number;
if (index < 0 || !Array.isArray(run.artifacts) || !run.artifacts[index]) throw new Error('Invalid artifact index');
artifact = obj(obj(run.artifacts[index]).location);
}
let uri = str(artifact.uri);
if (artifact.uriBaseId !== undefined) {
const baseId = str(artifact.uriBaseId);
const bases = obj(run.originalUriBaseIds);
const base = str(obj(bases[baseId]).uri);
// The base is evidence, not authority to access another directory.
if (base !== `file://${root}/` && base !== `file://${root}`) throw new Error('Unsafe location');
}
const region = physical.region === undefined ? {} : obj(physical.region);
loc = location(uri, region.startLine, region.startColumn, root);
}
const properties = result.properties === undefined ? {} : obj(result.properties);
const aliases = properties.tags === undefined ? [] : arr(properties.tags).filter(v => typeof v === 'string' && /^(CVE-|GHSA-|OSV-)/.test(v));
add(candidate(tool, { ruleId, message: str(message.text ?? message.markdown ?? message.id), location: loc, reportedSeverity: severity(result.level ?? (rule?.defaultConfiguration as Obj | undefined)?.level), advisoryIds: aliases as string[], suppressed: Array.isArray(result.suppressions) && result.suppressions.length > 0 }));
} catch (error) { gap(error instanceof Error && /[Ll]ocation|URI|source root/.test(error.message) ? 'UNSAFE_LOCATION' : 'INVALID_OUTPUT', 'A SARIF result could not be safely normalized.'); }
}
}
}
function parseResults(plan: ScannerPlan, document: unknown, add: (value: ScannerCandidate) => void, gap: (code: ScannerGap['code'], message: string) => void): void {
const root = plan.sourceRoot;
if (plan.format === 'sarif') { parseSarif(document, plan.id, root, add, gap); return; }
if (plan.format === 'gitleaks-json') {
for (const value of arr(document)) {
const row = obj(value);
// Never retain Match, Secret, Line, commit message, author, or scanner fingerprint.
add(candidate(plan.id, { ruleId: str(row.RuleID), message: str(row.Description), reportedSeverity: 'unknown', location: location(row.File, row.StartLine, row.StartColumn, root), advisoryIds: [] }));
}
return;
}
const doc = obj(document);
switch (plan.format) {
case 'semgrep-json':
for (const value of arr(doc.results)) {
const row = obj(value), extra = obj(row.extra), start = obj(row.start);
add(candidate(plan.id, { ruleId: str(row.check_id), message: str(extra.message), reportedSeverity: severity(extra.severity), location: location(row.path, start.line, start.col, root), advisoryIds: [], suppressed: extra.is_ignored === true }));
}
if (arr(doc.errors).length) gap('TOOL_FAILED', 'Semgrep reported parser, rule, or execution errors; inspect affected coverage.');
if (!arr(obj(doc.paths).scanned).length) gap('SKIPPED_INPUT', 'Semgrep did not scan any source files.');
if (Array.isArray(obj(doc.paths).skipped) && (obj(doc.paths).skipped as unknown[]).length) gap('SKIPPED_INPUT', 'Semgrep skipped source files.');
return;
case 'osv-json':
for (const value of arr(doc.results)) {
const result = obj(value), source = obj(result.source);
for (const entry of arr(result.packages)) {
const pkg = obj(entry), detail = obj(pkg.package);
for (const input of arr(pkg.vulnerabilities)) {
const vuln = obj(input), id = str(vuln.id);
const aliases = vuln.aliases === undefined ? [] : arr(vuln.aliases).map(str);
add(candidate(plan.id, { ruleId: id, message: optionalString(vuln.summary) ?? id, reportedSeverity: 'unknown', location: location(source.path, undefined, undefined, root), advisoryIds: [...new Set([id, ...aliases])], dependency: { name: str(detail.name), version: optionalString(detail.version), ecosystem: optionalString(detail.ecosystem), reachability: 'unknown', exposure: 'unknown' } }));
}
}
}
return;
case 'trivy-json':
if (doc.SchemaVersion !== 2) throw new Error('Trivy schema version 2 required');
if (doc.Results === undefined && (typeof doc.ArtifactName !== 'string' || doc.ArtifactType !== 'filesystem')) throw new Error('Missing Trivy assessment metadata');
for (const value of arr(doc.Results ?? [])) {
const result = obj(value);
for (const key of ['Vulnerabilities', 'Misconfigurations', 'Secrets'] as const) {
for (const input of result[key] === undefined ? [] : arr(result[key])) {
const row = obj(input), id = str(row.VulnerabilityID ?? row.ID ?? row.RuleID);
const cause = row.CauseMetadata === undefined ? {} : obj(row.CauseMetadata);
// Some filesystem package scanners add " (type)" after their target.
const target = str(result.Target).replace(/ \([a-zA-Z0-9_. -]+\)$/, '');
add(candidate(plan.id, { ruleId: id, message: optionalString(row.Title) ?? optionalString(row.Description) ?? id, reportedSeverity: severity(row.Severity), location: location(target, cause.StartLine ?? row.StartLine, undefined, root), advisoryIds: row.VulnerabilityID ? [id] : [], ...(key === 'Vulnerabilities' ? { dependency: { name: str(row.PkgName), version: optionalString(row.InstalledVersion), ecosystem: optionalString(result.Type), reachability: 'unknown' as const, exposure: 'unknown' as const } } : {}) }));
}
}
}
return;
case 'schemathesis-json': {
str(doc.schemathesis_version);
const operations = doc.operations === null ? null : obj(doc.operations);
if (doc.complete !== true || doc.stop_reason !== 'completed') gap('SKIPPED_INPUT', 'Schemathesis did not finish its declared operation assessment.');
if (!operations || typeof operations.tested !== 'number' || operations.tested === 0) gap('SKIPPED_INPUT', 'Schemathesis exercised no operations.');
if (operations && (Number(operations.errored) > 0 || Number(operations.skipped) > 0 || Number(operations.tested) < Number(operations.selected))) gap('SKIPPED_INPUT', 'Schemathesis skipped or failed to exercise selected operations.');
if (arr(doc.errors).length) gap('TOOL_FAILED', 'Schemathesis reported setup or test-generation errors.');
for (const value of arr(doc.failures)) {
const row = obj(value);
for (const op of arr(row.operations)) add(candidate(plan.id, { ruleId: str(row.type), message: str(row.title), reportedSeverity: severity(row.severity), advisoryIds: [], operation: str(op) }));
}
return;
}
}
}
/** Failed or malformed tools never become an empty-clean assessment. */
export function parseScannerOutput(plan: ScannerPlan, execution: ScannerExecution): ScannerOutcome {
const outcome: ScannerOutcome = {
tool: plan.id, version: null, status: 'not_assessed', candidates: [], gaps: [], scope: plan.coverage.scope.slice(), exclusions: plan.coverage.exclusions.slice(),
databaseUpdatedAt: null, exitCode: execution.exitCode, evidence: 'scanner-candidate', provenanceSources: plan.provenanceSources.slice(),
planSha256: createHash('sha256').update(JSON.stringify(plan)).digest('hex'), documentationInspectedAt: plan.documentationInspectedAt,
};
const gap = (code: ScannerGap['code'], message: string) => { if (!outcome.gaps.some(g => g.code === code && g.message === message)) outcome.gaps.push({ code, message }); };
if (execution.unavailable) { gap('UNAVAILABLE', `${plan.id} was unavailable; this scanner assessment did not run.`); return outcome; }
if (plan.prerequisites.length) { for (const value of plan.prerequisites) gap('PREREQUISITE', value); return outcome; }
if (execution.timedOut) gap('TIMEOUT', 'Scanner exceeded its execution deadline.');
const outputBytes = Buffer.byteLength(execution.stdout) + Buffer.byteLength(execution.stderr ?? '');
if (execution.truncated || outputBytes > Math.min(plan.maxOutputBytes, MAX_SCANNER_OUTPUT_BYTES)) { gap('OUTPUT_LIMIT', 'Scanner output exceeded the capture limit; payload withheld.'); return outcome; }
try {
if (execution.version) {
const safe = redactFindingSpans(execution.version);
if (safe === null) throw new RedactionFailure();
outcome.version = safe.slice(0, 200).replace(/[\x00-\x1f\x7f]/g, '');
}
if (redactFindingSpans(execution.stderr ?? '') === null) throw new RedactionFailure();
if (/\b(?:error|fatal|panic|failed to|unable to|no offline version)\b/i.test(execution.stderr ?? '')) gap('TOOL_FAILED', 'Scanner diagnostic output reported a failure; the JSON result does not establish complete coverage.');
const doc = decodedDocument(execution.stdout);
const seen = new Set<string>();
parseResults(plan, doc, item => {
if (outcome.candidates.length >= MAX_CANDIDATES) throw new Error('Candidate limit exceeded');
if (!seen.has(item.id)) { seen.add(item.id); outcome.candidates.push(item); }
}, gap);
outcome.status = 'complete';
} catch (error) {
if (error instanceof RedactionFailure) { outcome.candidates = []; gap('REDACTION_FAILED', 'Scanner payload could not be safely redacted and was withheld.'); }
else gap('INVALID_OUTPUT', 'Scanner report is malformed, unsupported, or exceeds structural limits.');
}
const successCodes = plan.id === 'gitleaks' ? [0, 10] : ['osv', 'schemathesis'].includes(plan.id) ? [0, 1] : [0];
if (execution.exitCode === null || !successCodes.includes(execution.exitCode)) gap('TOOL_FAILED', 'Scanner did not exit with a recognized assessment status.');
if ((plan.id === 'gitleaks' && execution.exitCode === 10 || plan.id === 'osv' && execution.exitCode === 1) && !outcome.candidates.length) gap('INVALID_OUTPUT', 'Scanner finding exit status disagrees with its empty report.');
if (['osv', 'trivy'].includes(plan.id)) {
if (execution.databaseUpdatedAt && /^\d{4}-\d\d-\d\dT/.test(execution.databaseUpdatedAt) && Number.isFinite(Date.parse(execution.databaseUpdatedAt))) outcome.databaseUpdatedAt = execution.databaseUpdatedAt;
else gap('UNKNOWN_FRESHNESS', 'The advisory database freshness is unknown.');
}
if (outcome.gaps.length) outcome.status = outcome.status === 'complete' || outcome.candidates.length ? 'partial' : 'not_assessed';
return outcome;
}
/** Import CodeQL or other SARIF as read-only candidates; never trust its verdict. */
export function importSarif(raw: string, opts: { sourceRoot: string; version?: string; scope?: string[] }): ScannerOutcome {
const root = absolutePath(opts.sourceRoot, 'sourceRoot');
const plan = scannerPlans({ snapshotRoot: root, offline: true, selected: ['zizmor'] })[0];
plan.coverage.scope = opts.scope ?? [root];
plan.provenanceSources = ['https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html'];
plan.coverage.exclusions = ['Imported scanner scope and suppressions require independent validation.'];
const outcome = parseScannerOutput(plan, { stdout: raw, exitCode: 0, version: opts.version });
outcome.tool = 'sarif';
outcome.candidates = outcome.candidates.map(item => candidate('sarif', item));
return outcome;
}
+287
View File
@@ -0,0 +1,287 @@
import * as fs from 'node:fs';
import { createHash } from 'node:crypto';
import { dirname, isAbsolute, join, resolve, relative, sep } from 'node:path';
import { CsoError, SnapshotManifest, SnapshotEntry, SnapshotPathIdentity, canonical, sha256, relativePath, snapshotOriginalIdentity, snapshotPathHandle, snapshotPathId, MAX_OUTPUT } from './contracts';
import { childEnvironment, executable, git, redact, runProcess } from './process';
import { secureDirectory, writeHelperJson, writeJson } from './state';
import { scan } from '../redact-engine';
import { atomicWriteSync } from '../fs-atomic';
const NO_READ_COMPONENTS=new Set(['.git','.hg','.svn','node_modules','.venv','venv','__pycache__','.bundle','.cache','.context','.gstack']);
const OMIT_COMPONENTS=new Set([...NO_READ_COMPONENTS,'.claude','.agents','.codex','.cursor']);
function containsDirectory(path:string,components:Set<string>,sequences:string[][]=[]):boolean{
const parts=path.split('/');if(parts.slice(0,-1).some(part=>components.has(part)))return true;
return sequences.some(sequence=>parts.slice(0,-1).some((_,index)=>sequence.every((part,offset)=>parts[index+offset]===part)));
}
function noReadPath(path:string):boolean{return containsDirectory(path,NO_READ_COMPONENTS,[['vendor','bundle']]);}
function omittedPath(path:string):boolean{return containsDirectory(path,OMIT_COMPONENTS,[['vendor','bundle'],['.github','agents']]);}
const SECRET_FILE = /(?:^|\/)(?:\.env(?:\..*)?|\.npmrc|\.yarnrc(?:\.yml)?|\.pypirc|pip\.conf|credentials(?:\.yml(?:\.enc)?)?|master\.key|id_(?:rsa|ed25519)|.*\.(?:pem|p12|pfx|key)|AGENTS\.md|CLAUDE\.md|GEMINI\.md|bunfig\.toml)$/i;
const SOURCE_LIMIT=64*1024*1024;
const SNAPSHOT_ENTRY_LIMIT=100_000;
const GIT_POINTER_LIMIT=8192;
export interface SnapshotCaptureLimits { deadlineMs?:number; maxEntries?:number }
type BoundPathIdentity={path:string;kind:'directory'|'file';dev:number;ino:number;mode:number;size:number;mtimeMs:number;ctimeMs:number;contentHash?:string};
type RepositoryIdentity={root:BoundPathIdentity;metadata:BoundPathIdentity[]};
function boundPath(path:string,label:string,maxBytes=GIT_POINTER_LIMIT):{identity:BoundPathIdentity;content?:string}{
let named:fs.Stats;try{named=fs.lstatSync(path);}catch{throw new CsoError('SNAPSHOT_RACE',`${label} disappeared during snapshot capture`);}
if(named.isSymbolicLink())throw new CsoError('UNSAFE_PATH',`${label} cannot be a symlink`);
if(named.isDirectory())return{identity:{path,kind:'directory',dev:named.dev,ino:named.ino,mode:named.mode,size:named.size,mtimeMs:named.mtimeMs,ctimeMs:named.ctimeMs}};
if(!named.isFile()||named.nlink!==1||named.size>maxBytes)throw new CsoError('UNSAFE_PATH',`${label} must be a bounded regular file or directory`);
let fd:number|undefined;try{
fd=fs.openSync(path,fs.constants.O_RDONLY|(fs.constants.O_NOFOLLOW??0)|(fs.constants.O_NONBLOCK??0));const opened=fs.fstatSync(fd);
if(!opened.isFile()||opened.nlink!==1||opened.dev!==named.dev||opened.ino!==named.ino||opened.mode!==named.mode||opened.size!==named.size)throw new CsoError('SNAPSHOT_RACE',`${label} changed before it could be read`);
const buffer=Buffer.alloc(maxBytes+1);let bytes=0,count=0;while(bytes<buffer.length&&(count=fs.readSync(fd,buffer,bytes,buffer.length-bytes,null))>0)bytes+=count;
const after=fs.fstatSync(fd),current=fs.lstatSync(path);if(bytes>maxBytes)throw new CsoError('UNSAFE_PATH',`${label} exceeds its bounded size limit`);
if(current.isSymbolicLink()||!current.isFile()||current.nlink!==1||current.dev!==opened.dev||current.ino!==opened.ino||current.mode!==opened.mode||after.size!==opened.size||after.mtimeMs!==opened.mtimeMs||after.ctimeMs!==opened.ctimeMs)throw new CsoError('SNAPSHOT_RACE',`${label} changed while it was read`);
const body=buffer.subarray(0,bytes);return{identity:{path,kind:'file',dev:after.dev,ino:after.ino,mode:after.mode,size:after.size,mtimeMs:after.mtimeMs,ctimeMs:after.ctimeMs,contentHash:sha256(body)},content:body.toString('utf8')};
}catch(error){if(error instanceof CsoError)throw error;const code=(error as NodeJS.ErrnoException).code;if(['ELOOP','ENOENT','ENOTDIR','ENXIO'].includes(code??''))throw new CsoError('SNAPSHOT_RACE',`${label} changed before it could be opened`);throw new CsoError('UNSAFE_PATH',`${label} could not be read as a bounded regular file`);}finally{if(fd!==undefined)fs.closeSync(fd);}
}
function sameIdentity(expected:BoundPathIdentity,current:BoundPathIdentity):boolean{return expected.path===current.path&&expected.kind===current.kind&&expected.dev===current.dev&&expected.ino===current.ino&&expected.mode===current.mode&&expected.size===current.size&&expected.mtimeMs===current.mtimeMs&&expected.ctimeMs===current.ctimeMs&&expected.contentHash===current.contentHash;}
function repositoryIdentity(repo:string):RepositoryIdentity{
const root=boundPath(repo,'Audited repository root').identity;if(root.kind!=='directory')throw new CsoError('MISSING_INPUT','Audited repository root is not a directory');
const markerPath=join(repo,'.git'),marker=boundPath(markerPath,'Repository .git marker'),metadata=[marker.identity];let gitDir:string;
if(marker.identity.kind==='directory')gitDir=fs.realpathSync(markerPath);
else{const value=marker.content??'',match=value.match(/^gitdir:\s*(.+?)\s*$/);if(!match||value.includes('\0')||value.split(/\r?\n/).filter(Boolean).length!==1)throw new CsoError('UNSAFE_PATH','Repository .git pointer is invalid');gitDir=fs.realpathSync(resolve(dirname(markerPath),match[1]));}
const gitDirIdentity=boundPath(gitDir,'Repository Git directory').identity;if(gitDirIdentity.kind!=='directory')throw new CsoError('UNSAFE_PATH','Repository Git directory is not a directory');metadata.push(gitDirIdentity);
const commonMarker=join(gitDir,'commondir');let commonDir=gitDir;
if(fs.existsSync(commonMarker)){const marker=boundPath(commonMarker,'Repository common Git directory pointer');if(marker.identity.kind!=='file')throw new CsoError('UNSAFE_PATH','Repository common Git directory pointer is invalid');metadata.push(marker.identity);const value=(marker.content??'').trim();if(!value||value.includes('\0')||value.includes('\n')||value.includes('\r'))throw new CsoError('UNSAFE_PATH','Repository common Git directory pointer is invalid');commonDir=fs.realpathSync(resolve(gitDir,value));}
const commonIdentity=boundPath(commonDir,'Repository common Git directory').identity;if(commonIdentity.kind!=='directory')throw new CsoError('UNSAFE_PATH','Repository common Git directory is not a directory');metadata.push(commonIdentity);
const unique=[...new Map(metadata.map(item=>[item.path,item])).values()];const identity={root,metadata:unique};assertRepositoryIdentity(identity);return identity;
}
function assertRepositoryIdentity(expected:RepositoryIdentity):void{
const compare=(item:BoundPathIdentity,label:string)=>{const current=boundPath(item.path,label,item.kind==='file'?Math.max(GIT_POINTER_LIMIT,item.size):GIT_POINTER_LIMIT).identity;if(!sameIdentity(item,current))throw new CsoError('SNAPSHOT_RACE',`${label} changed during snapshot capture`);};
compare(expected.root,'Audited repository root');for(const item of expected.metadata)compare(item,'Repository Git metadata identity');compare(expected.root,'Audited repository root');
}
function snapshotAdmission(limits:SnapshotCaptureLimits={}){
const deadlineMs=limits.deadlineMs??Date.now()+9*60_000,maxEntries=Math.min(limits.maxEntries??SNAPSHOT_ENTRY_LIMIT,SNAPSHOT_ENTRY_LIMIT);
if(!Number.isSafeInteger(deadlineMs)||!Number.isSafeInteger(maxEntries)||maxEntries<1)throw new CsoError('INVALID_ARGUMENT','Invalid snapshot admission limits');
const time=()=>{if(Date.now()>=deadlineMs)throw new CsoError('DEADLINE','Snapshot capture exhausted the investigation budget before a report could be created');};
const count=(entries:number)=>{if(entries>maxEntries)throw new CsoError('MISSING_INPUT',`Source tree exceeds the ${maxEntries}-entry snapshot admission limit`);};
return{time,count};
}
export function exclusion(path: string): string | undefined {
if (omittedPath(path)) return 'host dependencies, metadata, state, or agent configuration';
if (SECRET_FILE.test(path)) return 'credential or execution configuration';
}
export function containedFile(root: string, path: string): string {
const rel = relativePath(path), full = join(root,rel); let cursor = root;
for (const part of rel.split('/')) {
cursor = join(cursor,part);
try {
if (fs.lstatSync(cursor).isSymbolicLink()) throw new CsoError('UNSAFE_PATH',`Symlink is not an execution input: ${rel}`);
} catch (error) {
if (error instanceof CsoError) throw error;
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
}
}
if (!full.startsWith(root + sep)) throw new CsoError('UNSAFE_PATH','Path escaped snapshot');
return full;
}
type DirectoryIdentity={path:string;dev:number;ino:number;mode:number};
function inside(root:string,candidate:string):boolean{const relation=relative(root,candidate);return relation===''||(relation!=='..'&&!relation.startsWith(`..${sep}`)&&!isAbsolute(relation));}
function directoryIdentities(root:string,path:string):DirectoryIdentity[]{
const rel=relativePath(path),parts=rel.split('/'),identities:DirectoryIdentity[]=[];let cursor=root;
for(const part of ['',...parts.slice(0,-1)]){
if(part)cursor=join(cursor,part);
let stat:fs.Stats;try{stat=fs.lstatSync(cursor);}catch{throw new CsoError('SNAPSHOT_RACE',`Source ancestor changed while opening: ${rel}`);}
if(stat.isSymbolicLink()||!stat.isDirectory())throw new CsoError('UNSAFE_PATH',`Symlink or non-directory source ancestor: ${rel}`);
identities.push({path:cursor,dev:stat.dev,ino:stat.ino,mode:stat.mode});
}
return identities;
}
function assertDirectoryIdentities(identities:DirectoryIdentity[],path:string):void{
for(const expected of identities){let current:fs.Stats;try{current=fs.lstatSync(expected.path);}catch{throw new CsoError('SNAPSHOT_RACE',`Source ancestor changed while reading: ${path}`);}
if(current.isSymbolicLink()||!current.isDirectory()||current.dev!==expected.dev||current.ino!==expected.ino||current.mode!==expected.mode)throw new CsoError('SNAPSHOT_RACE',`Source ancestor changed while reading: ${path}`);
}
}
/** Validate the resolved inode after open so an ancestor-symlink swap cannot escape root. */
export function assertOpenedFileContained(root:string,full:string,fd:number,opened:fs.Stats):void{
if(process.platform==='linux'){
let actual:string,current:fs.Stats;try{actual=fs.readlinkSync(`/proc/self/fd/${fd}`);current=fs.fstatSync(fd);}catch{throw new CsoError('SNAPSHOT_RACE','Opened source identity could not be resolved');}
if(current.nlink!==1||current.dev!==opened.dev||current.ino!==opened.ino||current.mode!==opened.mode)throw new CsoError('SNAPSHOT_RACE','Opened source identity changed during containment validation');
if(!isAbsolute(actual)||!inside(root,actual))throw new CsoError('UNSAFE_PATH','Opened source escaped the audited root');
return;
}
let resolved:string,current:fs.Stats;try{resolved=fs.realpathSync(full);current=fs.lstatSync(resolved);}catch{throw new CsoError('SNAPSHOT_RACE','Opened source identity changed during containment validation');}
if(!inside(root,resolved))throw new CsoError('UNSAFE_PATH','Opened source escaped the audited root');
if(current.isSymbolicLink()||!current.isFile()||current.nlink!==1||current.dev!==opened.dev||current.ino!==opened.ino||current.mode!==opened.mode||current.size!==opened.size)throw new CsoError('SNAPSHOT_RACE','Opened source identity changed during containment validation');
}
function readStable(root: string, path: string, maxBytes=MAX_OUTPUT): {data:Buffer; mode:number} {
const ancestors=directoryIdentities(root,path),full = containedFile(root,path), named=fs.lstatSync(full);
// Prove the pathname is a regular single-link file before open. Opening a
// FIFO or device merely to discover its type can block or trigger host I/O.
if(named.isSymbolicLink()||!named.isFile()||named.nlink!==1)throw new CsoError('UNSAFE_PATH',`Special or hard-linked source file: ${path}`);
let fd:number;try{fd=fs.openSync(full,fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW??0) | (fs.constants.O_NONBLOCK??0));}catch(error){const code=(error as NodeJS.ErrnoException).code;if(['ELOOP','ENOENT','ENOTDIR','ENXIO'].includes(code??''))throw new CsoError('SNAPSHOT_RACE',`Source changed before it could be opened: ${path}`);throw new CsoError('UNSAFE_PATH',`Source could not be opened as a regular file: ${path}`);}
try {
const before = fs.fstatSync(fd);
if (!before.isFile() || before.nlink !== 1 || before.dev!==named.dev || before.ino!==named.ino || before.mode!==named.mode || before.size!==named.size) throw new CsoError('UNSAFE_PATH',`Special or hard-linked source file: ${path}`);
assertOpenedFileContained(root,full,fd,before);assertDirectoryIdentities(ancestors,path);
if (before.size > maxBytes) throw new CsoError('MISSING_INPUT',`Source file exceeds the ${maxBytes}-byte snapshot admission limit: ${path}`);
const buffer=Buffer.alloc(Math.min(maxBytes+1,before.size+1));let bytes=0,count=0;while(bytes<buffer.length&&(count=fs.readSync(fd,buffer,bytes,buffer.length-bytes,null))>0)bytes+=count;const data=buffer.subarray(0,bytes),after = fs.fstatSync(fd), current = fs.lstatSync(full);
if (!current.isFile()||current.nlink!==1||before.ino !== current.ino || before.dev !== current.dev || before.mode!==current.mode || before.size !== after.size || before.size!==bytes || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
throw new CsoError('SNAPSHOT_RACE',`Source changed while reading: ${path}`);
assertOpenedFileContained(root,full,fd,after);assertDirectoryIdentities(ancestors,path);
return {data,mode:before.mode & 0o777};
} finally { fs.closeSync(fd); }
}
async function resolveHeadCommit(repo:string,home:string):Promise<string|undefined>{
try{return(await git(repo,['rev-parse','--verify','HEAD^{commit}'],home)).trim();}
catch(error){
// A symbolic HEAD whose target does not exist is the normal unborn-branch
// state. A detached/malformed HEAD or a ref to a non-commit remains an
// input error instead of being silently treated as an empty history.
try{await git(repo,['symbolic-ref','--quiet','HEAD'],home);}catch{throw error;}
try{await git(repo,['rev-parse','--verify','HEAD'],home);}catch{return undefined;}
throw error;
}
}
async function paths(repo: string, home: string,headCommit:string|undefined,admission:ReturnType<typeof snapshotAdmission>): Promise<string[]> {
// The index omits staged deletions. Union the pinned HEAD tree so every
// tracked deletion is represented even when no comparison base was asked
// for, while still collecting nonignored untracked source.
admission.time();const [working,head]=await Promise.all([
git(repo,['ls-files','--cached','--others','--exclude-standard','-z'],home),
headCommit?git(repo,['ls-tree','-r','-z','--name-only','--full-tree',headCommit,'--'],home):Promise.resolve(''),
]),seen=new Set<string>();admission.time();
for(const data of [working,head])for(const value of data.split('\0')){admission.time();if(!value)continue;seen.add(relativePath(value));admission.count(seen.size);}
const result=[...seen].sort();admission.time();return result;
}
async function rejectSpecialFiles(repo:string,home:string,admission:ReturnType<typeof snapshotAdmission>):Promise<void>{
// Git intentionally omits untracked FIFOs and devices from ls-files. Walk
// pathnames without opening payloads, then ask Git which special names are
// ignored so a nonignored FIFO cannot silently disappear from the snapshot.
admission.time();const ignoredRaw=await git(repo,['ls-files','--others','--ignored','--exclude-standard','--directory','-z'],home),ignoredDirectories=new Set<string>();admission.time();
for(const value of ignoredRaw.split('\0')){admission.time();if(!value)continue;ignoredDirectories.add(relativePath(value.replace(/\/$/,'')));admission.count(ignoredDirectories.size);}
const ignoredDirectory=(path:string)=>{let candidate=path;for(;;){if(ignoredDirectories.has(candidate))return true;const slash=candidate.lastIndexOf('/');if(slash<0)return false;candidate=candidate.slice(0,slash);}};
const special:string[]=[];let visited=0;
const walk=(at:string,prefix='')=>{const directory=fs.opendirSync(at);try{let item:fs.Dirent|null;while((item=directory.readSync())!==null){
admission.time();const path=relativePath(prefix?`${prefix}/${item.name}`:item.name);if(noReadPath(path)||ignoredDirectory(path))continue;admission.count(++visited);
const full=join(at,item.name),stat=fs.lstatSync(full);if(stat.isDirectory()){walk(full,path);continue;}if(!stat.isFile())special.push(path);
}}finally{directory.closeSync();}};walk(repo);
if(!special.length)return;
const nullPath=process.platform==='win32'?'NUL':'/dev/null',result=await runProcess(executable('git'),['--no-optional-locks','-c','core.fsmonitor=false','-c',`core.hooksPath=${nullPath}`,'-c',`core.attributesFile=${nullPath}`,'-c','core.pager=cat','-C',repo,'check-ignore','--no-index','-z','--stdin'],{cwd:home,env:childEnvironment(home),raw:true,input:`${special.join('\0')}\0`,timeoutMs:15_000});
if(![0,1].includes(result.code)||result.timedOut||result.truncated)throw new CsoError('MISSING_INPUT','Could not determine whether special source paths are ignored');
admission.time();const ignored=new Set(result.stdout.split('\0').filter(Boolean).map(relativePath)),unsafe=special.find(path=>!ignored.has(path));
if(unsafe)throw new CsoError('UNSAFE_PATH',`Symlink or special source file: ${unsafe}`);
}
export async function capture(repo: string, runDir: string, base?: string, requiredAncestor?:string, limits:SnapshotCaptureLimits={}): Promise<SnapshotManifest> {
repo = fs.realpathSync(repo);
const state=fs.realpathSync(runDir),relation=relative(repo,state);if(relation===''||(!relation.startsWith(`..${sep}`)&&relation!=='..'&&!isAbsolute(relation)))throw new CsoError('UNSAFE_PATH','Security state must be outside the audited repository');
const repository=repositoryIdentity(repo),home = secureDirectory(join(runDir,'home')), snapshot = secureDirectory(join(runDir,'snapshot')), readable = secureDirectory(join(runDir,'readable')),admission=snapshotAdmission(limits),guard=()=>{admission.time();assertRepositoryIdentity(repository);};
try {
const entries: SnapshotEntry[] = [], gitHashes=new Map<string,string>(), gitModes=new Map<string,string>(), sensitiveEvidence:any[]=[], absentPaths=new Set<string>(); let total = 0;guard();
const objectFormat=(await git(repo,['rev-parse','--show-object-format'],home)).trim();guard();
if(!['sha1','sha256'].includes(objectFormat))throw new CsoError('INCOMPATIBLE_INPUT','Unsupported Git object format');
const headCommit = await resolveHeadCommit(repo,home);guard();
const list = await paths(repo,home,headCommit,admission);guard();await rejectSpecialFiles(repo,home,admission);guard();
const manifest: SnapshotManifest = {version:3,root:repo,createdAt:new Date().toISOString(),expiresAt:new Date(Date.now()+7*86400_000).toISOString(),entries,...(headCommit?{headCommit}:{}),originalHash:'',executionHash:''};
if (base) {
if (!/^[A-Za-z0-9_.\/-]+$/.test(base) || base.startsWith('-')) throw new CsoError('INVALID_ARGUMENT','Invalid comparison base');
manifest.baseCommit = (await git(repo,['rev-parse','--verify',`${base}^{commit}`],home)).trim();guard();
}
for (const path of list) {
guard();
try{fs.lstatSync(join(repo,path));}catch(error:any){if(error?.code==='ENOENT'){absentPaths.add(path);continue;}throw error;} // tracked deletions are represented by absence and the diff manifest
// Host dependency trees aren't copied or read. Their omission is still explicit.
if (noReadPath(path)) {
const stat = fs.lstatSync(containedFile(repo,path));
if (stat.isSymbolicLink() || !stat.isFile()) throw new CsoError('UNSAFE_PATH',`Special source input: ${path}`);
gitModes.set(path,(stat.mode&0o111)?'100755':'100644');
const reason=exclusion(path)??'host dependency input';
entries.push({path,pathId:snapshotPathId(repo,path),originalHash:'not-read',bytes:stat.size,mode:stat.mode & 0o777,transformation:`excluded: ${reason}`});guard();continue;
}
const {data,mode} = readStable(repo,path,SOURCE_LIMIT); total += data.length;
guard();
if (total > SOURCE_LIMIT) throw new CsoError('MISSING_INPUT','Source exceeds the 64 MiB snapshot admission limit');
const entry: SnapshotEntry = {path,pathId:snapshotPathId(repo,path),originalHash:sha256(data),bytes:data.length,mode}; entries.push(entry);
gitHashes.set(path,createHash(objectFormat).update(`blob ${data.length}\0`).update(data).digest('hex'));
gitModes.set(path,(mode&0o111)?'100755':'100644');
if(data.length>MAX_OUTPUT){entry.transformation='withheld: exceeds the 1 MiB redacting-reader limit';continue;}
let sanitized: string;
try {
sanitized = new TextDecoder('utf-8',{fatal:true}).decode(data);
if (sanitized.includes('\0')) throw new Error('binary');
const findings=scan(sanitized,{maxBytes:MAX_OUTPUT}).findings;
if(findings.length)sensitiveEvidence.push({path:snapshotPathHandle(entry.pathId),findings:findings.map(f=>({id:f.id,tier:f.tier,line:f.line,col:f.col}))});
sanitized = redact(sanitized);
} catch { entry.transformation = exclusion(path)?`excluded: ${exclusion(path)}; payload withheld because redaction could not safely preserve it`:'withheld: binary or redaction failed'; continue; }
const out = containedFile(readable,path); secureDirectory(dirname(out)); fs.writeFileSync(out,sanitized,{mode:0o600});
const reason = exclusion(path);
if (reason) { entry.transformation = `excluded: ${reason}`; continue; }
if (sha256(sanitized) !== entry.originalHash) entry.transformation = 'secret spans redacted';
const target = containedFile(snapshot,path); secureDirectory(dirname(target)); fs.writeFileSync(target,sanitized,{mode});
// writeFile's creation mode is filtered through the caller's umask. The
// skill deliberately starts with umask 077, while the manifest binds the
// original mode because executable bits are part of the application
// input. Restore the exact recorded mode after creation; the snapshot's
// owned 0700 ancestors still keep every retained source file private.
fs.chmodSync(target,mode);
entry.executionHash = sha256(sanitized);
}
const deletedPaths:SnapshotPathIdentity[]=[...absentPaths].sort().map(path=>({path,pathId:snapshotPathId(repo,path)}));if(deletedPaths.length)manifest.deletedPaths=deletedPaths;
const assertAbsent=()=>{for(const path of absentPaths){admission.time();try{fs.lstatSync(containedFile(repo,path));}catch(error:any){if(error?.code==='ENOENT')continue;throw error;}throw new CsoError('SNAPSHOT_RACE',`Deleted source path reappeared during snapshot capture: ${path}`);}};
const assertEntriesStable=(message:string)=>{for(const e of entries){guard();if(e.originalHash==='not-read'){const current=fs.lstatSync(containedFile(repo,e.path));if(current.isSymbolicLink()||!current.isFile()||current.nlink!==1||current.size!==e.bytes||(current.mode&0o777)!==e.mode)throw new CsoError('SNAPSHOT_RACE',`${message}: ${e.path}`);}else{const current=readStable(repo,e.path,SOURCE_LIMIT);if(sha256(current.data)!==e.originalHash||current.mode!==e.mode)throw new CsoError('SNAPSHOT_RACE',`${message}: ${e.path}`);}guard();}};
if (canonical(list) !== canonical(await paths(repo,home,headCommit,admission))) throw new CsoError('SNAPSHOT_RACE','Source file membership changed during snapshot');guard();assertAbsent();assertEntriesStable('Source changed during capture');
manifest.originalHash = snapshotOriginalIdentity(entries,deletedPaths);
manifest.executionHash = sha256(canonical(entries.filter(e => e.executionHash).map(e => [e.path,e.executionHash,e.mode])));
if(manifest.baseCommit){
const tree=await git(repo,['ls-tree','-r','-z','--full-tree',manifest.baseCommit,'--'],home),baseFiles=new Map<string,{hash:string,mode:string}>();guard();
for(const row of tree.split('\0').filter(Boolean)){admission.time();const match=row.match(/^(\d+) (?:blob|commit) ([a-f0-9]+)\t(.+)$/s);if(match)baseFiles.set(relativePath(match[3]),{mode:match[1],hash:match[2]});admission.count(baseFiles.size);}
const differs=(path:string):boolean=>{const baseEntry=baseFiles.get(path),hash=gitHashes.get(path),mode=gitModes.get(path);return !baseEntry||hash!==baseEntry.hash||mode!==baseEntry.mode;};
manifest.changedPaths=[...new Set([...list.filter(differs),...baseFiles.keys()].filter(path=>differs(path)||!gitModes.has(path)))].sort();
}
try{
if(!headCommit){atomicWriteSync(join(runDir,'history.txt'),'',{mode:0o600});writeJson(join(runDir,'history-status.json'),{status:'captured',range:'unborn HEAD',commits:0,bytes:0});}
else{
const range=manifest.baseCommit?`${manifest.baseCommit}..${headCommit}`:headCommit;
admission.time();const raw=await git(repo,['-c','core.quotePath=false','log','--no-ext-diff','--no-textconv','--max-count=100','--format=commit %H%nAuthor: %an%nDate: %aI%nSubject: %s','--unified=3','-p',range,'--'],home);guard();const safe=redact(raw);
atomicWriteSync(join(runDir,'history.txt'),safe,{mode:0o600});writeJson(join(runDir,'history-status.json'),{status:'captured',range,commits:'at most 100',bytes:Buffer.byteLength(safe)});
}
}catch(error){if(error instanceof CsoError&&['DEADLINE','SNAPSHOT_RACE','UNSAFE_PATH'].includes(error.code))throw error;writeJson(join(runDir,'history-status.json'),{status:'not_assessed',gap:error instanceof CsoError?error.message:'Historical evidence could not be safely retained'});}
if((await resolveHeadCommit(repo,home))!==headCommit)throw new CsoError('SNAPSHOT_RACE','HEAD changed during snapshot capture');guard();
if(base&&manifest.baseCommit&&(await git(repo,['rev-parse','--verify',`${base}^{commit}`],home)).trim()!==manifest.baseCommit)throw new CsoError('SNAPSHOT_RACE','Comparison base changed during snapshot capture');guard();
if(requiredAncestor){
if(!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(requiredAncestor))throw new CsoError('INCOMPATIBLE_INPUT','Original audit commit identity is invalid');
if(!headCommit)throw new CsoError('INCOMPATIBLE_INPUT','Captured current source has no commit descended from the original audit');
try{await git(repo,['merge-base','--is-ancestor',requiredAncestor,headCommit],home);}
catch{throw new CsoError('INCOMPATIBLE_INPUT','Captured current source is not a descendant of the original audited commit');}
guard();
}
// Finish with a complete source check. Nothing below this block reads the
// audited repository, so a late nonignored file or restored deletion cannot
// fall between the final inventory and manifest publication.
await rejectSpecialFiles(repo,home,admission);guard();assertEntriesStable('Source changed before snapshot persistence');
if(canonical(list)!==canonical(await paths(repo,home,headCommit,admission)))throw new CsoError('SNAPSHOT_RACE','Source file membership changed before snapshot persistence');guard();assertAbsent();
admission.time();writeHelperJson(join(runDir,'sensitive-evidence.json'),sensitiveEvidence);
// The manifest contains helper-computed identities and source pathnames but
// never source payloads. Persist it exactly in private state: generic
// content redaction would silently break the path/hash identity relation.
const serialized=JSON.stringify(manifest,null,2);if(Buffer.byteLength(serialized)+1>MAX_OUTPUT)throw new CsoError('MISSING_INPUT','Snapshot manifest exceeds the 1 MiB private-state admission limit');atomicWriteSync(join(runDir,'snapshot.json'),serialized+'\n',{mode:0o600}); return manifest;
} catch(e) {
fs.rmSync(snapshot,{recursive:true,force:true}); fs.rmSync(readable,{recursive:true,force:true});fs.rmSync(home,{recursive:true,force:true}); throw e;
}
}
export function assertSnapshot(runDir: string, manifest: SnapshotManifest): void {
const root=join(runDir,'snapshot');
if (manifest.version!==3||typeof manifest.root!=='string'||!isAbsolute(manifest.root)||!Array.isArray(manifest.entries)||(manifest.deletedPaths!==undefined&&!Array.isArray(manifest.deletedPaths))||(manifest.headCommit!==undefined&&!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(manifest.headCommit))||(manifest.baseCommit!==undefined&&!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(manifest.baseCommit))||!/^\d{4}-\d\d-\d\dT/.test(manifest.expiresAt)||Date.parse(manifest.expiresAt) <= Date.now() || !fs.existsSync(root)) throw new CsoError('MISSING_INPUT','Retained source expired or invalid; supply source with exactly matching required hashes');
const rootStat=fs.lstatSync(root);if(rootStat.isSymbolicLink()||!rootStat.isDirectory()||(process.getuid&&rootStat.uid!==process.getuid()))throw new CsoError('UNSAFE_PATH','Retained snapshot root is not a private owned directory');
const listed=():string[]=>{const out:string[]=[];const walk=(at:string,relativeRoot='')=>{for(const item of fs.readdirSync(at,{withFileTypes:true})){const rel=relativeRoot?`${relativeRoot}/${item.name}`:item.name,full=join(at,item.name),stat=fs.lstatSync(full);if(stat.isSymbolicLink()||(!stat.isDirectory()&&!stat.isFile()))throw new CsoError('UNSAFE_PATH',`Special file entered retained snapshot: ${rel}`);if(stat.isDirectory())walk(full,rel);else out.push(relativePath(rel));}};walk(root);return out.sort();};
const entries=manifest.entries.map(e=>{if(!e||typeof e!=='object')throw new CsoError('INCOMPATIBLE_INPUT','Snapshot manifest contains an invalid entry');const path=relativePath(e.path);if(!/^[a-f0-9]{32}$/.test(e.pathId)||e.pathId!==snapshotPathId(manifest.root,path)||!(/^[a-f0-9]{64}$/.test(e.originalHash)||e.originalHash==='not-read')||!Number.isSafeInteger(e.bytes)||e.bytes<0||!Number.isInteger(e.mode)||e.mode<0||e.mode>0o777||(e.transformation!==undefined&&typeof e.transformation!=='string'))throw new CsoError('INCOMPATIBLE_INPUT','Snapshot manifest contains an invalid source entry');if(e.executionHash!==undefined&&!/^[a-f0-9]{64}$/.test(e.executionHash))throw new CsoError('INCOMPATIBLE_INPUT','Snapshot manifest contains an invalid execution entry');if(e.originalHash==='not-read'&&(e.executionHash!==undefined||!e.transformation))throw new CsoError('INCOMPATIBLE_INPUT','Unread source cannot be represented as an execution input');return{...e,path};});
const deleted=(manifest.deletedPaths??[]).map(item=>{if(!item||typeof item!=='object'||Object.keys(item).some(key=>!['path','pathId'].includes(key)))throw new CsoError('INCOMPATIBLE_INPUT','Snapshot manifest contains an invalid deleted path');const path=relativePath(item.path);if(!/^[a-f0-9]{32}$/.test(item.pathId)||item.pathId!==snapshotPathId(manifest.root,path))throw new CsoError('INCOMPATIBLE_INPUT','Snapshot manifest contains an invalid deleted path');return{path,pathId:item.pathId};});
const presentPaths=new Set(entries.map(e=>e.path)),presentIds=new Set(entries.map(e=>e.pathId)),deletedPaths=new Set(deleted.map(item=>item.path)),deletedIds=new Set(deleted.map(item=>item.pathId));
if(presentPaths.size!==entries.length||presentIds.size!==entries.length||deletedPaths.size!==deleted.length||deletedIds.size!==deleted.length||deleted.some(item=>presentPaths.has(item.path)||presentIds.has(item.pathId))||canonical(deleted.map(item=>item.path))!==canonical([...deletedPaths].sort())||!/^([a-f0-9]{64})$/.test(manifest.originalHash)||snapshotOriginalIdentity(entries,deleted)!==manifest.originalHash)throw new CsoError('INCOMPATIBLE_INPUT','Snapshot original identity is inconsistent');
if(manifest.changedPaths!==undefined){if(!Array.isArray(manifest.changedPaths))throw new CsoError('INCOMPATIBLE_INPUT','Snapshot changed paths are invalid');const changed=manifest.changedPaths.map(relativePath);if(new Set(changed).size!==changed.length||canonical(changed)!==canonical([...changed].sort()))throw new CsoError('INCOMPATIBLE_INPUT','Snapshot changed paths are invalid');}
// Capture already records entries in Git's deterministic code-unit path
// order. Preserve that manifest order here: localeCompare can reorder an
// uppercase path such as README.md after lowercase source files, producing
// a different execution identity from the one written at capture time.
const expected=entries.filter(e=>e.executionHash);
if(!/^([a-f0-9]{64})$/.test(manifest.executionHash)||sha256(canonical(expected.map(e=>[e.path,e.executionHash,e.mode])))!==manifest.executionHash)throw new CsoError('INCOMPATIBLE_INPUT','Snapshot execution identity is inconsistent');
const before=listed();if(canonical(before)!==canonical(expected.map(e=>e.path)))throw new CsoError('INCOMPATIBLE_INPUT','Retained snapshot membership changed');
for (const e of expected) {
const current=readStable(root,e.path);
if (sha256(current.data) !== e.executionHash || current.mode!==e.mode) throw new CsoError('INCOMPATIBLE_INPUT',`Retained snapshot changed: ${e.path}`);
}
if(canonical(before)!==canonical(listed()))throw new CsoError('SNAPSHOT_RACE','Retained snapshot membership changed during validation');
}
+838
View File
@@ -0,0 +1,838 @@
import * as fs from 'node:fs';
import { basename, dirname, isAbsolute, join, resolve, parse, relative, sep } from 'node:path';
import { randomBytes } from 'node:crypto';
import { atomicWriteSync } from '../fs-atomic';
import { CsoError, RunReportV3, canonical, completeness, fingerprint, renderReport, sha256 } from './contracts';
import { redact, sanitizeForJson, sanitizeHelperForJson } from './process';
const MAX_STATE_FILE=1024*1024;
type AtomicRecoveryIdentity={dev:number;ino:number;nlink:number;size:number;mode:number;uid:number;mtimeMs:number;ctimeMs:number};
export interface AtomicNoReplaceRecoveryOptions {
label:string;maxBytes:number;
validate?:(value:unknown,publisherPid:number)=>void;
publisherAlive?:(value:unknown,publisherPid:number)=>boolean;
}
class AtomicPublicationTransition extends CsoError { constructor(message:string){super('SNAPSHOT_RACE',message);this.name='AtomicPublicationTransition';} }
function recoveryIdentity(stat:fs.Stats):AtomicRecoveryIdentity{return{dev:stat.dev,ino:stat.ino,nlink:stat.nlink,size:stat.size,mode:stat.mode,uid:stat.uid,mtimeMs:stat.mtimeMs,ctimeMs:stat.ctimeMs};}
function sameRecoveryIdentity(left:AtomicRecoveryIdentity,right:AtomicRecoveryIdentity):boolean{return left.dev===right.dev&&left.ino===right.ino&&left.nlink===right.nlink&&left.size===right.size&&left.mode===right.mode&&left.uid===right.uid&&left.mtimeMs===right.mtimeMs&&left.ctimeMs===right.ctimeMs;}
function recoveryProcessAlive(pid:number):boolean{try{process.kill(pid,0);return true;}catch(error:any){return error?.code==='EPERM';}}
function liveRecognizedPublication(temp:string,target:string,pid:number,options:AtomicNoReplaceRecoveryOptions):boolean{
if(!recoveryProcessAlive(pid))return false;
try{const temporary=fs.lstatSync(temp);if(temporary.isSymbolicLink()||!temporary.isFile()||temporary.nlink<1||temporary.nlink>2||(process.getuid&&temporary.uid!==process.getuid())||(process.platform!=='win32'&&(temporary.mode&0o077)!==0))return false;if(temporary.size===0)return temporary.nlink===1;if(temporary.nlink!==2||!privatePublicationFile(temporary,options))return false;const published=fs.lstatSync(target);return published.nlink===2&&samePublicationInode(temporary,published,options);}catch{return false;}
}
function liveEmptyPublication(path:string,pid:number):boolean{if(!recoveryProcessAlive(pid))return false;try{const stat=fs.lstatSync(path);return stat.isFile()&&!stat.isSymbolicLink()&&stat.size===0&&stat.nlink===1&&(!process.getuid||stat.uid===process.getuid())&&(process.platform==='win32'||(stat.mode&0o077)===0);}catch{return false;}}
function privatePublicationObservation(stat:fs.Stats,options:AtomicNoReplaceRecoveryOptions):boolean{return stat.isFile()&&!stat.isSymbolicLink()&&stat.size>=0&&stat.size<=options.maxBytes&&stat.nlink>=1&&stat.nlink<=2&&(!process.getuid||stat.uid===process.getuid())&&(process.platform==='win32'||(stat.mode&0o077)===0);}
function livePublicationAdvanced(temp:string,pid:number,observed:fs.Stats|undefined,options:AtomicNoReplaceRecoveryOptions):boolean{
if(!observed||!privatePublicationObservation(observed,options)||!recoveryProcessAlive(pid))return false;
Atomics.wait(LEASE_ELECTION_WAIT,0,0,LEASE_ELECTION_POLL_MS);
let current:fs.Stats;try{current=fs.lstatSync(temp);}catch(error:any){return error?.code==='ENOENT';}
if(!privatePublicationObservation(current,options)||current.dev!==observed.dev||current.ino!==observed.ino)return false;
if(current.nlink!==observed.nlink||current.size!==observed.size)return true;
return false;
}
function publicationOwnerAlive(value:unknown,publisherPid:number,options:AtomicNoReplaceRecoveryOptions):boolean{return options.publisherAlive?.(value,publisherPid)??recoveryProcessAlive(publisherPid);}
function privatePublicationFile(stat:fs.Stats,options:AtomicNoReplaceRecoveryOptions):boolean{return stat.isFile()&&!stat.isSymbolicLink()&&stat.size>0&&stat.size<=options.maxBytes&&
(!process.getuid||stat.uid===process.getuid())&&(process.platform==='win32'||(stat.mode&0o077)===0);}
function samePublicationObject(left:fs.Stats,right:fs.Stats,options:AtomicNoReplaceRecoveryOptions):boolean{return privatePublicationFile(left,options)&&privatePublicationFile(right,options)&&
left.dev===right.dev&&left.ino===right.ino&&left.size===right.size&&left.mode===right.mode&&left.uid===right.uid;}
function samePublicationInode(left:fs.Stats,right:fs.Stats,options:AtomicNoReplaceRecoveryOptions):boolean{return samePublicationObject(left,right,options)&&left.mtimeMs===right.mtimeMs;}
function publicationLinkTransition(observed:fs.Stats,current:fs.Stats,links:1|2,options:AtomicNoReplaceRecoveryOptions):boolean{
const from=links===1?1:2,to=links===1?2:1;
return observed.nlink===from&&current.nlink===to&&samePublicationInode(observed,current,options);
}
function publicationPathRemoved(observed:fs.Stats,current:fs.Stats,options:AtomicNoReplaceRecoveryOptions):boolean{return observed.nlink>=1&&observed.nlink<=2&&current.nlink>=0&&current.nlink<observed.nlink&&samePublicationObject(observed,current,options);}
function publicationProgress(left:fs.Stats,right:fs.Stats,options:AtomicNoReplaceRecoveryOptions):boolean{return left.nlink>=0&&left.nlink<=2&&right.nlink>=0&&right.nlink<=2&&left.nlink!==right.nlink&&samePublicationInode(left,right,options);}
function atomicTempTarget(path:string,publisherPid?:number):{target:string;pid:number}|undefined{
const match=basename(path).match(/^(.*)\.tmp\.(\d{1,10})\.[a-f0-9]{8}$/),pid=match?Number(match[2]):0;
return match&&match[1]&&Number.isSafeInteger(pid)&&pid>1&&(publisherPid===undefined||pid===publisherPid)?{target:join(dirname(path),match[1]),pid}:undefined;
}
function settledAtomicTemp(path:string,observed:fs.Stats,options:AtomicNoReplaceRecoveryOptions):boolean{
const publication=atomicTempTarget(path);if(!publication)return false;
let target:fs.Stats;try{target=fs.lstatSync(publication.target);}catch{return false;}
return observed.nlink>=1&&observed.nlink<=2&&target.nlink===1&&privatePublicationFile(observed,options)&&privatePublicationFile(target,options)&&
observed.dev===target.dev&&observed.ino===target.ino&&observed.size===target.size&&observed.mode===target.mode&&observed.uid===target.uid&&observed.mtimeMs===target.mtimeMs;
}
function readPublicationBytes(fd:number,size:number,label:string):string{
const bytes=Buffer.alloc(size);let offset=0;
while(offset<size){const count=fs.readSync(fd,bytes,offset,size-offset,offset);if(count<=0)throw new CsoError('SNAPSHOT_RACE',`${label} interrupted publication changed while it was read`);offset+=count;}
const extra=Buffer.alloc(1);if(fs.readSync(fd,extra,0,1,size)!==0)throw new CsoError('SNAPSHOT_RACE',`${label} interrupted publication changed while it was read`);
return bytes.toString('utf8');
}
function recoveryJson(path:string,links:1|2,options:AtomicNoReplaceRecoveryOptions,observed?:fs.Stats):{identity:AtomicRecoveryIdentity;value:unknown}{
let fd:number|undefined;
try{
const before=fs.lstatSync(path);
if(before.nlink===0){
let current:fs.Stats;try{current=fs.lstatSync(path);}catch(error:any){if(error?.code==='ENOENT')throw new CsoError('INSUFFICIENT_CAPACITY',`${options.label} was removed while it was inspected`);throw error;}
if(samePublicationInode(before,current,options)&&(current.nlink===0||current.nlink===links))throw new AtomicPublicationTransition(`${options.label} changed link state while it was inspected`);
throw new CsoError('UNSAFE_PATH',`${options.label} was replaced while it was inspected`);
}
if(observed&&publicationLinkTransition(observed,before,links,options))throw new AtomicPublicationTransition(`${options.label} interrupted publication changed link state`);
if(observed&&publicationProgress(observed,before,options)&&!sameRecoveryIdentity(recoveryIdentity(observed),recoveryIdentity(before)))throw new CsoError('INSUFFICIENT_CAPACITY',`${options.label} changed phase during concurrent recovery`);
if(!before.isFile()||before.isSymbolicLink()||before.nlink!==links||before.size<=0||before.size>options.maxBytes||
(process.getuid&&before.uid!==process.getuid())||(process.platform!=='win32'&&(before.mode&0o077)!==0))
throw new CsoError('UNSAFE_PATH',`${options.label} interrupted publication is not one private regular file`);
fd=fs.openSync(path,fs.constants.O_RDONLY|(fs.constants.O_NOFOLLOW??0));const opened=fs.fstatSync(fd);
if(!sameRecoveryIdentity(recoveryIdentity(before),recoveryIdentity(opened))){
if(publicationLinkTransition(before,opened,links,options))throw new AtomicPublicationTransition(`${options.label} interrupted publication changed link state while it was opened`);
if(publicationProgress(before,opened,options))throw new CsoError('INSUFFICIENT_CAPACITY',`${options.label} changed phase during concurrent recovery while it was opened`);
throw new CsoError('SNAPSHOT_RACE',`${options.label} interrupted publication changed while it was opened`);
}
const serialized=readPublicationBytes(fd,opened.size,options.label);let value:unknown;try{value=JSON.parse(serialized);}catch{throw new CsoError('UNSAFE_PATH',`${options.label} interrupted publication is not valid JSON`);}
const final=fs.fstatSync(fd);if(readPublicationBytes(fd,opened.size,options.label)!==serialized)throw new CsoError('SNAPSHOT_RACE',`${options.label} interrupted publication changed while it was read`);let after:fs.Stats;try{after=fs.lstatSync(path);}catch(error:any){if(error?.code==='ENOENT'&&publicationPathRemoved(opened,final,options))throw new CsoError('INSUFFICIENT_CAPACITY',`${options.label} was removed by another recovery helper while it was read`);throw error;}const openedIdentity=recoveryIdentity(opened),finalIdentity=recoveryIdentity(final),afterIdentity=recoveryIdentity(after);
if(!sameRecoveryIdentity(openedIdentity,finalIdentity)||!sameRecoveryIdentity(openedIdentity,afterIdentity)){
const coherentTransition=(sameRecoveryIdentity(openedIdentity,finalIdentity)&&publicationLinkTransition(opened,after,links,options))||
(publicationLinkTransition(opened,final,links,options)&&sameRecoveryIdentity(finalIdentity,afterIdentity));
if(coherentTransition)throw new AtomicPublicationTransition(`${options.label} interrupted publication changed link state while it was read`);
if(publicationProgress(opened,final,options)&&publicationProgress(final,after,options))throw new CsoError('INSUFFICIENT_CAPACITY',`${options.label} changed phase during concurrent recovery while it was read`);
throw new CsoError('SNAPSHOT_RACE',`${options.label} interrupted publication changed while it was read`);
}
return{identity:recoveryIdentity(opened),value};
}catch(error:any){if(error instanceof CsoError)throw error;if(error?.code==='ENOENT'){if(observed&&settledAtomicTemp(path,observed,options))throw new AtomicPublicationTransition(`${options.label} interrupted publication settled while it was observed`);if(observed&&!fs.existsSync(path))throw new CsoError('INSUFFICIENT_CAPACITY',`${options.label} was removed by another recovery helper`);throw new CsoError('SNAPSHOT_RACE',`${options.label} interrupted publication disappeared`);}throw new CsoError('UNSAFE_PATH',`${options.label} interrupted publication could not be validated`);}
finally{if(fd!==undefined)try{fs.closeSync(fd);}catch{}}
}
function atomicTempCandidates(target:string):Array<{path:string;pid:number}>{
const directory=dirname(target),name=basename(target),escaped=name.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'),pattern=new RegExp(`^${escaped}\\.tmp\\.(\\d{1,10})\\.([a-f0-9]{8})$`);
return fs.readdirSync(directory).flatMap(entry=>{const match=entry.match(pattern),pid=match?Number(match[1]):0;return match&&Number.isSafeInteger(pid)&&pid>1?[{path:join(directory,entry),pid}]:[];});
}
function matchesRecoveryInode(stat:fs.Stats,identity:AtomicRecoveryIdentity,options:AtomicNoReplaceRecoveryOptions):boolean{return privatePublicationFile(stat,options)&&stat.dev===identity.dev&&stat.ino===identity.ino&&stat.size===identity.size&&stat.mode===identity.mode&&stat.uid===identity.uid&&stat.mtimeMs===identity.mtimeMs;}
/** Recover only the hard-link publication window of atomicWriteSync(noReplace). */
export function recoverAtomicNoReplaceJson(target:string,options:AtomicNoReplaceRecoveryOptions):void{
let targetStat:fs.Stats;try{targetStat=fs.lstatSync(target);}catch(error:any){if(error?.code==='ENOENT')return;throw new CsoError('UNSAFE_PATH',`${options.label} could not be inspected`);}
// Callers own legacy-directory and special-file handling. Only a regular
// file can be the no-replace hard-link publication this helper recognizes.
if(!targetStat.isFile()||targetStat.isSymbolicLink())return;
if(targetStat.nlink===1)return;
if(targetStat.nlink===0){
let current:fs.Stats;try{current=fs.lstatSync(target);}catch(error:any){if(error?.code==='ENOENT')throw new AtomicPublicationTransition(`${options.label} was removed while it was inspected`);throw new CsoError('UNSAFE_PATH',`${options.label} could not be reinspected`);}
if(samePublicationInode(targetStat,current,options)&&current.nlink>=0&&current.nlink<=2)throw new AtomicPublicationTransition(`${options.label} changed link state while it was inspected`);
throw new CsoError('UNSAFE_PATH',`${options.label} was replaced while it was inspected`);
}
if(targetStat.nlink!==2)throw new CsoError('UNSAFE_PATH',`${options.label} has an unrecognized hard-link count`);
const canonical=recoveryJson(target,2,options,targetStat),matches=atomicTempCandidates(target).flatMap(candidate=>{try{const observed=fs.lstatSync(candidate.path);return observed.dev===canonical.identity.dev&&observed.ino===canonical.identity.ino?[{...candidate,observed}]:[];}catch{return[];}});
if(matches.length!==1){
let settled:fs.Stats|undefined;try{settled=fs.lstatSync(target);}catch{}
if(settled&&publicationLinkTransition(targetStat,settled,2,options))throw new AtomicPublicationTransition(`${options.label} interrupted publication settled during candidate enumeration`);
throw new CsoError('UNSAFE_PATH',`${options.label} hard link does not match one recognized interrupted publication`);
}
const candidate=matches[0],temporary=recoveryJson(candidate.path,2,options,candidate.observed);
if(!sameRecoveryIdentity(canonical.identity,temporary.identity))throw new CsoError('UNSAFE_PATH',`${options.label} hard link changed identity`);
options.validate?.(canonical.value,candidate.pid);options.validate?.(temporary.value,candidate.pid);
if(publicationOwnerAlive(canonical.value,candidate.pid,options))throw new CsoError('INSUFFICIENT_CAPACITY',`${options.label} publication is still owned by a live helper`);
let finalTarget:fs.Stats,finalTemp:fs.Stats;
try{finalTarget=fs.lstatSync(target);finalTemp=fs.lstatSync(candidate.path);}catch(error:any){
if(error?.code!=='ENOENT')throw error;
for(const path of [target,candidate.path]){try{const stat=fs.lstatSync(path);if(!matchesRecoveryInode(stat,canonical.identity,options))throw new CsoError('UNSAFE_PATH',`${options.label} was replaced during concurrent recovery`);}catch(recoveryError:any){if(recoveryError instanceof CsoError)throw recoveryError;if(recoveryError?.code!=='ENOENT')throw recoveryError;}}
throw new AtomicPublicationTransition(`${options.label} was settled by another recovery helper`);
}
if(!sameRecoveryIdentity(canonical.identity,recoveryIdentity(finalTarget))||!sameRecoveryIdentity(canonical.identity,recoveryIdentity(finalTemp))){
if(matchesRecoveryInode(finalTarget,canonical.identity,options)&&matchesRecoveryInode(finalTemp,canonical.identity,options)&&finalTarget.nlink<=2&&finalTemp.nlink<=2)throw new CsoError('INSUFFICIENT_CAPACITY',`${options.label} hard link changed during concurrent recovery`);
throw new CsoError('SNAPSHOT_RACE',`${options.label} hard link changed before recovery`);
}
try{fs.unlinkSync(candidate.path);}catch(error:any){if(error?.code!=='ENOENT')throw new CsoError('PERSISTENCE_FAILED',`${options.label} interrupted publication could not be recovered`);}
let recovered:{identity:AtomicRecoveryIdentity;value:unknown};try{recovered=recoveryJson(target,1,options);}catch(error){
if(error instanceof CsoError&&error.code==='SNAPSHOT_RACE'&&!fs.existsSync(target))throw new AtomicPublicationTransition(`${options.label} was removed by another recovery helper`);
throw error;
}
options.validate?.(recovered.value,candidate.pid);
if(recovered.identity.dev!==canonical.identity.dev||recovered.identity.ino!==canonical.identity.ino)throw new CsoError('UNSAFE_PATH',`${options.label} changed identity during recovery`);
}
/** Remove a never-published temp, or validate a temp that became published while observed. */
export function discardAtomicNoReplaceTemp(path:string,publisherPid:number,options:AtomicNoReplaceRecoveryOptions):void{
let observed:fs.Stats;try{observed=fs.lstatSync(path);}catch(error:any){
if(error?.code==='ENOENT'){
const publication=atomicTempTarget(path,publisherPid);
if(publication){
try{const settled=recoveryJson(publication.target,1,options);options.validate?.(settled.value,publisherPid);if(publicationOwnerAlive(settled.value,publisherPid,options))throw new CsoError('INSUFFICIENT_CAPACITY',`${options.label} publication is still owned by a live helper`);return;}catch(settledError){if(settledError instanceof CsoError&&settledError.code==='SNAPSHOT_RACE'&&!fs.existsSync(publication.target))return;if(settledError instanceof CsoError)throw settledError;}
}
throw new CsoError('SNAPSHOT_RACE',`${options.label} interrupted publication disappeared`);
}
throw new CsoError('UNSAFE_PATH',`${options.label} interrupted publication could not be inspected`);
}
if(observed.nlink===2&&privatePublicationFile(observed,options)){
const target=atomicTempTarget(path,publisherPid)?.target;
let published:fs.Stats|undefined;try{if(target)published=fs.lstatSync(target);}catch{}
if(target&&published&&published.dev===observed.dev&&published.ino===observed.ino&&published.nlink===2&&privatePublicationFile(published,options)){
recoverAtomicNoReplaceJson(target,options);
const settled=recoveryJson(target,1,options);
if(settled.identity.dev!==observed.dev||settled.identity.ino!==observed.ino)throw new CsoError('UNSAFE_PATH',`${options.label} published target changed identity while it settled`);
options.validate?.(settled.value,publisherPid);
if(publicationOwnerAlive(settled.value,publisherPid,options))throw new CsoError('INSUFFICIENT_CAPACITY',`${options.label} publication is still owned by a live helper`);
return;
}
}
const temporary=recoveryJson(path,1,options,observed);options.validate?.(temporary.value,publisherPid);
if(publicationOwnerAlive(temporary.value,publisherPid,options))throw new CsoError('INSUFFICIENT_CAPACITY',`${options.label} publication is still owned by a live helper`);
let final:fs.Stats;try{final=fs.lstatSync(path);}catch(error:any){if(error?.code==='ENOENT')throw new AtomicPublicationTransition(`${options.label} temp was removed by another recovery helper`);throw error;}
if(!sameRecoveryIdentity(temporary.identity,recoveryIdentity(final)))throw new CsoError('SNAPSHOT_RACE',`${options.label} temp changed before recovery`);
try{fs.unlinkSync(path);}catch(error:any){if(error?.code==='ENOENT')throw new AtomicPublicationTransition(`${options.label} temp was removed by another recovery helper`);throw new CsoError('PERSISTENCE_FAILED',`${options.label} unpublished temp could not be removed`);}
}
export function stateRoot(env: Record<string,string|undefined> = process.env): string {
// Mirrors bin/gstack-paths. Security artifacts are intentionally outside every sync allowlist.
const userHome=env.HOME||(process.platform==='win32'?env.USERPROFILE:'');
return resolve(env.GSTACK_HOME || (env.CLAUDE_PLUGIN_ROOT?.toLowerCase().includes('gstack') ? env.CLAUDE_PLUGIN_DATA : '') || join(userHome || '.', '.gstack'));
}
function ensureDirectory(path:string,hardenExistingLeaf:boolean):string{
const p=resolve(path),root=parse(p).root;
if(p===root)throw new CsoError('UNSAFE_PATH','Private state cannot use a filesystem root');
let cursor=root,leafCreated=false;
for (const part of relative(root,p).split(sep).filter(Boolean)) {
cursor = join(cursor,part);
let created=false;
try{fs.mkdirSync(cursor,{mode:0o700});created=true;}catch(error:any){if(error?.code!=='EEXIST')throw error;}
if(cursor===p)leafCreated=created;
const s = fs.lstatSync(cursor);
if (s.isSymbolicLink() || !s.isDirectory()) throw new CsoError('UNSAFE_PATH','Private state has a symlink or non-directory ancestor');
// Root-owned system ancestors are normal; a writable ancestor owned by anyone else is not.
if (s.uid !== process.getuid?.() && s.uid !== 0) throw new CsoError('UNSAFE_PATH','Private state ancestor has an unexpected owner');
if (process.platform!=='win32'&&(s.mode & 0o022) && !(s.mode & 0o1000)) throw new CsoError('UNSAFE_PATH','Private state has a group- or world-writable ancestor');
}
const s = fs.statSync(p);
if (process.getuid && s.uid !== process.getuid()) throw new CsoError('UNSAFE_PATH','Private directory must be owned by the current user');
if(hardenExistingLeaf||leafCreated)fs.chmodSync(p,0o700);
return p;
}
/** The supplied leaf is CSO-owned. Existing ancestors are validated, never mutated. */
export function secureDirectory(path:string):string{return ensureDirectory(path,true);}
export function privateRoot():string{
const container=ensureDirectory(stateRoot(),false);
return secureDirectory(join(container,'security','cso'));
}
export function assertStateOutside(repo:string):void{
const source=fs.realpathSync(repo),candidate=resolve(stateRoot(),'security','cso');
const relation=relative(source,candidate);if(relation===''||(!relation.startsWith(`..${sep}`)&&relation!=='..'&&!isAbsolute(relation)))throw new CsoError('UNSAFE_PATH','CSO private state must be outside the audited repository');
}
export function repoId(repo: string): string { return sha256(fs.realpathSync(repo)).slice(0,24); }
export function newRun(repo: string): {runId:string; dir:string; repoId:string} {
assertStateOutside(repo);
const id = repoId(repo), runId = `${Date.now()}-${randomBytes(8).toString('hex')}`;
return {runId, repoId:id, dir:secureDirectory(join(privateRoot(),id,runId))};
}
export function runDirectory(id: string): string {
if (!/^\d{13}-[a-f0-9]{16}$/.test(id)) throw new CsoError('INVALID_ARGUMENT','Run identifier must be the ID returned by start');
const root = privateRoot();
for (const item of fs.readdirSync(root)) {
if (!/^[a-f0-9]{24}$/.test(item)) continue;
const dir = join(root,item,id);
if (fs.existsSync(dir)) return secureDirectory(dir);
}
throw new CsoError('MISSING_INPUT','Run was not found or has expired');
}
export function writeJson(path: string, value: unknown): void {
try {
secureDirectory(dirname(path));
if (fs.existsSync(path) && fs.lstatSync(path).isSymbolicLink()) throw new CsoError('UNSAFE_PATH','State file cannot be a symlink');
const sanitized = JSON.stringify(sanitizeForJson(value),null,2);
if(Buffer.byteLength(sanitized)+1>MAX_STATE_FILE)throw new CsoError('PERSISTENCE_FAILED','Private state exceeds the 1 MiB persistence limit; the previous artifact was preserved');
JSON.parse(sanitized); atomicWriteSync(path,sanitized + '\n',{mode:0o600});
} catch(e) { if (e instanceof CsoError) throw e; throw new CsoError('PERSISTENCE_FAILED','Private report could not be written; no saved report is claimed'); }
}
export function writeHelperJson(path:string,value:unknown):void{
try{
secureDirectory(dirname(path));if(fs.existsSync(path)&&fs.lstatSync(path).isSymbolicLink())throw new CsoError('UNSAFE_PATH','State file cannot be a symlink');
const serialized=JSON.stringify(sanitizeHelperForJson(value),null,2);if(Buffer.byteLength(serialized)+1>MAX_STATE_FILE)throw new CsoError('PERSISTENCE_FAILED','Private helper state exceeds the 1 MiB persistence limit; the previous artifact was preserved');JSON.parse(serialized);atomicWriteSync(path,serialized+'\n',{mode:0o600});
}catch(error){if(error instanceof CsoError)throw error;throw new CsoError('PERSISTENCE_FAILED','Private helper state could not be written; no saved artifact is claimed');}
}
export function writeJsonExclusive(path:string,value:unknown):void{
try{
secureDirectory(dirname(path));
if(fs.existsSync(path)&&fs.lstatSync(path).isSymbolicLink())throw new CsoError('UNSAFE_PATH','State file cannot be a symlink');
const sanitized=JSON.stringify(sanitizeHelperForJson(value),null,2);JSON.parse(sanitized);
if(Buffer.byteLength(sanitized)+1>MAX_STATE_FILE)throw new CsoError('PERSISTENCE_FAILED','Private immutable artifact exceeds the 1 MiB persistence limit');
atomicWriteSync(path,sanitized+'\n',{mode:0o600,noReplace:true});
}catch(error:any){
if(error instanceof CsoError)throw error;
if(error?.code==='EEXIST')throw new CsoError('PERSISTENCE_FAILED','Immutable artifact already exists; it was not replaced');
throw new CsoError('PERSISTENCE_FAILED','Private immutable artifact could not be written');
}
}
function readPrivateJson(path:string):unknown{
let fd:number|undefined;
try{
const before=fs.lstatSync(path);
if(!before.isFile()||before.isSymbolicLink()||before.nlink!==1||before.size<=0||before.size>MAX_STATE_FILE||
(process.getuid&&before.uid!==process.getuid())||(process.platform!=='win32'&&(before.mode&0o077)!==0))
throw new CsoError('UNSAFE_PATH','Invalid private state file');
fd=fs.openSync(path,fs.constants.O_RDONLY|(fs.constants.O_NOFOLLOW??0));
const opened=fs.fstatSync(fd);
if(!sameRecoveryIdentity(recoveryIdentity(before),recoveryIdentity(opened)))
throw new CsoError('SNAPSHOT_RACE','Private state file changed while it was opened');
const raw=fs.readFileSync(fd,'utf8');
const final=fs.fstatSync(fd),after=fs.lstatSync(path);
if(!sameRecoveryIdentity(recoveryIdentity(opened),recoveryIdentity(final))||
!sameRecoveryIdentity(recoveryIdentity(opened),recoveryIdentity(after)))
throw new CsoError('SNAPSHOT_RACE','Private state file changed while it was read');
try{return JSON.parse(raw);}catch{throw new CsoError('MISSING_INPUT','Private state file is missing or invalid');}
}catch(error:any){
if(error instanceof CsoError)throw error;
if(error?.code==='ENOENT'||error?.code==='ELOOP')throw new CsoError('SNAPSHOT_RACE','Private state file changed while it was opened');
throw new CsoError('MISSING_INPUT','Private state file is missing or invalid');
}finally{if(fd!==undefined)try{fs.closeSync(fd);}catch{}}
}
export function readJson(path: string): any {
try {
secureDirectory(dirname(path));recoverAtomicNoReplaceJson(path,{label:'Private immutable artifact',maxBytes:MAX_STATE_FILE});return readPrivateJson(path);
} catch(e) { if (e instanceof CsoError) throw e; throw new CsoError('MISSING_INPUT','Private state file is missing or invalid'); }
}
export function saveReport(dir: string, report: RunReportV3): void {
report.completeness = completeness(report);
const safe=sanitizeHelperForJson(report) as RunReportV3;
for(const finding of safe.findings){const expected=fingerprint(finding);if(finding.id!==expected||finding.fingerprint!==expected)throw new CsoError('PERSISTENCE_FAILED','Finding identity changed during redaction; the previous report was preserved');}
try{secureDirectory(dir);const serialized=JSON.stringify(safe,null,2);if(Buffer.byteLength(serialized)+1>MAX_STATE_FILE)throw new CsoError('PERSISTENCE_FAILED','Private state exceeds the 1 MiB persistence limit; the previous artifact was preserved');atomicWriteSync(join(dir,'report.json'),serialized+'\n',{mode:0o600});}catch(error){if(error instanceof CsoError)throw error;throw new CsoError('PERSISTENCE_FAILED','Private report could not be written; no saved report is claimed');}
try { atomicWriteSync(join(dir,'report.md'),renderReport(safe),{mode:0o600}); }
catch { throw new CsoError('PERSISTENCE_FAILED','JSON was saved but the readable report could not be written'); }
}
export function loadReport(dir: string): RunReportV3 {
const v = readJson(join(dir,'report.json'));
if (v.schemaVersion !== 3 || !Array.isArray(v.coverage) || !Array.isArray(v.findings)) throw new CsoError('INCOMPATIBLE_INPUT','Expected a v3 run report');
return v;
}
export function event(report: RunReportV3, kind: string, message: string): void {
report.events.push({at:new Date().toISOString(),kind,message:redact(message)});
}
export function executionDeadline(report: RunReportV3): number { return Date.parse(report.deadline) - 60_000; }
export function requireTime(report: RunReportV3): void {
if (Date.now() >= executionDeadline(report)) throw new CsoError('DEADLINE','Investigation deadline reached; the final minute is reserved for reporting');
}
const LOCK_PROTOCOL='immutable-lease-set-v3';
const LOCK_OWNER_MAX_BYTES=4096;
const LOCK_TOKEN=/^[a-f0-9]{32}$/;
const PROCESS_IDENTITY=/^linux:\d+$/;
const LEASE_PUBLICATION_TEMP=/^([a-f0-9]{32})\.(json|decision)\.tmp\.(\d{1,10})\.[a-f0-9]{8}$/;
const LEASE_BLOCKED_WAIT_MS=250;
const LEASE_ELECTION_POLL_MS=1;
const LEASE_ELECTION_WAIT=new Int32Array(new SharedArrayBuffer(4));
const LEASE_CANDIDATE=/^([a-f0-9]{32})\.json$/;
const LEASE_DECISION=/^([a-f0-9]{32})\.decision$/;
const LEASE_ACTIVE=/^([a-f0-9]{32})\.active\.([a-f0-9]{16})$/;
type LockOwner={pid:number;processIdentity?:string;token:string;createdAt:number};
type LockIdentity={dev:number;ino:number};
type LeaseLinks=1|2;
type LeaseDecision={schemaVersion:1;token:string;kind:'ticket'|'withdraw';ticket?:string;candidateDev:string;candidateIno:string;ownerPid:number;ownerProcessIdentity?:string;ownerCreatedAt:number;publisherPid:number;publisherProcessIdentity?:string;createdAt:number};
function processAlive(pid:number):boolean{if(!Number.isInteger(pid)||pid<=1)return false;try{process.kill(pid,0);return true;}catch(error:any){return error?.code==='EPERM';}}
function processIdentity(pid:number):string|undefined{if(process.platform!=='linux')return;try{const raw=fs.readFileSync(`/proc/${pid}/stat`,'utf8'),tail=raw.slice(raw.lastIndexOf(')')+2).trim().split(/\s+/);return /^\d+$/.test(tail[19]??'')?`linux:${tail[19]}`:undefined;}catch{return;}}
function validateOwner(value:unknown,expectedToken?:string):LockOwner{
if(!value||typeof value!=='object'||Array.isArray(value))throw new CsoError('UNSAFE_PATH','Run mutation lease owner is malformed');
const owner=value as Record<string,unknown>;
if(!Number.isInteger(owner.pid)||Number(owner.pid)<=1||typeof owner.token!=='string'||!LOCK_TOKEN.test(owner.token)||
(expectedToken!==undefined&&owner.token!==expectedToken)||!Number.isFinite(owner.createdAt)||Number(owner.createdAt)<0||
(owner.processIdentity!==undefined&&(typeof owner.processIdentity!=='string'||!PROCESS_IDENTITY.test(owner.processIdentity))))
throw new CsoError('UNSAFE_PATH','Run mutation lease owner is malformed');
return {pid:Number(owner.pid),token:owner.token,createdAt:Number(owner.createdAt),...(owner.processIdentity===undefined?{}:{processIdentity:owner.processIdentity as string})};
}
function validateLeaseDecision(value:unknown,expectedToken?:string):LeaseDecision{
if(!value||typeof value!=='object'||Array.isArray(value))throw new CsoError('UNSAFE_PATH','Run mutation lease decision is malformed');
const decision=value as Record<string,unknown>,kind=decision.kind,ticket=decision.ticket;
if(decision.schemaVersion!==1||typeof decision.token!=='string'||!LOCK_TOKEN.test(decision.token)||(expectedToken!==undefined&&decision.token!==expectedToken)||
(kind!=='ticket'&&kind!=='withdraw')||(kind==='ticket'&&(typeof ticket!=='string'||!/^[a-f0-9]{16}$/.test(ticket)||ticket==='0000000000000000'))||(kind==='withdraw'&&ticket!==undefined)||
typeof decision.candidateDev!=='string'||!/^\d+$/.test(decision.candidateDev)||!Number.isSafeInteger(Number(decision.candidateDev))||
typeof decision.candidateIno!=='string'||!/^\d+$/.test(decision.candidateIno)||!Number.isSafeInteger(Number(decision.candidateIno))||
!Number.isInteger(decision.ownerPid)||Number(decision.ownerPid)<=1||!Number.isFinite(decision.ownerCreatedAt)||Number(decision.ownerCreatedAt)<0||
!Number.isInteger(decision.publisherPid)||Number(decision.publisherPid)<=1||!Number.isFinite(decision.createdAt)||Number(decision.createdAt)<0||
(decision.ownerProcessIdentity!==undefined&&(typeof decision.ownerProcessIdentity!=='string'||!PROCESS_IDENTITY.test(decision.ownerProcessIdentity)))||
(decision.publisherProcessIdentity!==undefined&&(typeof decision.publisherProcessIdentity!=='string'||!PROCESS_IDENTITY.test(decision.publisherProcessIdentity))))throw new CsoError('UNSAFE_PATH','Run mutation lease decision is malformed');
return decision as LeaseDecision;
}
function decisionOwner(decision:LeaseDecision):LockOwner{return{pid:decision.ownerPid,token:decision.token,createdAt:decision.ownerCreatedAt,...(decision.ownerProcessIdentity?{processIdentity:decision.ownerProcessIdentity}:{})};}
function decisionPublisher(decision:LeaseDecision):LockOwner{return{pid:decision.publisherPid,token:decision.token,createdAt:decision.createdAt,...(decision.publisherProcessIdentity?{processIdentity:decision.publisherProcessIdentity}:{})};}
function leaseDecisionRecoveryOptions(token:string):AtomicNoReplaceRecoveryOptions{return{label:'Run mutation lease decision',maxBytes:LOCK_OWNER_MAX_BYTES,
validate:(value,pid)=>{const decision=validateLeaseDecision(value,token);if(decision.publisherPid!==pid)throw new CsoError('UNSAFE_PATH','Run mutation lease decision temp does not match its publisher');},
publisherAlive:(value,pid)=>{const decision=validateLeaseDecision(value,token);if(decision.publisherPid!==pid)throw new CsoError('UNSAFE_PATH','Run mutation lease decision temp does not match its publisher');return ownerIsAlive(decisionPublisher(decision));}};}
function ownerLinkTransition(left:fs.Stats,right:fs.Stats):boolean{return left.isFile()&&right.isFile()&&left.dev===right.dev&&left.ino===right.ino&&left.size===right.size&&left.mode===right.mode&&left.uid===right.uid&&
left.nlink>=0&&left.nlink<=2&&right.nlink>=0&&right.nlink<=2&&left.nlink!==right.nlink;}
function readOwner(path:string,expectedToken?:string,expectedLinks:LeaseLinks=1,observed?:fs.Stats):{owner:LockOwner;identity:LockIdentity}{
let fd:number|undefined;
try{
const before=fs.lstatSync(path);
if(observed&&ownerLinkTransition(observed,before))throw new CsoError('INSUFFICIENT_CAPACITY','Run mutation lease changed phase while it was read');
if(before.isSymbolicLink()||!before.isFile()||before.nlink!==expectedLinks||before.size<=0||before.size>LOCK_OWNER_MAX_BYTES||
(process.getuid&&before.uid!==process.getuid())||(process.platform!=='win32'&&(before.mode&0o077)!==0))
throw new CsoError('UNSAFE_PATH','Run mutation lease is invalid');
fd=fs.openSync(path,fs.constants.O_RDONLY|(fs.constants.O_NOFOLLOW??0));
const opened=fs.fstatSync(fd);
if(ownerLinkTransition(before,opened))throw new CsoError('INSUFFICIENT_CAPACITY','Run mutation lease changed phase while it was read');
if(!opened.isFile()||opened.dev!==before.dev||opened.ino!==before.ino||opened.nlink!==expectedLinks||opened.size!==before.size)
throw new CsoError('UNSAFE_PATH','Run mutation lease changed while it was read');
let parsed:unknown;try{parsed=JSON.parse(fs.readFileSync(fd,'utf8'));}catch{throw new CsoError('UNSAFE_PATH','Run mutation lease is malformed');}
const final=fs.fstatSync(fd),after=fs.lstatSync(path);
const coherentTransition=(ownerLinkTransition(opened,final)&&final.dev===after.dev&&final.ino===after.ino&&final.nlink===after.nlink)||
(opened.dev===final.dev&&opened.ino===final.ino&&opened.nlink===final.nlink&&ownerLinkTransition(opened,after));
if(coherentTransition)throw new CsoError('INSUFFICIENT_CAPACITY','Run mutation lease changed phase while it was read');
if(after.isSymbolicLink()||after.dev!==opened.dev||after.ino!==opened.ino||after.nlink!==expectedLinks||final.dev!==opened.dev||final.ino!==opened.ino||final.nlink!==expectedLinks||final.size!==opened.size)
throw new CsoError('UNSAFE_PATH','Run mutation lease changed while it was read');
return {owner:validateOwner(parsed,expectedToken),identity:{dev:opened.dev,ino:opened.ino}};
}catch(error:any){
if(error instanceof CsoError)throw error;
if(error?.code==='ENOENT')throw new CsoError('INSUFFICIENT_CAPACITY','Run mutation lease changed during recovery');
throw new CsoError('UNSAFE_PATH','Run mutation lease could not be validated');
}finally{if(fd!==undefined)try{fs.closeSync(fd);}catch{}}
}
function ownerIsAlive(owner:LockOwner):boolean{
const pid=owner.pid;if(!processAlive(pid))return false;
const current=processIdentity(pid);
return !(typeof owner.processIdentity==='string'&&current!==undefined&&owner.processIdentity!==current);
}
function recoverLeasePublications(leases:string):void{
for(let attempt=0;attempt<4;attempt++){
try{
for(const name of fs.readdirSync(leases)){
const match=name.match(LEASE_PUBLICATION_TEMP);if(!match)continue;
const token=match[1],kind=match[2] as 'json'|'decision',publisherPid=Number(match[3]),temp=join(leases,name),target=join(leases,`${token}.${kind}`),options:AtomicNoReplaceRecoveryOptions=kind==='json'?{label:'Run mutation lease',maxBytes:LOCK_OWNER_MAX_BYTES,
validate:(value,pid)=>{const owner=validateOwner(value,token);if(owner.pid!==pid)throw new CsoError('UNSAFE_PATH','Run mutation lease temp does not match its publisher');},
publisherAlive:(value,pid)=>{const owner=validateOwner(value,token);if(owner.pid!==pid)throw new CsoError('UNSAFE_PATH','Run mutation lease temp does not match its publisher');return ownerIsAlive(owner);}}:
leaseDecisionRecoveryOptions(token);
let publicationObserved:fs.Stats|undefined;try{publicationObserved=fs.lstatSync(temp);}catch{}
if(liveEmptyPublication(temp,publisherPid))throw new CsoError('INSUFFICIENT_CAPACITY',`${options.label} publication is still changing under a live helper`);
try{
if(fs.existsSync(target))recoverAtomicNoReplaceJson(target,options);
if(fs.existsSync(temp))discardAtomicNoReplaceTemp(temp,publisherPid,options);
}catch(error){
// A live cooperating publisher may still be writing its private temp.
// Do not accept or remove unstable bytes; report ordinary contention.
const transientShape=error instanceof CsoError&&error.code==='UNSAFE_PATH'&&error.message===`${options.label} interrupted publication is not one private regular file`;
if(error instanceof CsoError&&((error.code==='SNAPSHOT_RACE'&&livePublicationAdvanced(temp,publisherPid,publicationObserved,options))||(transientShape&&(liveRecognizedPublication(temp,target,publisherPid,options)||livePublicationAdvanced(temp,publisherPid,publicationObserved,options)))))throw new AtomicPublicationTransition(`${options.label} publication advanced under its live helper`);
throw error;
}
}
return;
}catch(error){
// Retry only a proven same-inode no-replace transition. Foreign inode,
// content, permission, and pathname races remain visible failures.
if(!(error instanceof AtomicPublicationTransition))throw error;
}
}
throw new CsoError('INSUFFICIENT_CAPACITY','Another helper is publishing a run mutation lease');
}
function readLegacyOwner(path:string):{pid:number;processIdentity?:string;token:string;createdAt:number}{
let fd:number|undefined;
try{
const before=fs.lstatSync(path);
if(before.isSymbolicLink()||!before.isFile()||before.nlink!==1||before.size<=0||before.size>LOCK_OWNER_MAX_BYTES||(process.getuid&&before.uid!==process.getuid()))
throw new CsoError('UNSAFE_PATH','Legacy run mutation lock owner is invalid');
fd=fs.openSync(path,fs.constants.O_RDONLY|(fs.constants.O_NOFOLLOW??0));
const opened=fs.fstatSync(fd);
if(opened.dev!==before.dev||opened.ino!==before.ino||opened.nlink!==1)throw new CsoError('UNSAFE_PATH','Legacy run mutation lock owner changed while it was read');
let value:unknown;try{value=JSON.parse(fs.readFileSync(fd,'utf8'));}catch{throw new CsoError('UNSAFE_PATH','Legacy run mutation lock owner is malformed');}
const after=fs.lstatSync(path),record=value as Record<string,unknown>;
if(after.dev!==opened.dev||after.ino!==opened.ino||!record||typeof record!=='object'||Array.isArray(record)||!Number.isInteger(record.pid)||Number(record.pid)<=1||
typeof record.token!=='string'||record.token.length<1||record.token.length>256||
(record.processIdentity!==undefined&&(typeof record.processIdentity!=='string'||!PROCESS_IDENTITY.test(record.processIdentity))))
throw new CsoError('UNSAFE_PATH','Legacy run mutation lock owner is malformed');
return {pid:Number(record.pid),token:record.token,createdAt:typeof record.createdAt==='number'&&Number.isFinite(record.createdAt)?record.createdAt:0,...(record.processIdentity===undefined?{}:{processIdentity:record.processIdentity as string})};
}catch(error:any){
if(error instanceof CsoError)throw error;
if(error?.code==='ENOENT')throw new CsoError('INSUFFICIENT_CAPACITY','A legacy helper may still be initializing this run; its incomplete lock was left intact');
throw new CsoError('UNSAFE_PATH','Legacy run mutation lock owner could not be validated');
}finally{if(fd!==undefined)try{fs.closeSync(fd);}catch{}}
}
function exactUnlink(path:string,token:string,identity:LockIdentity,links:LeaseLinks=1):void{
let current:{owner:LockOwner;identity:LockIdentity};
try{current=readOwner(path,token,links);}catch{throw new CsoError('PERSISTENCE_FAILED','Run mutation lease ownership changed before exact release');}
if(current.identity.dev!==identity.dev||current.identity.ino!==identity.ino)throw new CsoError('PERSISTENCE_FAILED','Run mutation lease ownership changed before exact release');
// One final pathname check narrows lstat/read/unlink replacement races. Lease
// names are immutable and never reused by cooperating helpers.
let final:fs.Stats;try{final=fs.lstatSync(path);}catch{throw new CsoError('PERSISTENCE_FAILED','Run mutation lease ownership changed before exact release');}
if(final.isSymbolicLink()||final.dev!==identity.dev||final.ino!==identity.ino||final.nlink!==links)throw new CsoError('PERSISTENCE_FAILED','Run mutation lease ownership changed before exact release');
try{fs.unlinkSync(path);}catch{throw new CsoError('PERSISTENCE_FAILED','Run mutation lease ownership changed before exact release');}
}
function acquireMigrationClaim(path:string):{owner:LockOwner;identity:LockIdentity}{
for(let attempt=0;attempt<4;attempt++){
recoverAtomicNoReplaceJson(path,{label:'Legacy run mutation recovery claim',maxBytes:LOCK_OWNER_MAX_BYTES,
validate:(value,pid)=>{const owner=validateOwner(value);if(owner.pid!==pid)throw new CsoError('UNSAFE_PATH','Legacy recovery temp does not match its publisher');}});
const owner:LockOwner={pid:process.pid,processIdentity:processIdentity(process.pid),token:randomBytes(16).toString('hex'),createdAt:Date.now()};
try{
atomicWriteSync(path,JSON.stringify(owner)+'\n',{mode:0o600,noReplace:true});
return readOwner(path,owner.token);
}catch(error:any){
if(error instanceof CsoError)throw error;
if(error?.code!=='EEXIST')throw new CsoError('PERSISTENCE_FAILED','Legacy run mutation recovery claim could not be published');
const stale=readOwner(path);
if(ownerIsAlive(stale.owner))throw new CsoError('INSUFFICIENT_CAPACITY','Another helper is recovering this run');
try{exactUnlink(path,stale.owner.token,stale.identity);}catch(recoveryError){
if(attempt===3)throw recoveryError;
}
}
}
throw new CsoError('INSUFFICIENT_CAPACITY','Another helper is recovering this run');
}
function ensureLockProtocol(dir:string):string{
const lock=join(dir,'.mutation-lock'),marker=JSON.stringify({protocol:LOCK_PROTOCOL})+'\n';
try{atomicWriteSync(lock,marker,{mode:0o600,noReplace:true});}
catch(error:any){
if(error?.code!=='EEXIST')throw new CsoError('PERSISTENCE_FAILED','Run mutation lock protocol could not be initialized');
recoverAtomicNoReplaceJson(lock,{label:'Run mutation lock protocol',maxBytes:LOCK_OWNER_MAX_BYTES,
validate:value=>{if(!value||typeof value!=='object'||Array.isArray(value)||(value as any).protocol!==LOCK_PROTOCOL)throw new CsoError('UNSAFE_PATH','Run mutation lock protocol is invalid');}});
const stat=fs.lstatSync(lock);
if(stat.isSymbolicLink())throw new CsoError('UNSAFE_PATH','Run mutation lock is a symlink');
if(stat.isFile()){
let protocol='';try{if(stat.nlink!==1||stat.size<=0||stat.size>LOCK_OWNER_MAX_BYTES||(process.platform!=='win32'&&(stat.mode&0o077)!==0))throw new Error('invalid');protocol=JSON.parse(fs.readFileSync(lock,'utf8')).protocol;}catch{}
if(protocol!==LOCK_PROTOCOL||stat.nlink!==1||(process.getuid&&stat.uid!==process.getuid()))throw new CsoError('UNSAFE_PATH','Run mutation lock protocol is invalid');
}else if(stat.isDirectory()){
// v2 created the canonical directory before publishing owner.json. A
// missing/malformed owner can still belong to a paused live initializer,
// so it is never age-reclaimed. Fully published dead owners can migrate.
const owner=readLegacyOwner(join(lock,'owner.json'));
if(ownerIsAlive(owner as LockOwner))throw new CsoError('INSUFFICIENT_CAPACITY','Another helper is updating this run');
const migration=join(lock,'.v3-migration'),claim=acquireMigrationClaim(migration),current=fs.lstatSync(lock);
if(current.dev!==stat.dev||current.ino!==stat.ino){try{exactUnlink(migration,claim.owner.token,claim.identity);}catch{}throw new CsoError('INSUFFICIENT_CAPACITY','Another helper changed this run during recovery');}
const tomb=join(dir,`.mutation-lock.legacy-${process.pid}-${randomBytes(4).toString('hex')}`);
try{fs.renameSync(lock,tomb);atomicWriteSync(lock,marker,{mode:0o600,noReplace:true});fs.rmSync(tomb,{recursive:true,force:true});}
catch{try{if(!fs.existsSync(lock)&&fs.existsSync(tomb))fs.renameSync(tomb,lock);}catch{}try{if(fs.existsSync(migration))exactUnlink(migration,claim.owner.token,claim.identity);}catch{}throw new CsoError('PERSISTENCE_FAILED','Legacy run mutation lock could not be migrated safely');}
}else throw new CsoError('UNSAFE_PATH','Run mutation lock has an invalid file type');
}
const leases=join(dir,'.mutation-lock-leases');
if(!fs.existsSync(leases))try{fs.mkdirSync(leases,{mode:0o700});}catch(error:any){if(error?.code!=='EEXIST')throw error;}
const stat=fs.lstatSync(leases);if(stat.isSymbolicLink()||!stat.isDirectory()||(process.getuid&&stat.uid!==process.getuid()))throw new CsoError('UNSAFE_PATH','Run mutation lease directory is invalid');
if(process.platform!=='win32')fs.chmodSync(leases,0o700);
return leases;
}
type LeaseState={token:string;owner:LockOwner;identity:LockIdentity;candidate?:string;decisionPath?:string;decision?:LeaseDecision;decisionIdentity?:LockIdentity;active?:string;number?:bigint};
type HeldRunLease={path:string;decision:string;decisionIdentity:LockIdentity;active:string;token:string;identity:LockIdentity};
function privateLeaseArtifact(stat:fs.Stats):boolean{return stat.isFile()&&!stat.isSymbolicLink()&&stat.size>0&&stat.size<=LOCK_OWNER_MAX_BYTES&&
(!process.getuid||stat.uid===process.getuid())&&(process.platform==='win32'||(stat.mode&0o077)===0);}
function readLeaseDecision(path:string,token:string):{decision:LeaseDecision;identity:LockIdentity}{
const options=leaseDecisionRecoveryOptions(token);recoverAtomicNoReplaceJson(path,options);
const recovered=recoveryJson(path,1,options);
return{decision:validateLeaseDecision(recovered.value,token),identity:{dev:recovered.identity.dev,ino:recovered.identity.ino}};
}
function decisionMatchesIdentity(decision:LeaseDecision,identity:LockIdentity):boolean{return decision.candidateDev===String(identity.dev)&&decision.candidateIno===String(identity.ino);}
function decisionMatchesOwner(decision:LeaseDecision,owner:LockOwner):boolean{return decision.ownerPid===owner.pid&&decision.ownerCreatedAt===owner.createdAt&&decision.ownerProcessIdentity===owner.processIdentity;}
function scanRunLeases(leases:string):LeaseState[]{
const deadline=Date.now()+LEASE_BLOCKED_WAIT_MS;let contention:CsoError|undefined;
for(let attempt=0;attempt<LEASE_BLOCKED_WAIT_MS/LEASE_ELECTION_POLL_MS+16;attempt++){
contention=undefined;
const names=fs.readdirSync(leases).sort();
if(names.some(name=>LEASE_PUBLICATION_TEMP.test(name))){try{recoverLeasePublications(leases);contention=new CsoError('INSUFFICIENT_CAPACITY','Run mutation lease publication changed during the lease scan');}catch(error){if(!(error instanceof CsoError)||error.code!=='INSUFFICIENT_CAPACITY'||Date.now()>=deadline)throw error;contention=error;Atomics.wait(LEASE_ELECTION_WAIT,0,0,LEASE_ELECTION_POLL_MS);}continue;}
const grouped=new Map<string,{candidate?:string;decision?:string;actives:Array<{path:string;encoded:string}>}>();
for(const name of names){
const candidate=name.match(LEASE_CANDIDATE),decision=name.match(LEASE_DECISION),active=name.match(LEASE_ACTIVE),token=candidate?.[1]??decision?.[1]??active?.[1];
if(!token)throw new CsoError('UNSAFE_PATH','Run mutation lease directory contains an invalid artifact');
const group=grouped.get(token)??{actives:[]};
if(candidate){if(group.candidate)throw new CsoError('UNSAFE_PATH','Run mutation lease has duplicate candidate state');group.candidate=join(leases,name);}
else if(decision){if(group.decision)throw new CsoError('UNSAFE_PATH','Run mutation lease has duplicate decision state');group.decision=join(leases,name);}
else if(active)group.actives.push({path:join(leases,name),encoded:active[2]});
grouped.set(token,group);
}
let retry=false;const states:LeaseState[]=[];
for(const [token,group] of grouped){
if(group.actives.length>1||group.actives.length===1&&!group.decision||!group.candidate&&!group.decision){retry=true;break;}
let decisionRecord:{decision:LeaseDecision;identity:LockIdentity}|undefined;
if(group.decision)try{decisionRecord=readLeaseDecision(group.decision,token);}catch(error){if(error instanceof CsoError&&(error.code==='SNAPSHOT_RACE'||error.code==='INSUFFICIENT_CAPACITY')){if(error.code==='INSUFFICIENT_CAPACITY'){if(Date.now()>=deadline)throw error;contention=error;}retry=true;break;}throw error;}
if(group.actives[0]&&(!decisionRecord||decisionRecord.decision.kind!=='ticket'||decisionRecord.decision.ticket!==group.actives[0].encoded))throw new CsoError('UNSAFE_PATH','Run mutation lease active phase does not match its ticket decision');
const ownerPath=group.candidate??group.actives[0]?.path;let inspected:{owner:LockOwner;identity:LockIdentity}|undefined;
if(ownerPath){const expected=(group.candidate&&group.actives[0]?2:1) as LeaseLinks;let observed:fs.Stats;try{observed=fs.lstatSync(ownerPath);}catch(error:any){if(error?.code==='ENOENT'){retry=true;break;}throw error;}if(!privateLeaseArtifact(observed)){throw new CsoError('UNSAFE_PATH','Run mutation lease owner phase is not one private regular file');}if(observed.nlink!==expected){retry=true;break;}try{inspected=readOwner(ownerPath,token,expected,observed);}catch(error){if(error instanceof CsoError&&error.code==='INSUFFICIENT_CAPACITY'){if(Date.now()>=deadline)throw error;contention=error;retry=true;break;}throw error;}}
if(group.candidate&&group.actives[0]){let activeStat:fs.Stats;try{activeStat=fs.lstatSync(group.actives[0].path);}catch(error:any){if(error?.code==='ENOENT'){retry=true;break;}throw error;}if(!privateLeaseArtifact(activeStat)||activeStat.dev!==inspected!.identity.dev||activeStat.ino!==inspected!.identity.ino)throw new CsoError('UNSAFE_PATH','Run mutation lease active phase does not match its candidate inode');if(activeStat.nlink!==2){retry=true;break;}}
const identity=inspected?.identity??{dev:Number(decisionRecord!.decision.candidateDev),ino:Number(decisionRecord!.decision.candidateIno)},owner=inspected?.owner??decisionOwner(decisionRecord!.decision);
if(decisionRecord&&(!decisionMatchesIdentity(decisionRecord.decision,identity)||!decisionMatchesOwner(decisionRecord.decision,owner)))throw new CsoError('UNSAFE_PATH','Run mutation lease decision does not match its candidate owner');
const number=decisionRecord?.decision.kind==='ticket'?BigInt(`0x${decisionRecord.decision.ticket}`):undefined;
states.push({token,owner,identity,...(group.candidate?{candidate:group.candidate}:{}),...(group.decision&&decisionRecord?{decisionPath:group.decision,decision:decisionRecord.decision,decisionIdentity:decisionRecord.identity}:{}),...(group.actives[0]?{active:group.actives[0].path}:{}),...(number!==undefined?{number}:{})});
}
if(!retry&&fs.readdirSync(leases).sort().join('\0')===names.join('\0'))return states;
Atomics.wait(LEASE_ELECTION_WAIT,0,0,LEASE_ELECTION_POLL_MS);
}
if(contention)throw contention;
throw new CsoError('UNSAFE_PATH','Run mutation lease phases could not be validated as one coherent set');
}
function releaseLeaseState(state:LeaseState):void{
if(state.candidate)exactUnlink(state.candidate,state.token,state.identity,state.active?2:1);
if(state.active)exactUnlink(state.active,state.token,state.identity,1);
if(state.decisionPath&&state.decisionIdentity)exactDecisionUnlink(state.decisionPath,state.token,state.decisionIdentity);
}
function exactDecisionUnlink(path:string,token:string,identity:LockIdentity):void{
let current:{decision:LeaseDecision;identity:LockIdentity};try{current=readLeaseDecision(path,token);}catch{throw new CsoError('PERSISTENCE_FAILED','Run mutation lease decision changed before exact release');}
if(current.identity.dev!==identity.dev||current.identity.ino!==identity.ino)throw new CsoError('PERSISTENCE_FAILED','Run mutation lease decision changed before exact release');
let final:fs.Stats;try{final=fs.lstatSync(path);}catch{throw new CsoError('PERSISTENCE_FAILED','Run mutation lease decision changed before exact release');}
if(!privateLeaseArtifact(final)||final.nlink!==1||final.dev!==identity.dev||final.ino!==identity.ino)throw new CsoError('PERSISTENCE_FAILED','Run mutation lease decision changed before exact release');
try{fs.unlinkSync(path);}catch{throw new CsoError('PERSISTENCE_FAILED','Run mutation lease decision changed before exact release');}
}
function publishLeasePhase(candidate:string,target:string,token:string,identity:LockIdentity):void{
const before=readOwner(candidate,token,1);if(before.identity.dev!==identity.dev||before.identity.ino!==identity.ino)throw new CsoError('PERSISTENCE_FAILED','Run mutation lease changed before phase publication');
try{fs.linkSync(candidate,target);}catch{throw new CsoError('PERSISTENCE_FAILED','Run mutation lease phase could not be published');}
const source=fs.lstatSync(candidate),phase=fs.lstatSync(target);if(source.dev!==identity.dev||source.ino!==identity.ino||phase.dev!==identity.dev||phase.ino!==identity.ino||source.nlink!==2||phase.nlink!==2)throw new CsoError('PERSISTENCE_FAILED','Run mutation lease phase changed during publication');
}
function makeLeaseDecision(owner:LockOwner,identity:LockIdentity,kind:'ticket'|'withdraw',ticket?:string):LeaseDecision{
const publisherIdentity=processIdentity(process.pid);
return{schemaVersion:1,token:owner.token,kind,...(ticket?{ticket}:{}),candidateDev:String(identity.dev),candidateIno:String(identity.ino),ownerPid:owner.pid,...(owner.processIdentity?{ownerProcessIdentity:owner.processIdentity}:{}),ownerCreatedAt:owner.createdAt,publisherPid:process.pid,...(publisherIdentity?{publisherProcessIdentity:publisherIdentity}:{}),createdAt:Date.now()};
}
function publishLeaseDecision(path:string,decision:LeaseDecision):void{
try{atomicWriteSync(path,JSON.stringify(decision)+'\n',{mode:0o600,noReplace:true});}catch(error:any){if(error?.code!=='EEXIST')throw new CsoError('PERSISTENCE_FAILED','Run mutation lease decision could not be published');}
}
function releaseKnownLease(candidate:string,decisionPath:string,active:string|undefined,token:string,identity:LockIdentity,expectedDecisionIdentity?:LockIdentity,requireActive=false):void{
let candidateStat:fs.Stats;try{candidateStat=fs.lstatSync(candidate);}catch{throw new CsoError('PERSISTENCE_FAILED','Run mutation lease ownership changed before exact release');}
if(!privateLeaseArtifact(candidateStat)||candidateStat.dev!==identity.dev||candidateStat.ino!==identity.ino)throw new CsoError('PERSISTENCE_FAILED','Run mutation lease ownership changed before exact release');
let activeStat:fs.Stats|undefined;try{if(active)activeStat=fs.lstatSync(active);}catch(error:any){if(error?.code!=='ENOENT')throw new CsoError('PERSISTENCE_FAILED','Run mutation lease active phase changed before cleanup');}
if(requireActive&&!activeStat)throw new CsoError('PERSISTENCE_FAILED','Run mutation lease active phase changed before exact release');
if(activeStat&&(!privateLeaseArtifact(activeStat)||activeStat.dev!==identity.dev||activeStat.ino!==identity.ino))throw new CsoError('PERSISTENCE_FAILED','Run mutation lease active phase changed before cleanup');
const expected=activeStat?2:1;if(candidateStat.nlink!==expected||activeStat&&activeStat.nlink!==2)throw new CsoError('PERSISTENCE_FAILED','Run mutation lease link state changed before cleanup');
let decisionRecord:{decision:LeaseDecision;identity:LockIdentity}|undefined;try{decisionRecord=readLeaseDecision(decisionPath,token);}catch(error:any){if(!(error instanceof CsoError)||error.code!=='SNAPSHOT_RACE')throw new CsoError('PERSISTENCE_FAILED','Run mutation lease decision changed before cleanup');}
if(expectedDecisionIdentity&&!decisionRecord)throw new CsoError('PERSISTENCE_FAILED','Run mutation lease decision changed before exact release');
if(decisionRecord&&(!decisionMatchesIdentity(decisionRecord.decision,identity)||decisionRecord.decision.ownerPid!==process.pid||expectedDecisionIdentity&&(decisionRecord.identity.dev!==expectedDecisionIdentity.dev||decisionRecord.identity.ino!==expectedDecisionIdentity.ino)))throw new CsoError('PERSISTENCE_FAILED','Run mutation lease decision changed before cleanup');
exactUnlink(candidate,token,identity,expected);if(activeStat)exactUnlink(active!,token,identity,1);if(decisionRecord)exactDecisionUnlink(decisionPath,token,decisionRecord.identity);
}
function compareLeaseOrder(left:LeaseState,rightNumber:bigint,rightToken:string):number{return left.number!<rightNumber?-1:left.number!>rightNumber?1:left.token<rightToken?-1:left.token>rightToken?1:0;}
function recoverDeadLease(state:LeaseState):boolean{
if(ownerIsAlive(state.owner))return false;
try{releaseLeaseState(state);}catch(error){
if(!(error instanceof CsoError)||error.code!=='PERSISTENCE_FAILED')throw error;
for(const path of [state.candidate,state.active].filter((value):value is string=>Boolean(value))){try{const stat=fs.lstatSync(path);if(!privateLeaseArtifact(stat)||stat.dev!==state.identity.dev||stat.ino!==state.identity.ino)throw new CsoError('UNSAFE_PATH','Dead run mutation lease was replaced during recovery');}catch(recoveryError:any){if(recoveryError instanceof CsoError)throw recoveryError;if(recoveryError?.code!=='ENOENT')throw recoveryError;}}
if(state.decisionPath&&state.decisionIdentity)try{const stat=fs.lstatSync(state.decisionPath);if(!privateLeaseArtifact(stat)||stat.dev!==state.decisionIdentity.dev||stat.ino!==state.decisionIdentity.ino)throw new CsoError('UNSAFE_PATH','Dead run mutation lease decision was replaced during recovery');}catch(recoveryError:any){if(recoveryError instanceof CsoError)throw recoveryError;if(recoveryError?.code!=='ENOENT')throw recoveryError;}
Atomics.wait(LEASE_ELECTION_WAIT,0,0,LEASE_ELECTION_POLL_MS);
}
return true;
}
function chooseRunLeaseTicket(leases:string,token:string,owner:LockOwner,identity:LockIdentity):{path:string;number:bigint;identity:LockIdentity}{
for(;;){
const states=scanRunLeases(leases);let recovered=false,max=0n;
const own=states.find(state=>state.token===token);
if(!own?.candidate||own.identity.dev!==identity.dev||own.identity.ino!==identity.ino)throw new CsoError('PERSISTENCE_FAILED','Run mutation lease candidate changed before ticket selection');
if(own.decision){
if(own.decision.kind==='withdraw')throw new CsoError('INSUFFICIENT_CAPACITY','This run mutation lease was withdrawn before ticket selection');
if(own.number===undefined||!own.decisionPath||!own.decisionIdentity)throw new CsoError('PERSISTENCE_FAILED','Run mutation lease ticket decision is incomplete');
return{path:own.decisionPath,number:own.number,identity:own.decisionIdentity};
}
for(const state of states){
if(state.token===token)continue;
if(recoverDeadLease(state)){recovered=true;break;}
if(state.active)throw new CsoError('INSUFFICIENT_CAPACITY',state.owner.pid===process.pid?'Another operation in this helper is updating this run':'Another helper is updating this run');
if(!state.candidate||state.decision?.kind==='withdraw')continue;
if(state.owner.pid===process.pid)throw new CsoError('INSUFFICIENT_CAPACITY','Another operation in this helper is updating this run');
if(state.number!==undefined&&state.number>max)max=state.number;
}
if(recovered)continue;
const number=max+1n;if(number>0xffffffffffffffffn)throw new CsoError('INSUFFICIENT_CAPACITY','Run mutation lease ticket space is exhausted');
const encoded=number.toString(16).padStart(16,'0'),path=join(leases,`${token}.decision`);
publishLeaseDecision(path,makeLeaseDecision(owner,identity,'ticket',encoded));
}
}
function fenceRunLeaseCandidate(leases:string,state:LeaseState):void{
if(!state.candidate||state.decision)return;
const observed=readOwner(state.candidate,state.token,1);
if(observed.identity.dev!==state.identity.dev||observed.identity.ino!==state.identity.ino)throw new CsoError('UNSAFE_PATH','Run mutation lease candidate changed before withdrawal');
publishLeaseDecision(join(leases,`${state.token}.decision`),makeLeaseDecision(state.owner,state.identity,'withdraw'));
}
function activateRunLease(leases:string,token:string,candidate:string,decisionPath:string,active:string,number:bigint,identity:LockIdentity):void{
const blockedDeadline=Date.now()+LEASE_BLOCKED_WAIT_MS;
for(;;){
const states=scanRunLeases(leases),own=states.find(state=>state.token===token);
if(!own||own.candidate!==candidate||own.decisionPath!==decisionPath||own.decision?.kind!=='ticket'||own.number!==number||own.active||own.identity.dev!==identity.dev||own.identity.ino!==identity.ino)throw new CsoError(own?.decision?.kind==='withdraw'?'INSUFFICIENT_CAPACITY':'PERSISTENCE_FAILED',own?.decision?.kind==='withdraw'?'This run mutation lease was withdrawn before activation':'Run mutation lease ticket changed before activation');
let retry=false,lost=false;const pending:LeaseState[]=[];
for(const state of states){
if(state.token===token)continue;
if(recoverDeadLease(state)){retry=true;break;}
if(state.active)throw new CsoError('INSUFFICIENT_CAPACITY',state.owner.pid===process.pid?'Another operation in this helper is updating this run':'Another helper is updating this run');
if(!state.candidate||state.decision?.kind==='withdraw')continue;
if(state.owner.pid===process.pid)throw new CsoError('INSUFFICIENT_CAPACITY','Another operation in this helper is updating this run');
if(state.number===undefined)pending.push(state);else if(compareLeaseOrder(state,number,token)<0)lost=true;
}
if(retry)continue;
if(lost)throw new CsoError('INSUFFICIENT_CAPACITY','An earlier run mutation lease ticket won the election');
if(pending.length){if(Date.now()<blockedDeadline){Atomics.wait(LEASE_ELECTION_WAIT,0,0,LEASE_ELECTION_POLL_MS);continue;}for(const state of pending)fenceRunLeaseCandidate(leases,state);continue;}
publishLeasePhase(candidate,active,token,identity);
const verified=scanRunLeases(leases),current=verified.find(state=>state.token===token);if(!current||current.candidate!==candidate||current.decisionPath!==decisionPath||current.decision?.kind!=='ticket'||current.number!==number||current.active!==active||current.identity.dev!==identity.dev||current.identity.ino!==identity.ino||verified.some(state=>state.token!==token&&state.active))throw new CsoError('PERSISTENCE_FAILED','Run mutation lease activation could not be verified exclusively');
return;
}
}
function acquireRunLease(dir:string):HeldRunLease{
secureDirectory(dir);
const leases=ensureLockProtocol(dir);recoverLeasePublications(leases);
const token=randomBytes(16).toString('hex'),lease=join(leases,`${token}.json`),owner:LockOwner={pid:process.pid,processIdentity:processIdentity(process.pid),token,createdAt:Date.now()};
atomicWriteSync(lease,JSON.stringify(owner)+'\n',{mode:0o600,noReplace:true});
const ownStat=fs.lstatSync(lease),ownIdentity={dev:ownStat.dev,ino:ownStat.ino};
const decision=join(leases,`${token}.decision`);let decisionIdentity:LockIdentity|undefined,active:string|undefined;
try{
const chosen=chooseRunLeaseTicket(leases,token,owner,ownIdentity);decisionIdentity=chosen.identity;
active=join(leases,`${token}.active.${chosen.number.toString(16).padStart(16,'0')}`);activateRunLease(leases,token,lease,decision,active,chosen.number,ownIdentity);
const published=readOwner(active,token,2);if(published.identity.dev!==ownIdentity.dev||published.identity.ino!==ownIdentity.ino)throw new CsoError('PERSISTENCE_FAILED','Run mutation lease ownership changed before work began');
}catch(error){
try{releaseKnownLease(lease,decision,active,token,ownIdentity,decisionIdentity);}catch(releaseError){throw releaseError;}
throw error;
}
return{path:lease,decision,decisionIdentity:decisionIdentity!,active,token,identity:ownIdentity};
}
function releaseRunLease(lease:HeldRunLease,path=lease.path):void{const directory=dirname(path);releaseKnownLease(path,join(directory,basename(lease.decision)),join(directory,basename(lease.active)),lease.token,lease.identity,lease.decisionIdentity,true);}
export function withLock<T>(dir: string, fn: () => T): T | Promise<Awaited<T>> {
const lease=acquireRunLease(dir),unlock=()=>releaseRunLease(lease);
let value:T;
try {
value=fn();
} catch(error){
try{unlock();}catch(releaseError){throw releaseError;}
throw error;
}
if(value&&typeof (value as any).then==='function')return Promise.resolve(value).finally(unlock) as Promise<Awaited<T>>;
// Keep release errors outside the callback catch path. Retrying an exact
// release after it partially succeeds can only obscure which lease phase
// changed and attempts the same fail-closed cleanup twice.
unlock();return value as any;
}
function boundedMarker(path:string,admit:()=>void=()=>{}):string{
admit();
try{const stat=fs.lstatSync(path);if(stat.isSymbolicLink()||!stat.isFile()||stat.size>8192)return'';return fs.readFileSync(path,'utf8');}catch{return'';}
}
/** A detached watchdog owns these paths until it records exact cleanup or an acknowledgement. */
export function hasPendingWatchdogCleanup(dir:string,admit:()=>void=()=>{}):boolean{
let visited=0,pending=false;
const walk=(at:string,depth:number)=>{
if(pending||depth>6||visited++>4000)return;
const entries:fs.Dirent[]=[];let directory:fs.Dir;
admit();try{directory=fs.opendirSync(at);}catch{return;}
try{for(;;){admit();const entry=directory.readSync();if(!entry)break;entries.push(entry);}}finally{directory.closeSync();}
const names=new Set(entries.map(entry=>entry.name));
if(names.has('attempt.ready')&&!names.has('attempt.stopped')&&!boundedMarker(join(at,'attempt.event'),admit).includes('execution-copy cleanup complete')){pending=true;return;}
if(names.has('watchdog.ready')&&!names.has('watchdog.stopped')&&!boundedMarker(join(at,'watchdog.event'),admit).includes('cleanup complete')){pending=true;return;}
for(const entry of entries){if(!/^[A-Za-z0-9._-]{1,120}$/.test(entry.name)||!entry.isDirectory())continue;walk(join(at,entry.name),depth+1);if(pending)return;}
};
for(const name of ['supervision','preparation-execution']){
admit();const root=join(dir,name);if(!fs.existsSync(root))continue;
admit();
const rootStat=fs.lstatSync(root);if(rootStat.isSymbolicLink()||!rootStat.isDirectory())throw new CsoError('UNSAFE_PATH','Watchdog supervision state is not a private directory');
walk(root,0);if(pending)return true;
}
return false;
}
const EPHEMERAL_REPLAY='.ephemeral-replay.json';
/** Delete a replay-only snapshot unless detached cleanup still owns its control tree. */
export function finalizeReplayTemporary(dir:string):void{
if(hasPendingWatchdogCleanup(dir)){writeJsonExclusive(join(dir,EPHEMERAL_REPLAY),{schemaVersion:1,kind:'replay-temporary',retainedAt:new Date().toISOString()});return;}
fs.rmSync(dir,{recursive:true,force:true});
}
/** Remove one private tree cooperatively without following links or holding directory handles across checks. */
function boundedRemoveTree(root:string,admit:()=>void,preserveRootName?:string):void{
type Frame={path:string;root:boolean;names?:string[];index:number};
const stack:Frame[]=[{path:root,root:true,index:0}];
while(stack.length){
const frame=stack[stack.length-1];
if(!frame.names){
admit();let stat:fs.Stats;try{stat=fs.lstatSync(frame.path);}catch(error:any){if(error?.code==='ENOENT'){stack.pop();continue;}throw error;}
if(stat.isSymbolicLink()||!stat.isDirectory()){admit();fs.unlinkSync(frame.path);stack.pop();continue;}
const names:string[]=[];admit();const directory=fs.opendirSync(frame.path);
try{for(;;){admit();const entry=directory.readSync();if(!entry)break;if(!(frame.root&&entry.name===preserveRootName))names.push(entry.name);}}finally{directory.closeSync();}
frame.names=names;frame.index=0;
}
if(frame.index<frame.names.length){const name=frame.names[frame.index++];stack.push({path:join(frame.path,name),root:false,index:0});continue;}
if(frame.root&&preserveRootName)return;
admit();fs.rmdirSync(frame.path);stack.pop();
}
}
function consumeLeasedTree(root:string,admit:()=>void):void{
boundedRemoveTree(root,admit,'.mutation-lock-leases');
admit();const directory=fs.opendirSync(root);try{for(;;){admit();const entry=directory.readSync();if(!entry)break;if(entry.name!=='.mutation-lock-leases')throw new CsoError('SNAPSHOT_RACE','Private retention tree changed during bounded cleanup');}}finally{directory.closeSync();}
// Only the helper's fixed-size lease protocol remains. Consuming it with the
// directory preserves the exact-release invariant without an unbounded walk.
admit();fs.rmSync(root,{recursive:true,force:true});
}
function repairBundleExpiry(dir:string,run:string,now:number,runExpired:boolean,admit:()=>void):boolean{
admit();const bundles=join(dir,'bundles');if(!fs.existsSync(bundles))return false;
admit();
const stat=fs.lstatSync(bundles);if(stat.isSymbolicLink()||!stat.isDirectory())throw new CsoError('UNSAFE_PATH','Repair bundle archive is not a private directory');
let retained=false,remaining=0;admit();const directory=fs.opendirSync(bundles);
try{for(;;){
admit();const entry=directory.readSync();if(!entry)break;const name=entry.name;
const match=name.match(/^([a-f0-9]{32})\.json$/);if(!match){if(runExpired)boundedRemoveTree(join(bundles,name),admit);else remaining++;continue;}
admit();
const value=readJson(join(bundles,name)) as Record<string,any>,id=match[1],created=Date.parse(value?.createdAt),expires=Date.parse(value?.expiresAt);
if(value?.schemaVersion!==3||value?.id!==id||value?.runId!==run||value?.verification?.id!==id||value?.verification?.runId!==run||value?.verification?.createdAt!==value.createdAt||
!Number.isFinite(created)||new Date(created).toISOString()!==value.createdAt||!Number.isFinite(expires)||value.expiresAt!==new Date(created+30*86400_000).toISOString())
throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle retention identity is invalid');
if(expires<=now){admit();fs.unlinkSync(join(bundles,name));}else{retained=true;remaining++;}
}}finally{directory.closeSync();}
if(!remaining){admit();fs.rmdirSync(bundles);}
return retained;
}
function cleanupRun(dir:string,run:string,now:number,pinned:boolean,admit:()=>void):void{
admit();
let lease:HeldRunLease;
try{lease=acquireRunLease(dir);}catch(error){if(error instanceof CsoError&&error.code==='INSUFFICIENT_CAPACITY')return;throw error;}
let releasePath=lease.path,consumed=false;
try{
if(hasPendingWatchdogCleanup(dir,admit))return;
const created=Number(run.split('-')[0]),runExpired=now-created>30*86400_000,retainedBundle=repairBundleExpiry(dir,run,now,runExpired,admit);
admit();const ephemeral=fs.existsSync(join(dir,EPHEMERAL_REPLAY));
if(ephemeral||(runExpired&&!pinned&&!retainedBundle)){
admit();const before=fs.lstatSync(dir),tomb=join(dirname(dir),`.retired-${run}-${randomBytes(16).toString('hex')}`);fs.renameSync(dir,tomb);releasePath=join(tomb,'.mutation-lock-leases',basename(lease.path));admit();const after=fs.lstatSync(tomb);
if(before.dev!==after.dev||before.ino!==after.ino)throw new CsoError('SNAPSHOT_RACE','Expired run changed while it was retired');
// The retired name is outside the public run namespace. Consume the
// exclusive lease with the tree so no release/delete gap can admit a
// second helper against the same directory.
consumeLeasedTree(tomb,admit);consumed=true;return;
}
if(now-created>7*86400_000)for(const p of ['snapshot','readable'])boundedRemoveTree(join(dir,p),admit);
if(runExpired)for(const p of ['reviews','replays','dependency-closures','scanner-outcomes','verification-attempts'])boundedRemoveTree(join(dir,p),admit);
}finally{if(!consumed)releaseRunLease(lease,releasePath);}
}
function cleanupRetiredRun(dir:string,admit:()=>void):void{
admit();
let lease:HeldRunLease;try{lease=acquireRunLease(dir);}catch(error){if(error instanceof CsoError&&error.code==='INSUFFICIENT_CAPACITY')return;throw error;}
let consumed=false;try{if(hasPendingWatchdogCleanup(dir,admit))return;consumeLeasedTree(dir,admit);consumed=true;}finally{if(!consumed)releaseRunLease(lease);}
}
export interface RetentionOptions { deadlineMs?:number; maxEntries?:number }
export interface RetentionResult { complete:boolean; visited:number }
class RetentionBudgetExhausted extends Error {}
export function retention(now = Date.now(),options:RetentionOptions={}): RetentionResult {
const deadlineMs=options.deadlineMs??Number.MAX_SAFE_INTEGER,maxEntries=options.maxEntries??Number.MAX_SAFE_INTEGER;
if(!Number.isSafeInteger(deadlineMs)||deadlineMs<0||!Number.isSafeInteger(maxEntries)||maxEntries<1)throw new CsoError('INVALID_ARGUMENT','Invalid retention maintenance budget');
let visited=0;const admit=()=>{if(Date.now()>=deadlineMs||visited>=maxEntries)throw new RetentionBudgetExhausted();visited++;};
const names=(dir:string,pattern:RegExp):string[]=>{const found:string[]=[];admit();const directory=fs.opendirSync(dir);try{for(;;){admit();const entry=directory.readSync();if(!entry)break;if(pattern.test(entry.name))found.push(entry.name);}}finally{directory.closeSync();}return found;};
const root = privateRoot();
const retainedParents=new Set<string>(),repositories:{repo:string;repoDir:string;runs:string[];retired:string[]}[]=[];
const retainedReport=(repoDir:string,repo:string,run:string):RunReportV3|undefined=>{
const file=join(repoDir,run,'report.json');
try{
admit();
const stat=fs.lstatSync(file);
if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1||stat.size<=0||stat.size>MAX_STATE_FILE||
(process.getuid&&stat.uid!==process.getuid())||(process.platform!=='win32'&&(stat.mode&0o077)!==0))return;
admit();
const report=JSON.parse(fs.readFileSync(file,'utf8'));
if(report?.schemaVersion!==3||report.runId!==run||report.repoId!==repo||!Array.isArray(report.coverage)||!Array.isArray(report.findings)||
!['running','finished','interrupted'].includes(report.status))return;
return report as RunReportV3;
}catch(error){if(error instanceof RetentionBudgetExhausted)throw error;return;}
};
try{
for(const repo of names(root,/^[a-f0-9]{24}$/)){
admit();const repoDir=secureDirectory(join(root,repo)),entries=names(repoDir,/^(?:\d{13}-[a-f0-9]{16}|\.retired-\d{13}-[a-f0-9]{16}-[a-f0-9]{32})$/),runs=entries.filter(x=>/^\d{13}-/.test(x)),retired=entries.filter(x=>x.startsWith('.retired-'));
repositories.push({repo,repoDir,runs,retired});
}
// Discover every live recheck pin before destructive cleanup. An exhausted
// discovery pass returns without deleting a parent that may still be in use.
for(const {repo,repoDir,runs} of repositories)for(const run of runs){
if(now-Number(run.split('-')[0])>30*86400_000)continue;
const report=retainedReport(repoDir,repo,run),parent=report?.parent as Record<string,unknown>|undefined;
if(!report||!['running','interrupted'].includes(report.status)||!Number.isFinite(Date.parse(report.deadline))||Date.parse(report.deadline)<=now||
!parent||typeof parent!=='object'||Array.isArray(parent)||Object.keys(parent).sort().join(',')!=='findingId,kind,runId'||parent.kind!=='recheck'||
typeof parent.runId!=='string'||!/^\d{13}-[a-f0-9]{16}$/.test(parent.runId)||parent.runId===run||typeof parent.findingId!=='string'||!/^[a-f0-9]{32}$/.test(parent.findingId))continue;
const original=retainedReport(repoDir,repo,parent.runId);
if(original?.status==='finished'&&original.findings.some(f=>f.id===parent.findingId))retainedParents.add(`${repo}/${parent.runId}`);
}
for (const {repo,repoDir,runs,retired} of repositories) {
for(const name of retired)cleanupRetiredRun(join(repoDir,name),admit);
for (const run of runs) {
admit();const dir = secureDirectory(join(repoDir,run));
// Every destructive retention decision owns the same exclusive lease as
// writers and replay. Whole runs are atomically retired before release.
cleanupRun(dir,run,now,retainedParents.has(`${repo}/${run}`),admit);
}
}
admit();const legacy=join(root,'legacy-imports');
if(fs.existsSync(legacy)){
admit();const directory=fs.lstatSync(legacy);if(directory.isSymbolicLink()||!directory.isDirectory())throw new CsoError('UNSAFE_PATH','Legacy report archive is not a private directory');
for(const name of names(legacy,/^[a-f0-9]{64}\.json$/)){
admit();const file=join(legacy,name),stat=fs.lstatSync(file);if(stat.isSymbolicLink()||!stat.isFile()||(process.getuid&&stat.uid!==process.getuid()))throw new CsoError('UNSAFE_PATH','Legacy report archive contains an unsafe artifact');
admit();
if(now-stat.mtimeMs>30*86400_000)fs.unlinkSync(file);
}
}
return{complete:true,visited};
}catch(error){
if(error instanceof RetentionBudgetExhausted)return{complete:false,visited};
throw error;
}
}
+354
View File
@@ -0,0 +1,354 @@
import * as fs from 'node:fs';
import { randomBytes } from 'node:crypto';
import { spawn } from 'node:child_process';
import { basename, dirname, join } from 'node:path';
import {
AssertionWitnessBinding, AssertionWitnessReceipt, CsoError, PreparationProof, RepairBundle, RepairReviewArtifact, SnapshotManifest, VerificationManifest, VerificationObservation, VerificationRequest,
canonical, relativePath, sha256, snapshotPathHandleId, snapshotPathId, validateVerificationObservation, validateVerificationRequest,
} from './contracts';
import { QualifiedRuntime } from './runtime-catalog';
import { inspectPreparation, type CsoStack } from './preparation';
import { containedFile } from './snapshot';
import { DockerEndpoint, DockerGroup } from './docker';
import type { PreparedDatabaseContract } from './preparation-executor';
import { redact, sanitizeHelperForJson } from './process';
import { secureDirectory, writeJsonExclusive } from './state';
import { AssertionWitnessHandle, AssertionWitnessSession, WitnessedVerificationResult, assertionWitnessPairHash, testExecutionPassed, validateStoredAssertionWitnessReceipt, witnessObservationHash } from './witness';
export { testExecutionPassed } from './witness';
export interface VerificationExecutor {
observe(source:string,phase:'before'|'after',request:VerificationRequest,runtime:QualifiedRuntime,verifier:QualifiedRuntime,work:string,control:string,execution?:{environment:Record<string,string>;database?:PreparedDatabaseContract},testEvidence?:{minimumPassingTests:number[]},witness?:AssertionWitnessHandle):Promise<VerificationObservation|WitnessedVerificationResult>;
}
export interface FailedVerificationAttempt {
schemaVersion:3; artifactKind:'repair_candidate'; id:string; runId:string; findingId:string; createdAt:string; bundleIssued:false;
runtime:{image:string;platform:string;profile:string}; policyHash:string; requestHash:string; harnessHash:string; sourceHash:string;
request:VerificationRequest; patchHash:string; testToolchain:'runtime'|'project'; testCompletionAssurance:'self_reported'; preparationHash?:string;
before:VerificationObservation; after?:VerificationObservation; reproduction:'blocked'|'inconclusive'|'disproved'|'reproduced'; repair:'failed'|'proposed';
failure:{code:string;message:string};
}
export class VerificationAttemptError extends CsoError {
constructor(public causeError:CsoError,public attempt:FailedVerificationAttempt){super(causeError.code,`${causeError.message}; before-phase evidence retained as attempt ${attempt.id}`);this.name='VerificationAttemptError';}
}
async function attemptGuard(runDir:string,work:string,watchdog:string,deadline:number):Promise<()=>Promise<void>>{
const control=secureDirectory(join(runDir,'supervision',basename(work)));secureDirectory(work);const ready=join(control,'attempt.ready'),terminal=join(control,'attempt.terminal'),stopped=join(control,'attempt.stopped');
const child=spawn(watchdog,['--attempt-owner',String(process.pid),'--deadline',String(Math.ceil(deadline/1000)),'--control-dir',control,'--work-root',work,'--run-root',runDir],{cwd:control,env:{PATH:'/usr/bin:/bin'},detached:true,stdio:'ignore'});let failed=false;child.once('error',()=>{failed=true;});child.unref();for(let i=0;i<100&&!failed&&!fs.existsSync(ready);i++)await new Promise(resolve=>setTimeout(resolve,10));let alive=false;try{if(child.pid){process.kill(child.pid,0);alive=true;}}catch{}if(failed||!alive||!fs.existsSync(ready)){try{if(child.pid)process.kill(child.pid,'SIGKILL');}catch{}fs.rmSync(work,{recursive:true,force:true});throw new CsoError('ISOLATION_FAILED','Attempt execution-copy watchdog failed its startup handshake');}
return async()=>{fs.rmSync(work,{recursive:true,force:true});fs.writeFileSync(terminal,'normal cleanup complete\n',{mode:0o600,flag:'wx'});for(let i=0;i<100&&!fs.existsSync(stopped);i++)await new Promise(resolve=>setTimeout(resolve,10));if(!fs.existsSync(stopped))throw new CsoError('ISOLATION_FAILED','Attempt watchdog did not acknowledge execution-copy cleanup');};
}
function allFiles(root:string,at=root,ignore:((path:string)=>boolean)=()=>false):string[]{
const out:string[]=[];for(const entry of fs.readdirSync(at,{withFileTypes:true})){
const p=join(at,entry.name),relative=p.slice(root.length+1).replaceAll('\\','/');if(ignore(relative))continue;if(entry.isSymbolicLink()||(!entry.isDirectory()&&!entry.isFile()))throw new CsoError('UNSAFE_PATH','Execution copy contains a special file');
if(entry.isDirectory())out.push(...allFiles(root,p,ignore));else out.push(relative);
}return out.sort();
}
export function treeHash(root:string,predicate:((path:string)=>boolean)=()=>true):string{
return sha256(canonical(allFiles(root).filter(predicate).map(path=>{const file=containedFile(root,path),before=fs.lstatSync(file);if(!before.isFile()||before.isSymbolicLink()||before.nlink!==1)throw new CsoError('UNSAFE_PATH','Execution copy contains a special or hard-linked file');const body=fs.readFileSync(file),after=fs.lstatSync(file);if(before.ino!==after.ino||before.dev!==after.dev||before.size!==after.size||before.mtimeMs!==after.mtimeMs||before.ctimeMs!==after.ctimeMs)throw new CsoError('SNAPSHOT_RACE',`Execution copy changed while hashing: ${path}`);return[path,sha256(body),before.mode&0o777];})));
}
const DEPENDENCY=/(?:^|\/)(?:package(?:-lock)?\.json|npm-shrinkwrap\.json|bun\.lock|uv\.lock|requirements[^/]*\.txt|pyproject\.toml|setup\.(?:py|cfg)|Gemfile(?:\.lock)?|[^/]+\.gemspec)$/;
const CONFIG=/(?:^|\/)(?:config\/.+|\.env|Dockerfile|Procfile|.*\.(?:toml|ya?ml|json))$/;
export function fileEffect(path:string):'source'|'configuration'|'dependency'{return DEPENDENCY.test(path)?'dependency':CONFIG.test(path)?'configuration':'source';}
export function patchHash(request:Pick<VerificationRequest,'changes'>):string{return sha256(canonical(request.changes));}
export function resolveVerificationRequestPaths(manifest:SnapshotManifest,request:VerificationRequest):VerificationRequest{
const resolve=(reference:string):string=>{const id=snapshotPathHandleId(reference);if(!id)return relativePath(reference);const entry=manifest.entries.find(item=>item.pathId===id);if(entry)return entry.path;const deleted=manifest.deletedPaths?.find(item=>item.pathId===id);if(deleted)return deleted.path;const changed=manifest.changedPaths?.find(path=>snapshotPathId(manifest.root,path)===id);if(changed)return changed;throw new CsoError('INVALID_SCHEMA',`Verification path handle is outside the retained snapshot: ${reference}`);};
const argument=(value:string):string=>{const direct=snapshotPathHandleId(value);if(direct)return resolve(value);if(value.startsWith('./')&&snapshotPathHandleId(value.slice(2)))return `./${resolve(value.slice(2))}`;return value;};
const command=(value:VerificationRequest['start']):VerificationRequest['start']=>({...value,args:value.args.map(argument)});
return{...request,start:command(request.start),existingTests:request.existingTests.map(command),boundaryFiles:request.boundaryFiles.map(resolve),testFiles:request.testFiles.map(resolve),changes:request.changes.map(change=>({...change,path:resolve(change.path)}))};
}
export function reviewRequestHash(request:VerificationRequest):string{const {artifactId,...review}=request.review;return sha256(canonical({...request,review}));}
export function reviewArtifactIdentity(artifact:RepairReviewArtifact):string{const {id,...bound}=artifact;return sha256(canonical(bound)).slice(0,32);}
export function makeReviewArtifact(runId:string,request:VerificationRequest,producer:string):RepairReviewArtifact{
if(!producer||producer.length>200||producer===request.review.reviewer)throw new CsoError('INVALID_SCHEMA','Repair producer and independent reviewer identities must be distinct');
if(!request.review.independent)throw new CsoError('INVALID_SCHEMA','Repair review must be explicitly independent');
const artifact:RepairReviewArtifact={schemaVersion:3,id:'',runId,findingId:request.findingId,createdAt:new Date().toISOString(),producer,reviewer:request.review.reviewer,assurance:'self_attested',requestHash:reviewRequestHash(request),patchHash:patchHash(request),rootCauseRepaired:request.review.rootCauseRepaired,featurePreserved:request.review.featurePreserved,boundaryMocks:request.review.boundaryMocks,rationale:request.review.rationale};artifact.id=reviewArtifactIdentity(artifact);return artifact;
}
export function validateReviewArtifact(value:unknown,runId:string,request:VerificationRequest):RepairReviewArtifact{
const artifact=value as RepairReviewArtifact;if(!artifact||artifact.schemaVersion!==3||artifact.assurance!=='self_attested'||artifact.runId!==runId||artifact.findingId!==request.findingId||artifact.id!==request.review.artifactId||reviewArtifactIdentity(artifact)!==artifact.id||artifact.requestHash!==reviewRequestHash(request)||artifact.patchHash!==patchHash(request)||artifact.reviewer!==request.review.reviewer||artifact.producer===artifact.reviewer||artifact.rootCauseRepaired!==request.review.rootCauseRepaired||artifact.featurePreserved!==request.review.featurePreserved||artifact.boundaryMocks!==request.review.boundaryMocks||artifact.rationale!==request.review.rationale)throw new CsoError('INCOMPATIBLE_INPUT','Self-attested repair-review artifact does not bind this request');return artifact;
}
export function verificationIdentity(manifest:VerificationManifest):string{const {id,...bound}=manifest;return sha256(canonical(bound)).slice(0,32);}
export function verificationHarnessHash(request:VerificationRequest,sourceRoot:string):string{
const testInputs=request.testFiles.map(path=>{const file=containedFile(sourceRoot,path);if(!fs.existsSync(file))throw new CsoError('INCOMPATIBLE_INPUT',`Immutable existing-test input is missing: ${path}`);const stat=fs.lstatSync(file);if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1)throw new CsoError('UNSAFE_PATH',`Immutable existing-test input is unsafe: ${path}`);return[path,sha256(fs.readFileSync(file)),stat.mode&0o777];});
return sha256(canonical({port:request.port,start:request.start,legitimate:request.legitimate,security:request.security,existingTests:request.existingTests,testInputs}));
}
export interface CanonicalTestPlan { commands:VerificationRequest['existingTests'];files:string[];kind:string;toolchain:'runtime'|'project';minimumPassingTests:number[];signature:string }
export interface CanonicalStartPlan { command:VerificationRequest['start'];kind:string;signature:string;entrypointFiles:string[] }
const TEST_TREE=/(?:^|\/)(?:test|tests|__tests__|spec|fixtures|__fixtures__|testdata)(?:\/|$)/;
const NODE_TEST_CONFIG=/(?:^|\/)(?:(?:jest|vitest|vite|playwright|cypress|karma|babel|ava|webpack)\.(?:config|conf)\.[^/]+|(?:jest|vitest|playwright|cypress|babel|ava|webpack)\.config\.[^/]+|\.mocharc(?:\.[^/]+)?|\.babelrc(?:\.[^/]+)?|tsconfig(?:\.[^/]+)?\.json|bunfig\.toml)$/;
const BUN_RUNTIME_POLICY_ARGS=['--no-install','--config=/opt/cso/no-auto-install.toml'];
// -S prevents dependency-provided .pth startup code from running before the
// trusted bootstrap. Add the venv's fixed Linux purelib directory directly,
// without processing .pth files, and import each runner before application cwd.
const PYTHON_PURELIB="os.path.join(os.path.dirname(os.path.dirname(sys.executable)),'lib',f'python{sys.version_info.major}.{sys.version_info.minor}','site-packages')";
const PYTEST_BOOTSTRAP=`import os,sys;sys.path.append(${PYTHON_PURELIB});import pytest;sys.path.insert(0,os.getcwd());raise SystemExit(pytest.main(sys.argv[1:]))`;
const UNITTEST_BOOTSTRAP=`import os,sys,unittest;sys.path.append(${PYTHON_PURELIB});sys.path.insert(0,os.getcwd());unittest.main(module=None,argv=['unittest',*sys.argv[1:]])`;
const DJANGO_BOOTSTRAP=`import os,sys,runpy;sys.path.append(${PYTHON_PURELIB});import django;sys.path.insert(0,os.getcwd());sys.argv=['manage.py',*sys.argv[1:]];runpy.run_path('manage.py',run_name='__main__')`;
const FLASK_BOOTSTRAP=`import os,sys;sys.path.append(${PYTHON_PURELIB});from flask.cli import main as _cso_main;sys.path.insert(0,os.getcwd());sys.argv=['flask',*sys.argv[1:]];_cso_main()`;
const UVICORN_BOOTSTRAP=`import os,sys;sys.path.append(${PYTHON_PURELIB});from uvicorn.main import main as _cso_main;sys.path.insert(0,os.getcwd());sys.argv=['uvicorn',*sys.argv[1:]];_cso_main()`;
function fileText(root:string,path:string):string{try{return fs.readFileSync(containedFile(root,path),'utf8');}catch{throw new CsoError('MISSING_INPUT',`Canonical test input is missing or unreadable: ${path}`);}}
function packageTestProjection(root:string,paths:string[]):unknown[]{return paths.filter(path=>/(?:^|\/)package\.json$/.test(path)&&!TEST_TREE.test(path)).map(path=>{let value:Record<string,any>;try{value=JSON.parse(fileText(root,path));}catch{throw new CsoError('MISSING_INPUT',`Canonical package test configuration is invalid: ${path}`);}if(!value||typeof value!=='object'||Array.isArray(value))throw new CsoError('MISSING_INPUT',`Canonical package test configuration is invalid: ${path}`);return{path,type:value.type??null,workspaces:value.workspaces??null,scripts:value.scripts??null,jest:value.jest??null,vitest:value.vitest??null,mocha:value.mocha??null,ava:value.ava??null,nyc:value.nyc??null};});}
function withoutJsCommentsAndStrings(value:string):string{return value.replace(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g,match=>' '.repeat(match.length));}
function assertJavascriptTestRegistrations(root:string,tests:string[]):number{
const code=tests.map(path=>withoutJsCommentsAndStrings(fileText(root,path))).join('\n');
if(/\b(?:fdescribe|fit)\s*\(|\b(?:describe|test|it)\s*\.\s*(?:only|concurrent\s*\.\s*only)\b/.test(code))throw new CsoError('MISSING_INPUT','Canonical JavaScript tests cannot certify a focused-only suite');
const registrations=[...code.matchAll(/\b(?:test|it)\s*(?:\.\s*(?:concurrent|each)\s*(?:\([^)]*\))?)?\s*\(/g)].length;if(!registrations)throw new CsoError('MISSING_INPUT','Canonical JavaScript tests need static evidence of at least one non-skipped test or it registration');return registrations;
}
function boundedTestPaths(tests:string[]):string[]{
if(!tests.length)throw new CsoError('MISSING_INPUT','No canonical project test sources were found');
if(tests.length>1000)throw new CsoError('MISSING_INPUT','Canonical project test suite exceeds the 1,000-file verification limit');
const paths=tests.map(path=>`./${path}`);if(paths.reduce((bytes,path)=>bytes+Buffer.byteLength(path)+1,0)>128*1024)throw new CsoError('MISSING_INPUT','Canonical project test paths exceed the bounded direct-runner argument limit');return paths;
}
function directPackageTest(root:string,stack:'node'|'bun',manifest:Record<string,any>,tests:string[],configuration:string[]):{command:VerificationRequest['existingTests'][number];kind:string;runner:string;toolchain:'runtime'|'project';minimumPassingTests:number}{
const scripts=manifest.scripts;if(!scripts||typeof scripts!=='object'||Array.isArray(scripts))throw new CsoError('MISSING_INPUT',`Canonical ${stack} package test scripts are missing or invalid`);
const script=scripts.test;if(typeof script!=='string'||!script.trim()||script.length>4096||/no test specified|^\s*(?:true|:|exit\s+0)\s*$/i.test(script))throw new CsoError('MISSING_INPUT',`Canonical ${stack} test script is missing or a placeholder`);
for(const hook of ['pretest','posttest']){
const value=scripts[hook];if(value!==undefined&&typeof value!=='string')throw new CsoError('MISSING_INPUT',`Canonical ${stack} ${hook} lifecycle hook is invalid`);
if(typeof value==='string'&&value.trim())throw new CsoError('MISSING_INPUT',`Canonical ${stack} tests cannot certify through package lifecycle hooks; remove ${hook} or run the direct standard runner`);
}
if(/[;&|><`$()\\\r\n]/.test(script))throw new CsoError('MISSING_INPUT',`Canonical ${stack} tests require one recognized direct standard runner; local wrappers and shell composition are not admitted`);
const words=script.trim().split(/\s+/),standard=['jest','vitest','mocha','ava'],paths=boundedTestPaths(tests),minimumPassingTests=assertJavascriptTestRegistrations(root,tests);
if(canonical(words)===canonical(['node','--test'])){
return{command:{executable:'/usr/local/bin/node',args:['--test','--test-reporter=tap',...paths]},kind:'direct node --test with TAP count evidence',runner:'node',toolchain:'runtime',minimumPassingTests};
}
if(canonical(words)===canonical(['bun','test'])){
if(stack!=='bun')throw new CsoError('MISSING_INPUT','Canonical Node verification cannot depend on the Bun test runtime');
return{command:{executable:'/usr/local/bin/bun',args:[...BUN_RUNTIME_POLICY_ARGS,'test',...paths]},kind:'direct bun test with automatic installation disabled',runner:'bun',toolchain:'runtime',minimumPassingTests};
}
const runner=words.length===1&&standard.includes(words[0])?words[0]:words.length===3&&words[0]==='npx'&&words[1]==='--no-install'&&standard.includes(words[2])?words[2]:undefined;
if(!runner)throw new CsoError('MISSING_INPUT',`Canonical ${stack} tests require one recognized direct standard runner; local wrappers and shell composition are not admitted`);
const control=canonical({embedded:manifest[runner]??null,files:configuration.filter(path=>NODE_TEST_CONFIG.test(path)).map(path=>[path,fileText(root,path)])});
if(/(?:collectOnly|dryRun|passWithNoTests|testNamePattern|\b(?:grep|fgrep|match)\b|--(?:collect-only|dry-run|grep|fgrep|match|passWithNoTests))/i.test(control))throw new CsoError('MISSING_INPUT',`Canonical ${runner} configuration cannot focus, skip execution, or allow an empty suite`);
const args=runner==='jest'?['--runTestsByPath','--passWithNoTests=false','--json',...paths]:runner==='vitest'?['run','--passWithNoTests=false','--reporter=verbose',...paths]:runner==='mocha'?['--fail-zero','--no-dry-run','--forbid-only','--reporter','json',...paths]:['--tap',...paths];
return{command:{executable:`/work/node_modules/.bin/${runner}`,args},kind:`direct local ${runner}`,runner,toolchain:'project',minimumPassingTests};
}
function directPackageStartEntrypoint(root:string,stack:'node'|'bun',script:string):string{
if(/[;&|><`$()\\\r\n]/.test(script))throw new CsoError('MISSING_INPUT',`Canonical ${stack} startup cannot use shell composition`);
const words=script.trim().split(/\s+/),runner=words.shift();
if(runner!==stack||words.length!==1||!/^[A-Za-z0-9_./-]+\.(?:[cm]?[jt]s|jsx|tsx)$/.test(words[0])||words[0].startsWith('/')||words[0].split('/').includes('..'))throw new CsoError('MISSING_INPUT',`Canonical ${stack} package startup requires one direct contained ${stack} entrypoint`);
const path=relativePath(words[0]);if(!executableSource(root,path))throw new CsoError('MISSING_INPUT',`Canonical ${stack} package startup entrypoint is missing or unsafe: ${path}`);return path;
}
function pyprojectTestProjection(root:string,path:string):unknown{try{const value=Bun.TOML.parse(fileText(root,path)) as Record<string,any>;return{tool:{pytest:value?.tool?.pytest??null,coverage:value?.tool?.coverage??null},projectScripts:value?.project?.scripts??null};}catch{throw new CsoError('MISSING_INPUT','Canonical Python test configuration is invalid: pyproject.toml');}}
export function canonicalTestPlan(sourceRoot:string,stack:CsoStack):CanonicalTestPlan{
const generated=(path:string)=>{const first=path.split('/')[0];return stack==='node'?first==='node_modules'||first==='.cso-npm-cache':stack==='bun'?first==='node_modules'||first==='.cso-bun-cache':stack==='python'?first==='.venv'||first==='.cso-uv-cache'||path==='.gstack-cso-public-requirements.txt':path.startsWith('vendor/bundle/')||first==='.cso-bundle'||first==='.cso-gems';};
const all=allFiles(sourceRoot,sourceRoot,generated);let tests:string[]=[],configuration:string[]=[],commands:VerificationRequest['existingTests']=[],kind='',toolchain:'runtime'|'project'='runtime',minimumPassingTests:number[]=[],runnerEvidence:unknown={};
if(stack==='node'||stack==='bun'){
let manifest:Record<string,any>;try{manifest=JSON.parse(fileText(sourceRoot,'package.json'));}catch{throw new CsoError('MISSING_INPUT',`Canonical ${stack} package.json is missing or invalid`);}
tests=all.filter(path=>/(?:^|\/)(?:test|tests|__tests__)\/.*\.(?:[cm]?js|tsx?|jsx)$|\.(?:test|spec)\.(?:[cm]?js|tsx?|jsx)$/.test(path));configuration=all.filter(path=>TEST_TREE.test(path)||NODE_TEST_CONFIG.test(path));const direct=directPackageTest(sourceRoot,stack,manifest,tests,configuration);commands=[direct.command];kind=direct.kind;toolchain=direct.toolchain;minimumPassingTests=[direct.minimumPassingTests];runnerEvidence={runner:direct.runner,declaredScript:manifest.scripts.test,packages:packageTestProjection(sourceRoot,all),minimumPassingTests:direct.minimumPassingTests};
}else if(stack==='python'){
tests=all.filter(path=>/(?:^|\/)(?:test|tests)\/.*\.py$|(?:^|\/)test_[^/]+\.py$|_test\.py$/.test(path));configuration=all.filter(path=>TEST_TREE.test(path)||/(?:^|\/)(?:conftest\.py|\.?pytest\.ini|\.?pytest\.toml|setup\.cfg|tox\.ini|noxfile\.py)$/.test(path));const bodies=tests.map(path=>fileText(sourceRoot,path)),configBodies=configuration.map(path=>fileText(sourceRoot,path)),pyproject=all.includes('pyproject.toml')?pyprojectTestProjection(sourceRoot,'pyproject.toml'):null;const pytestEvidence=all.some(path=>/(?:^|\/)(?:conftest\.py|\.?pytest\.ini|\.?pytest\.toml)$/.test(path))||[...bodies,...configBodies].some(body=>/(?:^|\n)\s*(?:import pytest|from pytest\b|@pytest\.)/m.test(body)||/(?:^|\n)(?:async\s+)?def test_[A-Za-z0-9_]*\s*\(/m.test(body));const unittestEvidence=bodies.some(body=>/(?:^|\n)\s*(?:import unittest|from unittest\b)|unittest\.TestCase|TestCase\s*\)/m.test(body));if(!pytestEvidence&&!unittestEvidence)throw new CsoError('MISSING_INPUT','Python test runner is ambiguous; declare pytest evidence or a unittest suite');const usePytest=pytestEvidence,paths=boundedTestPaths(tests),pytestControl=[...configBodies,canonical(pyproject)].join('\n');if(usePytest&&/(?:--collect-only|\s--co\b|--setup-(?:only|plan)|--fixtures(?:-per-test)?|--no-summary|\baddopts[^\n]*(?:\s-k\b|\s-m\b|--ignore\b|--deselect\b|(?:^|\s)-q{2,}\b))/i.test(pytestControl))throw new CsoError('MISSING_INPUT','Canonical pytest configuration cannot collect only, focus, deselect, suppress its count, or skip test execution');commands=[{executable:'/work/.venv/bin/python',args:usePytest?['-I','-S','-c',PYTEST_BOOTSTRAP,'-q','--color=no','--',...paths]:['-I','-S','-c',UNITTEST_BOOTSTRAP,...paths]}];kind=usePytest?'isolated prepared pytest with explicit files, positive summary, and no .pth startup':'isolated standard-library unittest with explicit files and no .pth startup';toolchain=usePytest?'project':'runtime';minimumPassingTests=[1];runnerEvidence={runner:usePytest?'pytest':'unittest',bootstrap:usePytest?PYTEST_BOOTSTRAP:UNITTEST_BOOTSTRAP,siteInitialization:false,reporter:usePytest?'quiet-positive-summary-no-color':'unittest-summary',pyproject};
}else{
const specs=all.filter(path=>/(?:^|\/)spec\/.*_spec\.rb$/.test(path)),rails=all.filter(path=>/(?:^|\/)test\/.*_test\.rb$/.test(path));tests=[...specs,...rails];configuration=all.filter(path=>TEST_TREE.test(path)||/(?:^|\/)\.rspec(?:-local)?$/.test(path));const rspecControl=configuration.filter(path=>/(?:^|\/)\.rspec(?:-local)?$/.test(path)).map(path=>fileText(sourceRoot,path)).join('\n');if(/--(?:dry-run|tag|example|pattern|exclude-pattern|only-failures|next-failure)\b/.test(rspecControl))throw new CsoError('MISSING_INPUT','Canonical RSpec configuration cannot dry-run, focus, filter, or select only prior failures');commands=[...(specs.length?[{executable:'/usr/local/bin/bundle',args:['exec','rspec','--format','json','--',...boundedTestPaths(specs)]}]:[]),...(rails.length?[{executable:'/usr/local/bin/bundle',args:['exec','rails','test','--no-color',...boundedTestPaths(rails)]}]:[])];kind=commands.map(command=>command.args.join(' ')).join(' + ');toolchain='project';minimumPassingTests=commands.map(()=>1);runnerEvidence={rspec:specs.length>0,minitest:rails.length>0,reporters:specs.length?['rspec-json',...(rails.length?['rails-summary-no-color']:[])]:['rails-summary-no-color']};
}
tests=[...new Set(tests)].sort();configuration=[...new Set(configuration)].sort();if(!tests.length)throw new CsoError('MISSING_INPUT',`No canonical ${stack} project test sources were found`);const selected=[...new Set([...configuration,...tests])].sort();if(selected.length>1000)throw new CsoError('MISSING_INPUT','Canonical project test suite exceeds the 1,000-file verification limit');if(minimumPassingTests.length!==commands.length||minimumPassingTests.some(value=>!Number.isInteger(value)||value<1))throw new CsoError('MISSING_INPUT','Canonical test plan could not derive a positive execution-count floor');return{commands,files:selected,kind,toolchain,minimumPassingTests,signature:sha256(canonical({stack,runnerEvidence,commands,files:selected,toolchain,minimumPassingTests}))};
}
export function assertCanonicalTestPlan(request:VerificationRequest,sourceRoot:string,stack:CsoStack):CanonicalTestPlan{const plan=canonicalTestPlan(sourceRoot,stack);if(canonical(request.existingTests)!==canonical(plan.commands)||canonical([...request.testFiles].sort())!==canonical(plan.files))throw new CsoError('INVALID_SCHEMA',`Existing tests must use the helper-derived full ${plan.kind} suite and its immutable inputs`);return plan;}
function executableSource(root:string,path:string):boolean{try{const file=containedFile(root,relativePath(path)),stat=fs.lstatSync(file);return stat.isFile()&&!stat.isSymbolicLink()&&stat.nlink===1;}catch{return false;}}
function rejectPythonFrameworkShadows(root:string,framework:string,names:string[]):void{
for(const name of names)for(const candidate of [`${name}.py`,name]){
const file=containedFile(root,candidate);if(fs.existsSync(file))throw new CsoError('PREREQUISITE',`Canonical ${framework} startup rejects root import shadow: ${candidate}`);
}
}
export function canonicalStartPlan(sourceRoot:string,stack:CsoStack,port:number):CanonicalStartPlan{
if(!Number.isInteger(port)||port<1024||port>65535)throw new CsoError('INVALID_SCHEMA','Canonical application start needs a loopback port from 1024 to 65535');
let command:VerificationRequest['start'],kind:string,entrypointFiles:string[]=[],evidence:unknown;
if(stack==='node'||stack==='bun'){
let manifest:Record<string,any>;try{manifest=JSON.parse(fileText(sourceRoot,'package.json'));}catch{throw new CsoError('MISSING_INPUT',`Canonical ${stack} package.json is missing or invalid`);}const start=manifest?.scripts?.start;
if(typeof start==='string'&&start.trim()&&!/no start|^\s*(?:true|:|exit\s+0)\s*$/i.test(start)){for(const hook of ['prestart','poststart']){const value=manifest?.scripts?.[hook];if(value!==undefined&&typeof value!=='string')throw new CsoError('MISSING_INPUT',`Canonical ${stack} ${hook} lifecycle hook is invalid`);if(typeof value==='string'&&value.trim())throw new CsoError('MISSING_INPUT',`Canonical ${stack} startup cannot certify through package lifecycle hooks; remove ${hook} or run the direct entrypoint`);}const entry=directPackageStartEntrypoint(sourceRoot,stack,start);command={executable:stack==='node'?'/usr/local/bin/node':'/usr/local/bin/bun',args:stack==='node'?[entry]:[...BUN_RUNTIME_POLICY_ARGS,entry]};kind=`direct ${stack} package start`;evidence={script:start,entry};entrypointFiles=['package.json',entry];}
else{const declared=typeof manifest?.main==='string'&&manifest.main.length<4096?manifest.main:undefined,candidates=[...(declared?[declared]:[]),...'server.js,app.js,index.js,server.mjs,app.mjs,index.mjs'.split(',')].filter((value,index,all)=>all.indexOf(value)===index&&executableSource(sourceRoot,value));if(candidates.length!==1)throw new CsoError('MISSING_INPUT',`Canonical ${stack} startup is ambiguous; declare one non-placeholder start script or one conventional main entrypoint`);const entry=relativePath(candidates[0]);command={executable:stack==='node'?'/usr/local/bin/node':'/usr/local/bin/bun',args:stack==='node'?[entry]:[...BUN_RUNTIME_POLICY_ARGS,entry]};kind=`${stack} ${entry}`;evidence={entry};entrypointFiles=[entry];}
}else if(stack==='rails'){
entrypointFiles=['config/application.rb','config/environment.rb'].filter(path=>executableSource(sourceRoot,path));if(entrypointFiles.length!==2)throw new CsoError('MISSING_INPUT','Canonical Rails startup requires config/application.rb and config/environment.rb');command={executable:'/usr/local/bin/bundle',args:['exec','rails','server','-b','127.0.0.1','-p',String(port)]};kind='Rails loopback server';evidence={entrypointFiles};
}else{
const preparation=inspectPreparation(sourceRoot,'python');if(preparation.status!=='ready')throw new CsoError('PREREQUISITE',preparation.prerequisites.map(item=>item.message).join('; ')||'Python dependency metadata is incomplete');const dependencies=new Set(preparation.inputs.map(input=>input.name.toLowerCase().replaceAll('_','-'))),choices:Array<{kind:string;command:VerificationRequest['start'];files:string[];evidence:unknown}>=[];
if(dependencies.has('django')&&executableSource(sourceRoot,'manage.py')){rejectPythonFrameworkShadows(sourceRoot,'Django',['django']);choices.push({kind:'isolated Django loopback server without .pth startup',command:{executable:'/work/.venv/bin/python',args:['-I','-S','-c',DJANGO_BOOTSTRAP,'runserver',`127.0.0.1:${port}`,'--noreload']},files:['manage.py'],evidence:{framework:'django',bootstrap:DJANGO_BOOTSTRAP,siteInitialization:false,rootImportShadowsRejected:['django.py','django/']}});}
for(const file of ['app.py','application.py','wsgi.py'])if(dependencies.has('flask')&&executableSource(sourceRoot,file)){const body=fileText(sourceRoot,file),match=body.match(/(?:^|\n)\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*Flask\s*\(/m);if(match){rejectPythonFrameworkShadows(sourceRoot,'Flask',['flask']);choices.push({kind:'isolated Flask loopback server without .pth startup',command:{executable:'/work/.venv/bin/python',args:['-I','-S','-c',FLASK_BOOTSTRAP,'--app',`${file.replace(/\.py$/,'')}:${match[1]}`,'run','--host','127.0.0.1','--port',String(port)]},files:[file],evidence:{framework:'flask',module:file,symbol:match[1],bootstrap:FLASK_BOOTSTRAP,siteInitialization:false,rootImportShadowsRejected:['flask.py','flask/']}});}}
for(const file of ['main.py','app.py','server.py'])if(dependencies.has('fastapi')&&dependencies.has('uvicorn')&&executableSource(sourceRoot,file)){const body=fileText(sourceRoot,file),match=body.match(/(?:^|\n)\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*FastAPI\s*\(/m);if(match){rejectPythonFrameworkShadows(sourceRoot,'FastAPI/Uvicorn',['fastapi','uvicorn']);choices.push({kind:'isolated FastAPI loopback server without .pth startup',command:{executable:'/work/.venv/bin/python',args:['-I','-S','-c',UVICORN_BOOTSTRAP,`${file.replace(/\.py$/,'')}:${match[1]}`,'--app-dir','/work','--host','127.0.0.1','--port',String(port)]},files:[file],evidence:{framework:'fastapi',module:file,symbol:match[1],bootstrap:UVICORN_BOOTSTRAP,siteInitialization:false,rootImportShadowsRejected:['fastapi.py','fastapi/','uvicorn.py','uvicorn/']}});}}
if(preparation.inputs.length===0){const direct=['app.py','application.py','server.py','main.py'].filter(file=>executableSource(sourceRoot,file));if(direct.length===1){const entry=direct[0];choices.push({kind:'isolated standard-library Python application',command:{executable:'/usr/local/bin/python',args:['-I',entry]},files:[entry],evidence:{framework:'standard-library',entry,isolatedMode:true,dependencyClosure:'empty'}});}}
const unique=choices.filter((choice,index)=>choices.findIndex(other=>canonical(other.command)===canonical(choice.command))===index);if(unique.length!==1)throw new CsoError('MISSING_INPUT','Canonical Python startup is unavailable or ambiguous; use one supported Django, Flask, or FastAPI entrypoint with locked runtime dependencies');({command,kind,evidence}=unique[0]);entrypointFiles=unique[0].files;
}
const entrypointEvidence=entrypointFiles.map(path=>{const file=containedFile(sourceRoot,path),stat=fs.lstatSync(file);if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1)throw new CsoError('UNSAFE_PATH',`Canonical startup input is unsafe: ${path}`);return{path,sha256:sha256(fs.readFileSync(file)),mode:stat.mode&0o777};});
return{command,kind,entrypointFiles,signature:sha256(canonical({stack,kind,evidence,command,entrypointEvidence}))};
}
export function assertCanonicalStartPlan(request:VerificationRequest,sourceRoot:string,stack:CsoStack):CanonicalStartPlan{const plan=canonicalStartPlan(sourceRoot,stack,request.port);if(canonical(request.start)!==canonical(plan.command))throw new CsoError('INVALID_SCHEMA',`Application start must use the helper-derived ${plan.kind} command`);return plan;}
export function preparePatchedSource(snapshot:string,target:string,request:VerificationRequest):void{
secureDirectory(target);fs.cpSync(snapshot,target,{recursive:true,errorOnExist:false,force:true,preserveTimestamps:false});
for(const change of request.changes){
const path=relativePath(change.path),file=containedFile(target,path),exists=fs.existsSync(file);
if(change.beforeSha256===null&&exists)throw new CsoError('INCOMPATIBLE_INPUT',`Expected new patch path already exists: ${path}`);
if(change.beforeSha256!==null&&(!exists||sha256(fs.readFileSync(file))!==change.beforeSha256))throw new CsoError('INCOMPATIBLE_INPUT',`Patch preimage does not match: ${path}`);
const derived=fileEffect(path);if(change.effect!==derived)throw new CsoError('INVALID_SCHEMA',`${path} must be declared as ${derived}, not ${change.effect}`);
if(change.after===null){fs.unlinkSync(file);continue;}
const safe=redact(change.after);if(safe!==change.after)throw new CsoError('REDACTION_FAILED',`Patch content for ${path} contains material that cannot enter a repair bundle`);
const mode=exists?(fs.statSync(file).mode&0o777):0o600;secureDirectory(dirname(file));fs.writeFileSync(file,change.after,{mode});
}
}
export function certify(params:{runId:string;manifest:SnapshotManifest;request:VerificationRequest;identityRequest?:VerificationRequest;runtime:QualifiedRuntime;verifier:QualifiedRuntime;before:VerificationObservation;after:VerificationObservation;beforeRoot:string;afterRoot:string;policyHash:string;auditPolicyHash?:string;archives:string[];dependencyClosures?:{before:unknown;after:unknown};preparation?:{before:PreparationProof;after:PreparationProof};reviewArtifact?:RepairReviewArtifact;startPlanHash?:string;testPlanHash?:string;testToolchain:'runtime'|'project';minimumPassingTests?:number[];witness?:{before:AssertionWitnessReceipt;after:AssertionWitnessReceipt}}):{manifest:VerificationManifest;bundle?:RepairBundle}{
const {request}=params,identityRequest=params.identityRequest??request,pHash=patchHash(identityRequest),harnessHash=verificationHarnessHash(request,params.beforeRoot),afterHarnessHash=verificationHarnessHash(request,params.afterRoot),fixturesHash=sha256(canonical(identityRequest.fixtures));if(afterHarnessHash!==harnessHash)throw new CsoError('ASSERTION_FAILED','Repair changed immutable existing-test inputs');
if(!['runtime','project'].includes(params.testToolchain))throw new CsoError('INVALID_SCHEMA','Verification test toolchain must be helper-derived as runtime or project');
if(identityRequest.review.reviewedPatchHash!==pHash)throw new CsoError('INVALID_SCHEMA',`Independent review binds the wrong patch hash; expected ${pHash}`);
const sourceAfter=treeHash(params.afterRoot),dependenciesBefore=treeHash(params.beforeRoot,p=>DEPENDENCY.test(p)),dependenciesAfter=treeHash(params.afterRoot,p=>DEPENDENCY.test(p)),configurationBefore=treeHash(params.beforeRoot,p=>CONFIG.test(p)&&!DEPENDENCY.test(p)),configurationAfter=treeHash(params.afterRoot,p=>CONFIG.test(p)&&!DEPENDENCY.test(p));
const mechanical=params.before.booted&&params.before.legitimate&&params.before.security==='intended_failure'&&params.before.existingTests&&params.after.booted&&params.after.legitimate&&params.after.security==='pass'&&params.after.existingTests;
const reviewGate=identityRequest.review.independent&&identityRequest.review.rootCauseRepaired&&identityRequest.review.featurePreserved&&!identityRequest.review.boundaryMocks;
// Application code shares the project test process and can forge reporter
// output or terminate the runner. The signed receipt authenticates the
// separate verifier assertions; project-test completion stays self-reported.
const testCompletionAssurance='self_reported' as const;
const reviewAssurance=params.reviewArtifact?.assurance??'self_attested';
const inconclusive=!params.before.booted||!params.before.legitimate||params.before.security==='inconclusive'||!params.after.booted||params.after.security==='inconclusive';
if(params.preparation&&(canonical(params.preparation.before.transformations)!==canonical(params.preparation.after.transformations)||
params.preparation.before.databaseHash!==params.preparation.after.databaseHash))throw new CsoError('ASSERTION_FAILED','Repair changed the synthetic preparation or database boundary');
if(mechanical&&reviewGate&&params.testToolchain==='project'){
if(request.changes.some(change=>change.effect==='dependency'))throw new CsoError('PREREQUISITE','Runtime-tested dependency repairs require a test runner pinned in the qualified runtime; project-installed test toolchains may change with the repair');
if(!params.preparation)throw new CsoError('ASSERTION_FAILED','Project-installed test toolchains require before/after prepared dependency proofs');
if(params.preparation.before.preparedDependencyHash!==params.preparation.after.preparedDependencyHash)throw new CsoError('ASSERTION_FAILED','Project-installed test toolchain bytes changed between source phases');
}
const transformations=params.manifest.entries.filter(e=>e.transformation),transformationsHash=sha256(canonical(transformations)),archivesHash=sha256(canonical([...params.archives].sort())),requestHash=sha256(canonical(identityRequest)),preparationHash=params.preparation?sha256(canonical(params.preparation)):undefined,startPlanHash=params.startPlanHash??sha256(canonical(request.start)),testPlanHash=params.testPlanHash??sha256(canonical({commands:request.existingTests,files:[...request.testFiles].sort()})),auditPolicyHash=params.auditPolicyHash??sha256(canonical({})),assertionHash=sha256(canonical({legitimate:identityRequest.legitimate,security:identityRequest.security})),minimumPassingTests=params.minimumPassingTests??request.existingTests.map(()=>1),runner={testToolchain:params.testToolchain,startPlanHash,testPlanHash,commandsHash:sha256(canonical(request.existingTests)),minimumPassingTestsHash:sha256(canonical(minimumPassingTests))};
let assertionAssurance:VerificationManifest['assertionAssurance'],witnessHash:string|undefined;
if(params.witness){
const beforeReceipt=validateStoredAssertionWitnessReceipt(params.witness.before),afterReceipt=validateStoredAssertionWitnessReceipt(params.witness.after),stable=(binding:AssertionWitnessBinding)=>{const {nonce:_,issuedAt:__,expiresAt:___,...value}=binding;return value;},expected=(phase:'before'|'after',sourceHash:string,dependencyHash:string,configurationHash:string)=>({schemaVersion:1,protocol:'gstack-cso-assertion-witness-v1',phase,runId:params.runId,findingId:identityRequest.findingId,policyHash:params.policyHash,auditPolicyHash,runtime:{image:params.runtime.image,verifierImage:params.verifier.image,platform:params.runtime.platform,profile:params.runtime.id},runner,sourceHash,dependencyHash,configurationHash,requestHash,patchHash:pHash,harnessHash,assertionHash,fixturesHash});
if(canonical(stable(beforeReceipt.binding))!==canonical(expected('before',params.manifest.executionHash,dependenciesBefore,configurationBefore))||canonical(stable(afterReceipt.binding))!==canonical(expected('after',sourceAfter,dependenciesAfter,configurationAfter))||beforeReceipt.publicKey!==afterReceipt.publicKey||beforeReceipt.keyId!==afterReceipt.keyId||beforeReceipt.binding.nonce===afterReceipt.binding.nonce)throw new CsoError('INCOMPATIBLE_INPUT','Authenticated assertion witness receipts do not bind this verification');
const beforeObservation={...params.before,existingTests:beforeReceipt.diagnosticTestsPassed,inputHash:harnessHash},afterObservation={...params.after,existingTests:afterReceipt.diagnosticTestsPassed,inputHash:harnessHash};
if(beforeReceipt.observationHash!==witnessObservationHash(beforeObservation)||afterReceipt.observationHash!==witnessObservationHash(afterObservation)||params.before.existingTests!==beforeReceipt.diagnosticTestsPassed||params.after.existingTests!==afterReceipt.diagnosticTestsPassed||!beforeReceipt.externalAssertionsPassed||!afterReceipt.externalAssertionsPassed)throw new CsoError('INCOMPATIBLE_INPUT','Authenticated assertion witness receipts do not match the verifier observations');
const stableExecutions=(receipt:AssertionWitnessReceipt)=>receipt.executions.map(({outputHash:_,...execution})=>execution);
if(canonical(stableExecutions(beforeReceipt))!==canonical(stableExecutions(afterReceipt)))throw new CsoError('ASSERTION_FAILED','Repair changed the existing-test execution count or outcome');
assertionAssurance='authenticated_out_of_process';witnessHash=assertionWitnessPairHash({before:beforeReceipt,after:afterReceipt});
}
const passed=mechanical&&reviewGate&&assertionAssurance==='authenticated_out_of_process',preservationUnattested=mechanical&&reviewGate&&!passed,createdAt=new Date().toISOString(),verification:VerificationManifest={version:3,id:'',runId:params.runId,findingId:identityRequest.findingId,createdAt,helperAbi:3,runtime:{image:params.runtime.image,platform:params.runtime.platform,profile:params.runtime.id},testToolchain:params.testToolchain,policyHash:params.policyHash,auditPolicyHash,harnessHash,requestHash,startPlanHash,testPlanHash,fixturesHash,patchHash:pHash,originalSourceHash:params.manifest.originalHash,transformationsHash,archivesHash,...(preparationHash?{preparationHash}:{}),beforeSourceHash:params.manifest.executionHash,afterSourceHash:sourceAfter,beforeDependencies:dependenciesBefore,afterDependencies:dependenciesAfter,beforeConfiguration:configurationBefore,afterConfiguration:configurationAfter,before:{...params.before,inputHash:harnessHash},after:{...params.after,inputHash:harnessHash},review:identityRequest.review,reviewAssurance,...(assertionAssurance?{assertionAssurance}:{}),testCompletionAssurance,...(witnessHash?{witnessHash}:{}),result:passed?'runtime_tested':(inconclusive||preservationUnattested)?'inconclusive':'failed'};const id=verificationIdentity(verification);verification.id=id;
if(!['runtime_tested','tested'].includes(verification.result))return{manifest:verification};
const bundle:RepairBundle={schemaVersion:3,runId:params.runId,id,createdAt,expiresAt:new Date(Date.parse(createdAt)+30*86400_000).toISOString(),requiredInputs:{sourceHash:params.manifest.executionHash,originalHash:params.manifest.originalHash,runtimeImage:params.runtime.image,platform:params.runtime.platform,archives:params.archives,...(params.dependencyClosures?{dependencyClosures:params.dependencyClosures}:{})},request:identityRequest,verification,transformations,...(params.preparation?{preparation:params.preparation}:{}),...(params.reviewArtifact?{reviewArtifact:params.reviewArtifact}:{}),witness:params.witness!};
return{manifest:verification,bundle};
}
export function validateRepairBundle(value:unknown,id:string,sourceRoot?:string,sourceManifest?:SnapshotManifest):RepairBundle{
const bundle=value as RepairBundle;if(!bundle||bundle.schemaVersion!==3||bundle.id!==id||bundle.runId!==bundle.verification?.runId||bundle.verification?.id!==id||verificationIdentity(bundle.verification)!==id)throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle identity or verification provenance is invalid');
const request=validateVerificationRequest(bundle.request),verification=bundle.verification,required=bundle.requiredInputs;
const createdAtMs=Date.parse(bundle.createdAt);if(!Number.isFinite(createdAtMs)||new Date(createdAtMs).toISOString()!==bundle.createdAt||bundle.createdAt!==verification.createdAt||bundle.expiresAt!==new Date(createdAtMs+30*86400_000).toISOString())throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle retention timestamps do not match their authenticated verification time');
if(!['runtime_tested','tested'].includes(verification.result)||!['self_attested','host_verified'].includes(verification.reviewAssurance)||verification.assertionAssurance!=='authenticated_out_of_process'||(verification.result==='tested'&&verification.testCompletionAssurance!=='authenticated_out_of_process')||(verification.result==='runtime_tested'&&verification.testCompletionAssurance!=='self_reported'))throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle lacks the assurance required by its repair label');
if(!['runtime','project'].includes(verification.testToolchain))throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle test toolchain provenance is invalid');
if(request.findingId!==verification.findingId||request.runtimeProfile!==verification.runtime.profile||sha256(canonical(request))!==verification.requestHash||canonical(request.review)!==canonical(verification.review)||patchHash(request)!==verification.patchHash||![verification.requestHash,verification.startPlanHash,verification.testPlanHash].every(value=>/^[a-f0-9]{64}$/.test(value))||sha256(canonical(request.fixtures))!==verification.fixturesHash||required.sourceHash!==verification.beforeSourceHash||required.originalHash!==verification.originalSourceHash||required.runtimeImage!==verification.runtime.image||required.platform!==verification.runtime.platform||sha256(canonical([...(required.archives??[])].sort()))!==verification.archivesHash||sha256(canonical(bundle.transformations??[]))!==verification.transformationsHash)throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle request, harness, or contents do not match their authenticated manifest');
if(!bundle.witness||!verification.witnessHash)throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle omitted its authenticated external assertion witness');
const beforeWitness=validateStoredAssertionWitnessReceipt(bundle.witness.before),afterWitness=validateStoredAssertionWitnessReceipt(bundle.witness.after),stable=(binding:AssertionWitnessBinding)=>{const {nonce:_,issuedAt:__,expiresAt:___,...rest}=binding;return rest;},assertionHash=sha256(canonical({legitimate:request.legitimate,security:request.security}));
if(assertionWitnessPairHash({before:beforeWitness,after:afterWitness})!==verification.witnessHash||beforeWitness.publicKey!==afterWitness.publicKey||beforeWitness.keyId!==afterWitness.keyId||beforeWitness.binding.nonce===afterWitness.binding.nonce)throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle assertion witness identity is invalid');
for(const [phase,receipt,observation,sourceHash,dependencyHash,configurationHash] of [['before',beforeWitness,verification.before,verification.beforeSourceHash,verification.beforeDependencies,verification.beforeConfiguration],['after',afterWitness,verification.after,verification.afterSourceHash,verification.afterDependencies,verification.afterConfiguration]] as const){
const binding=stable(receipt.binding);if(binding.phase!==phase||binding.runId!==verification.runId||binding.findingId!==verification.findingId||binding.policyHash!==verification.policyHash||binding.auditPolicyHash!==verification.auditPolicyHash||binding.runtime.image!==verification.runtime.image||binding.runtime.verifierImage!==verification.runtime.image||binding.runtime.platform!==verification.runtime.platform||binding.runtime.profile!==verification.runtime.profile||binding.runner.testToolchain!==verification.testToolchain||binding.runner.startPlanHash!==verification.startPlanHash||binding.runner.testPlanHash!==verification.testPlanHash||binding.sourceHash!==sourceHash||binding.dependencyHash!==dependencyHash||binding.configurationHash!==configurationHash||binding.requestHash!==verification.requestHash||binding.patchHash!==verification.patchHash||binding.harnessHash!==verification.harnessHash||binding.assertionHash!==assertionHash||binding.fixturesHash!==verification.fixturesHash||receipt.observationHash!==witnessObservationHash(observation)||receipt.diagnosticTestsPassed!==observation.existingTests||!receipt.externalAssertionsPassed)throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle assertion witness does not bind its verification manifest');
}
if(canonical(beforeWitness.binding.runner)!==canonical(afterWitness.binding.runner))throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle changed the witnessed runner between phases');
if(required.dependencyClosures){const hashes=new Set<string>();for(const phase of ['before','after'] as const){const closure=required.dependencyClosures[phase] as any;if(!closure||typeof closure!=='object'||Array.isArray(closure)||!Array.isArray(closure.archives)||!/^([a-f0-9]{64})$/.test(closure.closureHash??''))throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle dependency closure is malformed');const {closureHash,...body}=closure;if(sha256(canonical(body))!==closureHash)throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle dependency closure identity is invalid');for(const archive of closure.archives){if(!archive||typeof archive!=='object'||!/^[a-f0-9]{64}$/.test(archive.sha256??''))throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle dependency archive provenance is malformed');hashes.add(archive.sha256);}}if(canonical([...hashes].sort())!==canonical([...(required.archives??[])].sort()))throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle archive hashes do not match its dependency closures');}
if(required.dependencyClosures&&!bundle.preparation)throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle omitted prepared-source invariance proofs');
if(verification.testToolchain==='project'&&!bundle.preparation)throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle omitted project test-toolchain preparation proofs');
if(bundle.preparation){if(!verification.preparationHash||sha256(canonical(bundle.preparation))!==verification.preparationHash||canonical(bundle.preparation.before?.transformations)!==canonical(bundle.preparation.after?.transformations)||bundle.preparation.before?.databaseHash!==bundle.preparation.after?.databaseHash)throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle preparation proof is invalid');for(const phase of ['before','after'] as const){const proof=bundle.preparation[phase] as PreparationProof,closure=(required.dependencyClosures as any)?.[phase];if(!proof||proof.schemaVersion!==1||![proof.dependencyClosureHash,proof.configurationHash,proof.sourceProjectionHash,proof.preparedManifestHash,proof.preparedDependencyHash,proof.receiptHash,proof.executionEnvironmentHash,proof.databaseHash].every(value=>/^[a-f0-9]{64}$/.test(value))||!Array.isArray(proof.transformations)||proof.transformations.some(item=>!item||typeof item.path!=='string'||!/^[a-f0-9]{64}$/.test(item.sha256)||!Number.isInteger(item.mode)||typeof item.reason!=='string')||(closure&&proof.dependencyClosureHash!==closure.closureHash))throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle preparation proof does not bind its dependency closure');}}
if(verification.testToolchain==='project'&&bundle.preparation!.before.preparedDependencyHash!==bundle.preparation!.after.preparedDependencyHash)throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle project test toolchain changed between source phases');
if(request.review.artifactId){if(!bundle.reviewArtifact)throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle omitted its independent review artifact');validateReviewArtifact(bundle.reviewArtifact,bundle.runId,request);}
validateVerificationObservation(verification.before);validateVerificationObservation(verification.after);
if(sourceRoot){
const references=[...request.boundaryFiles,...request.testFiles,...request.changes.map(change=>change.path),...request.start.args,...request.existingTests.flatMap(command=>command.args)],hasHandles=references.some(reference=>Boolean(snapshotPathHandleId(reference)||reference.startsWith('./')&&snapshotPathHandleId(reference.slice(2))));
if(hasHandles&&!sourceManifest)throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle path handles require the matching snapshot manifest for source validation');
const executionRequest=sourceManifest?resolveVerificationRequestPaths(sourceManifest,request):request;
if(verificationHarnessHash(executionRequest,sourceRoot)!==verification.harnessHash)throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle harness does not match supplied source inputs');
const commandsHash=sha256(canonical(executionRequest.existingTests)),commandHashes=executionRequest.existingTests.map(command=>sha256(canonical(command)));if(beforeWitness.binding.runner.commandsHash!==commandsHash||canonical(beforeWitness.executions.map(item=>item.commandHash))!==canonical(commandHashes)||canonical(afterWitness.executions.map(item=>item.commandHash))!==canonical(commandHashes))throw new CsoError('INCOMPATIBLE_INPUT','Repair bundle witnessed a different test runner command set');
}
return{...bundle,request};
}
export class DockerVerificationExecutor implements VerificationExecutor{
private attemptDeadline:number;private executionStarted=false;
constructor(private endpoint:DockerEndpoint,private watchdogPath:string,private runExecutionDeadline=Date.now()+300_000,private onExecutionStarted?:()=>void|Promise<void>){this.attemptDeadline=Math.min(Date.now()+300_000,runExecutionDeadline);}
async observe(source:string,phase:'before'|'after',request:VerificationRequest,runtime:QualifiedRuntime,verifier:QualifiedRuntime,work:string,control:string,execution?:{environment:Record<string,string>;database?:PreparedDatabaseContract},testEvidence?:{minimumPassingTests:number[]},witness?:AssertionWitnessHandle):Promise<VerificationObservation|WitnessedVerificationResult>{
const phaseDir=secureDirectory(join(work,phase)),phaseControl=secureDirectory(join(control,phase)),policy=secureDirectory(join(phaseDir,'policy')),policyFile=join(policy,'verification.json'),fixtures=secureDirectory(join(phaseDir,'fixtures'));
const verifierPolicy=JSON.stringify({phase,port:request.port,legitimate:request.legitimate,security:request.security});
if(redact(verifierPolicy)!==verifierPolicy)throw new CsoError('REDACTION_FAILED','Verification harness contains secret-bearing data');fs.writeFileSync(policyFile,verifierPolicy,{mode:0o600});
for(const [path,body] of Object.entries(request.fixtures)){const file=containedFile(fixtures,path);secureDirectory(dirname(file));fs.writeFileSync(file,body,{mode:0o600});}
const deadline=this.attemptDeadline;if(deadline<=Date.now())throw new CsoError('DEADLINE','No execution time remains before the reporting reserve');let group:DockerGroup|undefined;
try{
group=await DockerGroup.create(this.endpoint,`${request.findingId.slice(0,12)}-${phase}-${Date.now()}-${randomBytes(6).toString('hex')}`,phaseControl,deadline,verifier.image,this.watchdogPath);
if(!this.executionStarted){this.executionStarted=true;await this.onExecutionStarted?.();}
const supplied=execution?.environment??{},allowed=new Set(['PATH','VIRTUAL_ENV','PYTHONNOUSERSITE','BUNDLE_PATH','BUNDLE_FROZEN','BUNDLE_DEPLOYMENT','BUNDLE_DISABLE_SHARED_GEMS','BUNDLE_IGNORE_CONFIG','BUNDLE_ALLOW_OFFLINE_INSTALL','BUNDLE_CACHE_PATH','BUNDLE_USER_HOME','GEM_HOME','GEM_PATH']);if(Object.entries(supplied).some(([key,value])=>!allowed.has(key)||typeof value!=='string'||value.includes('\0')))throw new CsoError('ISOLATION_FAILED','Prepared execution environment exceeded its fixed allowlist');
const env={...supplied,PORT:String(request.port),HOST:'127.0.0.1',NODE_ENV:'test',RAILS_ENV:'test',RACK_ENV:'test',PYTHONUNBUFFERED:'1',CI:'1',SECRET_KEY_BASE:'cso-synthetic-test-key',CSO_FIXTURES:'/fixtures'};
const database=execution?.database;
if(database&&runtime.stack!=='rails')throw new CsoError('INCOMPATIBLE_INPUT','Prepared database contract can only execute with Rails');
if(runtime.stack==='rails'&&!database)throw new CsoError('PREREQUISITE','Rails verification omitted its prepared database contract');
if(database?.adapter==='postgresql'){
const databaseFile=join(policy,'postgresql.databases'),names=database.connections.map(name=>`cso_${name}`);
if(!names.length||names.some(name=>!/^cso_[A-Za-z_][A-Za-z0-9_]{0,47}$/.test(name)))throw new CsoError('INCOMPATIBLE_INPUT','Prepared PostgreSQL connection names are invalid');
fs.writeFileSync(databaseFile,names.join('\n')+'\n',{mode:0o444,flag:'wx'});
const postgres=await group.createContainer({role:'postgres',image:database.sidecar.image,
command:['/opt/cso/run-postgresql','/policy/postgresql.databases'],postgresDatabasePolicy:databaseFile});
await group.start(postgres);let ready=false;
for(let attempt=0;attempt<100&&!ready;attempt++){
const checked=await group.execCapture(postgres,['/opt/cso/postgresql-ready','/policy/postgresql.databases']);
ready=checked.code===0;if(!ready)await new Promise(resolve=>setTimeout(resolve,50));
}
if(!ready)throw new CsoError('TOOL_FAILED','Disposable PostgreSQL did not create and accept connections for every declared Rails database');
}
const cleanCommand=(command:string[])=>['/usr/bin/env','-i',...Object.entries(env).sort(([a],[b])=>a.localeCompare(b)).map(([key,value])=>`${key}=${value}`),...command];
const dbPrepare=async(id:string)=>{
const result=await group!.execCapture(id,cleanCommand(['/usr/local/bin/bundle','exec','rails','db:prepare']),{workdir:'/work'});
if(result.code!==0)throw new CsoError('TOOL_FAILED','Rails database preparation failed in the isolated test environment');
};
let app:string;
if(runtime.stack==='rails'){
app=await group.createContainer({role:'app',image:runtime.image,source,env,command:['/opt/cso/run-app','/bin/sleep','2147483647'],readonlyDirectories:[{host:fixtures,container:'/fixtures'}]});
await group.start(app);await dbPrepare(app);await group.execDetached(app,[request.start.executable,...request.start.args]);
}else{
app=await group.createContainer({role:'app',image:runtime.image,source,env,command:['/opt/cso/run-app',request.start.executable,...request.start.args],readonlyDirectories:[{host:fixtures,container:'/fixtures'}]});await group.start(app);
}
const verifierId=await group.createContainer({role:'verifier',image:verifier.image,command:['/opt/cso/verifier','/policy/verification.json'],readonlyFiles:[{host:policyFile,container:'/policy/verification.json'}]});
const result=await group.startAttach(verifierId);let observation:VerificationObservation;
try{observation=validateVerificationObservation(JSON.parse(result.output.trim()));}catch{observation={booted:false,legitimate:false,security:'inconclusive',existingTests:false,output:'verifier returned invalid bounded output',inputHash:''};}
await group.removeContainer(verifierId);
await group.removeContainer(app);
const minimumPassingTests=testEvidence?.minimumPassingTests??request.existingTests.map(()=>1);if(minimumPassingTests.length!==request.existingTests.length||minimumPassingTests.some(value=>!Number.isInteger(value)||value<1))throw new CsoError('INCOMPATIBLE_INPUT','Helper-derived test execution-count floors do not match the canonical test commands');
let existingTests=true;const executions:Array<{command:VerificationRequest['existingTests'][number];code:number;output:string;minimumPassingTests:number}>=[];for(const [index,test] of request.existingTests.entries()){
const rails=runtime.stack==='rails';
const testId=await group.createContainer({role:'tests',image:runtime.image,source,env,
command:rails?['/opt/cso/run-app','/bin/sleep','2147483647']:['/opt/cso/run-app',test.executable,...test.args],readonlyDirectories:[{host:fixtures,container:'/fixtures'}]});
let testResult:{code:number;output:string};
if(rails){await group.start(testId);await dbPrepare(testId);const result=await group.execCapture(testId,cleanCommand([test.executable,...test.args]),{workdir:'/work'});testResult={code:result.code,output:result.stdout+result.stderr};}
else testResult=await group.startAttach(testId);
executions.push({command:test,code:testResult.code,output:testResult.output,minimumPassingTests:minimumPassingTests[index]});if(!testExecutionPassed(test,testResult.code,testResult.output,minimumPassingTests[index]))existingTests=false;await group.removeContainer(testId);
}
if(witness){if(observation.existingTests)throw new CsoError('INCOMPATIBLE_INPUT','External verifier attempted to assert project test completion');const receipt=await witness.attest(observation,executions),witnessed={...observation,existingTests:receipt.diagnosticTestsPassed,inputHash:witness.binding.harnessHash};return{observation:witnessed,witness:receipt};}
observation.existingTests=existingTests;return observation;
}finally{if(group)await group.cleanup();}
}
}
export async function verifyRepair(params:{runId:string;runDir:string;manifest:SnapshotManifest;rawRequest:unknown;runtime:QualifiedRuntime;verifier:QualifiedRuntime;policyHash:string;auditPolicyHash?:string;archives:string[];dependencyClosures?:{before:unknown;after:unknown};preparation?:{before:PreparationProof;after:PreparationProof};reviewArtifact?:RepairReviewArtifact;executor:VerificationExecutor;persist?:boolean;watchdogPath?:string;attemptDeadline?:number}):Promise<{manifest:VerificationManifest;bundle:RepairBundle}>{
const identityRequest=validateVerificationRequest(params.rawRequest),request=resolveVerificationRequestPaths(params.manifest,identityRequest),snapshot=join(params.runDir,'snapshot'),work=join(params.runDir,'verification',`${request.findingId}-${Date.now()}-${randomBytes(4).toString('hex')}`),after=join(work,'sources','after'),observations=join(work,'observations'),groupControls=secureDirectory(join(params.runDir,'supervision',basename(work),'docker-groups'));
if(canonical(sanitizeHelperForJson(identityRequest))!==canonical(identityRequest))throw new CsoError('REDACTION_FAILED','Verification request contains sensitive material that cannot enter a replayable bundle');
if(!['node','bun','python','rails'].includes(params.runtime.stack))throw new CsoError('INCOMPATIBLE_INPUT','Canonical project tests require an application runtime');const stack=params.runtime.stack as CsoStack,beforeTestPlan=assertCanonicalTestPlan(request,snapshot,stack),beforeStartPlan=assertCanonicalStartPlan(request,snapshot,stack);
if(beforeTestPlan.toolchain==='project'&&request.changes.some(change=>change.effect==='dependency'))throw new CsoError('PREREQUISITE','Runtime-tested dependency repairs require a test runner pinned in the qualified runtime; project-installed test toolchains may change with the repair');
for(const boundary of [...new Set([...request.boundaryFiles,...request.testFiles,...beforeStartPlan.entrypointFiles])]){const e=params.manifest.entries.find(x=>x.path===boundary);if(!e||e.transformation)throw new CsoError('INCOMPATIBLE_INPUT',`Snapshot transformation changes or withholds a verification input: ${boundary}`);}
for(const change of request.changes){const path=relativePath(change.path),entry=params.manifest.entries.find(x=>x.path===path);containedFile(snapshot,path);if(change.beforeSha256===null){if(entry)throw new CsoError('INCOMPATIBLE_INPUT',`Declared new repair path already exists in the snapshot: ${path}`);}else if(!entry||entry.transformation)throw new CsoError('INCOMPATIBLE_INPUT',`Snapshot transformation changes or withholds a repair input: ${path}`);}
let guardedCleanup:(()=>Promise<void>)|undefined;if(params.watchdogPath){const deadline=Math.min(params.attemptDeadline??Date.now()+300_000,Date.now()+300_000);guardedCleanup=await attemptGuard(params.runDir,work,params.watchdogPath,deadline);}secureDirectory(observations);
let certified:ReturnType<typeof certify>|undefined,beforeObs:VerificationObservation|undefined,afterObs:VerificationObservation|undefined,beforeWitness:AssertionWitnessReceipt|undefined,afterWitness:AssertionWitnessReceipt|undefined,failure:unknown,missingExternalWitness=false;
try{
preparePatchedSource(snapshot,after,request);
const afterTestPlan=canonicalTestPlan(after,stack),afterStartPlan=canonicalStartPlan(after,stack,request.port);if(canonical(afterTestPlan)!==canonical(beforeTestPlan))throw new CsoError('ASSERTION_FAILED','Repair changed the canonical project test suite, runner configuration, or discovered test inputs');
const startShape=(plan:CanonicalStartPlan)=>({command:plan.command,kind:plan.kind,entrypointFiles:plan.entrypointFiles}),changedPaths=new Set(request.changes.map(change=>change.path));
if(canonical(startShape(afterStartPlan))!==canonical(startShape(beforeStartPlan))||(afterStartPlan.signature!==beforeStartPlan.signature&&!beforeStartPlan.entrypointFiles.some(path=>changedPaths.has(path))))throw new CsoError('ASSERTION_FAILED','Repair changed the helper-derived application startup plan outside its declared patch');
const beforeInvariant=treeHash(snapshot),afterInvariant=treeHash(after);if(beforeInvariant!==params.manifest.executionHash)throw new CsoError('INCOMPATIBLE_INPUT','Retained source does not match the snapshot identity bound to this verification');
const testEvidence={minimumPassingTests:beforeTestPlan.minimumPassingTests},auditPolicyHash=params.auditPolicyHash??sha256(canonical({})),requestHash=sha256(canonical(identityRequest)),pHash=patchHash(identityRequest),harnessHash=verificationHarnessHash(request,snapshot),assertionHash=sha256(canonical({legitimate:identityRequest.legitimate,security:identityRequest.security})),runner={testToolchain:beforeTestPlan.toolchain,startPlanHash:beforeStartPlan.signature,testPlanHash:beforeTestPlan.signature,commandsHash:sha256(canonical(request.existingTests)),minimumPassingTestsHash:sha256(canonical(beforeTestPlan.minimumPassingTests))},session=new AssertionWitnessSession(observations,Math.min(params.attemptDeadline??Date.now()+300_000,Date.now()+300_000)),stable=(phase:'before'|'after',root:string,sourceHash:string):Omit<AssertionWitnessBinding,'schemaVersion'|'protocol'|'nonce'|'issuedAt'|'expiresAt'>=>({phase,runId:params.runId,findingId:identityRequest.findingId,policyHash:params.policyHash,auditPolicyHash,runtime:{image:params.runtime.image,verifierImage:params.verifier.image,platform:params.runtime.platform,profile:params.runtime.id},runner,sourceHash,dependencyHash:treeHash(root,p=>DEPENDENCY.test(p)),configurationHash:treeHash(root,p=>CONFIG.test(p)&&!DEPENDENCY.test(p)),requestHash,patchHash:pHash,harnessHash,assertionHash,fixturesHash:sha256(canonical(identityRequest.fixtures))}),beforeHandle=session.handle(stable('before',snapshot,beforeInvariant));
const rawBefore=await params.executor.observe(snapshot,'before',request,params.runtime,params.verifier,observations,groupControls,undefined,testEvidence,beforeHandle),observedBefore='observation'in(rawBefore as any)?validateVerificationObservation((rawBefore as WitnessedVerificationResult).observation):validateVerificationObservation(rawBefore as VerificationObservation);
if('observation'in(rawBefore as any))beforeWitness=beforeHandle.validate((rawBefore as WitnessedVerificationResult).witness,observedBefore);
if(treeHash(snapshot)!==beforeInvariant)throw new CsoError('ASSERTION_FAILED','Verification mutated the retained source snapshot');
beforeObs=observedBefore;
const afterHandle=session.handle(stable('after',after,afterInvariant)),rawAfter=await params.executor.observe(after,'after',request,params.runtime,params.verifier,observations,groupControls,undefined,testEvidence,afterHandle),observedAfter='observation'in(rawAfter as any)?validateVerificationObservation((rawAfter as WitnessedVerificationResult).observation):validateVerificationObservation(rawAfter as VerificationObservation);
if('observation'in(rawAfter as any))afterWitness=afterHandle.validate((rawAfter as WitnessedVerificationResult).witness,observedAfter);
if(treeHash(after)!==afterInvariant)throw new CsoError('ASSERTION_FAILED','Verification mutated the pristine patched source');
afterObs=observedAfter;
certified=certify({...params,request,identityRequest,before:beforeObs,after:afterObs,beforeRoot:snapshot,afterRoot:after,startPlanHash:beforeStartPlan.signature,testPlanHash:beforeTestPlan.signature,testToolchain:beforeTestPlan.toolchain,minimumPassingTests:beforeTestPlan.minimumPassingTests,...(beforeWitness&&afterWitness?{witness:{before:beforeWitness,after:afterWitness}}:{})});
}catch(error){failure=error;}
try{if(guardedCleanup)await guardedCleanup();else fs.rmSync(work,{recursive:true,force:true});}catch(error){failure=error;certified=undefined;missingExternalWitness=false;}
if(!failure&&certified&&!certified.bundle){const manifest=certified.manifest,review=manifest.review;missingExternalWitness=!manifest.assertionAssurance&&manifest.testCompletionAssurance==='self_reported'&&manifest.result==='inconclusive'&&manifest.before.booted&&manifest.before.legitimate&&manifest.before.security==='intended_failure'&&manifest.before.existingTests&&manifest.after.booted&&manifest.after.legitimate&&manifest.after.security==='pass'&&manifest.after.existingTests&&review.independent&&review.rootCauseRepaired&&review.featurePreserved&&!review.boundaryMocks;failure=missingExternalWitness?new CsoError('PREREQUISITE','Repair verification retained self-reported project-test diagnostics but requires a helper-authenticated out-of-process external assertion witness before a runtime-tested bundle can be issued'):new CsoError('ASSERTION_FAILED',manifest.result==='inconclusive'?'Repair verification was inconclusive; no repair bundle was issued':'Repair failed one or more required boot, control, security, existing-test, or review assertions; no repair bundle was issued');}
if(failure){
if(beforeObs){
const cause=failure instanceof CsoError?failure:new CsoError('ASSERTION_FAILED','Repair validation failed after the before-phase observation');
const reproduction=!beforeObs.booted?'blocked':!beforeObs.legitimate?'inconclusive':beforeObs.security==='intended_failure'?'reproduced':beforeObs.security==='pass'?'disproved':'inconclusive',harnessHash=verificationHarnessHash(request,snapshot);
const missingWitness=missingExternalWitness&&cause.code==='PREREQUISITE'&&reproduction==='reproduced'&&afterObs?.booted===true&&afterObs.legitimate===true&&afterObs.security==='pass'&&beforeObs.existingTests&&afterObs.existingTests;
const raw={schemaVersion:3 as const,artifactKind:'repair_candidate' as const,runId:params.runId,findingId:identityRequest.findingId,createdAt:new Date().toISOString(),bundleIssued:false as const,runtime:{image:params.runtime.image,platform:params.runtime.platform,profile:params.runtime.id},policyHash:params.policyHash,requestHash:sha256(canonical(identityRequest)),harnessHash,sourceHash:params.manifest.executionHash,request:identityRequest,patchHash:patchHash(identityRequest),testToolchain:beforeTestPlan.toolchain,testCompletionAssurance:'self_reported' as const,...(params.preparation?{preparationHash:sha256(canonical(params.preparation))}:{}),before:{...beforeObs,inputHash:harnessHash},...(afterObs?{after:{...afterObs,inputHash:harnessHash}}:{}),reproduction,repair:missingWitness?'proposed' as const:'failed' as const,failure:{code:cause.code,message:cause.message}},safe=sanitizeHelperForJson(raw) as Omit<FailedVerificationAttempt,'id'>,id=sha256(canonical(safe)).slice(0,32),attempt:FailedVerificationAttempt={...safe,id};
validateVerificationObservation(attempt.before);if(attempt.after)validateVerificationObservation(attempt.after);if(params.persist!==false)writeJsonExclusive(join(params.runDir,'verification-attempts',`${id}.json`),attempt);
throw new VerificationAttemptError(cause,attempt);
}
throw failure;
}
if(!certified?.bundle)throw new CsoError('ASSERTION_FAILED','Verification ended without a certifiable result');
if(params.persist!==false){const persistable=sanitizeHelperForJson(certified.bundle);if(canonical(persistable)!==canonical(certified.bundle))throw new CsoError('REDACTION_FAILED','Repair bundle provenance contains material that cannot be persisted without changing its identity');validateRepairBundle(persistable,certified.bundle.id,snapshot,params.manifest);writeJsonExclusive(join(params.runDir,'bundles',`${certified.bundle.id}.json`),persistable);}
return certified;
}
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bun
import * as fs from 'node:fs';
import { connect } from 'node:net';
import { HttpAssertion, VerificationObservation, object } from './contracts';
interface Config { phase:'before'|'after'; port:number; legitimate:HttpAssertion[]; security:HttpAssertion }
function matches(status:number,body:string,oracle:HttpAssertion['expected']):boolean{return status===oracle.status&&(oracle.includes===undefined||body.includes(oracle.includes))&&(oracle.excludes===undefined||!body.includes(oracle.excludes));}
export async function boundedResponseBody(response:Response,limit=65536):Promise<string>{
if(!Number.isSafeInteger(limit)||limit<1)throw new Error('invalid response limit');
const declared=response.headers.get('content-length');
if(declared!==null&&(/^\d+$/.test(declared)?Number(declared)>limit:true)){await response.body?.cancel();throw new Error('response too large');}
if(!response.body)return'';
const reader=response.body.getReader(),chunks:Uint8Array[]=[];let total=0;
try{
for(;;){const next=await reader.read();if(next.done)break;if(!next.value)continue;total+=next.value.byteLength;if(total>limit){await reader.cancel();throw new Error('response too large');}chunks.push(next.value);}
}finally{reader.releaseLock();}
const bytes=new Uint8Array(total);let offset=0;for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.byteLength;}return new TextDecoder().decode(bytes);
}
async function request(a:HttpAssertion,port:number):Promise<{status:number;body:string}>{
const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),5000);
try{const response=await fetch(`http://127.0.0.1:${port}${a.path}`,{method:a.method,headers:a.headers,body:['GET'].includes(a.method)?undefined:a.body,redirect:'manual',signal:controller.signal});return{status:response.status,body:await boundedResponseBody(response)};}finally{clearTimeout(timer);}
}
async function ready(port:number):Promise<boolean>{return await new Promise(resolve=>{const socket=connect({host:'127.0.0.1',port}),done=(value:boolean)=>{socket.removeAllListeners();socket.destroy();resolve(value);},timer=setTimeout(()=>done(false),500);socket.once('connect',()=>{clearTimeout(timer);done(true);});socket.once('error',()=>{clearTimeout(timer);done(false);});});}
async function main(){
const file=process.argv[2];if(!file||!file.startsWith('/policy/'))throw new Error('trusted policy path required');const raw=fs.readFileSync(file,'utf8');if(Buffer.byteLength(raw)>1024*1024)throw new Error('policy too large');const v=object(JSON.parse(raw),'verifier policy') as any;
if(!['before','after'].includes(v.phase)||!Number.isInteger(v.port)||v.port<1024||v.port>65535||!Array.isArray(v.legitimate)||!v.security)throw new Error('invalid verifier policy');const config=v as Config;
let booted=false;for(let attempt=0;attempt<60;attempt++){if(await ready(config.port)){booted=true;break;}await Bun.sleep(250);}
let legitimate=false,security:VerificationObservation['security']='inconclusive',summary='application did not answer a legitimate control';
if(booted){try{legitimate=(await Promise.all(config.legitimate.map(async a=>{const r=await request(a,config.port);return matches(r.status,r.body,a.expected);}))).every(Boolean);const r=await request(config.security,config.port),fixed=matches(r.status,r.body,config.security.expected),vulnerable=matches(r.status,r.body,config.security.vulnerable!);security=config.phase==='before'?(vulnerable&&!fixed?'intended_failure':fixed&&!vulnerable?'pass':'inconclusive'):(fixed&&!vulnerable?'pass':'inconclusive');summary=`boot=true legitimate=${legitimate} security=${security}`;}catch{summary='bounded verifier request failed';}}
process.stdout.write(JSON.stringify({booted,legitimate,security,existingTests:false,output:summary,inputHash:''})+'\n');
}
if(import.meta.main)main().catch(()=>{process.stdout.write(JSON.stringify({booted:false,legitimate:false,security:'inconclusive',existingTests:false,output:'verifier setup failed',inputHash:''})+'\n');process.exitCode=1;});
+208
View File
@@ -0,0 +1,208 @@
/* Independent CSO lease watchdog. It accepts only exact journaled container IDs. */
#ifdef __APPLE__
#define _DARWIN_C_SOURCE 1
#endif
#define _XOPEN_SOURCE 700
#include <errno.h>
#include <dirent.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
static int safe_token(const char *s, size_t min, size_t max) {
size_t n = s ? strlen(s) : 0;
if (n < min || n > max) return 0;
for (size_t i=0;i<n;i++) if (!((s[i]>='a'&&s[i]<='z')||(s[i]>='A'&&s[i]<='Z')||(s[i]>='0'&&s[i]<='9')||strchr("._-/:@",s[i]))) return 0;
return 1;
}
static int is_id(const char *s) {
if (!s || strlen(s)!=64) return 0;
for (size_t i=0;i<64;i++) if (!((s[i]>='a'&&s[i]<='f')||(s[i]>='0'&&s[i]<='9'))) return 0;
return 1;
}
static int is_token(const char *s){
if(!s||strlen(s)!=32)return 0;
for(size_t i=0;i<32;i++)if(!((s[i]>='a'&&s[i]<='f')||(s[i]>='0'&&s[i]<='9')))return 0;
return 1;
}
static int safe_path(const char *s){
if(!s||s[0]!='/'||strstr(s,"/../")||strstr(s,"//"))return 0;
size_t n=strlen(s);if(n>4095||(n>=3&&!strcmp(s+n-3,"/..")))return 0;
for(size_t i=0;i<n;i++)if((unsigned char)s[i]<32||(unsigned char)s[i]==127)return 0;
return 1;
}
static int owner_alive(pid_t pid) { return pid > 1 && (kill(pid,0)==0 || errno==EPERM); }
static unsigned long long process_start(pid_t pid){
#ifdef __linux__
char path[64],line[4096];if(snprintf(path,sizeof path,"/proc/%ld/stat",(long)pid)>=(int)sizeof path)return 0;FILE *f=fopen(path,"r");if(!f)return 0;if(!fgets(line,sizeof line,f)){fclose(f);return 0;}fclose(f);char *cursor=strrchr(line,')');if(!cursor||cursor[1]!=' ')return 0;cursor+=2;char *save=NULL,*token=strtok_r(cursor," ",&save);for(int field=3;token;field++,token=strtok_r(NULL," ",&save))if(field==22){char *end=NULL;unsigned long long value=strtoull(token,&end,10);return end&&(*end=='\0'||*end=='\n')?value:0;}return 0;
#else
(void)pid;return 0;
#endif
}
static int same_owner(pid_t pid,unsigned long long started){if(!owner_alive(pid))return 0;unsigned long long current=process_start(pid);return !started||!current||started==current;}
static int wait_bounded(pid_t child,int *status,int seconds){
struct timespec delay={0,100000000};
for(int i=0;i<seconds*10;i++){pid_t r=waitpid(child,status,WNOHANG);if(r==child)return 0;if(r<0&&errno!=EINTR)return -1;while(nanosleep(&delay,&delay)&&errno==EINTR){}delay.tv_sec=0;delay.tv_nsec=100000000;}
(void)kill(child,SIGKILL);
for(int i=0;i<10;i++){pid_t r=waitpid(child,status,WNOHANG);if(r==child)return -1;if(r<0&&errno!=EINTR)return -1;while(nanosleep(&delay,&delay)&&errno==EINTR){}delay.tv_sec=0;delay.tv_nsec=100000000;}
return -1;
}
static int resource_present(const char *run_dir,const char *docker,const char *endpoint,const char *id){
char output[4096];if(snprintf(output,sizeof output,"%s/watchdog.ps.%ld",run_dir,(long)getpid())>=(int)sizeof output)return -1;
int fd=open(output,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW,0600);if(fd<0)return -1;
pid_t child=fork();if(child<0){close(fd);unlink(output);return -1;}
if(child==0){
if(dup2(fd,STDOUT_FILENO)<0)_exit(127);
close(fd);
int nullfd=open("/dev/null",O_WRONLY);if(nullfd>=0){(void)dup2(nullfd,STDERR_FILENO);close(nullfd);}
char filter[80];if(snprintf(filter,sizeof filter,"id=%s",id)>=(int)sizeof filter)_exit(127);
char *const argv[]={(char*)docker,"--host",(char*)endpoint,"ps","--all","--quiet","--no-trunc","--filter",filter,NULL};
char *const envp[]={"PATH=/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin",NULL};execve(docker,argv,envp);_exit(127);
}
close(fd);int status=0;if(wait_bounded(child,&status,10)!=0){unlink(output);return -1;}
if(!WIFEXITED(status)||WEXITSTATUS(status)!=0){unlink(output);return -1;}
fd=open(output,O_RDONLY|O_NOFOLLOW);if(fd<0){unlink(output);return -1;}char value[128]={0};ssize_t n=read(fd,value,sizeof value-1);close(fd);unlink(output);if(n<0)return -1;
value[strcspn(value,"\r\n")]=0;if(value[0]==0)return 0;return strcmp(value,id)==0?1:-1;
}
static int remove_container(const char *docker, const char *endpoint, const char *id) {
pid_t child=fork(); if(child<0)return -1;
if(child==0){
char *const argv[]={(char*)docker,"--host",(char*)endpoint,"rm","--force","--volumes",(char*)id,NULL};
char *const envp[]={"PATH=/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin",NULL};
execve(docker,argv,envp); _exit(127);
}
int status=0;if(wait_bounded(child,&status,10)!=0)return -1;
return WIFEXITED(status)&&WEXITSTATUS(status)==0?0:-1;
}
static int cleanup_label(const char *run_dir,const char *docker,const char *endpoint,const char *label){
char output[4096],filter[160];if(snprintf(output,sizeof output,"%s/watchdog.labels.%ld",run_dir,(long)getpid())>=(int)sizeof output||snprintf(filter,sizeof filter,"label=com.gstack.cso.run=%s",label)>=(int)sizeof filter)return -1;
int fd=open(output,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW,0600);if(fd<0)return -1;pid_t child=fork();if(child<0){close(fd);unlink(output);return -1;}
if(child==0){if(dup2(fd,STDOUT_FILENO)<0)_exit(127);close(fd);int nullfd=open("/dev/null",O_WRONLY);if(nullfd>=0){(void)dup2(nullfd,STDERR_FILENO);close(nullfd);}char *const argv[]={(char*)docker,"--host",(char*)endpoint,"ps","--all","--quiet","--no-trunc","--filter",filter,NULL};char *const envp[]={"PATH=/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin",NULL};execve(docker,argv,envp);_exit(127);}
close(fd);int status=0;if(wait_bounded(child,&status,10)!=0||!WIFEXITED(status)||WEXITSTATUS(status)!=0){unlink(output);return -1;}
FILE *f=fopen(output,"r");if(!f){unlink(output);return -1;}char line[256];int failed=0;while(fgets(line,sizeof line,f)){line[strcspn(line,"\r\n")]=0;if(!is_id(line)||remove_container(docker,endpoint,line)!=0)failed=1;}fclose(f);unlink(output);return failed?-1:0;
}
static int cleanup(const char *run_dir,const char *docker,const char *endpoint,const char *label){
char journal[4096]; if(snprintf(journal,sizeof journal,"%s/resources.journal",run_dir)>=(int)sizeof journal)return -1;
FILE *f=fopen(journal,"r");char line[256];int malformed=0;
if(f){while(fgets(line,sizeof line,f)){size_t length=strlen(line);int complete=length>0&&(line[length-1]=='\n'||feof(f));line[strcspn(line,"\r\n")]=0;if(!complete||strncmp(line,"container:",10)!=0||!is_id(line+10)){malformed=1;continue;}int present=resource_present(run_dir,docker,endpoint,line+10);if(present>0)(void)remove_container(docker,endpoint,line+10);}if(ferror(f))malformed=1;fclose(f);}else if(errno!=ENOENT)malformed=1;
/* The journal is an optimization and may end in a torn append. Two exact
* label sweeps are the authoritative cleanup proof after owner death. */
if(cleanup_label(run_dir,docker,endpoint,label)!=0)return -1;
if(cleanup_label(run_dir,docker,endpoint,label)!=0)return -1;
return malformed?1:0;
}
static int remove_tree(const char *path);
static int owned_regular(const char *path){struct stat st;return !lstat(path,&st)&&S_ISREG(st.st_mode)&&st.st_uid==getuid()&&st.st_nlink==1;}
static int lease_token_matches(const char *dir,const char *token){
char path[4096];if(snprintf(path,sizeof path,"%s/lease.token",dir)>=(int)sizeof path)return -1;
struct stat before;if(lstat(path,&before))return errno==ENOENT?0:-1;if(!S_ISREG(before.st_mode)||before.st_uid!=getuid()||before.st_nlink!=1||before.st_size<=0||before.st_size>64)return -1;
int fd=open(path,O_RDONLY|O_NOFOLLOW);if(fd<0)return -1;struct stat opened;if(fstat(fd,&opened)||opened.st_dev!=before.st_dev||opened.st_ino!=before.st_ino||opened.st_nlink!=1){close(fd);return -1;}char value[64]={0};ssize_t n=read(fd,value,sizeof value-1);struct stat after;int bad=fstat(fd,&after)||after.st_dev!=opened.st_dev||after.st_ino!=opened.st_ino||after.st_nlink!=1;close(fd);if(n<0||bad)return -1;value[strcspn(value,"\r\n")]=0;return strcmp(value,token)==0?1:-1;
}
static int cleanup_release_tomb(const char *tomb,const char *token){
struct stat root;if(lstat(tomb,&root))return errno==ENOENT?0:-1;if(!S_ISDIR(root.st_mode)||root.st_uid!=getuid())return -1;
int authenticated=lease_token_matches(tomb,token);if(authenticated<0)return -1;
/* A deterministic token-qualified tomb is itself the recovery capability
* after a crash or a concurrent authenticated TS release removed the token.
* Delete the token last so ordinary interrupted cleanup remains verifiable. */
DIR *directory=opendir(tomb);if(!directory)return -1;struct dirent *entry;int failed=0;
while((entry=readdir(directory))){const char *name=entry->d_name;if(!strcmp(name,".")||!strcmp(name,"..")||!strcmp(name,"lease.token"))continue;char child[4096];if(snprintf(child,sizeof child,"%s/%s",tomb,name)>=(int)sizeof child){failed=1;break;}
if(!strcmp(name,".recovery")){struct stat st;if(lstat(child,&st)||st.st_uid!=getuid()){failed=1;break;}if(S_ISREG(st.st_mode)&&st.st_nlink==1){if(unlink(child)&&errno!=ENOENT){failed=1;break;}}else if(S_ISDIR(st.st_mode)){if(remove_tree(child)!=0){failed=1;break;}}else{failed=1;break;}continue;}
if(strcmp(name,"lease.json")&&strncmp(name,".recovery.tmp.",14)&&strncmp(name,"lease.json.tmp.",15)){failed=1;break;}
if(!owned_regular(child)||(unlink(child)&&errno!=ENOENT)){failed=1;break;}
}
if(closedir(directory))failed=1;
if(failed)return -1;
char token_path[4096];if(snprintf(token_path,sizeof token_path,"%s/lease.token",tomb)>=(int)sizeof token_path)return -1;
if(authenticated>0&&unlink(token_path)&&errno!=ENOENT)return -1;
if(rmdir(tomb)&&errno!=ENOENT)return -1;
return 0;
}
static int release_lease(const char *path,const char *token){
if(!safe_path(path)||!is_token(token))return -1;
char tomb[4096];if(snprintf(tomb,sizeof tomb,"%s.watchdog-release-%s",path,token)>=(int)sizeof tomb)return -1;
struct stat tomb_st;if(!lstat(tomb,&tomb_st))return cleanup_release_tomb(tomb,token);if(errno!=ENOENT)return -1;
struct stat observed;if(lstat(path,&observed))return errno==ENOENT?0:-1;if(!S_ISDIR(observed.st_mode)||observed.st_uid!=getuid())return -1;
if(lease_token_matches(path,token)!=1)return -1;
if(rename(path,tomb)){if(errno==ENOENT&&!lstat(tomb,&tomb_st))return cleanup_release_tomb(tomb,token);return -1;}
struct stat moved;if(lstat(tomb,&moved)||!S_ISDIR(moved.st_mode)||moved.st_dev!=observed.st_dev||moved.st_ino!=observed.st_ino||moved.st_uid!=observed.st_uid)return -1;
return cleanup_release_tomb(tomb,token);
}
static void marker(const char *run_dir,const char *name,const char *value){
char path[4096],tmp[4096];
if(snprintf(path,sizeof path,"%s/%s",run_dir,name)>=(int)sizeof path)return;
if(snprintf(tmp,sizeof tmp,"%s/%s.tmp.%ld",run_dir,name,(long)getpid())>=(int)sizeof tmp)return;
int fd=open(tmp,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW,0600);if(fd<0)return;
size_t remaining=strlen(value);const char *cursor=value;int failed=0;
while(remaining){ssize_t written=write(fd,cursor,remaining);if(written>0){cursor+=written;remaining-=(size_t)written;continue;}if(written<0&&errno==EINTR)continue;failed=1;break;}
if(!failed&&fsync(fd))failed=1;
if(close(fd))failed=1;
if(!failed&&!rename(tmp,path))return;
(void)unlink(tmp);
}
static int descendant(const char *root,const char *path){size_t n=strlen(root);return n>1&&!strncmp(root,path,n)&&path[n]=='/';}
static int remove_tree(const char *path){
struct stat st;if(lstat(path,&st)){return errno==ENOENT?0:-1;}if(!S_ISDIR(st.st_mode)||st.st_uid!=getuid())return -1;
pid_t child=fork();if(child<0)return -1;if(child==0){char *const args[]={"/bin/rm","-rf","--",(char*)path,NULL};char *const envp[]={"PATH=/usr/bin:/bin",NULL};execve("/bin/rm",args,envp);_exit(127);}int status=0;if(wait_bounded(child,&status,10)!=0)return -1;return WIFEXITED(status)&&WEXITSTATUS(status)==0?0:-1;
}
static int attempt_watchdog(int argc,char **argv){
if(argc!=11||strcmp(argv[1],"--attempt-owner")||strcmp(argv[3],"--deadline")||strcmp(argv[5],"--control-dir")||strcmp(argv[7],"--work-root")||strcmp(argv[9],"--run-root"))return 64;
char *end=NULL;long owner_l=strtol(argv[2],&end,10);if(!end||*end||owner_l<=1)return 64;end=NULL;long long deadline=strtoll(argv[4],&end,10);if(!end||*end||deadline<=0)return 64;
const char *control=argv[6],*work=argv[8],*run=argv[10];if(!safe_path(control)||!safe_path(work)||!safe_path(run)||!descendant(run,control)||!descendant(run,work))return 64;
char real_run[4096],real_control[4096],real_work[4096];if(!realpath(run,real_run)||!realpath(control,real_control)||!realpath(work,real_work)||strcmp(real_run,run)||strcmp(real_control,control)||strcmp(real_work,work)||!descendant(real_run,real_control)||!descendant(real_run,real_work))return 64;
struct stat st;if(lstat(control,&st)||!S_ISDIR(st.st_mode)||st.st_uid!=getuid()||lstat(work,&st)||!S_ISDIR(st.st_mode)||st.st_uid!=getuid())return 64;
char terminal[4096];if(snprintf(terminal,sizeof terminal,"%s/attempt.terminal",control)>=(int)sizeof terminal)return 64;unsigned long long owner_start=process_start((pid_t)owner_l);marker(control,"attempt.ready","ready\n");
for(;;){if(access(terminal,F_OK)==0){marker(control,"attempt.stopped","normal cleanup acknowledged\n");return 0;}time_t now=time(NULL);int alive=same_owner((pid_t)owner_l,owner_start);if(!alive||(long long)now>=deadline){if(remove_tree(work)==0){marker(control,"attempt.event",!alive?"supervisor-death execution-copy cleanup complete\n":"deadline execution-copy cleanup complete\n");return 0;}marker(control,"attempt.event","execution-copy cleanup incomplete; retrying\n");}struct timespec delay={0,100000000};while(nanosleep(&delay,&delay)&&errno==EINTR){}delay.tv_sec=0;delay.tv_nsec=100000000;}
}
static int ephemeral_watchdog(int argc,char **argv){
if(argc!=11||strcmp(argv[1],"--ephemeral-owner")||strcmp(argv[3],"--deadline")||strcmp(argv[5],"--control-dir")||strcmp(argv[7],"--work-root")||strcmp(argv[9],"--run-root"))return 64;
char *end=NULL;long owner_l=strtol(argv[2],&end,10);if(!end||*end||owner_l<=1)return 64;end=NULL;long long deadline=strtoll(argv[4],&end,10);if(!end||*end||deadline<=0)return 64;
const char *control=argv[6],*work=argv[8],*run=argv[10];if(!safe_path(control)||!safe_path(work)||!safe_path(run)||!descendant(run,work)||!descendant(work,control))return 64;
char real_run[4096],real_control[4096],real_work[4096];if(!realpath(run,real_run)||!realpath(control,real_control)||!realpath(work,real_work)||strcmp(real_run,run)||strcmp(real_control,control)||strcmp(real_work,work)||!descendant(real_run,real_work)||!descendant(real_work,real_control))return 64;
struct stat control_st,work_st;if(lstat(control,&control_st)||!S_ISDIR(control_st.st_mode)||control_st.st_uid!=getuid()||lstat(work,&work_st)||!S_ISDIR(work_st.st_mode)||work_st.st_uid!=getuid())return 64;
char terminal[4096];if(snprintf(terminal,sizeof terminal,"%s/attempt.terminal",control)>=(int)sizeof terminal)return 64;unsigned long long owner_start=process_start((pid_t)owner_l);marker(control,"attempt.ready","ready\n");
for(;;){
int terminal_requested=access(terminal,F_OK)==0;time_t now=time(NULL);int alive=same_owner((pid_t)owner_l,owner_start);
if(terminal_requested||!alive||(long long)now>=deadline){
struct stat current;if(lstat(work,&current)){if(errno==ENOENT)return 0;marker(control,"attempt.event","ephemeral cleanup incomplete; retrying\n");}
else if(!S_ISDIR(current.st_mode)||current.st_uid!=getuid()||current.st_dev!=work_st.st_dev||current.st_ino!=work_st.st_ino){marker(control,"attempt.event","ephemeral cleanup identity changed; refusing removal\n");}
else if(remove_tree(work)==0)return 0;
else marker(control,"attempt.event","ephemeral cleanup incomplete; retrying\n");
}
struct timespec delay={0,100000000};while(nanosleep(&delay,&delay)&&errno==EINTR){}delay.tv_sec=0;delay.tv_nsec=100000000;
}
}
int main(int argc,char **argv){
if(argc>1&&!strcmp(argv[1],"--attempt-owner"))return attempt_watchdog(argc,argv);
if(argc>1&&!strcmp(argv[1],"--ephemeral-owner"))return ephemeral_watchdog(argc,argv);
if(argc!=21||strcmp(argv[1],"--owner")||strcmp(argv[3],"--deadline")||strcmp(argv[5],"--run-dir")||strcmp(argv[7],"--docker")||strcmp(argv[9],"--endpoint")||strcmp(argv[11],"--socket-device")||strcmp(argv[13],"--socket-inode")||strcmp(argv[15],"--run-label")||strcmp(argv[17],"--lease-path")||strcmp(argv[19],"--lease-token"))return 64;
char *end=NULL;long owner_l=strtol(argv[2],&end,10);if(!end||*end||owner_l<=1)return 64;
end=NULL;long long deadline=strtoll(argv[4],&end,10);if(!end||*end||deadline<=0)return 64;
end=NULL;unsigned long long socket_device=strtoull(argv[12],&end,10);if(!end||*end)return 64;
end=NULL;unsigned long long socket_inode=strtoull(argv[14],&end,10);if(!end||*end||!socket_inode)return 64;
const char *run_dir=argv[6],*docker=argv[8],*endpoint=argv[10],*label=argv[16],*lease_path=argv[18],*lease_token=argv[20];
if(!safe_path(run_dir)||!safe_path(docker)||!safe_token(label,1,100)||!safe_path(lease_path)||!is_token(lease_token)||strncmp(endpoint,"unix:///",8)||!safe_path(endpoint+7))return 64;
struct stat docker_st;if(lstat(docker,&docker_st)||!S_ISREG(docker_st.st_mode)||(docker_st.st_mode&0111)==0)return 64;
struct stat st;if(lstat(run_dir,&st)||!S_ISDIR(st.st_mode)||st.st_uid!=getuid())return 64;
const char *socket_path=endpoint+7;if(lstat(socket_path,&st)||!S_ISSOCK(st.st_mode)||(unsigned long long)st.st_dev!=socket_device||(unsigned long long)st.st_ino!=socket_inode)return 64;
char terminal[4096];if(snprintf(terminal,sizeof terminal,"%s/watchdog.terminal",run_dir)>=(int)sizeof terminal)return 64;
unsigned long long owner_start=process_start((pid_t)owner_l);marker(run_dir,"watchdog.ready","ready\n");
for(;;){
while(waitpid(-1,NULL,WNOHANG)>0){}
if(access(terminal,F_OK)==0){marker(run_dir,"watchdog.stopped","normal cleanup acknowledged\n");return 0;}
time_t now=time(NULL);
int alive=same_owner((pid_t)owner_l,owner_start);if(!alive||(long long)now>=deadline){
if(lstat(socket_path,&st)||!S_ISSOCK(st.st_mode)||(unsigned long long)st.st_dev!=socket_device||(unsigned long long)st.st_ino!=socket_inode){marker(run_dir,"watchdog.event","Docker socket identity changed; cleanup and lease release blocked\n");continue;}
int cleaned=cleanup(run_dir,docker,endpoint,label);
if(cleaned>=0&&(!lstat(socket_path,&st)&&S_ISSOCK(st.st_mode)&&(unsigned long long)st.st_dev==socket_device&&(unsigned long long)st.st_ino==socket_inode)&&release_lease(lease_path,lease_token)==0){if(cleaned>0)marker(run_dir,"watchdog.event",!alive?"supervisor-death cleanup complete; malformed journal ignored after two exact label sweeps\n":"deadline cleanup complete; malformed journal ignored after two exact label sweeps\n");else marker(run_dir,"watchdog.event",!alive?"supervisor-death cleanup complete\n":"deadline cleanup complete\n");return 0;}
marker(run_dir,"watchdog.event","cleanup incomplete; retrying exact journaled resources\n");
}
struct timespec delay={0,100000000};while(nanosleep(&delay,&delay)&&errno==EINTR){}
}
}
+136
View File
@@ -0,0 +1,136 @@
import { generateKeyPairSync, createPrivateKey, createPublicKey, randomBytes, sign, verify } from 'node:crypto';
import { lstatSync, realpathSync } from 'node:fs';
import { basename, dirname } from 'node:path';
import {
AssertionWitnessBinding, AssertionWitnessReceipt, Command, CsoError, MAX_OUTPUT,
VerificationObservation, canonical, object, oneOf, sha256, string, validateCommand,
validateVerificationObservation,
} from './contracts';
import { runProcess } from './process';
export interface WitnessTestExecution { command:Command; code:number; output:string; minimumPassingTests:number }
export interface WitnessedVerificationResult { observation:VerificationObservation; witness:AssertionWitnessReceipt }
export interface AssertionWitnessHandle {
readonly binding:AssertionWitnessBinding;
attest(observation:VerificationObservation,executions:WitnessTestExecution[]):Promise<AssertionWitnessReceipt>;
validate(receipt:unknown,observation:VerificationObservation,now?:number):AssertionWitnessReceipt;
}
const HASH=/^[a-f0-9]{64}$/;
const PUBLIC_KEY=/^[a-f0-9]{88}$/;
const SIGNATURE=/^[a-f0-9]{128}$/;
const PROTOCOL='gstack-cso-assertion-witness-v1' as const;
const MAX_RECEIPT_AGE=300_000;
const exact=(value:Record<string,any>,allowed:readonly string[],name:string)=>{for(const key of Object.keys(value))if(!allowed.includes(key))throw new CsoError('INVALID_SCHEMA',`Unexpected ${name} field: ${key}`);};
const hash=(value:unknown,name:string):string=>{if(typeof value!=='string'||!HASH.test(value))throw new CsoError('INVALID_SCHEMA',`${name} must be a sha256 hash`);return value;};
const timestamp=(value:unknown,name:string):string=>{const result=string(value,name,64),ms=Date.parse(result);if(!Number.isFinite(ms)||new Date(ms).toISOString()!==result)throw new CsoError('INVALID_SCHEMA',`${name} must be a canonical UTC timestamp`);return result;};
export function validateAssertionWitnessBinding(value:unknown):AssertionWitnessBinding{
const v=object(value,'assertion witness binding'),runtime=object(v.runtime,'assertion witness runtime'),runner=object(v.runner,'assertion witness runner');
exact(v,['schemaVersion','protocol','nonce','phase','issuedAt','expiresAt','runId','findingId','policyHash','auditPolicyHash','runtime','runner','sourceHash','dependencyHash','configurationHash','requestHash','patchHash','harnessHash','assertionHash','fixturesHash'],'assertion witness binding');
exact(runtime,['image','verifierImage','platform','profile'],'assertion witness runtime');exact(runner,['testToolchain','startPlanHash','testPlanHash','commandsHash','minimumPassingTestsHash'],'assertion witness runner');
if(v.schemaVersion!==1||v.protocol!==PROTOCOL)throw new CsoError('INVALID_SCHEMA','Unsupported assertion witness protocol');
const issuedAt=timestamp(v.issuedAt,'assertion witness issuedAt'),expiresAt=timestamp(v.expiresAt,'assertion witness expiresAt'),duration=Date.parse(expiresAt)-Date.parse(issuedAt);
if(duration<=0||duration>MAX_RECEIPT_AGE)throw new CsoError('INVALID_SCHEMA','Assertion witness lifetime exceeds the bounded attempt policy');
if(typeof v.nonce!=='string'||!HASH.test(v.nonce))throw new CsoError('INVALID_SCHEMA','Assertion witness nonce must be 32 random bytes');
const findingId=string(v.findingId,'assertion witness findingId',64);if(!/^[a-f0-9]{32}$/.test(findingId))throw new CsoError('INVALID_SCHEMA','Assertion witness findingId is invalid');
return{schemaVersion:1,protocol:PROTOCOL,nonce:v.nonce,phase:oneOf(v.phase,['before','after'],'assertion witness phase'),issuedAt,expiresAt,runId:string(v.runId,'assertion witness runId',200),findingId,policyHash:hash(v.policyHash,'assertion witness policyHash'),auditPolicyHash:hash(v.auditPolicyHash,'assertion witness auditPolicyHash'),runtime:{image:string(runtime.image,'assertion witness runtime image',500),verifierImage:string(runtime.verifierImage,'assertion witness verifier image',500),platform:string(runtime.platform,'assertion witness runtime platform',100),profile:string(runtime.profile,'assertion witness runtime profile',100)},runner:{testToolchain:oneOf(runner.testToolchain,['runtime','project'],'assertion witness test toolchain'),startPlanHash:hash(runner.startPlanHash,'assertion witness start plan'),testPlanHash:hash(runner.testPlanHash,'assertion witness test plan'),commandsHash:hash(runner.commandsHash,'assertion witness commands'),minimumPassingTestsHash:hash(runner.minimumPassingTestsHash,'assertion witness execution floors')},sourceHash:hash(v.sourceHash,'assertion witness sourceHash'),dependencyHash:hash(v.dependencyHash,'assertion witness dependencyHash'),configurationHash:hash(v.configurationHash,'assertion witness configurationHash'),requestHash:hash(v.requestHash,'assertion witness requestHash'),patchHash:hash(v.patchHash,'assertion witness patchHash'),harnessHash:hash(v.harnessHash,'assertion witness harnessHash'),assertionHash:hash(v.assertionHash,'assertion witness assertionHash'),fixturesHash:hash(v.fixturesHash,'assertion witness fixturesHash')};
}
function receiptUnsigned(receipt:AssertionWitnessReceipt):Omit<AssertionWitnessReceipt,'signature'>{const {signature:_,...unsigned}=receipt;return unsigned;}
function observationForReceipt(observation:VerificationObservation,binding:AssertionWitnessBinding,diagnosticTestsPassed:boolean):VerificationObservation{
const checked=validateVerificationObservation(observation);
return{...checked,existingTests:diagnosticTestsPassed,inputHash:binding.harnessHash};
}
export function witnessObservationHash(observation:VerificationObservation):string{return sha256(canonical(validateVerificationObservation(observation)));}
export function validateStoredAssertionWitnessReceipt(value:unknown):AssertionWitnessReceipt{
const v=object(value,'assertion witness receipt'),binding=validateAssertionWitnessBinding(v.binding);
exact(v,['schemaVersion','binding','keyId','publicKey','observationHash','externalAssertionsPassed','diagnosticTestsPassed','executions','signature'],'assertion witness receipt');
if(v.schemaVersion!==1||typeof v.keyId!=='string'||!HASH.test(v.keyId)||typeof v.publicKey!=='string'||!PUBLIC_KEY.test(v.publicKey)||typeof v.signature!=='string'||!SIGNATURE.test(v.signature))throw new CsoError('INVALID_SCHEMA','Assertion witness cryptographic metadata is invalid');
if(sha256(Buffer.from(v.publicKey,'hex'))!==v.keyId)throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness key identity does not match its public key');
if(typeof v.observationHash!=='string'||!HASH.test(v.observationHash)||typeof v.externalAssertionsPassed!=='boolean'||typeof v.diagnosticTestsPassed!=='boolean'||!Array.isArray(v.executions)||!v.executions.length||v.executions.length>100)throw new CsoError('INVALID_SCHEMA','Assertion witness outcomes are malformed');
const executions=v.executions.map((raw:any,index:number)=>{const item=object(raw,`assertion witness execution ${index}`);exact(item,['commandHash','exitCode','outputHash','minimumPassingTests','executedTests','passingTests','reportedPassed'],`assertion witness execution ${index}`);if(!Number.isSafeInteger(item.exitCode)||item.exitCode<-1||item.exitCode>255||!Number.isSafeInteger(item.minimumPassingTests)||item.minimumPassingTests<1||!Number.isSafeInteger(item.executedTests)||item.executedTests<0||!Number.isSafeInteger(item.passingTests)||item.passingTests<0||item.passingTests>item.executedTests||typeof item.reportedPassed!=='boolean'||(item.reportedPassed&&item.passingTests<item.minimumPassingTests))throw new CsoError('INVALID_SCHEMA','Assertion witness execution outcome is malformed');return{commandHash:hash(item.commandHash,'assertion witness commandHash'),exitCode:item.exitCode,outputHash:hash(item.outputHash,'assertion witness outputHash'),minimumPassingTests:item.minimumPassingTests,executedTests:item.executedTests,passingTests:item.passingTests,reportedPassed:item.reportedPassed};});
if(v.diagnosticTestsPassed!==executions.every(item=>item.reportedPassed))throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness diagnostic summary does not match its executions');
const receipt:AssertionWitnessReceipt={schemaVersion:1,binding,keyId:v.keyId,publicKey:v.publicKey,observationHash:v.observationHash,externalAssertionsPassed:v.externalAssertionsPassed,diagnosticTestsPassed:v.diagnosticTestsPassed,executions,signature:v.signature};
let valid=false;try{valid=verify(null,Buffer.from(canonical(receiptUnsigned(receipt))),createPublicKey({key:Buffer.from(receipt.publicKey,'hex'),format:'der',type:'spki'}),Buffer.from(receipt.signature,'hex'));}catch{}
if(!valid)throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness signature is invalid');return receipt;
}
export function validateAssertionWitnessReceipt(value:unknown,expected:AssertionWitnessBinding,expectedPublicKey:string,observation:VerificationObservation,now=Date.now()):AssertionWitnessReceipt{
const receipt=validateStoredAssertionWitnessReceipt(value),binding=validateAssertionWitnessBinding(expected);
if(canonical(receipt.binding)!==canonical(binding)||receipt.publicKey!==expectedPublicKey)throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness receipt does not bind this verification challenge');
if(now<Date.parse(binding.issuedAt)||now>Date.parse(binding.expiresAt))throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness receipt is stale');
const normalized=observationForReceipt(observation,binding,receipt.diagnosticTestsPassed),external=normalized.booted&&normalized.legitimate&&normalized.security!=='inconclusive';
if(receipt.observationHash!==witnessObservationHash(normalized)||receipt.externalAssertionsPassed!==external)throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness receipt does not bind the external verifier observation');
return receipt;
}
export function assertionWitnessSemanticValue(receipt:AssertionWitnessReceipt):unknown{
const checked=validateStoredAssertionWitnessReceipt(receipt),{nonce:_,issuedAt:__,expiresAt:___,...stable}=checked.binding;
return{binding:stable,observationHash:checked.observationHash,externalAssertionsPassed:checked.externalAssertionsPassed,diagnosticTestsPassed:checked.diagnosticTestsPassed,executions:checked.executions};
}
export function assertionWitnessPairHash(pair:{before:AssertionWitnessReceipt;after:AssertionWitnessReceipt}):string{
return sha256(canonical({before:assertionWitnessSemanticValue(pair.before),after:assertionWitnessSemanticValue(pair.after)}));
}
function witnessReplayValue(receipt:AssertionWitnessReceipt):unknown{
const checked=validateStoredAssertionWitnessReceipt(receipt),{nonce:_,issuedAt:__,expiresAt:___,...binding}=checked.binding;
return{binding,externalAssertionsPassed:checked.externalAssertionsPassed,diagnosticTestsPassed:checked.diagnosticTestsPassed,
executions:checked.executions.map(({outputHash:_,...execution})=>execution)};
}
export function assertionWitnessReplayHash(pair:{before:AssertionWitnessReceipt;after:AssertionWitnessReceipt}):string{
return sha256(canonical({before:witnessReplayValue(pair.before),after:witnessReplayValue(pair.after)}));
}
interface TestExecutionSummary {executedTests:number;passingTests:number;reportedPassed:boolean}
function testExecutionSummary(command:Command,code:number,output:string,minimumPassingTests=1):TestExecutionSummary{
const failed={executedTests:0,passingTests:0,reportedPassed:false};
if(!Number.isInteger(minimumPassingTests)||minimumPassingTests<1||code!==0||!output||output.includes('[sensitive process output redacted]'))return failed;
const clean=output.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g,''),args=command.args,name=basename(command.executable),json=()=>{const end=clean.lastIndexOf('}');if(end<0)return undefined;for(let start=clean.lastIndexOf('{',end);start>=0;start=clean.lastIndexOf('{',start-1)){try{const value=JSON.parse(clean.slice(start,end+1));if(value&&typeof value==='object')return value;}catch{}}};
let executedTests=0,passingTests=0,valid=false;
if(name==='node'&&args.includes('--test')&&args.includes('--test-reporter=tap')){const paths=args.filter(arg=>arg.startsWith('./')).map(arg=>arg.slice(2)),registered=[...clean.matchAll(/^# Subtest:\s+(.+?)\s*$/gm)].map(match=>match[1]),isPathWrapper=(label:string)=>paths.some(path=>label===path||label.endsWith(`/${path}`));executedTests=Number(clean.match(/^# tests\s+(\d+)\s*$/m)?.[1]);passingTests=Number(clean.match(/^# pass\s+(\d+)\s*$/m)?.[1]);valid=registered.some(label=>!isPathWrapper(label))&&!registered.some(isPathWrapper)&&executedTests>=passingTests&&/^# fail\s+0\s*$/m.test(clean)&&/^# cancelled\s+0\s*$/m.test(clean);}
else if(name==='bun'&&args.includes('test')){passingTests=Number(clean.match(/^\s*(\d+)\s+pass(?:es)?\s*$/mi)?.[1]);executedTests=Number(clean.match(/\bRan\s+(\d+)\s+tests?\b/i)?.[1]);valid=executedTests>=passingTests&&/^\s*0\s+fail(?:ures?)?\s*$/mi.test(clean);}
else if(name==='jest'&&args.includes('--json')){const value=json();passingTests=Number(value?.numPassedTests);executedTests=Number(value?.numTotalTests);valid=value?.success===true&&value?.numFailedTests===0&&value?.numRuntimeErrorTestSuites===0&&executedTests>=passingTests;}
else if(name==='vitest'&&args.includes('--reporter=verbose')){const match=clean.match(/^\s*Tests\s+.*?(\d+)\s+passed.*?\((\d+)\)\s*$/mi);passingTests=Number(match?.[1]);executedTests=Number(match?.[2]);valid=executedTests>=passingTests&&!/\b\d+\s+failed\b/i.test(match?.[0]??'');}
else if(name==='mocha'&&args.includes('json')){const stats=json()?.stats;passingTests=Number(stats?.passes);executedTests=Number(stats?.tests);valid=stats?.failures===0&&Number.isSafeInteger(stats?.pending)&&executedTests===passingTests+stats.pending;}
else if(name==='ava'&&args.includes('--tap')){executedTests=Number(clean.match(/^# tests\s+(\d+)\s*$/m)?.[1]);passingTests=Number(clean.match(/^# pass\s+(\d+)\s*$/m)?.[1]);valid=executedTests>=passingTests&&/^# fail\s+0\s*$/m.test(clean);}
else if(name==='python'&&args.some(arg=>arg.includes('import pytest;')&&arg.includes('pytest.main'))){passingTests=Number(clean.match(/(?:^|\s)(\d+)\s+passed\b/i)?.[1]);const skipped=Number(clean.match(/(?:^|\s)(\d+)\s+skipped\b/i)?.[1]??0);executedTests=passingTests+skipped;valid=true;}
else if(name==='python'&&args.some(arg=>arg.includes('import os,sys,unittest;')&&arg.includes('unittest.main'))){executedTests=Number(clean.match(/\bRan\s+(\d+)\s+tests?\b/i)?.[1]);const skipped=Number(clean.match(/\bskipped=(\d+)\b/i)?.[1]??0);passingTests=executedTests-skipped;valid=Number.isSafeInteger(skipped);}
else if(name==='bundle'&&args[0]==='exec'&&args[1]==='rspec'&&args.includes('json')){const summary=json()?.summary,pending=Number(summary?.pending_count??0);executedTests=Number(summary?.example_count);passingTests=executedTests-pending;valid=Number.isSafeInteger(pending)&&summary?.failure_count===0&&(summary?.errors_outside_of_examples_count??0)===0;}
else if(name==='bundle'&&args[0]==='exec'&&args[1]==='rails'&&args[2]==='test'&&args.includes('--no-color')){const match=clean.match(/\b(\d+)\s+runs?\s*,\s*(\d+)\s+assertions?\s*,\s*0\s+failures?\s*,\s*0\s+errors?\s*,\s*(\d+)\s+skips?\b/i),skipped=Number(match?.[3]);executedTests=Number(match?.[1]);passingTests=executedTests-skipped;valid=Number.isSafeInteger(skipped);}
const countsValid=Number.isSafeInteger(executedTests)&&executedTests>=0&&Number.isSafeInteger(passingTests)&&passingTests>=0&&executedTests>=passingTests;
return countsValid?{executedTests,passingTests,reportedPassed:valid&&passingTests>=minimumPassingTests}:failed;
}
export function testExecutionPassed(command:Command,code:number,output:string,minimumPassingTests=1):boolean{
return testExecutionSummary(command,code,output,minimumPassingTests).reportedPassed;
}
interface ChildRequest {privateKey:string;publicKey:string;binding:AssertionWitnessBinding;observation:VerificationObservation;executions:WitnessTestExecution[]}
async function readChildInput():Promise<string>{const chunks:Buffer[]=[];let bytes=0;for await(const value of process.stdin){const chunk=Buffer.from(value);bytes+=chunk.length;if(bytes>2*MAX_OUTPUT)throw new CsoError('INVALID_SCHEMA','Assertion witness request exceeds the bounded input limit');chunks.push(chunk);}return Buffer.concat(chunks).toString('utf8');}
function createReceipt(input:unknown):AssertionWitnessReceipt{
const v=object(input,'assertion witness child request');exact(v,['privateKey','publicKey','binding','observation','executions'],'assertion witness child request');const binding=validateAssertionWitnessBinding(v.binding);
if(Date.now()<Date.parse(binding.issuedAt)||Date.now()>Date.parse(binding.expiresAt))throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness challenge is stale');
if(typeof v.privateKey!=='string'||v.privateKey.length>4096||typeof v.publicKey!=='string'||!PUBLIC_KEY.test(v.publicKey))throw new CsoError('INVALID_SCHEMA','Assertion witness signing input is invalid');
let privateKey;try{privateKey=createPrivateKey(v.privateKey);const derived=createPublicKey(privateKey).export({format:'der',type:'spki'}).toString('hex');if(derived!==v.publicKey)throw new Error();}catch{throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness signing authority does not match the challenge');}
const rawObservation=validateVerificationObservation(v.observation);if(!Array.isArray(v.executions)||!v.executions.length||v.executions.length>100)throw new CsoError('INVALID_SCHEMA','Assertion witness needs one or more canonical test executions');
let outputBytes=0;const rawExecutions:WitnessTestExecution[]=v.executions.map((raw:any,index:number)=>{const item=object(raw,`witness execution ${index}`);exact(item,['command','code','output','minimumPassingTests'],`witness execution ${index}`);const command=validateCommand(item.command,`witness execution ${index}.command`);if(!Number.isSafeInteger(item.code)||item.code<-1||item.code>255||typeof item.output!=='string'||item.output.includes('\0')||!Number.isSafeInteger(item.minimumPassingTests)||item.minimumPassingTests<1)throw new CsoError('INVALID_SCHEMA','Assertion witness test execution is malformed');outputBytes+=Buffer.byteLength(item.output);if(outputBytes>MAX_OUTPUT)throw new CsoError('INVALID_SCHEMA','Assertion witness test output exceeds the group capture limit');return{command,code:item.code,output:item.output,minimumPassingTests:item.minimumPassingTests};});
if(sha256(canonical(rawExecutions.map(item=>item.command)))!==binding.runner.commandsHash||sha256(canonical(rawExecutions.map(item=>item.minimumPassingTests)))!==binding.runner.minimumPassingTestsHash)throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness executions do not match the helper-derived runner');
const executions=rawExecutions.map(item=>{const summary=testExecutionSummary(item.command,item.code,item.output,item.minimumPassingTests);return{commandHash:sha256(canonical(item.command)),exitCode:item.code,outputHash:sha256(item.output),minimumPassingTests:item.minimumPassingTests,...summary};}),diagnosticTestsPassed=executions.every(item=>item.reportedPassed),observation=observationForReceipt(rawObservation,binding,diagnosticTestsPassed),externalAssertionsPassed=observation.booted&&observation.legitimate&&observation.security!=='inconclusive';
const unsigned:Omit<AssertionWitnessReceipt,'signature'>={schemaVersion:1,binding,keyId:sha256(Buffer.from(v.publicKey,'hex')),publicKey:v.publicKey,observationHash:witnessObservationHash(observation),externalAssertionsPassed,diagnosticTestsPassed,executions};
return{...unsigned,signature:sign(null,Buffer.from(canonical(unsigned)),privateKey).toString('hex')};
}
export async function runAssertionWitnessChild():Promise<void>{const receipt=createReceipt(JSON.parse(await readChildInput()));process.stdout.write(JSON.stringify(receipt)+'\n');}
export class AssertionWitnessSession{
private privateKey:string;readonly publicKey:string;readonly keyId:string;private nonces=new Set<string>();
constructor(private workDirectory:string,private deadline:number){const stat=lstatSync(workDirectory),real=realpathSync(workDirectory),resolved=lstatSync(real);if(!stat.isDirectory()||stat.isSymbolicLink()||!resolved.isDirectory()||resolved.isSymbolicLink()||stat.dev!==resolved.dev||stat.ino!==resolved.ino||(process.getuid&&resolved.uid!==process.getuid())||(resolved.mode&0o022)!==0)throw new CsoError('UNSAFE_PATH','Assertion witness working directory must be private and owned');this.workDirectory=real;const pair=generateKeyPairSync('ed25519');this.privateKey=pair.privateKey.export({format:'pem',type:'pkcs8'}).toString();this.publicKey=pair.publicKey.export({format:'der',type:'spki'}).toString('hex');this.keyId=sha256(Buffer.from(this.publicKey,'hex'));}
handle(stable:Omit<AssertionWitnessBinding,'schemaVersion'|'protocol'|'nonce'|'issuedAt'|'expiresAt'>):AssertionWitnessHandle{
const now=Date.now(),expires=Math.min(this.deadline,now+MAX_RECEIPT_AGE);if(expires<=now)throw new CsoError('DEADLINE','No time remains for an authenticated assertion witness');let nonce='';do{nonce=randomBytes(32).toString('hex');}while(this.nonces.has(nonce));this.nonces.add(nonce);
const binding=validateAssertionWitnessBinding({schemaVersion:1,protocol:PROTOCOL,nonce,issuedAt:new Date(now).toISOString(),expiresAt:new Date(expires).toISOString(),...stable});let consumed=false;
return{binding,attest:async(observation,executions)=>{if(consumed)throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness challenge was already consumed');consumed=true;const input=JSON.stringify({privateKey:this.privateKey,publicKey:this.publicKey,binding,observation,executions} satisfies ChildRequest);if(Buffer.byteLength(input)>2*MAX_OUTPUT)throw new CsoError('REDACTION_FAILED','Assertion witness input exceeds the bounded helper channel');const bun=/^bun(?:\.exe)?$/i.test(basename(process.execPath)),file=bun?process.execPath:join(dirname(process.execPath),process.platform==='win32'?'gstack-cso-launcher.exe':'gstack-cso-launcher'),args=bun?[import.meta.path,'--child']:['__cso-assertion-witness'],env=process.platform==='win32'?{PATH:dirname(process.execPath),SYSTEMROOT:process.env.SYSTEMROOT??'C:\\Windows',WINDIR:process.env.WINDIR??'C:\\Windows'}:{PATH:'/usr/bin:/bin',LANG:'C.UTF-8',LC_ALL:'C.UTF-8',TZ:'UTC'},result=await runProcess(file,args,{cwd:this.workDirectory,env,timeoutMs:Math.max(1,expires-Date.now()),maxBytes:128*1024,input,raw:true});if(result.timedOut)throw new CsoError('DEADLINE','Assertion witness exceeded the verification deadline');if(result.truncated||result.code!==0)throw new CsoError('TOOL_FAILED','Authenticated assertion witness did not return a bounded receipt');let receipt:unknown;try{receipt=JSON.parse(result.stdout);}catch{throw new CsoError('TOOL_FAILED','Authenticated assertion witness returned invalid output');}return validateAssertionWitnessReceipt(receipt,binding,this.publicKey,observation);},validate:(receipt,observation,current=Date.now())=>validateAssertionWitnessReceipt(receipt,binding,this.publicKey,observation,current)};
}
}
if(import.meta.main&&process.argv.at(-1)==='--child')runAssertionWitnessChild().catch(()=>{process.stderr.write('assertion witness failed\n');process.exitCode=1;});
+8 -1
View File
@@ -28,6 +28,8 @@ import * as crypto from 'crypto';
export interface AtomicWriteOpts {
/** File mode for the tmp file at creation (e.g. 0o600). Default: umask. */
mode?: number;
/** Publish only when the target does not already exist. */
noReplace?: boolean;
}
function tmpPathFor(target: string): string {
@@ -47,7 +49,12 @@ export function atomicWriteSync(
} else {
fs.writeFileSync(tmp, data);
}
fs.renameSync(tmp, target);
if (opts.noReplace) {
// Publishing the complete temp inode with link(2) gives atomic
// no-replace semantics; unlink only removes the temporary name.
fs.linkSync(tmp,target);
fs.unlinkSync(tmp);
} else fs.renameSync(tmp, target);
} catch (err) {
try {
fs.unlinkSync(tmp);