@linzumi/cli 1.0.275 → 1.0.277
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 +10 -9
- package/dist/cloud-supervisor.mjs +295 -0
- package/dist/hosted-coding-account-worker.js +25 -0
- package/dist/index.js +2084 -3
- package/dist/mcp-server.mjs +174 -168
- package/dist/{impl-res/native → native}/darwin/manifest.json +5 -2
- package/dist/remote-codex-harness-worker.js +757 -0
- package/dist/tcp-tunnel-cli.mjs +25 -69
- package/dist/topics-cli.mjs +137 -13
- package/package.json +10 -17
- package/scripts/build.mjs +33 -356
- package/scripts/bump-cli-version.mjs +35 -36
- package/scripts/check-package-size-budget.mjs +13 -61
- package/scripts/vm-provision-proof.mjs +1 -2
- package/dist/impl-res/cloud-supervisor.mjs +0 -281
- package/dist/impl-res/index.js +0 -1546
- package/dist/impl-res/mcp-server.mjs +0 -461
- package/dist/impl-ts/assets/linzumi-logo.svg +0 -1
- package/dist/impl-ts/cloud-supervisor.mjs +0 -281
- package/dist/impl-ts/crossHarnessFailover.mjs +0 -14
- package/dist/impl-ts/index.js +0 -2617
- package/dist/impl-ts/mcp-server.mjs +0 -455
- package/dist/impl-ts/native/darwin/manifest.json +0 -21
- package/dist/impl-ts/native/darwin/preview-process-identity +0 -0
- package/dist/impl-ts/tcp-tunnel-cli.mjs +0 -93
- package/scripts/build-differential-entry-impl-res.mjs +0 -64
- package/scripts/build-differential-entry.mjs +0 -180
- package/scripts/build-onboarding-demo-evidence.mjs +0 -186
- package/scripts/build-tui-demo-evidence.mjs +0 -107
- package/scripts/diagnose-claude-probe.ts +0 -198
- package/scripts/differentialMutations.mjs +0 -114
- package/scripts/f5_live_cross_harness_failover_e2e.mjs +0 -239
- package/scripts/golden-chaos-harness.mjs +0 -1420
- package/scripts/golden-lifecycle-e2e.mjs +0 -969
- package/scripts/normalize-agent-replay-fixture.ts +0 -281
- package/scripts/promote-gstack-claude-smoke-fixture.ts +0 -891
- package/scripts/record-agent-replay-fixtures.ts +0 -610
- package/scripts/replay-agent-backend-browser.ts +0 -198
- package/scripts/replay-agent-backend.ts +0 -1961
- package/scripts/replay-agent-fixture.ts +0 -326
- package/scripts/typecheck.mjs +0 -42
- package/scripts/vm-smoke.mjs +0 -241
- /package/dist/{impl-res/assets → assets}/linzumi-logo.svg +0 -0
- /package/dist/{impl-res/crossHarnessFailover.mjs → crossHarnessFailover.mjs} +0 -0
- /package/dist/{impl-res/native → native}/darwin/preview-process-identity +0 -0
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
- Date: 2026-06-29
|
|
3
|
-
Spec: plans/2026-06-29-claude-code-raw-replay-harness-plan.md
|
|
4
|
-
Relationship: Local replay entrypoint for raw agent fixtures. It loads one or
|
|
5
|
-
more NDJSON fixtures, validates coverage, and writes static playground and
|
|
6
|
-
JSON summary artifacts without starting live providers.
|
|
7
|
-
*/
|
|
8
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
9
|
-
import { basename, join, resolve } from 'node:path';
|
|
10
|
-
import {
|
|
11
|
-
commanderReplayCoverageRequirements,
|
|
12
|
-
commanderReplayRealCoverageRequirements,
|
|
13
|
-
extractAgentReplayCoverage,
|
|
14
|
-
missingAgentReplayCoverage,
|
|
15
|
-
} from '../src/agentReplayCoverage';
|
|
16
|
-
import {
|
|
17
|
-
loadAgentReplayFixture,
|
|
18
|
-
type AgentReplayFixture,
|
|
19
|
-
} from '../src/agentReplayFixture';
|
|
20
|
-
import { renderAgentReplayPlaygroundHtml } from '../src/agentReplayPlayground';
|
|
21
|
-
import { collectAgentReplayDiagnostics } from '../src/agentReplayDiagnostics';
|
|
22
|
-
import {
|
|
23
|
-
evaluateAgentReplayFeatureReadiness,
|
|
24
|
-
loadAgentReplayManifestForFixture,
|
|
25
|
-
summarizeAgentReplayManifest,
|
|
26
|
-
type AgentReplayFeatureReadiness,
|
|
27
|
-
type AgentReplayManifestSummary,
|
|
28
|
-
} from '../src/agentReplayManifest';
|
|
29
|
-
|
|
30
|
-
type ReplayCliOptions = {
|
|
31
|
-
readonly fixtures: readonly string[];
|
|
32
|
-
readonly outDir: string;
|
|
33
|
-
readonly coverageMode: 'none' | 'real' | 'full';
|
|
34
|
-
readonly html: boolean;
|
|
35
|
-
readonly summary: boolean;
|
|
36
|
-
readonly requiredFeatureId: string | undefined;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
async function main(): Promise<void> {
|
|
40
|
-
const args = process.argv.slice(2);
|
|
41
|
-
|
|
42
|
-
if (args.includes('--help')) {
|
|
43
|
-
printHelp();
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const options = parseArgs(args);
|
|
48
|
-
const fixtures = await Promise.all(options.fixtures.map(loadFixture));
|
|
49
|
-
const manifestSummaries = await loadManifestSummaries(options.fixtures);
|
|
50
|
-
const featureReadiness = await loadManifestReadiness(options.fixtures);
|
|
51
|
-
await mkdir(options.outDir, { recursive: true });
|
|
52
|
-
|
|
53
|
-
if (options.html) {
|
|
54
|
-
for (const fixture of fixtures) {
|
|
55
|
-
const name = fixtureOutputName(fixture.path);
|
|
56
|
-
await writeFile(
|
|
57
|
-
join(options.outDir, `${name}.html`),
|
|
58
|
-
renderAgentReplayPlaygroundHtml(fixture),
|
|
59
|
-
'utf8'
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const coverage = extractAgentReplayCoverage(fixtures);
|
|
65
|
-
const diagnostics = combineDiagnostics(
|
|
66
|
-
fixtures.map(collectAgentReplayDiagnostics)
|
|
67
|
-
);
|
|
68
|
-
const requirements =
|
|
69
|
-
options.coverageMode === 'real'
|
|
70
|
-
? commanderReplayRealCoverageRequirements
|
|
71
|
-
: commanderReplayCoverageRequirements;
|
|
72
|
-
const gaps =
|
|
73
|
-
options.coverageMode === 'none'
|
|
74
|
-
? []
|
|
75
|
-
: missingAgentReplayCoverage(coverage, requirements);
|
|
76
|
-
const requiredFeatureGate = evaluateRequiredFeatureGate(
|
|
77
|
-
options.requiredFeatureId,
|
|
78
|
-
featureReadiness
|
|
79
|
-
);
|
|
80
|
-
const summary = {
|
|
81
|
-
fixtures: fixtures.map((fixture) => ({
|
|
82
|
-
path: fixture.path,
|
|
83
|
-
records: fixture.records.length,
|
|
84
|
-
})),
|
|
85
|
-
coverageMode: options.coverageMode,
|
|
86
|
-
coverage: coverageSummary(coverage),
|
|
87
|
-
featureCoverage: manifestSummaries,
|
|
88
|
-
featureReadiness,
|
|
89
|
-
diagnostics,
|
|
90
|
-
gaps,
|
|
91
|
-
requiredFeature: requiredFeatureGate,
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
if (options.summary) {
|
|
95
|
-
await writeFile(
|
|
96
|
-
join(options.outDir, 'summary.json'),
|
|
97
|
-
`${JSON.stringify(summary, null, 2)}\n`,
|
|
98
|
-
'utf8'
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
gaps.length > 0 ||
|
|
106
|
-
diagnostics.droppedEvents.length > 0 ||
|
|
107
|
-
requiredFeatureGate.status === 'failed'
|
|
108
|
-
) {
|
|
109
|
-
process.exitCode = 1;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function combineDiagnostics(
|
|
114
|
-
diagnostics: readonly ReturnType<typeof collectAgentReplayDiagnostics>[]
|
|
115
|
-
): ReturnType<typeof collectAgentReplayDiagnostics> {
|
|
116
|
-
return {
|
|
117
|
-
droppedEvents: diagnostics.flatMap((entry) => entry.droppedEvents),
|
|
118
|
-
debugOnlyEvents: diagnostics.flatMap((entry) => entry.debugOnlyEvents),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function parseArgs(args: readonly string[]): ReplayCliOptions {
|
|
123
|
-
const fixtures = valuesFor(args, '--fixture').map((path) => resolve(path));
|
|
124
|
-
|
|
125
|
-
if (fixtures.length === 0) {
|
|
126
|
-
throw new Error('provide at least one --fixture <path>');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
fixtures,
|
|
131
|
-
outDir: resolve(argValue(args, '--out-dir') ?? 'agent-replay-output'),
|
|
132
|
-
coverageMode: coverageModeValue(argValue(args, '--coverage') ?? 'full'),
|
|
133
|
-
html: !args.includes('--no-html'),
|
|
134
|
-
summary: !args.includes('--no-summary'),
|
|
135
|
-
requiredFeatureId: argValue(args, '--require-feature'),
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function coverageModeValue(value: string): ReplayCliOptions['coverageMode'] {
|
|
140
|
-
switch (value) {
|
|
141
|
-
case 'none':
|
|
142
|
-
case 'real':
|
|
143
|
-
case 'full':
|
|
144
|
-
return value;
|
|
145
|
-
default:
|
|
146
|
-
throw new Error(`unsupported --coverage value: ${value}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function loadManifestSummaries(
|
|
151
|
-
fixturePaths: readonly string[]
|
|
152
|
-
): Promise<readonly AgentReplayManifestSummary[]> {
|
|
153
|
-
const manifests: AgentReplayManifestSummary[] = [];
|
|
154
|
-
|
|
155
|
-
for (const fixturePath of fixturePaths) {
|
|
156
|
-
const loaded = await loadAgentReplayManifestForFixture(fixturePath);
|
|
157
|
-
|
|
158
|
-
if (loaded === undefined) {
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (loaded.type === 'error') {
|
|
163
|
-
throw new Error(JSON.stringify(loaded.error));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
manifests.push(summarizeAgentReplayManifest(loaded.manifest));
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return manifests;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function loadManifestReadiness(
|
|
173
|
-
fixturePaths: readonly string[]
|
|
174
|
-
): Promise<readonly AgentReplayFeatureReadiness[]> {
|
|
175
|
-
const readiness: AgentReplayFeatureReadiness[] = [];
|
|
176
|
-
|
|
177
|
-
for (const fixturePath of fixturePaths) {
|
|
178
|
-
const loaded = await loadAgentReplayManifestForFixture(fixturePath);
|
|
179
|
-
|
|
180
|
-
if (loaded === undefined) {
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (loaded.type === 'error') {
|
|
185
|
-
throw new Error(JSON.stringify(loaded.error));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
readiness.push(...evaluateAgentReplayFeatureReadiness(loaded.manifest));
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return readiness;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
type RequiredFeatureGate =
|
|
195
|
-
| { readonly status: 'not-required' }
|
|
196
|
-
| {
|
|
197
|
-
readonly status: 'passed';
|
|
198
|
-
readonly featureId: string;
|
|
199
|
-
readonly readiness: AgentReplayFeatureReadiness;
|
|
200
|
-
}
|
|
201
|
-
| {
|
|
202
|
-
readonly status: 'failed';
|
|
203
|
-
readonly featureId: string;
|
|
204
|
-
readonly reason: string;
|
|
205
|
-
readonly readiness?: AgentReplayFeatureReadiness | undefined;
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
function evaluateRequiredFeatureGate(
|
|
209
|
-
featureId: string | undefined,
|
|
210
|
-
readiness: readonly AgentReplayFeatureReadiness[]
|
|
211
|
-
): RequiredFeatureGate {
|
|
212
|
-
if (featureId === undefined) {
|
|
213
|
-
return { status: 'not-required' };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (readiness.length === 0) {
|
|
217
|
-
return {
|
|
218
|
-
status: 'failed',
|
|
219
|
-
featureId,
|
|
220
|
-
reason: '--require-feature needs a neighboring fixture manifest',
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const feature = readiness.find((row) => row.featureId === featureId);
|
|
225
|
-
|
|
226
|
-
if (feature === undefined) {
|
|
227
|
-
return {
|
|
228
|
-
status: 'failed',
|
|
229
|
-
featureId,
|
|
230
|
-
reason: `feature ${featureId} is not present in the fixture manifests`,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
switch (feature.status) {
|
|
235
|
-
case 'implemented':
|
|
236
|
-
case 'ready-to-implement':
|
|
237
|
-
return { status: 'passed', featureId, readiness: feature };
|
|
238
|
-
case 'blocked':
|
|
239
|
-
case 'missing-proof':
|
|
240
|
-
case 'not-started':
|
|
241
|
-
return {
|
|
242
|
-
status: 'failed',
|
|
243
|
-
featureId,
|
|
244
|
-
readiness: feature,
|
|
245
|
-
reason: feature.reason,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async function loadFixture(path: string): Promise<AgentReplayFixture> {
|
|
251
|
-
const loaded = await loadAgentReplayFixture(path);
|
|
252
|
-
|
|
253
|
-
if (loaded.type === 'error') {
|
|
254
|
-
throw new Error(JSON.stringify(loaded.error));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return loaded.fixture;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function valuesFor(args: readonly string[], name: string): readonly string[] {
|
|
261
|
-
const values: string[] = [];
|
|
262
|
-
|
|
263
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
264
|
-
if (args[index] === name) {
|
|
265
|
-
const value = args[index + 1];
|
|
266
|
-
|
|
267
|
-
if (value === undefined) {
|
|
268
|
-
throw new Error(`${name} requires a value`);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
values.push(value);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return values;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function argValue(args: readonly string[], name: string): string | undefined {
|
|
279
|
-
const index = args.indexOf(name);
|
|
280
|
-
|
|
281
|
-
if (index === -1) {
|
|
282
|
-
return undefined;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const value = args[index + 1];
|
|
286
|
-
|
|
287
|
-
if (value === undefined) {
|
|
288
|
-
throw new Error(`${name} requires a value`);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return value;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function fixtureOutputName(path: string): string {
|
|
295
|
-
return basename(path).replace(/\.ndjson$/u, '');
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function coverageSummary(
|
|
299
|
-
coverage: ReturnType<typeof extractAgentReplayCoverage>
|
|
300
|
-
): Record<string, readonly string[]> {
|
|
301
|
-
return {
|
|
302
|
-
codexNotificationMethods: sorted(coverage.codexNotificationMethods),
|
|
303
|
-
codexRequestMethods: sorted(coverage.codexRequestMethods),
|
|
304
|
-
codexThreadItemTypes: sorted(coverage.codexThreadItemTypes),
|
|
305
|
-
claudeSdkMessageTags: sorted(coverage.claudeSdkMessageTags),
|
|
306
|
-
claudeContentBlockTypes: sorted(coverage.claudeContentBlockTypes),
|
|
307
|
-
claudeToolNames: sorted(coverage.claudeToolNames),
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function sorted(values: ReadonlySet<string>): readonly string[] {
|
|
312
|
-
return Array.from(values).sort();
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function printHelp(): void {
|
|
316
|
-
process.stdout.write(
|
|
317
|
-
`Usage: pnpm run replay:agent-fixture -- --fixture <path> [--fixture <path>...] [options]\n\nOptions:\n --out-dir <path> Artifact directory. Default: agent-replay-output\n --coverage none|real|full Coverage gate. Default: full\n --no-html Do not write per-fixture playground HTML\n --no-summary Do not write summary.json\n\nExamples:\n pnpm --filter @linzumi/cli run replay:agent-fixture -- \\\n --fixture test/fixtures/agent-raw-replay/real-probe/codex.ndjson \\\n --fixture test/fixtures/agent-raw-replay/real-probe/claude.ndjson \\\n --coverage real \\\n --out-dir /tmp/agent-replay-real\n`
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
main().catch((error) => {
|
|
322
|
-
process.stderr.write(
|
|
323
|
-
`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`
|
|
324
|
-
);
|
|
325
|
-
process.exitCode = 1;
|
|
326
|
-
});
|
package/scripts/typecheck.mjs
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// Strict typecheck gate for @linzumi/cli: ZERO errors AND ZERO warnings.
|
|
2
|
-
//
|
|
3
|
-
// `tsc --noEmit` exits non-zero on errors, but some diagnostics (e.g.
|
|
4
|
-
// deprecated-option notices, future warning-class output, node runtime
|
|
5
|
-
// warnings from the toolchain) can reach stdout/stderr while tsc still exits
|
|
6
|
-
// 0. This wrapper treats ANY diagnostic output as a failure so the ratchet
|
|
7
|
-
// can never silently loosen: a green check means tsc printed NOTHING.
|
|
8
|
-
import { spawnSync } from 'node:child_process';
|
|
9
|
-
import { createRequire } from 'node:module';
|
|
10
|
-
import { dirname, join } from 'node:path';
|
|
11
|
-
import { fileURLToPath } from 'node:url';
|
|
12
|
-
|
|
13
|
-
const packageDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
14
|
-
const require = createRequire(import.meta.url);
|
|
15
|
-
// Resolve the workspace's own TypeScript (no PATH/npx ambiguity).
|
|
16
|
-
const tscBin = join(dirname(require.resolve('typescript/package.json')), 'bin', 'tsc');
|
|
17
|
-
|
|
18
|
-
const result = spawnSync(
|
|
19
|
-
process.execPath,
|
|
20
|
-
[tscBin, '--noEmit', '--pretty', 'false'],
|
|
21
|
-
{ cwd: packageDir, encoding: 'utf8' }
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
|
|
25
|
-
|
|
26
|
-
if (output !== '') {
|
|
27
|
-
console.error(output);
|
|
28
|
-
console.error(
|
|
29
|
-
'\n[typecheck] FAILED: tsc produced diagnostic output above ' +
|
|
30
|
-
'(zero errors AND zero warnings required).'
|
|
31
|
-
);
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (result.status !== 0) {
|
|
36
|
-
console.error(
|
|
37
|
-
`[typecheck] FAILED: tsc exited ${result.status} with no output.`
|
|
38
|
-
);
|
|
39
|
-
process.exit(result.status ?? 1);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
console.log('[typecheck] OK: zero errors, zero warnings.');
|
package/scripts/vm-smoke.mjs
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
// Local-VM smoke: boot a real microVM via startVmCodexAppServer, drive a codex
|
|
2
|
-
// `initialize` JSON-RPC round-trip through the reverse tunnel, prove the in-VM
|
|
3
|
-
// codex's MODEL traffic actually reaches the Linzumi LLM proxy (a host-side stub
|
|
4
|
-
// proxy in BOTH egress modes), then tear down and confirm `vm gc` reclaims the
|
|
5
|
-
// machine.
|
|
6
|
-
//
|
|
7
|
-
// RUN (from packages/linzumi-cli, smolvm runtime installed via `linzumi vm setup`):
|
|
8
|
-
//
|
|
9
|
-
// node scripts/vm-smoke.mjs
|
|
10
|
-
//
|
|
11
|
-
// Exit code is non-zero if the in-VM codex could NOT reach the model proxy, so
|
|
12
|
-
// this doubles as a manual red/green check for the VM model-proxy routing path.
|
|
13
|
-
//
|
|
14
|
-
// WHY the model-proxy leg exists: the codex `initialize` round-trip rides the
|
|
15
|
-
// host<->guest REVERSE TUNNEL (vmTunnel) and works regardless of how model
|
|
16
|
-
// traffic is routed. The MODEL call is different: the in-guest codex dials the
|
|
17
|
-
// model_provider.base_url itself. Under smolvm TSI, a guest dial to 127.0.0.1
|
|
18
|
-
// lands on the HOST loopback, so a host-side stub at 127.0.0.1:<port> is the
|
|
19
|
-
// faithful stand-in for the real (public) LLM proxy - and needs NO model creds.
|
|
20
|
-
// This leg ASSERTS the stub received the in-guest model request, in OPEN and in
|
|
21
|
-
// HARDENED egress mode, locking that proxy reachability is independent of the
|
|
22
|
-
// egress allowlist (the V-invariant's live counterpart).
|
|
23
|
-
//
|
|
24
|
-
// BLIND SPOT this smoke does NOT cover (the #2111 RCA): the stub is PLAIN HTTP on
|
|
25
|
-
// 127.0.0.1, so this leg proves the guest reaches the proxy IP+port but does NOT
|
|
26
|
-
// exercise (a) TLS to a PUBLIC HTTPS host - which needs the guest CA bundle
|
|
27
|
-
// (ca-certificates, added to guestProvisionCommand) - or (b) DNS resolution of a
|
|
28
|
-
// public hostname - which needs a guest /etc/resolv.conf nameserver
|
|
29
|
-
// (guestEnsureDnsCommand, run by the supervisor before codex starts). A guest
|
|
30
|
-
// passing THIS smoke could still "No response" against the REAL proxy if either
|
|
31
|
-
// were missing. To confirm those on an HVF Mac, run a REAL VM job against the
|
|
32
|
-
// deployed preview and read the supervisor's GUEST-DIAGNOSTICS block (now dumped
|
|
33
|
-
// to the runner log on a failed/closed turn) for any DNS/TLS error.
|
|
34
|
-
//
|
|
35
|
-
// TWO model-access paths, both mirroring RAW mode (the VM must support each):
|
|
36
|
-
// 1. WAFER: route through the Linzumi LLM proxy. The model_provider.base_url +
|
|
37
|
-
// LINZUMI_LLM_PROXY_TOKEN are forwarded into the guest; this leg exercises
|
|
38
|
-
// exactly that dial (the stub stands in for the proxy).
|
|
39
|
-
// 2. USER AUTH (native codex/Claude login): the in-guest codex must read the
|
|
40
|
-
// user's OWN ~/.codex/auth.json (ChatGPT OAuth tokens / OPENAI_API_KEY),
|
|
41
|
-
// forwarded into the guest tmpfs at $CODEX_HOME/auth.json (0600, scrubbed on
|
|
42
|
-
// teardown). buildVmSupervisorConfig.hostCodexAuthJson forwards it; the
|
|
43
|
-
// deterministic counterpart is vmModelProxyRouting.property.test's
|
|
44
|
-
// "user-auth path" invariant (auth.json forwarded in both egress modes).
|
|
45
|
-
import { createServer } from 'node:http';
|
|
46
|
-
import { fileURLToPath } from 'node:url';
|
|
47
|
-
import { dirname, join } from 'node:path';
|
|
48
|
-
|
|
49
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
50
|
-
const pkgRoot = dirname(here);
|
|
51
|
-
const cliEntry = join(pkgRoot, 'dist', 'index.js');
|
|
52
|
-
|
|
53
|
-
// A host-side stub LLM proxy. The in-guest codex (TSI 127.0.0.1 -> host
|
|
54
|
-
// loopback) dials this for any model request. It records that it was hit and
|
|
55
|
-
// answers a minimal OpenAI-`responses`-shaped SSE so codex does not hard-error
|
|
56
|
-
// before we have observed the hit. We only care that the request ARRIVED - that
|
|
57
|
-
// is the proof the in-VM model route reached the proxy.
|
|
58
|
-
function startStubProxy() {
|
|
59
|
-
const state = { hit: false, lastPath: undefined };
|
|
60
|
-
const server = createServer((req, res) => {
|
|
61
|
-
state.hit = true;
|
|
62
|
-
state.lastPath = req.url;
|
|
63
|
-
// Drain the body, then return a tiny SSE stream that completes immediately.
|
|
64
|
-
req.on('data', () => undefined);
|
|
65
|
-
req.on('end', () => {
|
|
66
|
-
res.writeHead(200, {
|
|
67
|
-
'content-type': 'text/event-stream',
|
|
68
|
-
'cache-control': 'no-cache',
|
|
69
|
-
});
|
|
70
|
-
res.write('event: response.completed\n');
|
|
71
|
-
res.write(
|
|
72
|
-
'data: {"type":"response.completed","response":{"id":"stub","output":[]}}\n\n'
|
|
73
|
-
);
|
|
74
|
-
res.end();
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
return new Promise((resolve) => {
|
|
78
|
-
server.listen(0, '127.0.0.1', () => {
|
|
79
|
-
const { port } = server.address();
|
|
80
|
-
resolve({
|
|
81
|
-
port,
|
|
82
|
-
wasHit: () => state.hit,
|
|
83
|
-
lastPath: () => state.lastPath,
|
|
84
|
-
close: () => server.close(),
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// The model-provider config codex needs to dial the stub: a custom provider
|
|
91
|
-
// whose base_url is the host stub (reached from the guest via TSI loopback).
|
|
92
|
-
function stubModelProvider(stubPort) {
|
|
93
|
-
return {
|
|
94
|
-
id: 'linzumi',
|
|
95
|
-
name: 'Linzumi Proxy (smoke stub)',
|
|
96
|
-
baseUrl: `http://127.0.0.1:${stubPort}/v1`,
|
|
97
|
-
envKey: 'LINZUMI_LLM_PROXY_TOKEN',
|
|
98
|
-
wireApi: 'responses',
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Boot a VM in the given egress mode, drive a model turn, and report whether the
|
|
103
|
-
// host stub proxy was hit by the in-guest codex's model request.
|
|
104
|
-
async function probeModelProxyReachable({
|
|
105
|
-
startVmCodexAppServer,
|
|
106
|
-
connectCodexAppServer,
|
|
107
|
-
egressMode,
|
|
108
|
-
}) {
|
|
109
|
-
const stub = await startStubProxy();
|
|
110
|
-
let started;
|
|
111
|
-
try {
|
|
112
|
-
// Hardened mode's fail-closed placeholder gate refuses to boot without a
|
|
113
|
-
// real allowlist override. Supply one that includes the host loopback (where
|
|
114
|
-
// the stub lives via TSI), so the hardened leg actually boots and we can
|
|
115
|
-
// assert proxy reachability is independent of the egress allowlist.
|
|
116
|
-
const env =
|
|
117
|
-
egressMode === 'hardened'
|
|
118
|
-
? { ...process.env, LINZUMI_VM_EGRESS_CIDRS: '127.0.0.0/8,10.0.0.0/8' }
|
|
119
|
-
: process.env;
|
|
120
|
-
started = await startVmCodexAppServer({
|
|
121
|
-
cwd: pkgRoot,
|
|
122
|
-
kandanThreadId: undefined,
|
|
123
|
-
appServerOptions: {
|
|
124
|
-
mcpServers: [],
|
|
125
|
-
modelProvider: stubModelProvider(stub.port),
|
|
126
|
-
},
|
|
127
|
-
// A non-empty token so the guest env file carries LINZUMI_LLM_PROXY_TOKEN
|
|
128
|
-
// (the bearer codex sends to the proxy) - the stub does not validate it.
|
|
129
|
-
llmProxyToken: 'smoke-stub-token',
|
|
130
|
-
egressMode,
|
|
131
|
-
env,
|
|
132
|
-
supervisorScriptPath: cliEntry,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
const client = await connectCodexAppServer(started.url);
|
|
136
|
-
// Drive a turn so codex makes a model request to base_url. The exact RPC
|
|
137
|
-
// shape varies by codex version; any turn-start triggers the model dial. We
|
|
138
|
-
// do not care about the codex-side result - only that the stub was hit.
|
|
139
|
-
try {
|
|
140
|
-
await client.request('thread/start', {});
|
|
141
|
-
} catch (_e) {
|
|
142
|
-
// Some codex versions name the turn RPC differently; the model dial may
|
|
143
|
-
// still have fired. Fall through to the stub-hit assertion either way.
|
|
144
|
-
}
|
|
145
|
-
// Give the in-guest model request a moment to land on the host stub.
|
|
146
|
-
await new Promise((r) => setTimeout(r, 3000));
|
|
147
|
-
client.close();
|
|
148
|
-
return { hit: stub.wasHit(), path: stub.lastPath() };
|
|
149
|
-
} finally {
|
|
150
|
-
if (started !== undefined) {
|
|
151
|
-
started.stop();
|
|
152
|
-
}
|
|
153
|
-
stub.close();
|
|
154
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async function main() {
|
|
159
|
-
const { startVmCodexAppServer } = await import(
|
|
160
|
-
join(pkgRoot, 'src', 'vmSandbox', 'startVmCodexAppServer.ts')
|
|
161
|
-
);
|
|
162
|
-
const { connectCodexAppServer } = await import(
|
|
163
|
-
join(pkgRoot, 'src', 'codexAppServer.ts')
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
const t0 = Date.now();
|
|
167
|
-
console.log('[smoke] booting VM via startVmCodexAppServer...');
|
|
168
|
-
const started = await startVmCodexAppServer({
|
|
169
|
-
cwd: pkgRoot,
|
|
170
|
-
kandanThreadId: undefined,
|
|
171
|
-
appServerOptions: { mcpServers: [] },
|
|
172
|
-
llmProxyToken: undefined,
|
|
173
|
-
supervisorScriptPath: cliEntry,
|
|
174
|
-
});
|
|
175
|
-
console.log(
|
|
176
|
-
`[smoke] supervisor ready in ${Date.now() - t0}ms; url=${started.url} machine=${started.machineName}`
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
console.log(
|
|
180
|
-
'[smoke] connecting codex app-server client (does initialize)...'
|
|
181
|
-
);
|
|
182
|
-
const client = await connectCodexAppServer(started.url);
|
|
183
|
-
console.log('[smoke] initialize round-trip OK');
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const resp = await client.request('thread/list');
|
|
187
|
-
console.log(
|
|
188
|
-
'[smoke] thread/list responded:',
|
|
189
|
-
JSON.stringify(resp).slice(0, 200)
|
|
190
|
-
);
|
|
191
|
-
} catch (e) {
|
|
192
|
-
console.log(
|
|
193
|
-
'[smoke] thread/list errored (acceptable):',
|
|
194
|
-
String(e).slice(0, 200)
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
client.close();
|
|
199
|
-
console.log('[smoke] stopping initialize-leg supervisor...');
|
|
200
|
-
started.stop();
|
|
201
|
-
await new Promise((r) => setTimeout(r, 4000));
|
|
202
|
-
|
|
203
|
-
// MODEL-PROXY REACHABILITY LEG (open + hardened). The in-VM codex must reach
|
|
204
|
-
// the LLM proxy in BOTH egress modes - proxy reachability is independent of
|
|
205
|
-
// the egress allowlist. A "No response" turn looks exactly like the stub NOT
|
|
206
|
-
// being hit, so this is the faithful local repro for that class of failure.
|
|
207
|
-
let allReachable = true;
|
|
208
|
-
for (const egressMode of ['open', 'hardened']) {
|
|
209
|
-
console.log(`[smoke] model-proxy leg: booting VM in ${egressMode} mode...`);
|
|
210
|
-
const result = await probeModelProxyReachable({
|
|
211
|
-
startVmCodexAppServer,
|
|
212
|
-
connectCodexAppServer,
|
|
213
|
-
egressMode,
|
|
214
|
-
});
|
|
215
|
-
const verdict = result.hit ? 'yes' : 'no';
|
|
216
|
-
console.log(
|
|
217
|
-
`[smoke] MODEL-PROXY REACHABLE (${egressMode}): ${verdict}` +
|
|
218
|
-
(result.hit ? ` (path=${result.path})` : '')
|
|
219
|
-
);
|
|
220
|
-
allReachable = allReachable && result.hit;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (!allReachable) {
|
|
224
|
-
console.error(
|
|
225
|
-
'[smoke] FAILED: the in-VM codex could NOT reach the model proxy in one ' +
|
|
226
|
-
'or more egress modes - this is the "No response" class of failure ' +
|
|
227
|
-
'(the model call never reached the Linzumi LLM proxy).'
|
|
228
|
-
);
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
console.log('[smoke] DONE-OK (model proxy reachable in open + hardened)');
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
main().then(
|
|
236
|
-
() => process.exit(0),
|
|
237
|
-
(err) => {
|
|
238
|
-
console.error('[smoke] FAILED:', err);
|
|
239
|
-
process.exit(1);
|
|
240
|
-
}
|
|
241
|
-
);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|