mirab 0.0.0-stage → 0.2.0

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.
Files changed (105) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +226 -2
  3. package/package.json +58 -4
  4. package/src/agent/approved.ts +261 -0
  5. package/src/agent/define.ts +235 -0
  6. package/src/agent/guest.ts +138 -0
  7. package/src/agent/init.ts +150 -0
  8. package/src/agent/manifest.ts +141 -0
  9. package/src/agent/modules.ts +23 -0
  10. package/src/agent/runtime.ts +239 -0
  11. package/src/agent-profiles.ts +327 -0
  12. package/src/api/bootstrap-contract.ts +95 -0
  13. package/src/api/channel-contract.ts +19 -0
  14. package/src/api/client.ts +100 -0
  15. package/src/api/contract.ts +2454 -0
  16. package/src/api/events-contract.ts +103 -0
  17. package/src/api/import-contract.ts +22 -0
  18. package/src/api/orchestrator-contract.ts +73 -0
  19. package/src/api/runner-contract.ts +25 -0
  20. package/src/api/scopes.ts +41 -0
  21. package/src/api/sessions-contract.ts +71 -0
  22. package/src/api/settings-contract.ts +50 -0
  23. package/src/channel/bridge.ts +97 -0
  24. package/src/channel/local.ts +154 -0
  25. package/src/channel/state.ts +11 -0
  26. package/src/channel/wire.ts +104 -0
  27. package/src/dev-sync.ts +206 -0
  28. package/src/dev.ts +453 -0
  29. package/src/events-tail.ts +75 -0
  30. package/src/images.ts +498 -0
  31. package/src/index.ts +21 -0
  32. package/src/invocation.ts +34 -0
  33. package/src/isolation/anchored-fs.ts +284 -0
  34. package/src/isolation/colima.ts +159 -0
  35. package/src/isolation/container.ts +189 -0
  36. package/src/isolation/firecracker.ts +414 -0
  37. package/src/isolation/guest-protocol.json +1 -0
  38. package/src/isolation/host-user.ts +247 -0
  39. package/src/isolation/image.ts +336 -0
  40. package/src/isolation/index.ts +477 -0
  41. package/src/isolation/multipass.ts +541 -0
  42. package/src/isolation/network.ts +167 -0
  43. package/src/isolation/policy.ts +86 -0
  44. package/src/isolation/session.ts +385 -0
  45. package/src/isolation/sync.ts +329 -0
  46. package/src/isolation/vz-network-setup.ts +247 -0
  47. package/src/isolation/vz.ts +312 -0
  48. package/src/mirab-main.ts +536 -0
  49. package/src/mirab.ts +13 -0
  50. package/src/runner/define.ts +187 -0
  51. package/src/runner/image.ts +239 -0
  52. package/src/runner/index.ts +37 -0
  53. package/src/runner/serve.ts +491 -0
  54. package/src/runner/toolsets.ts +205 -0
  55. package/src/spawn-adapters/claude.ts +112 -0
  56. package/src/spawn-adapters/codex-logs.ts +130 -0
  57. package/src/spawn-adapters/codex-notify.ts +73 -0
  58. package/src/spawn-adapters/codex.ts +89 -0
  59. package/src/spawn-adapters/index.ts +40 -0
  60. package/src/spawn-adapters/types.ts +55 -0
  61. package/src/spawn-attach.ts +243 -0
  62. package/src/spawn-binary.ts +90 -0
  63. package/src/spawn-bridge.ts +504 -0
  64. package/src/spawn-bundle.ts +16 -0
  65. package/src/spawn-detach.ts +301 -0
  66. package/src/spawn-events.ts +35 -0
  67. package/src/spawn-hook.ts +86 -0
  68. package/src/spawn-input.ts +107 -0
  69. package/src/spawn-native.ts +49 -0
  70. package/src/spawn-nudge-worker.ts +61 -0
  71. package/src/spawn-nudge.ts +446 -0
  72. package/src/spawn-output.ts +45 -0
  73. package/src/spawn-queue.ts +135 -0
  74. package/src/spawn-quiet.ts +69 -0
  75. package/src/spawn-report.ts +114 -0
  76. package/src/spawn-sessions.ts +166 -0
  77. package/src/spawn-vm.ts +356 -0
  78. package/src/spawn-worker.ts +766 -0
  79. package/src/spawn.ts +468 -0
  80. package/src/station/config.ts +89 -0
  81. package/src/station/gateway.ts +94 -0
  82. package/src/station/vault.ts +96 -0
  83. package/src/test-cli.ts +90 -0
  84. package/src/tools.ts +3 -0
  85. package/types/agent/define.d.ts +76 -0
  86. package/types/agent/manifest.d.ts +53 -0
  87. package/types/agent/modules.d.ts +1 -0
  88. package/types/images.d.ts +124 -0
  89. package/types/index.d.ts +6 -0
  90. package/types/isolation/colima.d.ts +37 -0
  91. package/types/isolation/container.d.ts +25 -0
  92. package/types/isolation/firecracker.d.ts +55 -0
  93. package/types/isolation/host-user.d.ts +47 -0
  94. package/types/isolation/image.d.ts +158 -0
  95. package/types/isolation/index.d.ts +128 -0
  96. package/types/isolation/network.d.ts +33 -0
  97. package/types/isolation/policy.d.ts +29 -0
  98. package/types/isolation/session.d.ts +99 -0
  99. package/types/isolation/vz.d.ts +63 -0
  100. package/types/runner/define.d.ts +101 -0
  101. package/types/runner/image.d.ts +52 -0
  102. package/types/runner/index.d.ts +13 -0
  103. package/types/runner/serve.d.ts +69 -0
  104. package/types/runner/toolsets.d.ts +35 -0
  105. package/types/tools.d.ts +2 -0
@@ -0,0 +1,536 @@
1
+ import { homedir } from 'node:os'
2
+ import { join, resolve } from 'node:path'
3
+ import { Command } from 'commander'
4
+ import pkg from '../package.json'
5
+ import { profileCommands } from './agent-profiles'
6
+ import { DEFAULT_MODEL, initAgent, initProblem } from './agent/init'
7
+ import { agentNameProblem } from './agent/define'
8
+ import { MULTIPASS_UNAVAILABLE, usesMultipass } from './isolation/multipass'
9
+ import { localEvents, spawnCommands } from './spawn'
10
+
11
+ export const program = new Command()
12
+ .enablePositionalOptions()
13
+ .option(
14
+ '--config-dir <path>',
15
+ "mirab's config directory (isolation.json, VM state)",
16
+ )
17
+ .hook('preAction', (_, command) => {
18
+ // One config directory: given before the command, it fills in the
19
+ // command's own --config-dir unless that was given too.
20
+ const global = program.opts().configDir
21
+ if (
22
+ global &&
23
+ command.options.some((option) => option.long === '--config-dir') &&
24
+ command.getOptionValueSource('configDir') !== 'cli'
25
+ )
26
+ command.setOptionValue('configDir', global)
27
+ })
28
+ .name('mirab')
29
+ .description('mirab: agents in VMs on your own machine')
30
+ .version(pkg.version)
31
+
32
+ // ---------- init: a new agent ----------
33
+
34
+ /** The agent's harar name: --name, else asked for when there is a terminal. */
35
+ async function askName(name?: string): Promise<string> {
36
+ if (name) return name
37
+ if (!process.stdin.isTTY)
38
+ throw Error(
39
+ 'Give the agent its harar name: mirab init --name <org>.agent.<name>',
40
+ )
41
+ const { default: inquirer } = await import('inquirer')
42
+ const answer = await inquirer.prompt([
43
+ {
44
+ type: 'input',
45
+ name: 'name',
46
+ message:
47
+ "The agent's harar name (<org>.agent.<name>, like gg.addis.agent.ops):",
48
+ validate: (value: string) => {
49
+ const problem = agentNameProblem(value.trim())
50
+ return problem ? `That name ${problem}` : true
51
+ },
52
+ },
53
+ ])
54
+ return String(answer.name).trim()
55
+ }
56
+
57
+ async function init(
58
+ directory: string,
59
+ opts: { name?: string; model?: string; git?: boolean },
60
+ ) {
61
+ // Refuse a busy folder before asking anything.
62
+ const taken = initProblem(directory)
63
+ if (taken) throw Error(taken)
64
+ const name = (await askName(opts.name)).trim()
65
+ const { written, notes } = await initAgent({
66
+ directory,
67
+ name,
68
+ model: opts.model,
69
+ git: opts.git,
70
+ })
71
+ console.log(
72
+ `Made ${name} in ${directory}: ${written.filter((f) => !f.endsWith('.gitkeep')).join(', ')}`,
73
+ )
74
+ for (const note of notes) console.log(note)
75
+ console.log('Model access comes from harar, never a .env.')
76
+ }
77
+
78
+ program
79
+ .command('init [directory]')
80
+ .description(
81
+ 'make an agent: agent/agent.ts, instructions, tools, skills, channels',
82
+ )
83
+ .option('--name <name>', "the agent's harar name, <org>.agent.<name>")
84
+ .option('--model <model>', 'provider/model', DEFAULT_MODEL)
85
+ .option('--no-git', "don't run git init")
86
+ .action(
87
+ async (
88
+ directory: string | undefined,
89
+ opts: { name?: string; model?: string; git?: boolean },
90
+ ) => {
91
+ await init(directory ?? '.', opts)
92
+ },
93
+ )
94
+
95
+ const manifestCmd = program
96
+ .command('manifest')
97
+ .description(
98
+ "the agent's manifest: what it asks to reach, and what you approved",
99
+ )
100
+
101
+ manifestCmd
102
+ .command('show [project]', { isDefault: true })
103
+ .description(
104
+ "the manifest, read from the agent's code in its own VM (never run on this machine), within what you approved",
105
+ )
106
+ .action(async (project: string | undefined) => {
107
+ const configDir =
108
+ program.opts().configDir ?? join(homedir(), '.config', 'mirab')
109
+ const { agentManifest } = await import('./dev')
110
+ const { pendingReach } = await import('./agent/approved')
111
+ const log = (line: string) => console.error(line)
112
+ const { manifest } = await agentManifest({
113
+ project: project ?? '.',
114
+ configDir,
115
+ log,
116
+ })
117
+ const pending = pendingReach(configDir, resolve(project ?? '.'))
118
+ console.log(
119
+ JSON.stringify(pending ? { ...manifest, pending } : manifest, null, 2),
120
+ )
121
+ })
122
+
123
+ manifestCmd
124
+ .command('approve [project]')
125
+ .description(
126
+ "approve what the agent's definition asks for beyond what you approved (run it here, never in the agent's VM)",
127
+ )
128
+ .option('--yes', 'approve without asking (no terminal to ask on)')
129
+ .action(async (project: string | undefined, opts: { yes?: boolean }) => {
130
+ const configDir =
131
+ program.opts().configDir ?? join(homedir(), '.config', 'mirab')
132
+ const { approvedReach, approvePending, beyond, describe, pendingReach } =
133
+ await import('./agent/approved')
134
+ const root = resolve(project ?? '.')
135
+ const shown = pendingReach(configDir, root)
136
+ if (!shown) {
137
+ console.error(
138
+ 'mirab manifest approve: nothing is waiting (mirab dev or mirab manifest shows what the agent asks for)',
139
+ )
140
+ process.exitCode = 1
141
+ return
142
+ }
143
+ const now = approvedReach(configDir, root) ?? {
144
+ domains: [],
145
+ packages: [],
146
+ gateway: {},
147
+ }
148
+ console.error(`The agent here asks for ${describe(beyond(now, shown))}.`)
149
+ if (!opts.yes) {
150
+ if (!process.stdin.isTTY) {
151
+ console.error('mirab manifest approve: no terminal to ask on; --yes')
152
+ process.exitCode = 2
153
+ return
154
+ }
155
+ const { createInterface } = await import('node:readline/promises')
156
+ const rl = createInterface({
157
+ input: process.stdin,
158
+ output: process.stderr,
159
+ })
160
+ const answer = await rl.question('Approve? [y/N] ')
161
+ rl.close()
162
+ if (!/^y(es)?$/i.test(answer.trim())) {
163
+ console.error('Not approved; it stays left out.')
164
+ return
165
+ }
166
+ }
167
+ const approved = approvePending(configDir, root, shown)
168
+ console.log(JSON.stringify({ approved }, null, 2))
169
+ })
170
+
171
+ program
172
+ .command('dev [project]')
173
+ .description(
174
+ "chat with the project's agent, running in its own VM on this machine",
175
+ )
176
+ .action(async (project: string | undefined) => {
177
+ const configDir =
178
+ program.opts().configDir ?? join(homedir(), '.config', 'mirab')
179
+ const { devInTerminal } = await import('./dev')
180
+ const { loadStationConfig } = await import('./station/config')
181
+ process.exitCode = await devInTerminal({
182
+ project: project ?? '.',
183
+ configDir,
184
+ station: await loadStationConfig(configDir),
185
+ auditLog: join(configDir, 'egress.log'),
186
+ log: (line) => console.error(line),
187
+ })
188
+ })
189
+
190
+ const imageCmd = program
191
+ .command('image')
192
+ .description("an agent's VM image: its packages, built once and cached")
193
+
194
+ imageCmd
195
+ .command('build [project]')
196
+ .description("build the agent's image from its packages, unless it's cached")
197
+ .action(async (project: string | undefined) => {
198
+ const configDir =
199
+ program.opts().configDir ?? join(homedir(), '.config', 'mirab')
200
+ const { agentManifest } = await import('./dev')
201
+ const { agentImage } = await import('./images')
202
+ const log = (line: string) => console.error(line)
203
+ const { manifest, vm } = await agentManifest({
204
+ project: project ?? '.',
205
+ configDir,
206
+ log,
207
+ })
208
+ if (!manifest.packages.length)
209
+ return console.log(
210
+ `${manifest.name} has no packages: it runs on the base image`,
211
+ )
212
+ console.log(
213
+ await agentImage({ configDir, vm, packages: manifest.packages, log }),
214
+ )
215
+ })
216
+
217
+ imageCmd
218
+ .command('list')
219
+ .description('the agent images cached on this machine')
220
+ .action(async () => {
221
+ const configDir =
222
+ program.opts().configDir ?? join(homedir(), '.config', 'mirab')
223
+ const { requireVm } = await import('./spawn-vm')
224
+ const { baseId, listImages } = await import('./images')
225
+ const vm = await requireVm(configDir, (line) => console.error(line))
226
+ const images = await listImages(configDir, await baseId(vm.rootfs))
227
+ console.log(
228
+ JSON.stringify(
229
+ images.map((i) => ({ ...i, usedAt: i.usedAt.toISOString() })),
230
+ null,
231
+ 2,
232
+ ),
233
+ )
234
+ })
235
+
236
+ imageCmd
237
+ .command('prune')
238
+ .description(
239
+ 'remove images built on an older base, unused ones, and what stopped builds left',
240
+ )
241
+ .option('--days <n>', 'how long unused before an image goes', '30')
242
+ .option('--dry-run', 'say what would go, remove nothing')
243
+ .action(async (options: { days: string; dryRun?: boolean }) => {
244
+ const days = /^[1-9][0-9]*$/.test(options.days) ? Number(options.days) : NaN
245
+ if (!Number.isInteger(days)) {
246
+ console.error(
247
+ 'mirab image prune: --days is a whole number of days, at least 1',
248
+ )
249
+ process.exitCode = 2
250
+ return
251
+ }
252
+ const configDir =
253
+ program.opts().configDir ?? join(homedir(), '.config', 'mirab')
254
+ const { requireVm } = await import('./spawn-vm')
255
+ const { baseId, pruneImages } = await import('./images')
256
+ const vm = await requireVm(configDir, (line) => console.error(line))
257
+ const { removed, failed } = await pruneImages({
258
+ configDir,
259
+ currentBase: await baseId(vm.rootfs),
260
+ days,
261
+ dryRun: options.dryRun,
262
+ })
263
+ const freed = removed.reduce((sum, r) => sum + r.bytes, 0)
264
+ console.log(
265
+ JSON.stringify(
266
+ {
267
+ removed,
268
+ failed,
269
+ freedBytes: freed,
270
+ dryRun: Boolean(options.dryRun),
271
+ },
272
+ null,
273
+ 2,
274
+ ),
275
+ )
276
+ if (failed.length) process.exitCode = 1
277
+ })
278
+
279
+ imageCmd
280
+ .command('update [project]')
281
+ .description(
282
+ "boot the agent's image, and upgrade it if apt has anything newer",
283
+ )
284
+ .action(async (project: string | undefined) => {
285
+ const configDir =
286
+ program.opts().configDir ?? join(homedir(), '.config', 'mirab')
287
+ const { agentManifest } = await import('./dev')
288
+ const { updateImage } = await import('./images')
289
+ const log = (line: string) => console.error(line)
290
+ const { manifest, vm } = await agentManifest({
291
+ project: project ?? '.',
292
+ configDir,
293
+ log,
294
+ })
295
+ if (!manifest.packages.length)
296
+ return console.log(
297
+ `${manifest.name} has no packages: it runs on the base image`,
298
+ )
299
+ const { upgraded } = await updateImage({
300
+ configDir,
301
+ vm,
302
+ packages: manifest.packages,
303
+ log,
304
+ })
305
+ console.log(
306
+ upgraded.length ? `upgraded: ${upgraded.join(' ')}` : 'up to date',
307
+ )
308
+ })
309
+
310
+ // ---------- escalations: what agents asked for, and a person's answer ----------
311
+
312
+ const configDirOf = () =>
313
+ program.opts().configDir ?? join(homedir(), '.config', 'mirab')
314
+
315
+ /** The agent whose ask `host` is: the one given, or the only one that asked. */
316
+ async function askFor(host: string, agent?: string) {
317
+ const { asks } = await import('rixse')
318
+ if (agent) return agent
319
+ const askers = asks(configDirOf()).filter((ask) => ask.host === host)
320
+ if (askers.length === 1) return askers[0].agent
321
+ throw Error(
322
+ askers.length
323
+ ? `${host} was asked for by ${askers.map((a) => a.agent).join(', ')}: say which with --agent`
324
+ : `nothing asked for ${host}; give --agent to allow it anyway`,
325
+ )
326
+ }
327
+
328
+ program
329
+ .command('escalations')
330
+ .description(
331
+ 'hosts agents asked for that their whitelist refused, most asked first',
332
+ )
333
+ .action(async () => {
334
+ const { asks } = await import('rixse')
335
+ const shown = asks(configDirOf()).map((ask) => ({
336
+ ...ask,
337
+ host: ask.host.length > 80 ? `${ask.host.slice(0, 77)}...` : ask.host,
338
+ // Punycode can look like a familiar name once a browser shows it.
339
+ ...(ask.host.split('.').some((label) => label.startsWith('xn--'))
340
+ ? { warning: 'punycode name: check what it spells' }
341
+ : {}),
342
+ }))
343
+ console.log(JSON.stringify(shown, null, 2))
344
+ })
345
+
346
+ const hostArg = (host: string) => host.toLowerCase().replace(/\.$/, '')
347
+
348
+ program
349
+ .command('allow <host>')
350
+ .description(
351
+ "let an agent reach a host it asked for (run it here, never in the agent's VM; quote wildcards: '*.example.com')",
352
+ )
353
+ .option('--agent <name>', 'whose ask: an agent name, or project:<path>')
354
+ .action(async (host: string, opts: { agent?: string }) => {
355
+ const { allow, allowPath } = await import('rixse')
356
+ const name = hostArg(host)
357
+ const agent = await askFor(name, opts.agent)
358
+ allow(configDirOf(), agent, name)
359
+ console.log(
360
+ `${agent} may now reach ${name}, for every session it runs (${allowPath(configDirOf(), agent)}); mirab revoke ${name} takes it back. To keep it in git, add '${name}' to domains in agent/agent.ts`,
361
+ )
362
+ })
363
+
364
+ program
365
+ .command('dismiss <host>')
366
+ .description('say no to an ask: it is dropped, and the host stays refused')
367
+ .option('--agent <name>', 'whose ask: an agent name, or project:<path>')
368
+ .action(async (host: string, opts: { agent?: string }) => {
369
+ const { dismiss } = await import('rixse')
370
+ const name = hostArg(host)
371
+ const agent = await askFor(name, opts.agent)
372
+ dismiss(configDirOf(), agent, name)
373
+ console.log(`dismissed ${agent}'s ask for ${name}`)
374
+ })
375
+
376
+ program
377
+ .command('revoke <host>')
378
+ .description(
379
+ 'take back a yes: the host is refused again from the next connection',
380
+ )
381
+ .requiredOption(
382
+ '--agent <name>',
383
+ 'whose grant: an agent name, or project:<path>',
384
+ )
385
+ .action(async (host: string, opts: { agent: string }) => {
386
+ const { revoke } = await import('rixse')
387
+ revoke(configDirOf(), opts.agent, hostArg(host))
388
+ console.log(`${opts.agent} may no longer reach ${hostArg(host)}`)
389
+ })
390
+
391
+ const secretsCmd = program
392
+ .command('secrets')
393
+ .description(
394
+ "which of your harar secrets an agent may use (a project's .env only names them); harar says where each may go",
395
+ )
396
+
397
+ secretsCmd
398
+ .command('allow <secret>')
399
+ .description(
400
+ "let an agent use a harar secret (run it here, never in the agent's VM)",
401
+ )
402
+ .option('--agent <name>', 'whose: project:<path> (default: the project here)')
403
+ .action(async (secret: string, opts: { agent?: string }) => {
404
+ const { allowSecret, projectAgent, secretsPath } = await import('rixse')
405
+ const agent = opts.agent ?? projectAgent('.')
406
+ try {
407
+ allowSecret(configDirOf(), agent, secret)
408
+ } catch (e) {
409
+ console.error(`mirab secrets allow: ${(e as Error).message}`)
410
+ process.exitCode = 2
411
+ return
412
+ }
413
+ console.log(
414
+ `${agent} may now use harar://${secret} (${secretsPath(configDirOf(), agent)}), on the hosts harar allows it: harar hosts list ${secret}`,
415
+ )
416
+ })
417
+
418
+ secretsCmd
419
+ .command('revoke <secret>')
420
+ .description('take back a secret from an agent, from its next session')
421
+ .option('--agent <name>', 'whose: project:<path> (default: the project here)')
422
+ .action(async (secret: string, opts: { agent?: string }) => {
423
+ const { projectAgent, revokeSecret } = await import('rixse')
424
+ const agent = opts.agent ?? projectAgent('.')
425
+ try {
426
+ revokeSecret(configDirOf(), agent, secret)
427
+ } catch (e) {
428
+ console.error(`mirab secrets revoke: ${(e as Error).message}`)
429
+ process.exitCode = 1
430
+ return
431
+ }
432
+ console.log(`${agent} may no longer use harar://${secret}`)
433
+ })
434
+
435
+ secretsCmd
436
+ .command('list')
437
+ .description('the secrets an agent may use')
438
+ .option('--agent <name>', 'whose: project:<path> (default: the project here)')
439
+ .action(async (opts: { agent?: string }) => {
440
+ const { projectAgent, secretGrants } = await import('rixse')
441
+ const agent = opts.agent ?? projectAgent('.')
442
+ console.log(
443
+ JSON.stringify(
444
+ { agent, secrets: secretGrants(configDirOf(), agent) },
445
+ null,
446
+ 2,
447
+ ),
448
+ )
449
+ })
450
+
451
+ // A bare `mirab`, with nothing else at all, in an empty folder makes an agent;
452
+ // anything else here (an option, a mistyped command) never does.
453
+ program.action(async () => {
454
+ if (process.argv.length > 2 || initProblem(process.cwd()))
455
+ return program.help()
456
+ await init('.', { model: DEFAULT_MODEL })
457
+ })
458
+
459
+ // ---------- agent: profiles ----------
460
+
461
+ profileCommands(program.command('agent').description('agent profiles'))
462
+
463
+ // ---------- spawn (with attach and stop) ----------
464
+
465
+ spawnCommands(program)
466
+
467
+ // ---------- vm ----------
468
+
469
+ const vmCmd = program
470
+ .command('vm')
471
+ .description('agents in a VM: vz on macOS, Firecracker on Linux')
472
+
473
+ vmCmd
474
+ .command('status')
475
+ .description('which VM backend mirab spawn --vm uses here')
476
+ .action(async () => {
477
+ const configDir =
478
+ program.opts().configDir ?? join(homedir(), '.config', 'mirab')
479
+ if (usesMultipass(configDir)) return console.log(MULTIPASS_UNAVAILABLE)
480
+ console.log(
481
+ process.platform === 'darwin'
482
+ ? 'mirab spawn --vm uses vz (the vm block in isolation.json); see docs/VM.md'
483
+ : 'mirab spawn --vm uses Firecracker; see docs/VM.md',
484
+ )
485
+ })
486
+
487
+ const vmNetwork = vmCmd
488
+ .command('network')
489
+ .description(
490
+ 'filtered egress for vz VMs on this Mac: internet yes, the Mac, LAN and tailnet no',
491
+ )
492
+
493
+ vmNetwork
494
+ .command('setup')
495
+ .description(
496
+ 'install the pf filter and its LaunchDaemon (root, once; safe to rerun)',
497
+ )
498
+ .option('--dry-run', 'print what would be written and run, change nothing')
499
+ .action(async (o: { dryRun?: boolean }) => {
500
+ const { setupVzNetwork } = await import('./isolation/vz-network-setup')
501
+ setupVzNetwork({ dryRun: o.dryRun })
502
+ })
503
+
504
+ vmNetwork
505
+ .command('remove')
506
+ .description('undo setup: vz VMs get no network afterwards')
507
+ .option('--dry-run', 'print what would be removed, change nothing')
508
+ .action(async (o: { dryRun?: boolean }) => {
509
+ const { removeVzNetwork } = await import('./isolation/vz-network-setup')
510
+ removeVzNetwork({ dryRun: o.dryRun })
511
+ })
512
+
513
+ vmNetwork
514
+ .command('status')
515
+ .description('the network mirab spawn --vm gives a vz VM here')
516
+ .action(async () => {
517
+ const { vzNetworkState, vzNetworkStatusLine } =
518
+ await import('./isolation/vz')
519
+ console.log(vzNetworkStatusLine('auto', vzNetworkState()))
520
+ })
521
+
522
+ // ---------- events ----------
523
+
524
+ program
525
+ .command('events <id>')
526
+ .description("a local agent session's events")
527
+ .action(async (id: string) => {
528
+ await localEvents(id)
529
+ })
530
+
531
+ export function main() {
532
+ return program.parseAsync().catch((error) => {
533
+ console.error(error.message)
534
+ process.exitCode = 1
535
+ })
536
+ }
package/src/mirab.ts ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bun
2
+ import { runHook } from './spawn-hook'
3
+ if (process.argv[2] === 'hook') {
4
+ // Hook failures are intentionally silent and never change agent behavior.
5
+ await runHook(process.argv.slice(3)).catch(() => {})
6
+ process.exit(0)
7
+ } else if (process.argv[2] === '__vm-worker') {
8
+ // VM sessions use the compiled Bun runtime for descriptor-anchored sync.
9
+ await import('./spawn-worker')
10
+ } else {
11
+ const { main } = await import('./mirab-main')
12
+ await main()
13
+ }