Skill Tracer
Skills › gstack › gstack › cso-verify-runtime-base.ts

cso-verify-runtime-base.ts

117 lines · scripts/cso-verify-runtime-base.ts at 3f79d81

1#!/usr/bin/env -S bun --no-env-file --config=/dev/null
2/** Native trusted-CI verification for reviewed OCI build inputs. */
3import { spawnSync } from 'node:child_process';
4import { committedImageBuildMatrix, type ImageBuildRow } from './cso-image-matrix';
5
6const MAX_OUTPUT = 1024 * 1024;
7const TIMEOUT = 120_000;
8
9interface VersionProbe {
10 image: string;
11 executable: string;
12 args: string[];
13 version: string;
14 name: string;
17export function assertVersionOutput(name: string, expected: string, output: string): void {
18 if (Buffer.byteLength(output) > 64 * 1024) throw new Error(`VERSION_OUTPUT_TOO_LARGE: ${name}`);
19 const first = output.match(/(?<!\d)(\d+\.\d+(?:\.\d+)?)(?![\d.])/);
20 if (!first || first[1] !== expected) throw new Error(`RUNTIME_VERSION_MISMATCH: ${name} expected ${expected}`);
23export function probesForBuildRow(row: ImageBuildRow): VersionProbe[] {
24 const base = row.baseImage;
25 switch (row.stack) {
26 case 'node':
27 return [
28 { image: base, executable: '/usr/local/bin/node', args: ['--version'], version: row.versions.node, name: 'node' },
29 { image: base, executable: '/usr/local/bin/npm', args: ['--version'], version: row.versions.npm, name: 'npm' },
30 ];
31 case 'bun':
32 return [{ image: base, executable: '/usr/local/bin/bun', args: ['--version'], version: row.versions.bun, name: 'bun' }];
33 case 'python':
34 return [
35 { image: base, executable: '/usr/local/bin/python', args: ['--version'], version: row.versions.python, name: 'python' },
36 { image: row.uvImage, executable: '/uv', args: ['--version'], version: row.versions.uv, name: 'uv' },
37 ];
38 case 'rails':
39 return [
40 { image: base, executable: '/usr/local/bin/ruby', args: ['--version'], version: row.versions.ruby, name: 'ruby' },
41 { image: base, executable: '/usr/local/bin/bundle', args: ['--version'], version: row.versions.bundler, name: 'bundler' },
42 ];
43 case 'postgresql':
44 return [{ image: base, executable: '/usr/lib/postgresql/17/bin/postgres', args: ['--version'], version: row.versions.postgresql, name: 'postgresql' }];
45 }
48function command(docker: string, args: string[], timeout = TIMEOUT): string {
49 const env: Record<string, string> = { PATH: process.env.PATH ?? '/usr/bin:/bin' };
50 if (process.env.HOME) env.HOME = process.env.HOME;
51 if (process.env.DOCKER_CONFIG) env.DOCKER_CONFIG = process.env.DOCKER_CONFIG;
52 const result = spawnSync(docker, args, { encoding: 'utf8', timeout, maxBuffer: MAX_OUTPUT, env });
53 if (result.error || result.status !== 0) {
54 const message = [result.error?.message, result.stderr, result.stdout].filter(Boolean).join('\n').slice(0, 4096);
55 throw new Error(`DOCKER_INPUT_VERIFICATION_FAILED: ${args[0]} ${message}`);
56 }
57 if (Buffer.byteLength(result.stdout) + Buffer.byteLength(result.stderr) > MAX_OUTPUT) throw new Error('DOCKER_INPUT_VERIFICATION_OUTPUT_TOO_LARGE');
58 return result.stdout;
61function inspectManifest(docker: string, image: string): any {
62 const raw = command(docker, ['buildx', 'imagetools', 'inspect', image, '--format', '{{json .Manifest}}']);
63 try { return JSON.parse(raw); } catch { throw new Error(`INVALID_OCI_MANIFEST: ${image}`); }
66function inspectImage(docker: string, image: string): any {
67 const raw = command(docker, ['buildx', 'imagetools', 'inspect', image, '--format', '{{json .Image}}']);
68 try { return JSON.parse(raw); } catch { throw new Error(`INVALID_OCI_IMAGE_CONFIG: ${image}`); }
71function digestOf(image: string): string {
72 return image.slice(image.lastIndexOf('@') + 1);
75function assertSourceIndex(docker: string, source: string, indexImage: string, platformImage: string, row: ImageBuildRow): void {
76 const sourceManifest = inspectManifest(docker, source);
77 if (sourceManifest?.digest !== digestOf(indexImage)) throw new Error(`SOURCE_TAG_MOVED: ${source}`);
78 const indexManifest = inspectManifest(docker, indexImage);
79 const platformDigest = digestOf(platformImage);
80 const match = indexManifest?.manifests?.find((manifest: any) =>
81 manifest?.digest === platformDigest && manifest?.platform?.os === 'linux' && manifest?.platform?.architecture === row.arch);
82 if (!match) throw new Error(`PLATFORM_MANIFEST_NOT_IN_INDEX: ${platformImage}`);
83 const config = inspectImage(docker, platformImage);
84 if (config?.os !== 'linux' || config?.architecture !== row.arch) throw new Error(`PLATFORM_CONFIG_MISMATCH: ${platformImage}`);
87export function verifyRuntimeBuildRow(row: ImageBuildRow, docker = Bun.which('docker')): void {
88 if (!docker || !docker.startsWith('/')) throw new Error('TRUSTED_DOCKER_NOT_FOUND');
89 command(docker, ['--host', 'unix:///var/run/docker.sock', 'info'], 30_000);
90 assertSourceIndex(docker, row.baseSource, row.baseIndexImage, row.baseImage, row);
91 assertSourceIndex(docker, row.sbomGeneratorSource, row.sbomGeneratorIndexImage, row.sbomGeneratorImage, row);
92 if (row.stack === 'python') assertSourceIndex(docker, row.uvSource, row.uvIndexImage, row.uvImage, row);
93 const images = [...new Set(probesForBuildRow(row).map(probe => probe.image))];
94 for (const image of images) command(docker, ['--host', 'unix:///var/run/docker.sock', 'pull', '--platform', row.platform, image], 5 * 60_000);
95 for (const probe of probesForBuildRow(row)) {
96 const output = command(docker, [
97 '--host', 'unix:///var/run/docker.sock', 'run', '--rm', '--pull', 'never', '--network', 'none',
98 '--entrypoint', probe.executable, probe.image, ...probe.args,
99 ], 30_000);
100 assertVersionOutput(probe.name, probe.version, output);
101 }
104if (import.meta.main) {
105 try {
106 if (process.argv.length !== 2) throw new Error('No arguments accepted; the selected row comes from committed inputs and trusted CI environment.');
107 const stack = process.env.CSO_STACK;
108 const platform = process.env.CSO_PLATFORM;
109 const rows = committedImageBuildMatrix().include.filter(row => row.stack === stack && row.platform === platform);
110 if (rows.length !== 1) throw new Error('INVALID_RUNTIME_BUILD_ROW');
111 verifyRuntimeBuildRow(rows[0]);
112 process.stdout.write(`verified ${rows[0].runtimeId} base manifests and versions\n`);
113 } catch (error) {
114 process.stderr.write((error instanceof Error ? error.message : 'RUNTIME_BASE_VERIFICATION_FAILED') + '\n');
115 process.exitCode = 1;
116 }