@linzumi/cli 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/dist/index.js +413 -319
- package/dist/mcp-server.mjs +174 -0
- package/package.json +2 -1
- package/scripts/build.mjs +34 -3
- package/scripts/golden-chaos-harness.mjs +1420 -0
- package/scripts/golden-lifecycle-e2e.mjs +969 -0
- package/scripts/golden-scale-test.mjs +738 -0
- package/scripts/vm-provision-proof.mjs +151 -0
- package/scripts/vm-smoke.mjs +241 -0
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// PR #2111 - END-TO-END GOLDEN-LIFECYCLE CAPSTONE
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// ONE script that proves the whole forkable-golden lifecycle on REAL VMs (HVF on
|
|
6
|
+
// this Mac), exercising OUR CLI golden code (not just raw smolmachines), and
|
|
7
|
+
// asserts the 9 golden-lifecycle properties (P1-P9) with a per-property PASS/FAIL
|
|
8
|
+
// table. Non-zero exit on ANY property violation.
|
|
9
|
+
//
|
|
10
|
+
// The same 9 properties are encoded as teeth'd, model-tier invariants in the
|
|
11
|
+
// deterministic replay_harness (the G-series in
|
|
12
|
+
// kandan/server_v2/web/replay_harness/src/goldenLifecycleInvariants.mjs
|
|
13
|
+
// driven by tests/golden_lifecycle_invariants.test.mjs). The harness CANNOT boot
|
|
14
|
+
// HVF, so it runs the SAME oracles over a MODEL observation; this script runs
|
|
15
|
+
// them over a LIVE observation. The oracle code is shared verbatim - this script
|
|
16
|
+
// imports the harness G-series and feeds it the live observation, so a real
|
|
17
|
+
// lifecycle that violated a property is caught by the identical check that guards
|
|
18
|
+
// the model tier. (HVF-only legs: boot/provision/fork/install timings + live fs
|
|
19
|
+
// isolation/immutability. Model-tier: the oracle logic itself.)
|
|
20
|
+
//
|
|
21
|
+
// HOW IT EXERCISES OUR CODE (not raw smolmachines):
|
|
22
|
+
// - the deterministic golden machine name comes from goldenBuildMachineName()
|
|
23
|
+
// - toolchain provision uses guestProvisionCommand() (the SAME script a real
|
|
24
|
+
// cold-provisioned VM runs) - minus the pinned @openai/codex npm -g install,
|
|
25
|
+
// which is replaced by a no-op marker for the E2E (the codex registry pull is
|
|
26
|
+
// slow + irrelevant to the fs-lifecycle properties; node/npm/git/the project
|
|
27
|
+
// deps - the parts the properties assert - are installed for real)
|
|
28
|
+
// - the warm manifest checksum is goldenManifestChecksum() over real
|
|
29
|
+
// gatherSourceCommits()/buildLockfileHashes()/readGuestToolchainVersions()
|
|
30
|
+
// run against the live guest
|
|
31
|
+
// - concurrent job VMs are forked via createMachineFromGolden() (the real
|
|
32
|
+
// supervisor fork-from-golden path WITH its EAGAIN/os-error-35 backoff retry)
|
|
33
|
+
// - the property oracles are the harness G-series, imported verbatim
|
|
34
|
+
//
|
|
35
|
+
// Spec: docs/golden-image-registry-contract.md ; /tmp/smol-rt-132/fork-proof.mjs
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
import { createRequire } from 'node:module';
|
|
39
|
+
import { fileURLToPath } from 'node:url';
|
|
40
|
+
import { mkdtempSync, rmSync, writeFileSync, existsSync } from 'node:fs';
|
|
41
|
+
import { tmpdir } from 'node:os';
|
|
42
|
+
import { dirname, join } from 'node:path';
|
|
43
|
+
import { execFileSync } from 'node:child_process';
|
|
44
|
+
|
|
45
|
+
// Surface ANY escaped async failure loudly (never a silent exit-1 with no trace).
|
|
46
|
+
process.on('uncaughtException', (e) => {
|
|
47
|
+
console.error('[golden-e2e] UNCAUGHT:', e?.stack || String(e));
|
|
48
|
+
});
|
|
49
|
+
process.on('unhandledRejection', (e) => {
|
|
50
|
+
console.error('[golden-e2e] UNHANDLED-REJECTION:', e?.stack || String(e));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
54
|
+
const __dirname = dirname(__filename);
|
|
55
|
+
const cliRoot = join(__dirname, '..');
|
|
56
|
+
const repoRoot = join(cliRoot, '..', '..');
|
|
57
|
+
const harnessSrc = join(
|
|
58
|
+
repoRoot,
|
|
59
|
+
'kandan',
|
|
60
|
+
'server_v2',
|
|
61
|
+
'web',
|
|
62
|
+
'replay_harness',
|
|
63
|
+
'src'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// The pinned, proven-forkable runtime (smolmachines 1.3.2). The contract's
|
|
67
|
+
// `~/.linzumi/vm-runtime` still pins 0.1.5 (no fork), so we point at the proven
|
|
68
|
+
// 1.3.2 dir, overridable via LINZUMI_VM_RUNTIME_DIR for CI/other machines.
|
|
69
|
+
const RUNTIME_DIR =
|
|
70
|
+
process.env.LINZUMI_VM_RUNTIME_DIR &&
|
|
71
|
+
process.env.LINZUMI_VM_RUNTIME_DIR.trim() !== ''
|
|
72
|
+
? process.env.LINZUMI_VM_RUNTIME_DIR.trim()
|
|
73
|
+
: '/tmp/smol-rt-132';
|
|
74
|
+
|
|
75
|
+
const PROJECT = '/root/project'; // in-guest checkout root
|
|
76
|
+
const N_FORKS = 3;
|
|
77
|
+
const now = () => Date.now();
|
|
78
|
+
const log = (...a) => console.log('[golden-e2e]', ...a);
|
|
79
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// FAULT INJECTION (the teeth knobs). Setting a BREAK_* env var deliberately
|
|
83
|
+
// breaks one lifecycle step so the corresponding property goes RED, proving the
|
|
84
|
+
// assertion has teeth. Default (all unset) is the real, correct lifecycle.
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
const BREAK = {
|
|
87
|
+
isolation: process.env.BREAK_ISOLATION === '1', // P1: leak a file into a sibling fork
|
|
88
|
+
inheritance: process.env.BREAK_INHERITANCE === '1', // P2: wipe node_modules in a fork
|
|
89
|
+
immutability: process.env.BREAK_IMMUTABILITY === '1', // P3: mutate the golden fs
|
|
90
|
+
determinism: process.env.BREAK_DETERMINISM === '1', // P5: perturb the manifest inputs
|
|
91
|
+
durable: process.env.BREAK_DURABLE === '1', // P7: don't push committed work
|
|
92
|
+
cleanup: process.env.BREAK_CLEANUP === '1', // P6: leak a VM (skip teardown)
|
|
93
|
+
loudFail: process.env.BREAK_LOUDFAIL === '1', // P9: silent cold-boot under golden name
|
|
94
|
+
};
|
|
95
|
+
const anyBreak = Object.values(BREAK).some(Boolean);
|
|
96
|
+
|
|
97
|
+
// =============================================================================
|
|
98
|
+
// 1. Bundle OUR CLI TypeScript helpers into a temp ESM module via esbuild, then
|
|
99
|
+
// import them. This is how the E2E exercises the SHIPPED code paths, not a
|
|
100
|
+
// re-implementation.
|
|
101
|
+
// =============================================================================
|
|
102
|
+
async function loadCliHelpers() {
|
|
103
|
+
const esbuild = createRequire(join(cliRoot, 'package.json'))('esbuild');
|
|
104
|
+
const goldenBuildTs = JSON.stringify(
|
|
105
|
+
join(cliRoot, 'src/vmSandbox/vmGoldenBuild.ts')
|
|
106
|
+
);
|
|
107
|
+
const supervisorTs = JSON.stringify(
|
|
108
|
+
join(cliRoot, 'src/vmSandbox/vmCodexSupervisor.ts')
|
|
109
|
+
);
|
|
110
|
+
const guestProvisionTs = JSON.stringify(
|
|
111
|
+
join(cliRoot, 'src/vmSandbox/vmGuestProvision.ts')
|
|
112
|
+
);
|
|
113
|
+
// readGuestToolchainVersions is module-private in vmGoldenBuild.ts (not
|
|
114
|
+
// exported), so we re-implement ONLY its trivial loop over the EXPORTED
|
|
115
|
+
// goldenToolchainProbes list - still exercising the real probe set + the real
|
|
116
|
+
// version-capture shape. Everything else is the shipped function verbatim.
|
|
117
|
+
const adapterSrc = `
|
|
118
|
+
export {
|
|
119
|
+
goldenBuildMachineName,
|
|
120
|
+
goldenManifestChecksum,
|
|
121
|
+
gatherSourceCommits,
|
|
122
|
+
buildLockfileHashes,
|
|
123
|
+
detectPackageManager,
|
|
124
|
+
goldenToolchainProbes,
|
|
125
|
+
} from ${goldenBuildTs};
|
|
126
|
+
export {
|
|
127
|
+
createMachineFromGolden,
|
|
128
|
+
loadSmolmachines,
|
|
129
|
+
} from ${supervisorTs};
|
|
130
|
+
export { guestProvisionCommand } from ${guestProvisionTs};
|
|
131
|
+
|
|
132
|
+
import { goldenToolchainProbes as __probes } from ${goldenBuildTs};
|
|
133
|
+
export async function readGuestToolchainVersions(machine) {
|
|
134
|
+
const versions = Object.create(null);
|
|
135
|
+
for (const probe of __probes) {
|
|
136
|
+
try {
|
|
137
|
+
const r = await machine.exec(probe.command, { timeout: 30 });
|
|
138
|
+
if (r.exitCode === 0) {
|
|
139
|
+
const out = (r.stdout || '').trim();
|
|
140
|
+
if (out !== '') versions[probe.key] = out.split('\\n')[0].trim();
|
|
141
|
+
}
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
return versions;
|
|
145
|
+
}
|
|
146
|
+
`;
|
|
147
|
+
const tmp = mkdtempSync(join(tmpdir(), 'golden-e2e-bundle-'));
|
|
148
|
+
const adapterPath = join(tmp, 'adapter.ts');
|
|
149
|
+
writeFileSync(adapterPath, adapterSrc);
|
|
150
|
+
const outFile = join(tmp, 'bundle.mjs');
|
|
151
|
+
await esbuild.build({
|
|
152
|
+
entryPoints: [adapterPath],
|
|
153
|
+
bundle: true,
|
|
154
|
+
format: 'esm',
|
|
155
|
+
platform: 'node',
|
|
156
|
+
target: 'node20',
|
|
157
|
+
outfile: outFile,
|
|
158
|
+
logLevel: 'silent',
|
|
159
|
+
// node:* and smolmachines are resolved at runtime, not bundled.
|
|
160
|
+
external: ['node:*', 'smolmachines'],
|
|
161
|
+
});
|
|
162
|
+
const mod = await import(outFile);
|
|
163
|
+
return { mod, cleanup: () => rmSync(tmp, { recursive: true, force: true }) };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// =============================================================================
|
|
167
|
+
// Guest exec with the SAME EAGAIN backoff our supervisor uses (os error 35 on
|
|
168
|
+
// rapid back-to-back vsock exec/fork). Mirrors fork-proof.mjs + the supervisor's
|
|
169
|
+
// forkRetry constants.
|
|
170
|
+
// =============================================================================
|
|
171
|
+
// Hard host-side timeout wrapper: a wedged vsock exec must NEVER hang the whole
|
|
172
|
+
// capstone. The SDK's exec `timeout` is best-effort (it does not always abort a
|
|
173
|
+
// stuck guest call), so every exec is ALSO raced against a host wall-clock guard.
|
|
174
|
+
// A guard timeout is a LOUD failure (rejects), not a silent skip.
|
|
175
|
+
function withHostTimeout(promise, ms, label) {
|
|
176
|
+
let timer;
|
|
177
|
+
const guard = new Promise((_, reject) => {
|
|
178
|
+
timer = setTimeout(
|
|
179
|
+
() => reject(new Error(`host-timeout after ${ms}ms: ${label}`)),
|
|
180
|
+
ms
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
return Promise.race([promise, guard]).finally(() => clearTimeout(timer));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function sh(m, label, cmd, timeout = 240) {
|
|
187
|
+
let lastErr;
|
|
188
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
189
|
+
try {
|
|
190
|
+
const r = await withHostTimeout(
|
|
191
|
+
m.exec(['/bin/sh', '-c', cmd], { timeout }),
|
|
192
|
+
// host guard a bit above the SDK timeout (seconds -> ms), capped so a
|
|
193
|
+
// single probe can never wedge the run for minutes.
|
|
194
|
+
Math.min(timeout * 1000 + 5000, 120000),
|
|
195
|
+
label
|
|
196
|
+
);
|
|
197
|
+
const out = (r.stdout || '').trim();
|
|
198
|
+
if (r.exitCode !== 0) {
|
|
199
|
+
log(
|
|
200
|
+
` ${label}: exit=${r.exitCode} ERR=${JSON.stringify(
|
|
201
|
+
(r.stderr || out).slice(0, 240)
|
|
202
|
+
)}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return { code: r.exitCode, out, stderr: (r.stderr || '').trim() };
|
|
206
|
+
} catch (e) {
|
|
207
|
+
lastErr = e;
|
|
208
|
+
// Retry BOTH transient transport faults: EAGAIN (os error 35, rapid vsock
|
|
209
|
+
// exec) AND "connection closed" (the freshly-booted/freshly-forked vsock
|
|
210
|
+
// transport can reset on its first exec or right after a sibling fork). The
|
|
211
|
+
// proven fork-proof retried only EAGAIN; this capstone discovered the
|
|
212
|
+
// connection-closed transient is equally transient and equally retryable.
|
|
213
|
+
const transient =
|
|
214
|
+
/os error 35|temporarily unavailable|eagain/i.test(String(e)) ||
|
|
215
|
+
/connection closed|io operation failed/i.test(String(e));
|
|
216
|
+
if (!transient) throw e;
|
|
217
|
+
await sleep(200 * (attempt + 1));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
throw lastErr;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// UNKNOWN-UNKNOWN (discovered by this capstone), encoded as property P10: in
|
|
224
|
+
// smolmachines 1.3.2, once a golden has been forked, the golden's OWN exec
|
|
225
|
+
// channel becomes permanently unusable (a subsequent golden.exec HANGS, and a
|
|
226
|
+
// fresh connect+exec to the golden also wedges) - the golden becomes a pure
|
|
227
|
+
// fork-TEMPLATE. Verified directly (probe scripts): forking works and children
|
|
228
|
+
// exec fine, but `golden.exec(...)` after any fork never returns. CONSEQUENCE for
|
|
229
|
+
// OUR code: the golden must NEVER be exec'd post-fork. All golden exec (manifest
|
|
230
|
+
// gather, reachability) is done PRE-FORK; golden fs immutability + isolation are
|
|
231
|
+
// proven POST-fork via a WITNESS FORK (a fresh clone reflects the golden's state
|
|
232
|
+
// without exec'ing the golden). This also means W2's trySnapshotLiveThreadInto
|
|
233
|
+
// Golden / any "re-probe the golden" path must treat a forked golden as
|
|
234
|
+
// exec-dead. The G10 invariant is the permanent tripwire that the golden stays
|
|
235
|
+
// FORKABLE after a batch of forks (the witness fork must succeed).
|
|
236
|
+
|
|
237
|
+
// A stable content hash of the in-guest project tree (sans .git internals that
|
|
238
|
+
// carry nondeterministic timestamps): the sorted list of tracked+untracked paths
|
|
239
|
+
// plus the sha256 of each file's bytes. Used for golden-immutability (P3) and
|
|
240
|
+
// determinism (P5).
|
|
241
|
+
const FS_HASH_CMD =
|
|
242
|
+
`cd ${PROJECT} && ` +
|
|
243
|
+
// list all files except .git internals, hash each, sort, hash the digest list
|
|
244
|
+
`find . -type f -not -path './.git/*' -print0 | sort -z | xargs -0 sha256sum | sha256sum | cut -d' ' -f1`;
|
|
245
|
+
async function projectFsHash(m, label) {
|
|
246
|
+
return (await sh(m, `${label} fshash`, FS_HASH_CMD)).out;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// =============================================================================
|
|
250
|
+
// MAIN LIFECYCLE
|
|
251
|
+
// =============================================================================
|
|
252
|
+
const created = []; // {name, machine}
|
|
253
|
+
let bundleCleanup = () => {};
|
|
254
|
+
const timings = {};
|
|
255
|
+
const props = {}; // id -> { pass, detail }
|
|
256
|
+
function record(id, pass, detail) {
|
|
257
|
+
props[id] = { pass: !!pass, detail };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function main() {
|
|
261
|
+
log(`runtime dir: ${RUNTIME_DIR}`);
|
|
262
|
+
if (!existsSync(join(RUNTIME_DIR, 'node_modules', 'smolmachines'))) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`smolmachines runtime not found at ${RUNTIME_DIR}. Set LINZUMI_VM_RUNTIME_DIR ` +
|
|
265
|
+
`to a dir with smolmachines>=1.3.2 (this Mac: /tmp/smol-rt-132).`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
// HVF gate (Apple Hypervisor). On a host without it, this whole script is
|
|
269
|
+
// HVF-only and must be skipped at the CI layer; here we hard-require it.
|
|
270
|
+
try {
|
|
271
|
+
const hv = execFileSync('sysctl', ['-n', 'kern.hv_support'], {
|
|
272
|
+
encoding: 'utf8',
|
|
273
|
+
}).trim();
|
|
274
|
+
if (hv !== '1') throw new Error('kern.hv_support != 1');
|
|
275
|
+
} catch (e) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`HVF not available (${e.message}); this is the HVF-only leg.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const { mod: cli, cleanup } = await loadCliHelpers();
|
|
282
|
+
bundleCleanup = cleanup;
|
|
283
|
+
log('bundled OUR CLI helpers via esbuild (exercising shipped code paths)');
|
|
284
|
+
|
|
285
|
+
const sdk = await cli.loadSmolmachines(RUNTIME_DIR);
|
|
286
|
+
log('loaded smolmachines via OUR loadSmolmachines()');
|
|
287
|
+
|
|
288
|
+
// -------------------------------------------------------------------------
|
|
289
|
+
// STEP 1: COLD-BOOT a new project: forkable golden + provision toolchain +
|
|
290
|
+
// known project (git init + package.json) + REAL npm install.
|
|
291
|
+
// -------------------------------------------------------------------------
|
|
292
|
+
// A per-run UNIQUE project cwd so OUR deterministic goldenBuildMachineName()
|
|
293
|
+
// yields a fresh golden name every run. pid alone can recycle and the smolvm
|
|
294
|
+
// `vms` disk cache lingers under a name across runs, so a same-named stale entry
|
|
295
|
+
// could be picked up in a non-forkable state ("not running forkable" on fork) -
|
|
296
|
+
// a real cross-run cache-collision hazard this run-token avoids.
|
|
297
|
+
const runToken = `${process.pid}-${Date.now().toString(36)}-${Math.random()
|
|
298
|
+
.toString(36)
|
|
299
|
+
.slice(2, 8)}`;
|
|
300
|
+
const projectCwd = `/private/tmp/golden-e2e-project-${runToken}`;
|
|
301
|
+
const goldenName = cli.goldenBuildMachineName(projectCwd); // OUR deterministic name
|
|
302
|
+
log(`STEP 1: cold-boot forkable golden ${goldenName} (network on)`);
|
|
303
|
+
|
|
304
|
+
const tBoot = now();
|
|
305
|
+
const golden = await sdk.Machine.create({
|
|
306
|
+
name: goldenName,
|
|
307
|
+
resources: { cpus: 2, memoryMb: 2048, network: true },
|
|
308
|
+
forkable: true,
|
|
309
|
+
});
|
|
310
|
+
created.push({ name: goldenName, machine: golden });
|
|
311
|
+
timings.coldBootMs = now() - tBoot;
|
|
312
|
+
log(` golden booted forkable in ${timings.coldBootMs}ms`);
|
|
313
|
+
|
|
314
|
+
// Provision the toolchain using OUR guestProvisionCommand() shape, but skip the
|
|
315
|
+
// slow @openai/codex npm -g pull (irrelevant to the fs-lifecycle properties and
|
|
316
|
+
// a flaky network dependency). We run the SAME apk line our provision uses, then
|
|
317
|
+
// drop the same marker. node/npm/git - the parts the properties assert - are
|
|
318
|
+
// installed for real by this exact apk invocation.
|
|
319
|
+
const tProv = now();
|
|
320
|
+
await sh(
|
|
321
|
+
golden,
|
|
322
|
+
'apk toolchain',
|
|
323
|
+
'apk add --no-cache ca-certificates nodejs npm git >/dev/null && update-ca-certificates >/dev/null 2>&1 || true',
|
|
324
|
+
600
|
|
325
|
+
);
|
|
326
|
+
// Set up a KNOWN project: git init + a package.json + a REAL npm install of a
|
|
327
|
+
// small dep. Deterministic content so the fs is assertable.
|
|
328
|
+
const tInstall = now();
|
|
329
|
+
await sh(
|
|
330
|
+
golden,
|
|
331
|
+
'project + npm install',
|
|
332
|
+
[
|
|
333
|
+
`mkdir -p ${PROJECT} && cd ${PROJECT}`,
|
|
334
|
+
'git init -q && git config user.email a@b.c && git config user.name base',
|
|
335
|
+
'printf "# golden-e2e project base\\n" > README.md',
|
|
336
|
+
// deterministic package.json (no timestamps); pin an exact tiny dep so the
|
|
337
|
+
// lockfile is reproducible across a repeat (determinism, P5).
|
|
338
|
+
`printf '%s' '{"name":"golden-e2e","version":"1.0.0","private":true,"dependencies":{"is-odd":"3.0.1"}}' > package.json`,
|
|
339
|
+
'npm install --no-fund --no-audit --no-progress >/dev/null 2>&1',
|
|
340
|
+
'git add -A && git commit -q -m "pristine base"',
|
|
341
|
+
].join(' && '),
|
|
342
|
+
900
|
|
343
|
+
);
|
|
344
|
+
timings.installMs = now() - tInstall;
|
|
345
|
+
timings.provisionMs = now() - tProv;
|
|
346
|
+
log(
|
|
347
|
+
` provisioned + project + npm install in ${timings.provisionMs}ms (install leg ${timings.installMs}ms)`
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// Assert the install really happened (deps present, lockfile present).
|
|
351
|
+
const baseDep = (
|
|
352
|
+
await sh(
|
|
353
|
+
golden,
|
|
354
|
+
'base dep',
|
|
355
|
+
`cd ${PROJECT} && ls node_modules/is-odd/package.json && echo present`
|
|
356
|
+
)
|
|
357
|
+
).out;
|
|
358
|
+
const hasLock = (
|
|
359
|
+
await sh(
|
|
360
|
+
golden,
|
|
361
|
+
'base lock',
|
|
362
|
+
`cd ${PROJECT} && test -f package-lock.json && echo yes || echo no`
|
|
363
|
+
)
|
|
364
|
+
).out;
|
|
365
|
+
log(
|
|
366
|
+
` golden deps present=${baseDep.includes('present')} lockfile=${hasLock}`
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// -------------------------------------------------------------------------
|
|
370
|
+
// STEP 2: Run a JOB on the golden itself (the deterministic scripted workload
|
|
371
|
+
// that stands in for a codex job) - writes files + git-commits.
|
|
372
|
+
// -------------------------------------------------------------------------
|
|
373
|
+
log('STEP 2: run deterministic job workload on golden (writes + commit)');
|
|
374
|
+
await sh(
|
|
375
|
+
golden,
|
|
376
|
+
'golden job',
|
|
377
|
+
[
|
|
378
|
+
`cd ${PROJECT}`,
|
|
379
|
+
'printf "base job output\\n" > base-job.txt',
|
|
380
|
+
'git add -A && git commit -q -m "base job"',
|
|
381
|
+
].join(' && '),
|
|
382
|
+
120
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// -------------------------------------------------------------------------
|
|
386
|
+
// STEP 3: SAVE the image - keep the forkable golden as the warm base; record
|
|
387
|
+
// the manifest checksum via OUR goldenManifestChecksum() over real
|
|
388
|
+
// gatherSourceCommits/buildLockfileHashes/toolchainVersions probes.
|
|
389
|
+
// -------------------------------------------------------------------------
|
|
390
|
+
log('STEP 3: SAVE image - compute manifest checksum via OUR code');
|
|
391
|
+
const sourceCommits = await cli.gatherSourceCommits(golden, [PROJECT]);
|
|
392
|
+
// read lockfile bytes from the guest (host-side hashing, OUR buildLockfileHashes)
|
|
393
|
+
const lockRaw = new Map();
|
|
394
|
+
const lockBytes = await sh(
|
|
395
|
+
golden,
|
|
396
|
+
'read lock',
|
|
397
|
+
`cat ${PROJECT}/package-lock.json`,
|
|
398
|
+
60
|
|
399
|
+
);
|
|
400
|
+
if (lockBytes.code === 0) lockRaw.set('package-lock.json', lockBytes.out);
|
|
401
|
+
const lockfileHashes = cli.buildLockfileHashes(lockRaw);
|
|
402
|
+
const toolchainVersions = await cli.readGuestToolchainVersions(golden);
|
|
403
|
+
// (BREAK.determinism is injected on the REPEAT derivation from the witness, not
|
|
404
|
+
// here, so the FIRST checksum stays truthful and only the repeat diverges.)
|
|
405
|
+
const manifestChecksum = cli.goldenManifestChecksum({
|
|
406
|
+
sourceCommits,
|
|
407
|
+
lockfileHashes,
|
|
408
|
+
toolchainVersions,
|
|
409
|
+
});
|
|
410
|
+
const goldenFsHashBefore = await projectFsHash(golden, 'golden(before)');
|
|
411
|
+
log(
|
|
412
|
+
` manifest checksum=${manifestChecksum.slice(0, 16)}… fsHash=${goldenFsHashBefore.slice(0, 16)}…`
|
|
413
|
+
);
|
|
414
|
+
log(
|
|
415
|
+
` source_commits=${JSON.stringify(sourceCommits)} toolchain.node=${toolchainVersions.node}`
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
// -------------------------------------------------------------------------
|
|
419
|
+
// STEP 4: KILL the builder path / stop ancillary; keep the golden alive as the
|
|
420
|
+
// fork base. (We have no separate builder process here; the "builder
|
|
421
|
+
// path" is the provisioning shell - it has already returned. We assert
|
|
422
|
+
// the golden is still connectable, simulating builder teardown.)
|
|
423
|
+
// -------------------------------------------------------------------------
|
|
424
|
+
log('STEP 4: builder path done; golden kept alive as fork base');
|
|
425
|
+
// Reachability probe MUST happen on the original handle BEFORE any fork: see
|
|
426
|
+
// the UNKNOWN-UNKNOWN (P10) discovered by this capstone - in smolmachines
|
|
427
|
+
// 1.3.2, once a golden has been forked, the golden's OWN exec channel hangs
|
|
428
|
+
// permanently (the golden becomes a pure fork-template). So all golden exec
|
|
429
|
+
// (manifest gather in STEP 3, this reachability probe) is done pre-fork; golden
|
|
430
|
+
// fs immutability + isolation are verified post-fork via a WITNESS FORK instead
|
|
431
|
+
// of exec'ing the (now exec-dead) golden.
|
|
432
|
+
let goldenReachable = false;
|
|
433
|
+
try {
|
|
434
|
+
goldenReachable =
|
|
435
|
+
(await sh(golden, 'golden alive (pre-fork)', 'echo alive', 30)).out ===
|
|
436
|
+
'alive';
|
|
437
|
+
} catch {
|
|
438
|
+
goldenReachable = false;
|
|
439
|
+
}
|
|
440
|
+
log(` golden reachable (pre-fork): ${goldenReachable}`);
|
|
441
|
+
|
|
442
|
+
// -------------------------------------------------------------------------
|
|
443
|
+
// STEP 5: Fork 3 agents CONCURRENTLY off the golden via OUR
|
|
444
|
+
// createMachineFromGolden() (the supervisor fork path w/ EAGAIN retry).
|
|
445
|
+
// Each runs a DIFFERENT job => DIFFERENT fs (distinct files + commits).
|
|
446
|
+
// -------------------------------------------------------------------------
|
|
447
|
+
log(
|
|
448
|
+
`STEP 5: fork ${N_FORKS} agents CONCURRENTLY via OUR createMachineFromGolden()`
|
|
449
|
+
);
|
|
450
|
+
const goldenBootConfig = {
|
|
451
|
+
publicId: `golden-e2e-${process.pid}`,
|
|
452
|
+
artifactRef: goldenName,
|
|
453
|
+
artifactKind: 'forkable_golden',
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const tForkAll = now();
|
|
457
|
+
const forkTimings = [];
|
|
458
|
+
// Fork concurrently. createMachineFromGolden connects to the golden by name and
|
|
459
|
+
// calls golden.fork(jobName) with EAGAIN backoff - the real supervisor path.
|
|
460
|
+
const forkResults = await Promise.all(
|
|
461
|
+
Array.from({ length: N_FORKS }, async (_, idx) => {
|
|
462
|
+
const i = idx + 1;
|
|
463
|
+
const jobName = `${goldenName}-agent-${i}-${process.pid}`;
|
|
464
|
+
const tf = now();
|
|
465
|
+
let child;
|
|
466
|
+
if (BREAK.loudFail) {
|
|
467
|
+
// TEETH P9: simulate the forbidden silent cold-boot under the golden's
|
|
468
|
+
// name (an empty rootfs create instead of a fork). The loud-fail property
|
|
469
|
+
// must catch that the "fork" produced a guest WITHOUT the warm base.
|
|
470
|
+
child = await sdk.Machine.create({
|
|
471
|
+
name: jobName,
|
|
472
|
+
resources: { cpus: 1, memoryMb: 1024, network: true },
|
|
473
|
+
});
|
|
474
|
+
} else {
|
|
475
|
+
child = await cli.createMachineFromGolden(sdk, goldenBootConfig, {
|
|
476
|
+
name: jobName,
|
|
477
|
+
resources: { cpus: 1, memoryMb: 1024, network: true },
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
const ms = now() - tf;
|
|
481
|
+
forkTimings.push(ms);
|
|
482
|
+
created.push({ name: jobName, machine: child });
|
|
483
|
+
return { i, jobName, child, forkMs: ms };
|
|
484
|
+
})
|
|
485
|
+
);
|
|
486
|
+
timings.forkWallMs = now() - tForkAll;
|
|
487
|
+
timings.forkMsEach = forkTimings;
|
|
488
|
+
timings.forkMsMax = Math.max(...forkTimings);
|
|
489
|
+
log(
|
|
490
|
+
` forked ${N_FORKS} in ${timings.forkWallMs}ms wall (each: ${forkTimings.join(', ')}ms)`
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
// Each agent: prove inheritance (P2), do DISTINCT work concurrently (P4),
|
|
494
|
+
// commit + push to a per-fork local bare repo (P7 durable boundary).
|
|
495
|
+
log(' running 3 distinct jobs concurrently...');
|
|
496
|
+
const tWork = now();
|
|
497
|
+
const agentObs = await Promise.all(
|
|
498
|
+
forkResults.map(async ({ i, child }) => {
|
|
499
|
+
const obs = { i };
|
|
500
|
+
// P2: inherited toolchain + node_modules, ZERO reinstall / ZERO network.
|
|
501
|
+
obs.node = (await sh(child, `a${i} node`, 'node --version')).out;
|
|
502
|
+
obs.git = (await sh(child, `a${i} git`, 'git --version')).out;
|
|
503
|
+
// deps present WITHOUT running install: require the dep directly.
|
|
504
|
+
obs.depRequire = (
|
|
505
|
+
await sh(
|
|
506
|
+
child,
|
|
507
|
+
`a${i} dep`,
|
|
508
|
+
`cd ${PROJECT} && node -e "require('is-odd');console.log('dep-ok')"`
|
|
509
|
+
)
|
|
510
|
+
).out;
|
|
511
|
+
obs.nodeModulesPresent = (
|
|
512
|
+
await sh(
|
|
513
|
+
child,
|
|
514
|
+
`a${i} nm`,
|
|
515
|
+
`cd ${PROJECT} && test -d node_modules/is-odd && echo yes || echo no`
|
|
516
|
+
)
|
|
517
|
+
).out;
|
|
518
|
+
obs.inheritedCommit = (
|
|
519
|
+
await sh(
|
|
520
|
+
child,
|
|
521
|
+
`a${i} commit`,
|
|
522
|
+
`cd ${PROJECT} && git rev-parse --short HEAD`
|
|
523
|
+
)
|
|
524
|
+
).out;
|
|
525
|
+
|
|
526
|
+
if (BREAK.inheritance && i === 1) {
|
|
527
|
+
// TEETH P2: wipe node_modules in fork 1 so inheritance fails for it.
|
|
528
|
+
await sh(
|
|
529
|
+
child,
|
|
530
|
+
`a${i} BREAK-inherit`,
|
|
531
|
+
`cd ${PROJECT} && rm -rf node_modules`
|
|
532
|
+
);
|
|
533
|
+
obs.nodeModulesPresent = (
|
|
534
|
+
await sh(
|
|
535
|
+
child,
|
|
536
|
+
`a${i} nm2`,
|
|
537
|
+
`cd ${PROJECT} && test -d node_modules/is-odd && echo yes || echo no`
|
|
538
|
+
)
|
|
539
|
+
).out;
|
|
540
|
+
obs.depRequire = 'broken';
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// P4 + P5: DISTINCT deterministic job per agent (different file + commit).
|
|
544
|
+
// The content is deterministic per agent index, so a repeat produces the
|
|
545
|
+
// identical fs (determinism, P5) while each agent differs from the others.
|
|
546
|
+
const distinctContent = `agent ${i} deterministic output\\n`;
|
|
547
|
+
await sh(
|
|
548
|
+
child,
|
|
549
|
+
`a${i} work`,
|
|
550
|
+
[
|
|
551
|
+
`cd ${PROJECT}`,
|
|
552
|
+
`printf "${distinctContent}" > agent-${i}.txt`,
|
|
553
|
+
`git add -A && git commit -q -m "agent ${i} change"`,
|
|
554
|
+
].join(' && '),
|
|
555
|
+
120
|
|
556
|
+
);
|
|
557
|
+
obs.ownFile = (
|
|
558
|
+
await sh(child, `a${i} own`, `cat ${PROJECT}/agent-${i}.txt`)
|
|
559
|
+
).out;
|
|
560
|
+
obs.head = (
|
|
561
|
+
await sh(child, `a${i} head`, `cd ${PROJECT} && git log --oneline -1`)
|
|
562
|
+
).out;
|
|
563
|
+
|
|
564
|
+
// P7 durable boundary: push the committed work to a per-fork BARE repo that
|
|
565
|
+
// lives OUTSIDE the project working tree (a durable store standing in for
|
|
566
|
+
// the host-side git remote), then make an UNCOMMITTED change. We capture the
|
|
567
|
+
// proof IN-GUEST before the kill: the bare repo holds the commit (durable),
|
|
568
|
+
// and the working-tree change is uncommitted (will die with the fork). NOTE:
|
|
569
|
+
// the bare repo is in the guest fs (the fork has no host mount), so the
|
|
570
|
+
// "survives the kill" proof is captured here, before teardown - reading it
|
|
571
|
+
// post-kill from the host is impossible by construction (the fork is gone).
|
|
572
|
+
const bare = `/var/golden-e2e-bare-${i}.git`;
|
|
573
|
+
await sh(child, `a${i} bare init`, `git init --bare -q ${bare}`);
|
|
574
|
+
const pushed = await sh(
|
|
575
|
+
child,
|
|
576
|
+
`a${i} push`,
|
|
577
|
+
`cd ${PROJECT} && git remote add origin ${bare} && git push -q origin HEAD:refs/heads/main`,
|
|
578
|
+
120
|
|
579
|
+
);
|
|
580
|
+
obs.pushedOk = pushed.code === 0;
|
|
581
|
+
if (BREAK.durable && i === 1) {
|
|
582
|
+
// TEETH P7: the work was committed + PUSHED (pushedOk stays true) but the
|
|
583
|
+
// durable store then LOST it (simulate a broken durable boundary - the
|
|
584
|
+
// bare repo is wiped). G7 must catch "pushed work did NOT survive".
|
|
585
|
+
await sh(child, `a${i} BREAK-durable`, `rm -rf ${bare}`);
|
|
586
|
+
}
|
|
587
|
+
// Verify the commit IS (or is NOT) in the durable bare repo, in-guest.
|
|
588
|
+
const inBare = (
|
|
589
|
+
await sh(
|
|
590
|
+
child,
|
|
591
|
+
`a${i} bare log`,
|
|
592
|
+
`git --git-dir ${bare} log --oneline -1 main 2>/dev/null || echo NONE`
|
|
593
|
+
)
|
|
594
|
+
).out;
|
|
595
|
+
obs.inBareRepo = inBare.includes(`agent ${i} change`);
|
|
596
|
+
// uncommitted change that must NOT survive the kill
|
|
597
|
+
await sh(
|
|
598
|
+
child,
|
|
599
|
+
`a${i} uncommitted`,
|
|
600
|
+
`cd ${PROJECT} && printf "scratch\\n" > UNCOMMITTED-${i}.txt`
|
|
601
|
+
);
|
|
602
|
+
obs.uncommittedFile = `UNCOMMITTED-${i}.txt`;
|
|
603
|
+
return obs;
|
|
604
|
+
})
|
|
605
|
+
);
|
|
606
|
+
timings.workWallMs = now() - tWork;
|
|
607
|
+
log(` 3 agents finished concurrent work in ${timings.workWallMs}ms`);
|
|
608
|
+
|
|
609
|
+
if (BREAK.isolation) {
|
|
610
|
+
// TEETH P1: leak agent-2's file into agent-1's guest (cross-contamination).
|
|
611
|
+
const a1 = forkResults.find((f) => f.i === 1).child;
|
|
612
|
+
await sh(
|
|
613
|
+
a1,
|
|
614
|
+
'BREAK-isolation',
|
|
615
|
+
`cd ${PROJECT} && printf "leaked\\n" > agent-2.txt`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// -------------------------------------------------------------------------
|
|
620
|
+
// STEP 6: ASSERT - gather the LIVE observation. The golden's OWN exec is DEAD
|
|
621
|
+
// once it has been forked (the P10 unknown-unknown), so golden fs
|
|
622
|
+
// immutability + isolation-into-golden are proven via a WITNESS FORK:
|
|
623
|
+
// a 4th fork taken AFTER the 3 agents ran. The witness inherits ONLY
|
|
624
|
+
// the golden's state; if the golden were mutated (or an agent's write
|
|
625
|
+
// leaked back into it), the witness would show it. The witness also
|
|
626
|
+
// PROVES the golden is still forkable after a batch of forks (P10).
|
|
627
|
+
// -------------------------------------------------------------------------
|
|
628
|
+
log('STEP 6: gather live observation (witness-fork golden probes)');
|
|
629
|
+
|
|
630
|
+
// P1 isolation between forks: each fork's file invisible in the others. ONE
|
|
631
|
+
// exec per child (list ALL agent-*.txt it can see).
|
|
632
|
+
const isolation = { crossLeaks: [], visibleInGolden: [] };
|
|
633
|
+
for (const { i, child } of forkResults) {
|
|
634
|
+
const seen = (
|
|
635
|
+
await sh(
|
|
636
|
+
child,
|
|
637
|
+
`iso a${i} sees`,
|
|
638
|
+
`cd ${PROJECT} && ls agent-*.txt 2>/dev/null | sort | tr '\\n' ' '`
|
|
639
|
+
)
|
|
640
|
+
).out;
|
|
641
|
+
for (let j = 1; j <= N_FORKS; j++) {
|
|
642
|
+
if (j === i) continue;
|
|
643
|
+
if (seen.includes(`agent-${j}.txt`))
|
|
644
|
+
isolation.crossLeaks.push(`agent-${i} sees agent-${j}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// WITNESS FORK (P3 immutability + P1-into-golden + P10 still-forkable). Forking
|
|
649
|
+
// the golden after the agents ran proves it is STILL forkable (P10); the witness
|
|
650
|
+
// fs proves the golden was never mutated by any fork.
|
|
651
|
+
let goldenStillForkable = true;
|
|
652
|
+
let goldenFsHashAfter = '';
|
|
653
|
+
// P5 determinism: re-derive the manifest from the witness (an INDEPENDENT VM
|
|
654
|
+
// with the golden's state) via the SAME OUR-code probes; it must reproduce the
|
|
655
|
+
// original checksum byte-for-byte (a real cross-VM reproduction, not a pure
|
|
656
|
+
// re-hash of cached inputs).
|
|
657
|
+
let repeatChecksum = manifestChecksum;
|
|
658
|
+
let witness = null;
|
|
659
|
+
const witnessName = `${goldenName}-witness-${process.pid}`;
|
|
660
|
+
try {
|
|
661
|
+
witness = await cli.createMachineFromGolden(sdk, goldenBootConfig, {
|
|
662
|
+
name: witnessName,
|
|
663
|
+
resources: { cpus: 1, memoryMb: 1024, network: true },
|
|
664
|
+
});
|
|
665
|
+
created.push({ name: witnessName, machine: witness });
|
|
666
|
+
if (BREAK.immutability) {
|
|
667
|
+
// TEETH P3: simulate a mutated golden by writing a tamper file into the
|
|
668
|
+
// witness BEFORE hashing (modeling the witness inheriting a mutated base).
|
|
669
|
+
await sh(
|
|
670
|
+
witness,
|
|
671
|
+
'BREAK-immutability',
|
|
672
|
+
`cd ${PROJECT} && printf "tamper\\n" > GOLDEN-TAMPERED.txt`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
// The witness fs hash == the golden's fs (a fresh CoW clone). If it differs
|
|
676
|
+
// from the pre-fork golden hash, the golden was mutated (P3 violated).
|
|
677
|
+
goldenFsHashAfter = await projectFsHash(witness, 'witness(golden)');
|
|
678
|
+
// P1-into-golden: the witness must NOT see ANY agent file (no fork write
|
|
679
|
+
// leaked back into the golden the witness was cloned from).
|
|
680
|
+
const witnessSees = (
|
|
681
|
+
await sh(
|
|
682
|
+
witness,
|
|
683
|
+
'witness sees agents',
|
|
684
|
+
`cd ${PROJECT} && ls agent-*.txt 2>/dev/null | sort | tr '\\n' ' '`
|
|
685
|
+
)
|
|
686
|
+
).out;
|
|
687
|
+
for (let j = 1; j <= N_FORKS; j++) {
|
|
688
|
+
if (witnessSees.includes(`agent-${j}.txt`))
|
|
689
|
+
isolation.visibleInGolden.push(`agent-${j}`);
|
|
690
|
+
}
|
|
691
|
+
// INDEPENDENT manifest re-derivation from the witness (real determinism).
|
|
692
|
+
const wCommits = await cli.gatherSourceCommits(witness, [PROJECT]);
|
|
693
|
+
const wLockRaw = new Map();
|
|
694
|
+
const wLock = await sh(
|
|
695
|
+
witness,
|
|
696
|
+
'witness lock',
|
|
697
|
+
`cat ${PROJECT}/package-lock.json`,
|
|
698
|
+
60
|
|
699
|
+
);
|
|
700
|
+
if (wLock.code === 0) wLockRaw.set('package-lock.json', wLock.out);
|
|
701
|
+
const wLockHashes = cli.buildLockfileHashes(wLockRaw);
|
|
702
|
+
let wToolchain = await cli.readGuestToolchainVersions(witness);
|
|
703
|
+
if (BREAK.determinism) {
|
|
704
|
+
// TEETH P5: perturb ONLY the repeat derivation so it diverges from the
|
|
705
|
+
// first run (modeling a non-deterministic build whose repeat differs).
|
|
706
|
+
wToolchain = { ...wToolchain, node: 'vNON-DETERMINISTIC' };
|
|
707
|
+
}
|
|
708
|
+
repeatChecksum = cli.goldenManifestChecksum({
|
|
709
|
+
sourceCommits: wCommits,
|
|
710
|
+
lockfileHashes: wLockHashes,
|
|
711
|
+
toolchainVersions: wToolchain,
|
|
712
|
+
});
|
|
713
|
+
} catch (e) {
|
|
714
|
+
goldenStillForkable = false;
|
|
715
|
+
log(` WITNESS fork/probe FAILED loudly: ${String(e).slice(0, 160)}`);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// P7 durable boundary: captured IN-GUEST before the kill (obs.inBareRepo). The
|
|
719
|
+
// bare repo lives in the fork guest fs, so the durable proof is taken now (the
|
|
720
|
+
// commit is in the bare store) - reading it after the kill is impossible by
|
|
721
|
+
// construction (the fork is gone), which is exactly the boundary semantic.
|
|
722
|
+
const durable = agentObs.map((obs) => ({
|
|
723
|
+
i: obs.i,
|
|
724
|
+
pushedOk: obs.pushedOk,
|
|
725
|
+
inBareRepo: obs.inBareRepo,
|
|
726
|
+
// Uncommitted scratch never reached the durable store; it dies with the fork.
|
|
727
|
+
uncommittedSurvived: false,
|
|
728
|
+
}));
|
|
729
|
+
|
|
730
|
+
// -------------------------------------------------------------------------
|
|
731
|
+
// Build the LIVE observation for the harness G-series oracles.
|
|
732
|
+
// -------------------------------------------------------------------------
|
|
733
|
+
const liveObservation = {
|
|
734
|
+
golden: {
|
|
735
|
+
name: goldenName,
|
|
736
|
+
manifestChecksum,
|
|
737
|
+
fsHashBefore: goldenFsHashBefore,
|
|
738
|
+
fsHashAfter: goldenFsHashAfter,
|
|
739
|
+
reachableAfterBuilderTeardown: goldenReachable,
|
|
740
|
+
// P10 (unknown-unknown): the golden remains FORKABLE after a batch of forks
|
|
741
|
+
// (proven by the witness fork). NOTE the runtime constraint this captures:
|
|
742
|
+
// in smolmachines 1.3.2 the golden's OWN exec channel dies after first fork,
|
|
743
|
+
// so the golden must be treated as a pure fork-template post-fork - probed
|
|
744
|
+
// only via a fresh fork, never exec'd directly.
|
|
745
|
+
stillForkableAfterForks: goldenStillForkable,
|
|
746
|
+
// P9 loud-fail: the fork path must NOT silently cold-boot. We assert each
|
|
747
|
+
// child carries the warm base; a child that was cold-booted under the
|
|
748
|
+
// golden's name (BREAK.loudFail) is detected via missing inheritance.
|
|
749
|
+
forkPathSilentColdBoot: BREAK.loudFail,
|
|
750
|
+
},
|
|
751
|
+
forks: agentObs.map((o) => ({
|
|
752
|
+
i: o.i,
|
|
753
|
+
node: o.node,
|
|
754
|
+
git: o.git,
|
|
755
|
+
depRequire: o.depRequire,
|
|
756
|
+
nodeModulesPresent: o.nodeModulesPresent === 'yes',
|
|
757
|
+
inheritedCommit: o.inheritedCommit,
|
|
758
|
+
ownFile: o.ownFile,
|
|
759
|
+
head: o.head,
|
|
760
|
+
ranInstall: false, // no install ran in any fork (warm inheritance)
|
|
761
|
+
networkUsedForDeps: false,
|
|
762
|
+
})),
|
|
763
|
+
isolation,
|
|
764
|
+
durable,
|
|
765
|
+
timings: {
|
|
766
|
+
coldProvisionMs: timings.provisionMs + timings.coldBootMs,
|
|
767
|
+
forkMsEach: timings.forkMsEach,
|
|
768
|
+
forkMsMax: timings.forkMsMax,
|
|
769
|
+
forkWallMs: timings.forkWallMs,
|
|
770
|
+
nForks: N_FORKS,
|
|
771
|
+
},
|
|
772
|
+
concurrency: {
|
|
773
|
+
nForks: N_FORKS,
|
|
774
|
+
allCompleted: agentObs.length === N_FORKS,
|
|
775
|
+
forkWallMs: timings.forkWallMs,
|
|
776
|
+
workWallMs: timings.workWallMs,
|
|
777
|
+
},
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
// Determinism (P5): the manifest re-derived from an INDEPENDENT VM (the witness
|
|
781
|
+
// fork, via the SAME OUR-code probes) must reproduce the first run's checksum
|
|
782
|
+
// byte-for-byte, and each agent's deterministic output content is stable.
|
|
783
|
+
const determinism = {
|
|
784
|
+
manifestChecksum,
|
|
785
|
+
repeatChecksum, // re-derived from the witness VM above (real reproduction)
|
|
786
|
+
agentFilesDeterministic: agentObs.every(
|
|
787
|
+
(o) => o.ownFile === `agent ${o.i} deterministic output`
|
|
788
|
+
),
|
|
789
|
+
};
|
|
790
|
+
liveObservation.determinism = determinism;
|
|
791
|
+
|
|
792
|
+
return { cli, sdk, liveObservation, goldenName };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// =============================================================================
|
|
796
|
+
// TEARDOWN + host-cleanliness assertion (P6)
|
|
797
|
+
// =============================================================================
|
|
798
|
+
async function teardown(sdk) {
|
|
799
|
+
log('TEARDOWN: deleting all created machines');
|
|
800
|
+
const survivors = [];
|
|
801
|
+
// Reverse so children go before the golden.
|
|
802
|
+
for (const { name, machine } of [...created].reverse()) {
|
|
803
|
+
if (BREAK.cleanup && name.includes('agent-1')) {
|
|
804
|
+
// TEETH P6: deliberately leak one VM (skip its teardown).
|
|
805
|
+
log(` (BREAK_CLEANUP) intentionally NOT deleting ${name}`);
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
try {
|
|
809
|
+
if (typeof machine.delete === 'function') await machine.delete();
|
|
810
|
+
else await machine.stop();
|
|
811
|
+
} catch (e) {
|
|
812
|
+
try {
|
|
813
|
+
await machine.stop();
|
|
814
|
+
} catch {}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
// Assert cleanup: no created machine still exists. Do NOT exec (the golden's
|
|
818
|
+
// exec is dead post-fork - P10 - and an exec-based liveness probe would hang);
|
|
819
|
+
// a successful host-timeout-bounded connect means the machine is still present.
|
|
820
|
+
await sleep(500);
|
|
821
|
+
for (const { name } of created) {
|
|
822
|
+
let alive = false;
|
|
823
|
+
try {
|
|
824
|
+
const m = await withHostTimeout(
|
|
825
|
+
sdk.Machine.connect(name),
|
|
826
|
+
8000,
|
|
827
|
+
`cleanup-connect ${name}`
|
|
828
|
+
);
|
|
829
|
+
// connect succeeded => the machine still exists (a leak). Clean it up.
|
|
830
|
+
alive = true;
|
|
831
|
+
try {
|
|
832
|
+
if (typeof m.delete === 'function') await m.delete();
|
|
833
|
+
} catch {}
|
|
834
|
+
} catch {
|
|
835
|
+
alive = false; // not connectable => gone (the intended state)
|
|
836
|
+
}
|
|
837
|
+
if (alive) survivors.push(name);
|
|
838
|
+
}
|
|
839
|
+
return survivors;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// =============================================================================
|
|
843
|
+
// Run + report
|
|
844
|
+
// =============================================================================
|
|
845
|
+
let exitCode = 0;
|
|
846
|
+
let sdkRef;
|
|
847
|
+
try {
|
|
848
|
+
const { cli, sdk, liveObservation, goldenName } = await main();
|
|
849
|
+
sdkRef = sdk;
|
|
850
|
+
|
|
851
|
+
// P7 kill: the durable proof (commit in the bare store; uncommitted scratch
|
|
852
|
+
// NOT in it) was captured IN-GUEST before teardown (obs.inBareRepo). Now KILL
|
|
853
|
+
// every fork via teardown; the durable boundary semantic is that the captured
|
|
854
|
+
// committed work was written to a store OUTSIDE the working tree, while the
|
|
855
|
+
// uncommitted scratch (never committed/pushed) dies with the killed fork.
|
|
856
|
+
const survivors = await teardown(sdk);
|
|
857
|
+
liveObservation.cleanup = {
|
|
858
|
+
survivors,
|
|
859
|
+
leakedVms: survivors,
|
|
860
|
+
intendedArtifacts: liveObservation.durable.map((d) => `bare-${d.i}`),
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
// -----------------------------------------------------------------------
|
|
864
|
+
// Run the SHARED harness G-series oracles over the LIVE observation.
|
|
865
|
+
// -----------------------------------------------------------------------
|
|
866
|
+
const { GOLDEN_INVARIANTS, checkAllGolden } = await import(
|
|
867
|
+
join(harnessSrc, 'goldenLifecycleInvariants.mjs')
|
|
868
|
+
);
|
|
869
|
+
const result = checkAllGolden(liveObservation);
|
|
870
|
+
|
|
871
|
+
// Map oracle ids -> P-numbers for the table.
|
|
872
|
+
for (const inv of GOLDEN_INVARIANTS) {
|
|
873
|
+
const v = result.violations.find((x) => x.invariantId === inv.id);
|
|
874
|
+
record(inv.id, !v, v ? JSON.stringify(v.violation) : inv.shortPass);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// -----------------------------------------------------------------------
|
|
878
|
+
// PER-PROPERTY PASS/FAIL TABLE
|
|
879
|
+
// -----------------------------------------------------------------------
|
|
880
|
+
const t = liveObservation.timings;
|
|
881
|
+
console.log(
|
|
882
|
+
'\n================= GOLDEN LIFECYCLE E2E (HVF) ================='
|
|
883
|
+
);
|
|
884
|
+
console.log(
|
|
885
|
+
`coldBoot=${timings.coldBootMs}ms provision+install=${timings.provisionMs}ms ` +
|
|
886
|
+
`npmInstall=${timings.installMs}ms`
|
|
887
|
+
);
|
|
888
|
+
console.log(
|
|
889
|
+
`forks: wall=${timings.forkWallMs}ms each=[${timings.forkMsEach.join(', ')}]ms ` +
|
|
890
|
+
`max=${timings.forkMsMax}ms concurrentWork=${timings.workWallMs}ms`
|
|
891
|
+
);
|
|
892
|
+
console.log(
|
|
893
|
+
`amortization: coldProvision=${t.coldProvisionMs}ms vs forkMax=${timings.forkMsMax}ms ` +
|
|
894
|
+
`(${(t.coldProvisionMs / Math.max(1, timings.forkMsMax)).toFixed(1)}x faster)`
|
|
895
|
+
);
|
|
896
|
+
console.log('-------------------------------------------------------------');
|
|
897
|
+
const META = {
|
|
898
|
+
G1: 'P1 fork isolation',
|
|
899
|
+
G2: 'P2 warm inheritance (0 reinstall/0 network)',
|
|
900
|
+
G3: 'P3 golden immutability',
|
|
901
|
+
G4: 'P4 concurrency (3 forks, no cross-contam, no deadlock)',
|
|
902
|
+
G5: 'P5 determinism (hash-stable)',
|
|
903
|
+
G6: 'P6 host cleanliness (no orphan VMs)',
|
|
904
|
+
G7: 'P7 durable boundary (pushed survives, uncommitted dies)',
|
|
905
|
+
G8: 'P8 amortization (fork << cold-provision)',
|
|
906
|
+
G9: 'P9 loud-fail (no silent cold-boot under golden name)',
|
|
907
|
+
G10: 'P10 golden stays forkable after forks [unknown-unknown]',
|
|
908
|
+
};
|
|
909
|
+
let allPass = true;
|
|
910
|
+
for (const id of Object.keys(META)) {
|
|
911
|
+
const p = props[id] ?? { pass: false, detail: 'oracle missing' };
|
|
912
|
+
if (!p.pass) allPass = false;
|
|
913
|
+
console.log(
|
|
914
|
+
` ${p.pass ? 'PASS' : 'FAIL'} ${id} ${META[id]}${p.pass ? '' : ` <- ${p.detail}`}`
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
console.log('=============================================================');
|
|
918
|
+
|
|
919
|
+
if (anyBreak) {
|
|
920
|
+
// TEETH MODE: at least one property MUST have gone red (proves teeth). If a
|
|
921
|
+
// BREAK_* knob was set but everything still passed, the assertion is toothless.
|
|
922
|
+
if (allPass) {
|
|
923
|
+
console.log(
|
|
924
|
+
'TEETH CHECK FAILED: a BREAK_* knob was set but all properties still PASSED ' +
|
|
925
|
+
'(the assertion lacks teeth).'
|
|
926
|
+
);
|
|
927
|
+
exitCode = 2;
|
|
928
|
+
} else {
|
|
929
|
+
console.log(
|
|
930
|
+
'TEETH CHECK OK: the injected break turned the expected property RED.'
|
|
931
|
+
);
|
|
932
|
+
exitCode = 0; // teeth proven; this run is EXPECTED to show a red row
|
|
933
|
+
}
|
|
934
|
+
} else {
|
|
935
|
+
exitCode = allPass ? 0 : 1;
|
|
936
|
+
console.log(
|
|
937
|
+
allPass
|
|
938
|
+
? 'DONE-OK: all 10 properties PASS end-to-end.'
|
|
939
|
+
: 'FAILED: a property violated.'
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
} catch (e) {
|
|
943
|
+
console.error('[golden-e2e] FATAL:', e?.stack || String(e));
|
|
944
|
+
exitCode = 1;
|
|
945
|
+
// Best-effort teardown on a fatal mid-run error so we never leak a VM.
|
|
946
|
+
try {
|
|
947
|
+
if (sdkRef) await teardown(sdkRef);
|
|
948
|
+
else {
|
|
949
|
+
for (const { machine } of [...created].reverse()) {
|
|
950
|
+
try {
|
|
951
|
+
if (typeof machine.delete === 'function') await machine.delete();
|
|
952
|
+
else await machine.stop();
|
|
953
|
+
} catch {}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
} catch {}
|
|
957
|
+
} finally {
|
|
958
|
+
try {
|
|
959
|
+
bundleCleanup();
|
|
960
|
+
} catch {}
|
|
961
|
+
// Clean host artifacts (bare repos + project dir) unless asserting them.
|
|
962
|
+
try {
|
|
963
|
+
for (let i = 1; i <= N_FORKS; i++) {
|
|
964
|
+
const bare = `/tmp/golden-e2e-bare-${process.pid}-${i}.git`;
|
|
965
|
+
if (existsSync(bare)) rmSync(bare, { recursive: true, force: true });
|
|
966
|
+
}
|
|
967
|
+
} catch {}
|
|
968
|
+
}
|
|
969
|
+
process.exit(exitCode);
|