Files
gstack/lib/cso/preparation.ts
T
Garry TanandOpenAI Codex 4a3c6a8a3c 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>
2026-09-14 15:14:58 -07:00

513 lines
39 KiB
TypeScript

/** 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;
}