@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.
@@ -0,0 +1,151 @@
1
+ // PROOF that a smolvm guest can really install the coding toolchain.
2
+ //
3
+ // Unlike vm-smoke.mjs (which stubs the model proxy over HTTP loopback), this
4
+ // boots a REAL microVM with network:true and runs the ACTUAL
5
+ // guestProvisionCommand from the #2111 branch - i.e. `apk add --no-cache
6
+ // ca-certificates nodejs npm git ripgrep bash curl` fetched over real egress
7
+ // from the Alpine package mirrors, then `npm install -g @openai/codex` - and
8
+ // then execs `--version` for each tool inside the guest and verifies the CA
9
+ // bundle the TLS fix installs. If any tool is missing or the CA bundle is
10
+ // empty, it exits non-zero.
11
+ //
12
+ // RUN (from packages/linzumi-cli, requires HVF + the smolvm runtime installed):
13
+ // node scripts/vm-provision-proof.mjs
14
+ import { homedir } from 'node:os';
15
+ import { join } from 'node:path';
16
+
17
+ const here = new URL('.', import.meta.url).pathname;
18
+ const pkgRoot = join(here, '..');
19
+
20
+ // vmGuestProvision.ts is self-contained (no relative imports), so Node's native
21
+ // type-stripping can import it directly - this is the REAL provision command
22
+ // from the #2111 branch, not a copy.
23
+ const { guestProvisionCommand, guestProvisionCheckCommand, guestCaBundlePath } =
24
+ await import(join(pkgRoot, 'src', 'vmSandbox', 'vmGuestProvision.ts'));
25
+
26
+ // Inline of vmCodexSupervisor.loadSmolmachines (avoids pulling in that module's
27
+ // extensionless relative imports, which Node's stripper won't resolve).
28
+ async function loadSmolmachines(runtimeDir) {
29
+ const { createRequire } = await import('node:module');
30
+ const requireFromRuntime = createRequire(join(runtimeDir, 'package.json'));
31
+ return requireFromRuntime('smolmachines');
32
+ }
33
+
34
+ const runtimeDir =
35
+ process.env.LINZUMI_VM_RUNTIME_DIR ||
36
+ join(homedir(), '.linzumi', 'vm-runtime');
37
+ const CODEX_VERSION = process.env.LINZUMI_VM_CODEX_VERSION || '0.139.0';
38
+
39
+ function log(...a) {
40
+ console.log('[provision-proof]', ...a);
41
+ }
42
+
43
+ async function run(machine, label, cmd, timeout = 240) {
44
+ const r = await machine.exec(cmd, { timeout });
45
+ const out = (r.stdout || '').trim();
46
+ const err = (r.stderr || '').trim();
47
+ log(
48
+ `${label}: exit=${r.exitCode}` +
49
+ (out ? ` stdout=${JSON.stringify(out.slice(0, 200))}` : '') +
50
+ (err && r.exitCode !== 0
51
+ ? ` stderr=${JSON.stringify(err.slice(0, 200))}`
52
+ : '')
53
+ );
54
+ return r;
55
+ }
56
+
57
+ const t0 = Date.now();
58
+ log(`loading smolvm runtime from ${runtimeDir} ...`);
59
+ const sdk = await loadSmolmachines(runtimeDir);
60
+
61
+ const name = `linzumi-provision-proof-${process.pid}`;
62
+ log(`creating microVM "${name}" (network:true, full egress) ...`);
63
+ const machine = await sdk.Machine.create({
64
+ name,
65
+ mounts: [],
66
+ resources: { cpus: 2, memoryMb: 2048, network: true },
67
+ persistent: false,
68
+ });
69
+ log(`VM booted in ${Date.now() - t0}ms`);
70
+
71
+ let failures = [];
72
+ try {
73
+ // Sanity: the guest has outbound network at all.
74
+ await run(machine, 'guest uname', ['/bin/sh', '-c', 'uname -a']);
75
+
76
+ // THE REAL THING: run the exact provision command #2111 ships.
77
+ log('running REAL guestProvisionCommand (apk add + npm i -g codex) ...');
78
+ const prov = await run(
79
+ machine,
80
+ 'provision',
81
+ guestProvisionCommand(CODEX_VERSION),
82
+ 600
83
+ );
84
+ if (prov.exitCode !== 0) failures.push('provision command failed');
85
+
86
+ // Prove each tool is actually installed and runnable in the guest.
87
+ const tools = [
88
+ ['node', ['/bin/sh', '-c', 'node --version']],
89
+ ['npm', ['/bin/sh', '-c', 'npm --version']],
90
+ ['git', ['/bin/sh', '-c', 'git --version']],
91
+ ['ripgrep (rg)', ['/bin/sh', '-c', 'rg --version | head -1']],
92
+ ['bash', ['/bin/sh', '-c', 'bash --version | head -1']],
93
+ ['curl', ['/bin/sh', '-c', 'curl --version | head -1']],
94
+ [
95
+ 'codex',
96
+ [
97
+ '/bin/sh',
98
+ '-c',
99
+ 'command -v codex && codex --version 2>/dev/null | head -1',
100
+ ],
101
+ ],
102
+ ];
103
+ for (const [label, cmd] of tools) {
104
+ const r = await run(machine, `verify ${label}`, cmd, 60);
105
+ if (r.exitCode !== 0) failures.push(`${label} not runnable`);
106
+ }
107
+
108
+ // Prove the CA bundle (the TLS "No response" fix) is materialized + non-empty.
109
+ const ca = await run(machine, 'verify CA bundle', [
110
+ '/bin/sh',
111
+ '-c',
112
+ `test -s ${guestCaBundlePath} && wc -c <${guestCaBundlePath}`,
113
+ ]);
114
+ if (ca.exitCode !== 0) failures.push('CA bundle missing/empty');
115
+
116
+ // Prove the provision-check predicate the supervisor gates on passes.
117
+ const check = await run(
118
+ machine,
119
+ 'provisionCheckCommand',
120
+ guestProvisionCheckCommand(CODEX_VERSION)
121
+ );
122
+ if (check.exitCode !== 0) failures.push('provisionCheckCommand failed');
123
+
124
+ // Bonus: real outbound TLS from inside the guest (DNS + CA chain end-to-end).
125
+ await run(machine, 'guest TLS egress (https HEAD)', [
126
+ '/bin/sh',
127
+ '-c',
128
+ 'curl -sS -o /dev/null -w "%{http_code}" https://registry.npmjs.org/ || echo CURL_FAILED',
129
+ ]);
130
+ } finally {
131
+ log('tearing down VM ...');
132
+ try {
133
+ await machine.stop();
134
+ } catch (e) {
135
+ log('stop error (non-fatal):', String(e).slice(0, 120));
136
+ }
137
+ if (typeof machine.delete === 'function') {
138
+ try {
139
+ await machine.delete();
140
+ } catch (e) {
141
+ log('delete error (non-fatal):', String(e).slice(0, 120));
142
+ }
143
+ }
144
+ }
145
+
146
+ if (failures.length > 0) {
147
+ console.error('[provision-proof] FAILED:', failures.join('; '));
148
+ process.exit(1);
149
+ }
150
+ log(`DONE-OK - full toolchain installed + verified in ${Date.now() - t0}ms`);
151
+ process.exit(0);
@@ -0,0 +1,241 @@
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
+ );