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,477 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { join, resolve } from 'node:path'
|
|
5
|
+
import {
|
|
6
|
+
defaultFirecracker,
|
|
7
|
+
firecrackerAvailable,
|
|
8
|
+
firecrackerHostReady,
|
|
9
|
+
startFirecracker,
|
|
10
|
+
type FirecrackerConfig,
|
|
11
|
+
type VmStartOptions,
|
|
12
|
+
} from './firecracker'
|
|
13
|
+
import { networkState, networkStatusLine } from './network'
|
|
14
|
+
import {
|
|
15
|
+
startVz,
|
|
16
|
+
vzAvailable,
|
|
17
|
+
vzHostReady,
|
|
18
|
+
vzNetworkState,
|
|
19
|
+
vzNetworkStatusLine,
|
|
20
|
+
} from './vz'
|
|
21
|
+
import { ociAvailable, startOci, type OciConfig } from './container'
|
|
22
|
+
import {
|
|
23
|
+
colimaMountFor,
|
|
24
|
+
colimaStatusLine,
|
|
25
|
+
detectColima,
|
|
26
|
+
systemColimaProbe,
|
|
27
|
+
type ColimaState,
|
|
28
|
+
} from './colima'
|
|
29
|
+
import {
|
|
30
|
+
hostUserState,
|
|
31
|
+
startHostUser,
|
|
32
|
+
systemProbe,
|
|
33
|
+
type HostUserState,
|
|
34
|
+
} from './host-user'
|
|
35
|
+
import {
|
|
36
|
+
IsolationRefused,
|
|
37
|
+
LEVELS,
|
|
38
|
+
labelFor,
|
|
39
|
+
resolveIsolation,
|
|
40
|
+
type Level,
|
|
41
|
+
type Resolved,
|
|
42
|
+
} from './policy'
|
|
43
|
+
import {
|
|
44
|
+
ensureVmImage,
|
|
45
|
+
vmImagePaths,
|
|
46
|
+
vmPlatform,
|
|
47
|
+
VmImageError,
|
|
48
|
+
type VmPlatform,
|
|
49
|
+
} from './image'
|
|
50
|
+
import type { Isolation } from './session'
|
|
51
|
+
|
|
52
|
+
export * from './policy'
|
|
53
|
+
export type { Isolation } from './session'
|
|
54
|
+
export { vmImagePaths } from './image'
|
|
55
|
+
|
|
56
|
+
const level = z.enum(LEVELS)
|
|
57
|
+
|
|
58
|
+
/** `<config dir>/isolation.json`: what the owner set once on this host. */
|
|
59
|
+
export const hostConfigSchema = (configDir: string) =>
|
|
60
|
+
z.strictObject({
|
|
61
|
+
floor: level.optional(),
|
|
62
|
+
default: level.optional(),
|
|
63
|
+
vm: z
|
|
64
|
+
.strictObject({
|
|
65
|
+
/** Linux: the bundled one next to mirab, else `firecracker` on PATH. */
|
|
66
|
+
firecracker: z.string().default(() => defaultFirecracker()),
|
|
67
|
+
/** macOS: the signed Virtualization.framework helper: the published
|
|
68
|
+
* one in <config dir>/vm when it was fetched, else `mirab-vz` on PATH
|
|
69
|
+
* (scripts/build-vz-helper.sh). */
|
|
70
|
+
vz: z
|
|
71
|
+
.string()
|
|
72
|
+
.default(() =>
|
|
73
|
+
existsSync(vmImagePaths(configDir).vz)
|
|
74
|
+
? vmImagePaths(configDir).vz
|
|
75
|
+
: 'mirab-vz',
|
|
76
|
+
),
|
|
77
|
+
kernel: z.string().default(vmImagePaths(configDir).kernel),
|
|
78
|
+
rootfs: z.string().default(vmImagePaths(configDir).rootfs),
|
|
79
|
+
memoryMiB: z.number().int().min(128).default(1024),
|
|
80
|
+
vcpus: z.number().int().min(1).default(2),
|
|
81
|
+
egress: z.enum(['auto', 'block', 'allow']).default('auto'),
|
|
82
|
+
})
|
|
83
|
+
.optional(),
|
|
84
|
+
container: z
|
|
85
|
+
.strictObject({
|
|
86
|
+
engine: z.string().default('docker'),
|
|
87
|
+
image: z.string().default('ubuntu:24.04'),
|
|
88
|
+
agent: z.string(),
|
|
89
|
+
memory: z.string().default('2g'),
|
|
90
|
+
cpus: z.string().default('2'),
|
|
91
|
+
pids: z.number().int().default(512),
|
|
92
|
+
egress: z.enum(['auto', 'block', 'allow']).default('auto'),
|
|
93
|
+
})
|
|
94
|
+
.optional(),
|
|
95
|
+
/** `host` level: the dedicated OS user scripts/host-user-setup.sh created. */
|
|
96
|
+
host: z
|
|
97
|
+
.strictObject({
|
|
98
|
+
user: z.string().regex(/^mirab-[a-z0-9-]{1,30}$/),
|
|
99
|
+
})
|
|
100
|
+
.optional(),
|
|
101
|
+
})
|
|
102
|
+
export type HostConfig = z.infer<ReturnType<typeof hostConfigSchema>>
|
|
103
|
+
export type VmConfig = NonNullable<HostConfig['vm']>
|
|
104
|
+
|
|
105
|
+
/** The `vm` level's backend: Virtualization.framework on macOS, Firecracker elsewhere. */
|
|
106
|
+
export const vmBackend = (platform: NodeJS.Platform = process.platform) =>
|
|
107
|
+
platform === 'darwin' ? 'vz' : 'firecracker'
|
|
108
|
+
|
|
109
|
+
export function vmAvailable(
|
|
110
|
+
vm: VmConfig,
|
|
111
|
+
platform: NodeJS.Platform = process.platform,
|
|
112
|
+
) {
|
|
113
|
+
return vmBackend(platform) === 'vz'
|
|
114
|
+
? vzAvailable({ helper: vm.vz, kernel: vm.kernel, rootfs: vm.rootfs })
|
|
115
|
+
: firecrackerAvailable(vm)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type { VmStartOptions } from './firecracker'
|
|
119
|
+
|
|
120
|
+
/** Boot one microVM with `work` at /workspace (null: no work directory and
|
|
121
|
+
* nothing of the host's inside), on this platform's backend. */
|
|
122
|
+
export function startVm(
|
|
123
|
+
vm: VmConfig,
|
|
124
|
+
work: string | null,
|
|
125
|
+
vmDir: string,
|
|
126
|
+
log?: (line: string) => void,
|
|
127
|
+
platform: NodeJS.Platform = process.platform,
|
|
128
|
+
opts: VmStartOptions = {},
|
|
129
|
+
): Promise<Isolation> {
|
|
130
|
+
if (vmBackend(platform) === 'vz')
|
|
131
|
+
return startVz(
|
|
132
|
+
{ ...vm, helper: vm.vz, port: 9002 },
|
|
133
|
+
work,
|
|
134
|
+
vmDir,
|
|
135
|
+
log,
|
|
136
|
+
undefined,
|
|
137
|
+
opts,
|
|
138
|
+
)
|
|
139
|
+
const cfg: FirecrackerConfig = { ...vm, port: 9002 }
|
|
140
|
+
return startFirecracker(cfg, work, vmDir, log, opts)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function loadHostConfig(configDir: string): Promise<HostConfig> {
|
|
144
|
+
try {
|
|
145
|
+
return hostConfigSchema(configDir).parse(
|
|
146
|
+
JSON.parse(await readFile(join(configDir, 'isolation.json'), 'utf8')),
|
|
147
|
+
)
|
|
148
|
+
} catch (e) {
|
|
149
|
+
if ((e as NodeJS.ErrnoException).code === 'ENOENT') return {}
|
|
150
|
+
throw new Error(`isolation.json: ${(e as Error).message}`)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The level a run asks for, before availability: what decides whether the
|
|
155
|
+
* published VM image is worth fetching. */
|
|
156
|
+
export const requestedLevel = (host: HostConfig, req: Request) =>
|
|
157
|
+
req.flag ?? req.job ?? req.profile ?? host.default
|
|
158
|
+
|
|
159
|
+
export interface PreparedVm {
|
|
160
|
+
host: HostConfig
|
|
161
|
+
/** Why the published VM image could not be fetched; `vm` then falls to the next tier. */
|
|
162
|
+
vmUnavailable?: string
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Load isolation.json and, when this run asks for `vm` and the owner did not
|
|
167
|
+
* point it at their own kernel and rootfs, make sure the published guest
|
|
168
|
+
* image for this mirab version is in <config dir>/vm (downloaded once, then
|
|
169
|
+
* used offline) and describe `vm` with it.
|
|
170
|
+
*/
|
|
171
|
+
export async function prepareVm(
|
|
172
|
+
configDir: string,
|
|
173
|
+
o: {
|
|
174
|
+
want: boolean
|
|
175
|
+
log?: (line: string) => void
|
|
176
|
+
base?: string
|
|
177
|
+
/** The mirab release whose image to use (default: this copy's own). A
|
|
178
|
+
* package that embeds mirab from git names the canary of that commit. */
|
|
179
|
+
version?: string
|
|
180
|
+
platform?: VmPlatform
|
|
181
|
+
/** The backend's own prerequisites (Firecracker, /dev/kvm) are present. */
|
|
182
|
+
backendReady?: (host: HostConfig) => boolean
|
|
183
|
+
},
|
|
184
|
+
): Promise<PreparedVm> {
|
|
185
|
+
const host = await loadHostConfig(configDir)
|
|
186
|
+
if (!o.want) return { host }
|
|
187
|
+
const paths = vmImagePaths(configDir)
|
|
188
|
+
if (
|
|
189
|
+
host.vm &&
|
|
190
|
+
(resolve(host.vm.kernel) !== paths.kernel ||
|
|
191
|
+
resolve(host.vm.rootfs) !== paths.rootfs)
|
|
192
|
+
)
|
|
193
|
+
return { host } // the owner's own image: theirs to manage
|
|
194
|
+
const platform = o.platform ?? vmPlatform()
|
|
195
|
+
const ready =
|
|
196
|
+
o.backendReady ??
|
|
197
|
+
((h: HostConfig) =>
|
|
198
|
+
vmBackend() === 'vz'
|
|
199
|
+
? vzHostReady()
|
|
200
|
+
: firecrackerHostReady(h.vm?.firecracker ?? defaultFirecracker()))
|
|
201
|
+
// No Firecracker, /dev/kvm or hypervisor: an image would not help; vmAvailable says what is missing.
|
|
202
|
+
if (platform && !ready(host)) return { host }
|
|
203
|
+
try {
|
|
204
|
+
await ensureVmImage({
|
|
205
|
+
configDir,
|
|
206
|
+
log: o.log,
|
|
207
|
+
base: o.base,
|
|
208
|
+
version: o.version,
|
|
209
|
+
platform,
|
|
210
|
+
})
|
|
211
|
+
} catch (e) {
|
|
212
|
+
if (!(e instanceof VmImageError)) throw e
|
|
213
|
+
return { host, vmUnavailable: e.message }
|
|
214
|
+
}
|
|
215
|
+
// Load again after the download: the vz helper default looks in <config dir>/vm.
|
|
216
|
+
const fresh = await loadHostConfig(configDir)
|
|
217
|
+
return {
|
|
218
|
+
host: {
|
|
219
|
+
...fresh,
|
|
220
|
+
vm: fresh.vm ?? hostConfigSchema(configDir).parse({ vm: {} }).vm,
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** What this machine can do; tests pass their own. */
|
|
226
|
+
export interface Probes {
|
|
227
|
+
platform: NodeJS.Platform
|
|
228
|
+
/** The `vm` backend (vmBackend) works with this config. */
|
|
229
|
+
vm(vm: VmConfig): boolean
|
|
230
|
+
oci(engine: string, agent: string, endpoint?: string): boolean
|
|
231
|
+
colima(): ColimaState
|
|
232
|
+
hostUser(user: string): HostUserState
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export const systemProbes = (): Probes => ({
|
|
236
|
+
platform: process.platform,
|
|
237
|
+
vm: (v) => vmAvailable(v),
|
|
238
|
+
oci: ociAvailable,
|
|
239
|
+
colima: () => detectColima(systemColimaProbe()),
|
|
240
|
+
hostUser: (user) => hostUserState(user, systemProbe()),
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
export interface Detected {
|
|
244
|
+
levels: Level[]
|
|
245
|
+
/** One line per backend that is configured or detected, for `mirab status`. */
|
|
246
|
+
notes: string[]
|
|
247
|
+
colima?: ColimaState
|
|
248
|
+
hostUser?: HostUserState
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function detect(host: HostConfig, p: Probes = systemProbes()): Detected {
|
|
252
|
+
const levels: Level[] = [],
|
|
253
|
+
notes: string[] = []
|
|
254
|
+
if (host.vm && p.vm(host.vm)) levels.push('vm')
|
|
255
|
+
let colima: ColimaState | undefined
|
|
256
|
+
if (p.platform === 'darwin') {
|
|
257
|
+
// macOS has no local OCI kernel: containers run inside Colima's VM.
|
|
258
|
+
colima = p.colima()
|
|
259
|
+
notes.push(colimaStatusLine(colima))
|
|
260
|
+
if (host.container && colima.running) {
|
|
261
|
+
if (!colimaMountFor(host.container.agent, colima.mounts, false))
|
|
262
|
+
notes.push(
|
|
263
|
+
`container: agent ${host.container.agent} is outside Colima's mounts`,
|
|
264
|
+
)
|
|
265
|
+
else if (
|
|
266
|
+
p.oci(host.container.engine, host.container.agent, colima.socket)
|
|
267
|
+
)
|
|
268
|
+
levels.push('container')
|
|
269
|
+
}
|
|
270
|
+
} else if (
|
|
271
|
+
host.container &&
|
|
272
|
+
p.oci(host.container.engine, host.container.agent)
|
|
273
|
+
)
|
|
274
|
+
levels.push('container')
|
|
275
|
+
let hostUser: HostUserState | undefined
|
|
276
|
+
if (host.host) {
|
|
277
|
+
hostUser = p.hostUser(host.host.user)
|
|
278
|
+
notes.push(
|
|
279
|
+
hostUser.ok
|
|
280
|
+
? `host tier: configured, steps run as ${host.host.user} (uid ${hostUser.marker.uid}), LAN ${hostUser.marker.network === 'lan-denied' ? 'denied' : 'not filtered'}`
|
|
281
|
+
: `host tier: not configured (${hostUser.reason}); ${hostUser.fix}`,
|
|
282
|
+
)
|
|
283
|
+
if (hostUser.ok) levels.push('host')
|
|
284
|
+
}
|
|
285
|
+
levels.push('none')
|
|
286
|
+
return { levels, notes, colima, hostUser }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function availableLevels(host: HostConfig, p?: Probes): Level[] {
|
|
290
|
+
return detect(host, p).levels
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export interface Request {
|
|
294
|
+
flag?: Level
|
|
295
|
+
job?: Level
|
|
296
|
+
profile?: Level
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Resolve a run's isolation. With `vmUnavailable` (the published VM image
|
|
301
|
+
* could not be fetched), a request for `vm` falls to the strongest other
|
|
302
|
+
* tier the floor allows, never to `none`, and the result says so.
|
|
303
|
+
*/
|
|
304
|
+
export function resolveForHost(
|
|
305
|
+
host: HostConfig,
|
|
306
|
+
req: Request,
|
|
307
|
+
p?: Probes,
|
|
308
|
+
vmUnavailable?: string,
|
|
309
|
+
): Resolved {
|
|
310
|
+
const available = availableLevels(host, p)
|
|
311
|
+
const input = {
|
|
312
|
+
...req,
|
|
313
|
+
hostDefault: host.default,
|
|
314
|
+
floor: host.floor,
|
|
315
|
+
available,
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
return resolveIsolation(input)
|
|
319
|
+
} catch (e) {
|
|
320
|
+
if (
|
|
321
|
+
!vmUnavailable ||
|
|
322
|
+
!(e instanceof IsolationRefused) ||
|
|
323
|
+
requestedLevel(host, req) !== 'vm' ||
|
|
324
|
+
available.includes('vm')
|
|
325
|
+
)
|
|
326
|
+
throw e
|
|
327
|
+
let next: Resolved
|
|
328
|
+
try {
|
|
329
|
+
next = resolveIsolation({ floor: host.floor, available })
|
|
330
|
+
} catch (e2) {
|
|
331
|
+
throw new IsolationRefused(
|
|
332
|
+
`isolation "vm" is unavailable: ${vmUnavailable}; and no other tier can take it: ${(e2 as Error).message}`,
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
const source = resolveIsolation({
|
|
336
|
+
...input,
|
|
337
|
+
available: [...available, 'vm'],
|
|
338
|
+
}).source
|
|
339
|
+
return {
|
|
340
|
+
level: next.level,
|
|
341
|
+
source,
|
|
342
|
+
fallback: `vm image unavailable, fell back to ${next.level}: ${vmUnavailable}`,
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const BACKEND: Record<Exclude<Level, 'vm'>, string> = {
|
|
348
|
+
container: 'oci',
|
|
349
|
+
host: 'dedicated OS user',
|
|
350
|
+
none: 'none',
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export const backendName = (level: Level, platform = process.platform) =>
|
|
354
|
+
level === 'vm'
|
|
355
|
+
? vmBackend(platform)
|
|
356
|
+
: level === 'container' && platform === 'darwin'
|
|
357
|
+
? 'oci in Colima VM'
|
|
358
|
+
: BACKEND[level]
|
|
359
|
+
|
|
360
|
+
/** One line for `mirab status` and job logs. */
|
|
361
|
+
export function describe(resolved: Resolved, backend?: string) {
|
|
362
|
+
const line = `isolation: ${labelFor(resolved.level, backend ?? backendName(resolved.level))}, requested by ${resolved.source}`
|
|
363
|
+
return resolved.fallback ? `${line} (${resolved.fallback})` : line
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export async function startIsolation(
|
|
367
|
+
level: Level,
|
|
368
|
+
host: HostConfig,
|
|
369
|
+
work: string,
|
|
370
|
+
vmDir: string,
|
|
371
|
+
log?: (line: string) => void,
|
|
372
|
+
p: Probes = systemProbes(),
|
|
373
|
+
): Promise<Isolation | null> {
|
|
374
|
+
if (level === 'vm') return startVm(host.vm!, work, vmDir, log, p.platform)
|
|
375
|
+
if (level === 'container') {
|
|
376
|
+
const c: OciConfig = host.container!
|
|
377
|
+
if (p.platform !== 'darwin') return startOci(c, work, log)
|
|
378
|
+
const colima = p.colima()
|
|
379
|
+
if (!colima.running)
|
|
380
|
+
throw new Error(`container: ${colima.reason}; ${colima.fix}`)
|
|
381
|
+
if (!colimaMountFor(work, colima.mounts, true))
|
|
382
|
+
throw new Error(
|
|
383
|
+
`container: ${work} is not inside a writable Colima mount (${colima.mounts.map((m) => m.location).join(', ')}). ` +
|
|
384
|
+
`Needs Sami: add it under mounts in ~/.colima/default/colima.yaml, then colima restart`,
|
|
385
|
+
)
|
|
386
|
+
return startOci(
|
|
387
|
+
{ ...c, host: colima.socket, where: `Colima ${colima.vmType} VM` },
|
|
388
|
+
work,
|
|
389
|
+
log,
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
if (level === 'host') {
|
|
393
|
+
const s = p.hostUser(host.host!.user)
|
|
394
|
+
if (!s.ok) throw new Error(`host tier: ${s.reason}; ${s.fix}`)
|
|
395
|
+
return startHostUser(s.marker, work, log)
|
|
396
|
+
}
|
|
397
|
+
return null // none: the runner keeps its existing local execution
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function statusLines(
|
|
401
|
+
host: HostConfig,
|
|
402
|
+
requested: Request = {},
|
|
403
|
+
p: Probes = systemProbes(),
|
|
404
|
+
) {
|
|
405
|
+
const found = detect(host, p),
|
|
406
|
+
available = found.levels
|
|
407
|
+
const lines = [
|
|
408
|
+
...(host.vm
|
|
409
|
+
? [
|
|
410
|
+
vmBackend(p.platform) === 'vz'
|
|
411
|
+
? vzNetworkStatusLine(host.vm.egress, vzNetworkState())
|
|
412
|
+
: networkStatusLine(host.vm.egress, networkState()),
|
|
413
|
+
]
|
|
414
|
+
: []),
|
|
415
|
+
...found.notes,
|
|
416
|
+
`isolation backends available: ${available.filter((l) => l !== 'none').join(', ') || 'none'}`,
|
|
417
|
+
`isolation floor: ${host.floor ?? 'none set'}`,
|
|
418
|
+
`isolation host default: ${host.default ?? 'none set'}`,
|
|
419
|
+
]
|
|
420
|
+
try {
|
|
421
|
+
const r = resolveIsolation({
|
|
422
|
+
...requested,
|
|
423
|
+
hostDefault: host.default,
|
|
424
|
+
floor: host.floor,
|
|
425
|
+
available,
|
|
426
|
+
})
|
|
427
|
+
lines.push(describe(r, backendName(r.level, p.platform)))
|
|
428
|
+
} catch (e) {
|
|
429
|
+
lines.push(`isolation: refused (${(e as Error).message})`)
|
|
430
|
+
}
|
|
431
|
+
return lines
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Called once before a session starts: one that cannot isolate does not start. */
|
|
435
|
+
export function assertCanIsolate(
|
|
436
|
+
host: HostConfig,
|
|
437
|
+
flag?: Level,
|
|
438
|
+
p?: Probes,
|
|
439
|
+
vmUnavailable?: string,
|
|
440
|
+
) {
|
|
441
|
+
try {
|
|
442
|
+
return resolveForHost(host, { flag }, p, vmUnavailable)
|
|
443
|
+
} catch (e) {
|
|
444
|
+
const message = (e as Error).message
|
|
445
|
+
// Only the "nothing available" case needs the how-to-fix text; a floor or
|
|
446
|
+
// availability refusal already says exactly what is wrong.
|
|
447
|
+
throw new Error(
|
|
448
|
+
message.includes('no isolation backend')
|
|
449
|
+
? `${message}. This runner would run workflow steps with no isolation. ` +
|
|
450
|
+
`Fix it by describing a microVM, container or host-user backend in <config dir>/isolation.json ` +
|
|
451
|
+
`(see FINDINGS.md), or start with --isolation none to run unisolated on purpose.`
|
|
452
|
+
: message,
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** The VM this machine runs (the published image, fetched on first use), or why not. */
|
|
458
|
+
export async function requireVm(
|
|
459
|
+
configDir: string,
|
|
460
|
+
log?: (line: string) => void,
|
|
461
|
+
version?: string,
|
|
462
|
+
): Promise<VmConfig> {
|
|
463
|
+
// Fetched on first use; later runs find it installed.
|
|
464
|
+
const { host, vmUnavailable } = await prepareVm(configDir, {
|
|
465
|
+
want: true,
|
|
466
|
+
log,
|
|
467
|
+
version,
|
|
468
|
+
})
|
|
469
|
+
if (vmUnavailable) throw new Error(vmUnavailable)
|
|
470
|
+
if (!host.vm || !vmAvailable(host.vm))
|
|
471
|
+
throw new Error(
|
|
472
|
+
vmBackend() === 'vz'
|
|
473
|
+
? 'A VM needs a working vz setup: the published guest image and mirab-vz helper (fetched on first use), or `vm` (kernel, rootfs, vz helper) in <config dir>/isolation.json (docs/VM.md)'
|
|
474
|
+
: 'A VM needs a working Firecracker setup: Firecracker (shipped next to mirab in the npm package; else on PATH or `vm.firecracker` in <config dir>/isolation.json), mke2fs, and an accessible /dev/kvm (docs/VM.md)',
|
|
475
|
+
)
|
|
476
|
+
return host.vm
|
|
477
|
+
}
|