Files
gstack/lib/cso/preparation.ts
T
garrytan d93d61f7ba style(cso): format lib/cso TypeScript with pinned Prettier
Mechanical reformat only. Minified transpile output is byte-identical for
21 of 22 files; witness.ts differs only in three regex flag orders
(/mi -> /im), which JavaScript canonicalizes. Source-text assertions over
lib/cso now compare whitespace-insensitively with the same tokens.
2026-09-29 14:22:48 +00:00

1224 lines
44 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;
}