mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-16 09:55:29 +02:00
* 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>
183 lines
14 KiB
TypeScript
183 lines
14 KiB
TypeScript
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;}
|
|
}
|