mirab 0.0.0 → 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.
- package/LICENSE +201 -0
- package/README.md +223 -3
- package/package.json +56 -7
- package/src/agent/approved.ts +261 -0
- package/src/agent/define.ts +235 -0
- package/src/agent/guest.ts +138 -0
- package/src/agent/init.ts +150 -0
- package/src/agent/manifest.ts +141 -0
- package/src/agent/modules.ts +23 -0
- package/src/agent/runtime.ts +239 -0
- package/src/agent-profiles.ts +327 -0
- package/src/api/bootstrap-contract.ts +95 -0
- package/src/api/channel-contract.ts +19 -0
- package/src/api/client.ts +100 -0
- package/src/api/contract.ts +2454 -0
- package/src/api/events-contract.ts +103 -0
- package/src/api/import-contract.ts +22 -0
- package/src/api/orchestrator-contract.ts +73 -0
- package/src/api/runner-contract.ts +25 -0
- package/src/api/scopes.ts +41 -0
- package/src/api/sessions-contract.ts +71 -0
- package/src/api/settings-contract.ts +50 -0
- package/src/channel/bridge.ts +97 -0
- package/src/channel/local.ts +154 -0
- package/src/channel/state.ts +11 -0
- package/src/channel/wire.ts +104 -0
- package/src/dev-sync.ts +206 -0
- package/src/dev.ts +453 -0
- package/src/events-tail.ts +75 -0
- package/src/images.ts +498 -0
- package/src/index.ts +21 -0
- package/src/invocation.ts +34 -0
- package/src/isolation/anchored-fs.ts +284 -0
- package/src/isolation/colima.ts +159 -0
- package/src/isolation/container.ts +189 -0
- package/src/isolation/firecracker.ts +414 -0
- package/src/isolation/guest-protocol.json +1 -0
- package/src/isolation/host-user.ts +247 -0
- package/src/isolation/image.ts +336 -0
- package/src/isolation/index.ts +477 -0
- package/src/isolation/multipass.ts +541 -0
- package/src/isolation/network.ts +167 -0
- package/src/isolation/policy.ts +86 -0
- package/src/isolation/session.ts +385 -0
- package/src/isolation/sync.ts +329 -0
- package/src/isolation/vz-network-setup.ts +247 -0
- package/src/isolation/vz.ts +312 -0
- package/src/mirab-main.ts +536 -0
- package/src/mirab.ts +13 -0
- package/src/runner/define.ts +187 -0
- package/src/runner/image.ts +239 -0
- package/src/runner/index.ts +37 -0
- package/src/runner/serve.ts +491 -0
- package/src/runner/toolsets.ts +205 -0
- package/src/spawn-adapters/claude.ts +112 -0
- package/src/spawn-adapters/codex-logs.ts +130 -0
- package/src/spawn-adapters/codex-notify.ts +73 -0
- package/src/spawn-adapters/codex.ts +89 -0
- package/src/spawn-adapters/index.ts +40 -0
- package/src/spawn-adapters/types.ts +55 -0
- package/src/spawn-attach.ts +243 -0
- package/src/spawn-binary.ts +90 -0
- package/src/spawn-bridge.ts +504 -0
- package/src/spawn-bundle.ts +16 -0
- package/src/spawn-detach.ts +301 -0
- package/src/spawn-events.ts +35 -0
- package/src/spawn-hook.ts +86 -0
- package/src/spawn-input.ts +107 -0
- package/src/spawn-native.ts +49 -0
- package/src/spawn-nudge-worker.ts +61 -0
- package/src/spawn-nudge.ts +446 -0
- package/src/spawn-output.ts +45 -0
- package/src/spawn-queue.ts +135 -0
- package/src/spawn-quiet.ts +69 -0
- package/src/spawn-report.ts +114 -0
- package/src/spawn-sessions.ts +166 -0
- package/src/spawn-vm.ts +356 -0
- package/src/spawn-worker.ts +766 -0
- package/src/spawn.ts +468 -0
- package/src/station/config.ts +89 -0
- package/src/station/gateway.ts +94 -0
- package/src/station/vault.ts +96 -0
- package/src/test-cli.ts +90 -0
- package/src/tools.ts +3 -0
- package/types/agent/define.d.ts +76 -0
- package/types/agent/manifest.d.ts +53 -0
- package/types/agent/modules.d.ts +1 -0
- package/types/images.d.ts +124 -0
- package/types/index.d.ts +6 -0
- package/types/isolation/colima.d.ts +37 -0
- package/types/isolation/container.d.ts +25 -0
- package/types/isolation/firecracker.d.ts +55 -0
- package/types/isolation/host-user.d.ts +47 -0
- package/types/isolation/image.d.ts +158 -0
- package/types/isolation/index.d.ts +128 -0
- package/types/isolation/network.d.ts +33 -0
- package/types/isolation/policy.d.ts +29 -0
- package/types/isolation/session.d.ts +99 -0
- package/types/isolation/vz.d.ts +63 -0
- package/types/runner/define.d.ts +101 -0
- package/types/runner/image.d.ts +52 -0
- package/types/runner/index.d.ts +13 -0
- package/types/runner/serve.d.ts +69 -0
- package/types/runner/toolsets.d.ts +35 -0
- package/types/tools.d.ts +2 -0
- package/index.js +0 -1
|
@@ -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
|
+
}
|