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.
- package/LICENSE +201 -0
- package/README.md +226 -2
- package/package.json +58 -4
- 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
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Where mirab's releases and guest images live. */
|
|
3
|
+
export declare const RELEASES = "https://mirab-releases.addis.gg";
|
|
4
|
+
export declare const MIRAB_VERSION: string;
|
|
5
|
+
/** The release this copy of mirab was installed from (`0.1.0-canary-abc1234`):
|
|
6
|
+
* the release workflow stamps release.json into what it ships. From a
|
|
7
|
+
* checkout there is none, and the package version stands in. */
|
|
8
|
+
export declare const MIRAB_RELEASE: string;
|
|
9
|
+
/** Where the guest kernel, rootfs and (macOS) vz helper live, published or
|
|
10
|
+
* built by `bun run vm:image`; `vm` uses them when isolation.json names no
|
|
11
|
+
* other files. */
|
|
12
|
+
export declare function vmImagePaths(configDir: string): {
|
|
13
|
+
dir: string;
|
|
14
|
+
kernel: string;
|
|
15
|
+
rootfs: string;
|
|
16
|
+
vz: string;
|
|
17
|
+
record: string;
|
|
18
|
+
};
|
|
19
|
+
/** The VM platforms a release publishes, and the file each role is built as. */
|
|
20
|
+
export declare const VM_PLATFORMS: {
|
|
21
|
+
readonly 'linux-x64': {
|
|
22
|
+
readonly kernel: 'vmlinux';
|
|
23
|
+
readonly rootfs: 'rootfs.ext4';
|
|
24
|
+
};
|
|
25
|
+
readonly 'darwin-x64': {
|
|
26
|
+
readonly kernel: 'bzImage';
|
|
27
|
+
readonly rootfs: 'rootfs.ext4';
|
|
28
|
+
readonly vz: 'mirab-vz';
|
|
29
|
+
};
|
|
30
|
+
readonly 'darwin-arm64': {
|
|
31
|
+
readonly kernel: 'Image';
|
|
32
|
+
readonly rootfs: 'rootfs.ext4';
|
|
33
|
+
readonly vz: 'mirab-vz';
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export type VmPlatform = keyof typeof VM_PLATFORMS;
|
|
37
|
+
export type VmRole = 'kernel' | 'rootfs' | 'vz';
|
|
38
|
+
export declare const vmPlatform: (platform?: string, arch?: string) => VmPlatform | undefined;
|
|
39
|
+
declare const entry: z.ZodObject<{
|
|
40
|
+
file: z.ZodString;
|
|
41
|
+
sha256: z.ZodString;
|
|
42
|
+
size: z.ZodNumber;
|
|
43
|
+
unpacked: z.ZodObject<{
|
|
44
|
+
sha256: z.ZodString;
|
|
45
|
+
size: z.ZodNumber;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
}, z.core.$strip>;
|
|
48
|
+
export type VmFileEntry = z.infer<typeof entry>;
|
|
49
|
+
export declare const vmManifestSchema: z.ZodObject<{
|
|
50
|
+
version: z.ZodString;
|
|
51
|
+
protocol: z.ZodNumber;
|
|
52
|
+
commit: z.ZodString;
|
|
53
|
+
platforms: z.ZodObject<{
|
|
54
|
+
'linux-x64': z.ZodOptional<z.ZodObject<{
|
|
55
|
+
kernel: z.ZodObject<{
|
|
56
|
+
file: z.ZodString;
|
|
57
|
+
sha256: z.ZodString;
|
|
58
|
+
size: z.ZodNumber;
|
|
59
|
+
unpacked: z.ZodObject<{
|
|
60
|
+
sha256: z.ZodString;
|
|
61
|
+
size: z.ZodNumber;
|
|
62
|
+
}, z.core.$strip>;
|
|
63
|
+
}, z.core.$strip>;
|
|
64
|
+
rootfs: z.ZodObject<{
|
|
65
|
+
file: z.ZodString;
|
|
66
|
+
sha256: z.ZodString;
|
|
67
|
+
size: z.ZodNumber;
|
|
68
|
+
unpacked: z.ZodObject<{
|
|
69
|
+
sha256: z.ZodString;
|
|
70
|
+
size: z.ZodNumber;
|
|
71
|
+
}, z.core.$strip>;
|
|
72
|
+
}, z.core.$strip>;
|
|
73
|
+
}, z.core.$strip>>;
|
|
74
|
+
'darwin-x64': z.ZodOptional<z.ZodObject<{
|
|
75
|
+
kernel: z.ZodObject<{
|
|
76
|
+
file: z.ZodString;
|
|
77
|
+
sha256: z.ZodString;
|
|
78
|
+
size: z.ZodNumber;
|
|
79
|
+
unpacked: z.ZodObject<{
|
|
80
|
+
sha256: z.ZodString;
|
|
81
|
+
size: z.ZodNumber;
|
|
82
|
+
}, z.core.$strip>;
|
|
83
|
+
}, z.core.$strip>;
|
|
84
|
+
rootfs: z.ZodObject<{
|
|
85
|
+
file: z.ZodString;
|
|
86
|
+
sha256: z.ZodString;
|
|
87
|
+
size: z.ZodNumber;
|
|
88
|
+
unpacked: z.ZodObject<{
|
|
89
|
+
sha256: z.ZodString;
|
|
90
|
+
size: z.ZodNumber;
|
|
91
|
+
}, z.core.$strip>;
|
|
92
|
+
}, z.core.$strip>;
|
|
93
|
+
vz: z.ZodObject<{
|
|
94
|
+
file: z.ZodString;
|
|
95
|
+
sha256: z.ZodString;
|
|
96
|
+
size: z.ZodNumber;
|
|
97
|
+
unpacked: z.ZodObject<{
|
|
98
|
+
sha256: z.ZodString;
|
|
99
|
+
size: z.ZodNumber;
|
|
100
|
+
}, z.core.$strip>;
|
|
101
|
+
}, z.core.$strip>;
|
|
102
|
+
}, z.core.$strip>>;
|
|
103
|
+
'darwin-arm64': z.ZodOptional<z.ZodObject<{
|
|
104
|
+
kernel: z.ZodObject<{
|
|
105
|
+
file: z.ZodString;
|
|
106
|
+
sha256: z.ZodString;
|
|
107
|
+
size: z.ZodNumber;
|
|
108
|
+
unpacked: z.ZodObject<{
|
|
109
|
+
sha256: z.ZodString;
|
|
110
|
+
size: z.ZodNumber;
|
|
111
|
+
}, z.core.$strip>;
|
|
112
|
+
}, z.core.$strip>;
|
|
113
|
+
rootfs: z.ZodObject<{
|
|
114
|
+
file: z.ZodString;
|
|
115
|
+
sha256: z.ZodString;
|
|
116
|
+
size: z.ZodNumber;
|
|
117
|
+
unpacked: z.ZodObject<{
|
|
118
|
+
sha256: z.ZodString;
|
|
119
|
+
size: z.ZodNumber;
|
|
120
|
+
}, z.core.$strip>;
|
|
121
|
+
}, z.core.$strip>;
|
|
122
|
+
vz: z.ZodObject<{
|
|
123
|
+
file: z.ZodString;
|
|
124
|
+
sha256: z.ZodString;
|
|
125
|
+
size: z.ZodNumber;
|
|
126
|
+
unpacked: z.ZodObject<{
|
|
127
|
+
sha256: z.ZodString;
|
|
128
|
+
size: z.ZodNumber;
|
|
129
|
+
}, z.core.$strip>;
|
|
130
|
+
}, z.core.$strip>;
|
|
131
|
+
}, z.core.$strip>>;
|
|
132
|
+
}, z.core.$strip>;
|
|
133
|
+
}, z.core.$strip>;
|
|
134
|
+
export type VmManifest = z.infer<typeof vmManifestSchema>;
|
|
135
|
+
export declare class VmImageError extends Error {
|
|
136
|
+
}
|
|
137
|
+
/** What the next tier hint says when no image could be fetched. */
|
|
138
|
+
export declare const VM_IMAGE_FALLBACK_HINT = "connect and retry, build the image yourself (docs/VM.md), or use --isolation container|host";
|
|
139
|
+
/** The published image for this CLI version and platform is installed and intact (cheap: sizes). */
|
|
140
|
+
export declare function releaseImageInstalled(configDir: string, version?: string, platform?: "darwin-arm64" | "darwin-x64" | "linux-x64" | undefined): boolean;
|
|
141
|
+
/** `bun run vm:image` (or another hand build) owns <config dir>/vm: never overwrite it. */
|
|
142
|
+
export declare const localImageInstalled: (configDir: string) => boolean;
|
|
143
|
+
export interface EnsureOptions {
|
|
144
|
+
configDir: string;
|
|
145
|
+
version?: string;
|
|
146
|
+
platform?: VmPlatform;
|
|
147
|
+
/** The release root (cli/ on the bucket); tests point it at a fake. */
|
|
148
|
+
base?: string;
|
|
149
|
+
log?: (line: string) => void;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Make sure <config dir>/vm holds the published guest image for this mirab
|
|
153
|
+
* version and platform: nothing to do when it is already there (offline is
|
|
154
|
+
* fine), otherwise download, verify and install it. Throws VmImageError with
|
|
155
|
+
* the reason and the fallback when it cannot.
|
|
156
|
+
*/
|
|
157
|
+
export declare function ensureVmImage(o: EnsureOptions): Promise<'installed' | 'downloaded' | 'local'>;
|
|
158
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type VmStartOptions } from './firecracker';
|
|
3
|
+
import { type ColimaState } from './colima';
|
|
4
|
+
import { type HostUserState } from './host-user';
|
|
5
|
+
import { type Level, type Resolved } from './policy';
|
|
6
|
+
import { type VmPlatform } from './image';
|
|
7
|
+
import type { Isolation } from './session';
|
|
8
|
+
export * from './policy';
|
|
9
|
+
export type { Isolation } from './session';
|
|
10
|
+
export { vmImagePaths } from './image';
|
|
11
|
+
/** `<config dir>/isolation.json`: what the owner set once on this host. */
|
|
12
|
+
export declare const hostConfigSchema: (configDir: string) => z.ZodObject<{
|
|
13
|
+
floor: z.ZodOptional<z.ZodEnum<{
|
|
14
|
+
container: "container";
|
|
15
|
+
host: "host";
|
|
16
|
+
none: "none";
|
|
17
|
+
vm: "vm";
|
|
18
|
+
}>>;
|
|
19
|
+
default: z.ZodOptional<z.ZodEnum<{
|
|
20
|
+
container: "container";
|
|
21
|
+
host: "host";
|
|
22
|
+
none: "none";
|
|
23
|
+
vm: "vm";
|
|
24
|
+
}>>;
|
|
25
|
+
vm: z.ZodOptional<z.ZodObject<{
|
|
26
|
+
firecracker: z.ZodDefault<z.ZodString>;
|
|
27
|
+
vz: z.ZodDefault<z.ZodString>;
|
|
28
|
+
kernel: z.ZodDefault<z.ZodString>;
|
|
29
|
+
rootfs: z.ZodDefault<z.ZodString>;
|
|
30
|
+
memoryMiB: z.ZodDefault<z.ZodNumber>;
|
|
31
|
+
vcpus: z.ZodDefault<z.ZodNumber>;
|
|
32
|
+
egress: z.ZodDefault<z.ZodEnum<{
|
|
33
|
+
allow: "allow";
|
|
34
|
+
auto: "auto";
|
|
35
|
+
block: "block";
|
|
36
|
+
}>>;
|
|
37
|
+
}, z.core.$strict>>;
|
|
38
|
+
container: z.ZodOptional<z.ZodObject<{
|
|
39
|
+
engine: z.ZodDefault<z.ZodString>;
|
|
40
|
+
image: z.ZodDefault<z.ZodString>;
|
|
41
|
+
agent: z.ZodString;
|
|
42
|
+
memory: z.ZodDefault<z.ZodString>;
|
|
43
|
+
cpus: z.ZodDefault<z.ZodString>;
|
|
44
|
+
pids: z.ZodDefault<z.ZodNumber>;
|
|
45
|
+
egress: z.ZodDefault<z.ZodEnum<{
|
|
46
|
+
allow: "allow";
|
|
47
|
+
auto: "auto";
|
|
48
|
+
block: "block";
|
|
49
|
+
}>>;
|
|
50
|
+
}, z.core.$strict>>;
|
|
51
|
+
host: z.ZodOptional<z.ZodObject<{
|
|
52
|
+
user: z.ZodString;
|
|
53
|
+
}, z.core.$strict>>;
|
|
54
|
+
}, z.core.$strict>;
|
|
55
|
+
export type HostConfig = z.infer<ReturnType<typeof hostConfigSchema>>;
|
|
56
|
+
export type VmConfig = NonNullable<HostConfig['vm']>;
|
|
57
|
+
/** The `vm` level's backend: Virtualization.framework on macOS, Firecracker elsewhere. */
|
|
58
|
+
export declare const vmBackend: (platform?: NodeJS.Platform) => "firecracker" | "vz";
|
|
59
|
+
export declare function vmAvailable(vm: VmConfig, platform?: NodeJS.Platform): boolean;
|
|
60
|
+
export type { VmStartOptions } from './firecracker';
|
|
61
|
+
/** Boot one microVM with `work` at /workspace (null: no work directory and
|
|
62
|
+
* nothing of the host's inside), on this platform's backend. */
|
|
63
|
+
export declare function startVm(vm: VmConfig, work: string | null, vmDir: string, log?: (line: string) => void, platform?: NodeJS.Platform, opts?: VmStartOptions): Promise<Isolation>;
|
|
64
|
+
export declare function loadHostConfig(configDir: string): Promise<HostConfig>;
|
|
65
|
+
/** The level a run asks for, before availability: what decides whether the
|
|
66
|
+
* published VM image is worth fetching. */
|
|
67
|
+
export declare const requestedLevel: (host: HostConfig, req: Request) => "container" | "host" | "none" | "vm" | undefined;
|
|
68
|
+
export interface PreparedVm {
|
|
69
|
+
host: HostConfig;
|
|
70
|
+
/** Why the published VM image could not be fetched; `vm` then falls to the next tier. */
|
|
71
|
+
vmUnavailable?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Load isolation.json and, when this run asks for `vm` and the owner did not
|
|
75
|
+
* point it at their own kernel and rootfs, make sure the published guest
|
|
76
|
+
* image for this mirab version is in <config dir>/vm (downloaded once, then
|
|
77
|
+
* used offline) and describe `vm` with it.
|
|
78
|
+
*/
|
|
79
|
+
export declare function prepareVm(configDir: string, o: {
|
|
80
|
+
want: boolean;
|
|
81
|
+
log?: (line: string) => void;
|
|
82
|
+
base?: string;
|
|
83
|
+
/** The mirab release whose image to use (default: this copy's own). A
|
|
84
|
+
* package that embeds mirab from git names the canary of that commit. */
|
|
85
|
+
version?: string;
|
|
86
|
+
platform?: VmPlatform;
|
|
87
|
+
/** The backend's own prerequisites (Firecracker, /dev/kvm) are present. */
|
|
88
|
+
backendReady?: (host: HostConfig) => boolean;
|
|
89
|
+
}): Promise<PreparedVm>;
|
|
90
|
+
/** What this machine can do; tests pass their own. */
|
|
91
|
+
export interface Probes {
|
|
92
|
+
platform: NodeJS.Platform;
|
|
93
|
+
/** The `vm` backend (vmBackend) works with this config. */
|
|
94
|
+
vm(vm: VmConfig): boolean;
|
|
95
|
+
oci(engine: string, agent: string, endpoint?: string): boolean;
|
|
96
|
+
colima(): ColimaState;
|
|
97
|
+
hostUser(user: string): HostUserState;
|
|
98
|
+
}
|
|
99
|
+
export declare const systemProbes: () => Probes;
|
|
100
|
+
export interface Detected {
|
|
101
|
+
levels: Level[];
|
|
102
|
+
/** One line per backend that is configured or detected, for `mirab status`. */
|
|
103
|
+
notes: string[];
|
|
104
|
+
colima?: ColimaState;
|
|
105
|
+
hostUser?: HostUserState;
|
|
106
|
+
}
|
|
107
|
+
export declare function detect(host: HostConfig, p?: Probes): Detected;
|
|
108
|
+
export declare function availableLevels(host: HostConfig, p?: Probes): Level[];
|
|
109
|
+
export interface Request {
|
|
110
|
+
flag?: Level;
|
|
111
|
+
job?: Level;
|
|
112
|
+
profile?: Level;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve a run's isolation. With `vmUnavailable` (the published VM image
|
|
116
|
+
* could not be fetched), a request for `vm` falls to the strongest other
|
|
117
|
+
* tier the floor allows, never to `none`, and the result says so.
|
|
118
|
+
*/
|
|
119
|
+
export declare function resolveForHost(host: HostConfig, req: Request, p?: Probes, vmUnavailable?: string): Resolved;
|
|
120
|
+
export declare const backendName: (level: Level, platform?: NodeJS.Platform) => string;
|
|
121
|
+
/** One line for `mirab status` and job logs. */
|
|
122
|
+
export declare function describe(resolved: Resolved, backend?: string): string;
|
|
123
|
+
export declare function startIsolation(level: Level, host: HostConfig, work: string, vmDir: string, log?: (line: string) => void, p?: Probes): Promise<Isolation | null>;
|
|
124
|
+
export declare function statusLines(host: HostConfig, requested?: Request, p?: Probes): string[];
|
|
125
|
+
/** Called once before a session starts: one that cannot isolate does not start. */
|
|
126
|
+
export declare function assertCanIsolate(host: HostConfig, flag?: Level, p?: Probes, vmUnavailable?: string): Resolved;
|
|
127
|
+
/** The VM this machine runs (the published image, fetched on first use), or why not. */
|
|
128
|
+
export declare function requireVm(configDir: string, log?: (line: string) => void, version?: string): Promise<VmConfig>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const BRIDGE = "mirabbr0";
|
|
2
|
+
export declare const SETUP_COMMAND = "sudo scripts/vm-network-setup.sh";
|
|
3
|
+
export interface NetworkState {
|
|
4
|
+
ready: boolean;
|
|
5
|
+
/** Tap slots this user can use. */
|
|
6
|
+
slots: number[];
|
|
7
|
+
reason?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function networkState(uid?: number, sys?: string): NetworkState;
|
|
10
|
+
export declare function guestNetwork(slot: number): {
|
|
11
|
+
tap: string;
|
|
12
|
+
mac: string;
|
|
13
|
+
ip: string;
|
|
14
|
+
cidr: number;
|
|
15
|
+
gateway: string;
|
|
16
|
+
dns: string;
|
|
17
|
+
};
|
|
18
|
+
/** Kernel args read by /init-mirab.sh in the guest. */
|
|
19
|
+
export declare function bootNetArgs(n: ReturnType<typeof guestNetwork>): string;
|
|
20
|
+
/** Claim a free tap slot with a pid lock file; stale locks of dead pids are taken over. */
|
|
21
|
+
export declare function claimTap(slots: number[], lockDir: string): {
|
|
22
|
+
slot: number;
|
|
23
|
+
release: () => void;
|
|
24
|
+
};
|
|
25
|
+
export declare function networkStatusLine(mode: 'auto' | 'block' | 'allow', s: NetworkState): string;
|
|
26
|
+
export interface NetworkPlan {
|
|
27
|
+
net?: ReturnType<typeof guestNetwork>;
|
|
28
|
+
release: () => void;
|
|
29
|
+
/** Printed in the job log. */
|
|
30
|
+
note: string;
|
|
31
|
+
}
|
|
32
|
+
/** Decide whether a VM gets a network device. `allow` without setup is an error, `auto` degrades loudly. */
|
|
33
|
+
export declare function planNetwork(mode: 'auto' | 'block' | 'allow', state: NetworkState, lockDir: string): NetworkPlan;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const LEVELS: readonly ['vm', 'container', 'host', 'none'];
|
|
2
|
+
export type Level = (typeof LEVELS)[number];
|
|
3
|
+
export declare const strength: (level: Level) => number;
|
|
4
|
+
export type RequestSource = 'flag' | 'job' | 'profile' | 'host-default' | 'auto';
|
|
5
|
+
export interface PolicyInput {
|
|
6
|
+
/** `--isolation` on this run. Most specific, wins over everything below. */
|
|
7
|
+
flag?: Level;
|
|
8
|
+
/** `isolation:` on the workflow job. */
|
|
9
|
+
job?: Level;
|
|
10
|
+
/** `isolation` in the agent profile. */
|
|
11
|
+
profile?: Level;
|
|
12
|
+
/** The host default the owner set once in the host config. */
|
|
13
|
+
hostDefault?: Level;
|
|
14
|
+
/** Owner floor: levels weaker than this are refused, never upgraded. */
|
|
15
|
+
floor?: Level;
|
|
16
|
+
/** Backends usable on this machine. */
|
|
17
|
+
available: readonly Level[];
|
|
18
|
+
}
|
|
19
|
+
export interface Resolved {
|
|
20
|
+
level: Level;
|
|
21
|
+
source: RequestSource;
|
|
22
|
+
/** Set when the requested level could not be had and a weaker tier took the run. */
|
|
23
|
+
fallback?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare class IsolationRefused extends Error {
|
|
26
|
+
}
|
|
27
|
+
export declare function parseLevel(text: string | undefined, where: string): Level | undefined;
|
|
28
|
+
export declare function resolveIsolation(input: PolicyInput): Resolved;
|
|
29
|
+
export declare const labelFor: (level: Level, backend: string) => string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Level } from './policy';
|
|
2
|
+
/** The guest-agent wire protocol this CLI speaks; guest-agent/src/main.rs reads the same file. */
|
|
3
|
+
export declare const GUEST_PROTOCOL: number;
|
|
4
|
+
/** Where the guest image is rebuilt from; printed with every mismatch. */
|
|
5
|
+
export declare const REBUILD_HINT: string;
|
|
6
|
+
export interface Channel {
|
|
7
|
+
write(data: Uint8Array): void;
|
|
8
|
+
onData(cb: (chunk: Buffer) => void): void;
|
|
9
|
+
onClose(cb: (error?: Error) => void): void;
|
|
10
|
+
close(): void;
|
|
11
|
+
}
|
|
12
|
+
export interface ExecOptions {
|
|
13
|
+
cwd: string;
|
|
14
|
+
env: Record<string, string>;
|
|
15
|
+
timeoutMs: number;
|
|
16
|
+
signal: AbortSignal;
|
|
17
|
+
/** One call per output line (stdout and stderr), already de-framed. */
|
|
18
|
+
onLine: (line: string) => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export interface Isolation {
|
|
21
|
+
readonly level: Level;
|
|
22
|
+
readonly backend: string;
|
|
23
|
+
/** What `mirab status` and job logs print. */
|
|
24
|
+
readonly label: string;
|
|
25
|
+
/** Host path of the job work directory -> where the guest sees it. */
|
|
26
|
+
guestPath(hostPath: string): string;
|
|
27
|
+
put(hostPath: string, content: string | Uint8Array, mode?: number): Promise<void>;
|
|
28
|
+
/** Run a step; resolves with its exit code (124 timeout, 130 cancelled). */
|
|
29
|
+
exec(argv: string[], options: ExecOptions): Promise<number>;
|
|
30
|
+
/** Interactive program on a guest pty (spawn --vm). */
|
|
31
|
+
pty?(request: PtyRequest): Promise<PtyHandle>;
|
|
32
|
+
/** Read the guest file tree (working-directory sync); backends without a copy omit it. */
|
|
33
|
+
files?: GuestFiles;
|
|
34
|
+
/** Messages the guest initiates toward the host on a vsock port (hooks). */
|
|
35
|
+
onGuestMessage?(port: number, handler: (message: any) => void): void;
|
|
36
|
+
/** Serve the VM's guest-to-host `port` (e.g. the station's egress): `serve`
|
|
37
|
+
* listens on the socket path given, which belongs to this VM alone. */
|
|
38
|
+
onGuestConnect?(port: number, serve: (socketPath: string) => Promise<import('node:net').Server>): Promise<void>;
|
|
39
|
+
/** Stop the backend; preserve its workspace for recovery after incomplete sync. */
|
|
40
|
+
close(options?: {
|
|
41
|
+
preserveWorkspace?: boolean;
|
|
42
|
+
/** Keep this VM's root disk (its own copy) at this path once it has
|
|
43
|
+
* stopped: how an agent's image is built and updated. */
|
|
44
|
+
keepRoot?: string;
|
|
45
|
+
}): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
/** One entry under a sync root, the same shape the guest agent's `list` op returns. */
|
|
48
|
+
export interface Entry {
|
|
49
|
+
/** Path relative to the root, `/`-separated. */
|
|
50
|
+
p: string;
|
|
51
|
+
/** `f` regular file, `d` directory, `l` symlink. */
|
|
52
|
+
k: 'f' | 'd' | 'l';
|
|
53
|
+
/** Permission bits (0 for symlinks). */
|
|
54
|
+
m: number;
|
|
55
|
+
s?: number;
|
|
56
|
+
/** sha256 hex of the content (files). */
|
|
57
|
+
h?: string;
|
|
58
|
+
/** Link target (symlinks). */
|
|
59
|
+
t?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface GuestFiles {
|
|
62
|
+
list(root: string): Promise<Entry[]>;
|
|
63
|
+
read(path: string): Promise<Buffer>;
|
|
64
|
+
}
|
|
65
|
+
export interface PtyRequest {
|
|
66
|
+
argv: string[];
|
|
67
|
+
env: Record<string, string>;
|
|
68
|
+
cwd: string;
|
|
69
|
+
cols: number;
|
|
70
|
+
rows: number;
|
|
71
|
+
}
|
|
72
|
+
export interface PtyHandle {
|
|
73
|
+
write(data: Uint8Array | string): void;
|
|
74
|
+
resize(cols: number, rows: number): void;
|
|
75
|
+
onData(cb: (data: Buffer) => void): void;
|
|
76
|
+
onExit(cb: (code: number) => void): void;
|
|
77
|
+
/** Drop the connection: the guest hangs up the session. */
|
|
78
|
+
close(): void;
|
|
79
|
+
}
|
|
80
|
+
export type Opener = () => Promise<Channel>;
|
|
81
|
+
export interface GuestHello {
|
|
82
|
+
/** 1 for agents that predate the handshake (their ping reply has no number). */
|
|
83
|
+
protocol: number;
|
|
84
|
+
agent?: string;
|
|
85
|
+
}
|
|
86
|
+
export declare function guestPing(open: Opener): Promise<GuestHello>;
|
|
87
|
+
export declare class GuestProtocolError extends Error {
|
|
88
|
+
}
|
|
89
|
+
/** Refuse a guest agent that speaks another protocol, at connect time and
|
|
90
|
+
* with the fix, instead of failing mid-session on an op it lacks. */
|
|
91
|
+
export declare function checkGuestProtocol(hello: GuestHello, image: string): void;
|
|
92
|
+
export declare function guestPut(open: Opener, path: string, content: string | Uint8Array, mode?: number): Promise<void>;
|
|
93
|
+
export declare function guestFiles(open: Opener): GuestFiles;
|
|
94
|
+
export declare function guestExec(open: Opener, argv: string[], o: ExecOptions): Promise<number>;
|
|
95
|
+
export declare function guestPty(open: Opener, request: PtyRequest): Promise<PtyHandle>;
|
|
96
|
+
/** Read one length-prefixed JSON message from a connection the guest opened. */
|
|
97
|
+
export declare function readOneMessage(stream: {
|
|
98
|
+
on(event: 'data', cb: (c: Buffer) => void): unknown;
|
|
99
|
+
}): Promise<any>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type BootTimings, type VmStartOptions } from './firecracker';
|
|
2
|
+
import { type Isolation } from './session';
|
|
3
|
+
export interface VzConfig {
|
|
4
|
+
/** The signed mirab-vz helper. */
|
|
5
|
+
helper: string;
|
|
6
|
+
/** Guest kernel with virtio-pci built in: the uncompressed arm64 `Image` on
|
|
7
|
+
* Apple silicon, a relocatable x86_64 `bzImage` on Intel Macs. */
|
|
8
|
+
kernel: string;
|
|
9
|
+
/** Base ext4 image; each VM boots an APFS clone of it. */
|
|
10
|
+
rootfs: string;
|
|
11
|
+
memoryMiB: number;
|
|
12
|
+
vcpus: number;
|
|
13
|
+
/** Guest vsock port the agent listens on. */
|
|
14
|
+
port: number;
|
|
15
|
+
egress: 'auto' | 'block' | 'allow';
|
|
16
|
+
}
|
|
17
|
+
/** Guest -> host vsock ports the helper relays: the spawn --vm hook relay
|
|
18
|
+
* (9100) and the way out through the station (9101). */
|
|
19
|
+
export declare const GUEST_TO_HOST_PORTS: number[];
|
|
20
|
+
/** The guest's loopback port for the way out, and the host vsock port it reaches. */
|
|
21
|
+
export declare const EGRESS_GUEST_PORT = 8790;
|
|
22
|
+
export declare const EGRESS_HOST_PORT = 9101;
|
|
23
|
+
export declare const VZ_NETWORK_MARKER = "/etc/mirab/vz-network.json";
|
|
24
|
+
export declare const VZ_SETUP_COMMAND = "sudo ~/.mirab/bin/mirab vm network setup";
|
|
25
|
+
/** What the marker says once the pf anchor denies guests the LAN. */
|
|
26
|
+
export declare const VZ_LAN_DENIED = "lan-denied";
|
|
27
|
+
/** This Mac can run Virtualization.framework guests (hosted CI Macs cannot). */
|
|
28
|
+
export declare function vzHostReady(): boolean;
|
|
29
|
+
export declare function vzAvailable(c: Pick<VzConfig, 'helper' | 'kernel' | 'rootfs'>, platform?: NodeJS.Platform): boolean;
|
|
30
|
+
export interface VzNetworkState {
|
|
31
|
+
/** The owner installed the pf anchor that denies VZ NAT guests the LAN. */
|
|
32
|
+
filtered: boolean;
|
|
33
|
+
reason?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Reads the root-owned marker `mirab vm network setup` writes; never runs sudo or pfctl. */
|
|
36
|
+
export declare function vzNetworkState(marker?: string): VzNetworkState;
|
|
37
|
+
export interface VzNetworkPlan {
|
|
38
|
+
net: 'none' | 'nat';
|
|
39
|
+
note: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* VZ NAT (vmnet shared mode) gives the guest the internet AND the Mac's LAN,
|
|
43
|
+
* the Mac itself and the tailnet. Only a root pf anchor can deny those, so NAT
|
|
44
|
+
* is used only when that anchor is installed; otherwise the VM has no network.
|
|
45
|
+
*/
|
|
46
|
+
export declare function planVzNetwork(egress: VzConfig['egress'], state: VzNetworkState): VzNetworkPlan;
|
|
47
|
+
export declare function vzNetworkStatusLine(egress: VzConfig['egress'], state: VzNetworkState): string;
|
|
48
|
+
export declare function kernelCommandLine(net: VzNetworkPlan['net'], now: Date): string;
|
|
49
|
+
/** The helper's `run` arguments; `share` null = no virtio-fs device at all. */
|
|
50
|
+
export declare function vzRunArgs(o: {
|
|
51
|
+
cfg: Pick<VzConfig, 'kernel' | 'vcpus' | 'memoryMiB'>;
|
|
52
|
+
root: string;
|
|
53
|
+
uds: string;
|
|
54
|
+
vmDir: string;
|
|
55
|
+
share: string | null;
|
|
56
|
+
net: VzNetworkPlan['net'];
|
|
57
|
+
listen: number[];
|
|
58
|
+
now?: Date;
|
|
59
|
+
}): string[];
|
|
60
|
+
export declare function startVz(cfg: VzConfig, work: string | null, vmDir: string, log?: (line: string) => void, network?: VzNetworkState, opts?: VmStartOptions): Promise<Isolation & {
|
|
61
|
+
timings: BootTimings;
|
|
62
|
+
pid: number;
|
|
63
|
+
}>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** The guests a runner can run. */
|
|
3
|
+
export declare const RUNNER_PLATFORMS: readonly ['linux-x64', 'linux-arm64'];
|
|
4
|
+
export type RunnerPlatform = (typeof RUNNER_PLATFORMS)[number];
|
|
5
|
+
/** Every runner carries `mirab-<os>-<arch>` (e.g. `mirab-linux-arm64`), so a
|
|
6
|
+
* job asks for a fresh VM of a platform with one label. */
|
|
7
|
+
export declare const platformLabel: (platform: RunnerPlatform) => string;
|
|
8
|
+
/** One command in the VM. Steps run as the unprivileged `runner` user unless
|
|
9
|
+
* `asRoot` (then the step is responsible for dropping to it, with RUNNER_SETPRIV). */
|
|
10
|
+
export interface RunnerStep {
|
|
11
|
+
argv: string[];
|
|
12
|
+
env?: Record<string, string>;
|
|
13
|
+
asRoot?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** A file put into the VM before the job's steps. `secret` files are never
|
|
16
|
+
* logged; they should live on tmpfs (/run) and be removed once read. */
|
|
17
|
+
export interface RunnerFile {
|
|
18
|
+
path: string;
|
|
19
|
+
mode: number;
|
|
20
|
+
content: string;
|
|
21
|
+
secret?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** A one-time credential for exactly one job. Every value in `secret` is
|
|
24
|
+
* redacted from mirab's logs and never written to the host's disk. */
|
|
25
|
+
export interface RunnerTicket {
|
|
26
|
+
name: string;
|
|
27
|
+
secret: Record<string, string>;
|
|
28
|
+
meta?: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
/** What a forge adapter provides. */
|
|
31
|
+
export interface Forge {
|
|
32
|
+
name: string;
|
|
33
|
+
/** Host side: register one runner called `name` for one job. */
|
|
34
|
+
ticket(req: {
|
|
35
|
+
name: string;
|
|
36
|
+
labels: string[];
|
|
37
|
+
platform: RunnerPlatform;
|
|
38
|
+
}): Promise<RunnerTicket>;
|
|
39
|
+
guest: {
|
|
40
|
+
/** Baked into the runner image as root, in the build VM. `key` names
|
|
41
|
+
* exactly what is installed (e.g. a version and checksum): a new key is
|
|
42
|
+
* a new image. Called again at most daily, to notice new releases. */
|
|
43
|
+
install(platform: RunnerPlatform): Promise<{
|
|
44
|
+
key: string;
|
|
45
|
+
steps: RunnerStep[];
|
|
46
|
+
}>;
|
|
47
|
+
/** Hosts the image build may reach for `install`. */
|
|
48
|
+
buildEgress: string[];
|
|
49
|
+
/** One job: files put first, then the steps in order; the last one is
|
|
50
|
+
* the runner, which takes one job and exits. */
|
|
51
|
+
job(ticket: RunnerTicket): {
|
|
52
|
+
files: RunnerFile[];
|
|
53
|
+
steps: RunnerStep[];
|
|
54
|
+
env: Record<string, string>;
|
|
55
|
+
};
|
|
56
|
+
/** True for the runner's output line that says a job has started: the
|
|
57
|
+
* job timer (timeoutMinutes) starts there, not at boot. */
|
|
58
|
+
started(line: string): boolean;
|
|
59
|
+
};
|
|
60
|
+
/** Delete this host's registrations named `prefix*` that aren't in `live`. */
|
|
61
|
+
reconcile(prefix: string, live: string[]): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
/** The toolsets a runner image can carry. */
|
|
64
|
+
export declare const TOOLSETS: readonly ['rust', 'node', 'go'];
|
|
65
|
+
export declare const runnerSchema: z.ZodObject<{
|
|
66
|
+
name: z.ZodString;
|
|
67
|
+
forge: z.ZodCustom<Forge, Forge>;
|
|
68
|
+
labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
69
|
+
image: z.ZodDefault<z.ZodObject<{
|
|
70
|
+
tools: z.ZodDefault<z.ZodObject<{
|
|
71
|
+
rust: z.ZodOptional<z.ZodString>;
|
|
72
|
+
node: z.ZodOptional<z.ZodString>;
|
|
73
|
+
go: z.ZodOptional<z.ZodString>;
|
|
74
|
+
}, z.core.$strict>>;
|
|
75
|
+
packages: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
76
|
+
}, z.core.$strict>>;
|
|
77
|
+
vm: z.ZodDefault<z.ZodObject<{
|
|
78
|
+
vcpus: z.ZodDefault<z.ZodNumber>;
|
|
79
|
+
memoryMiB: z.ZodDefault<z.ZodNumber>;
|
|
80
|
+
diskGiB: z.ZodDefault<z.ZodNumber>;
|
|
81
|
+
}, z.core.$strict>>;
|
|
82
|
+
pool: z.ZodDefault<z.ZodObject<{
|
|
83
|
+
warm: z.ZodDefault<z.ZodNumber>;
|
|
84
|
+
max: z.ZodDefault<z.ZodNumber>;
|
|
85
|
+
idleHours: z.ZodDefault<z.ZodNumber>;
|
|
86
|
+
}, z.core.$strict>>;
|
|
87
|
+
timeoutMinutes: z.ZodDefault<z.ZodNumber>;
|
|
88
|
+
}, z.core.$strict>;
|
|
89
|
+
export type RunnerDefinition = z.infer<typeof runnerSchema>;
|
|
90
|
+
export type RunnerInput = z.input<typeof runnerSchema>;
|
|
91
|
+
export declare function defineRunner(input: RunnerInput): RunnerDefinition;
|
|
92
|
+
/** The unprivileged user every job step runs as (created in the runner image). */
|
|
93
|
+
export declare const RUNNER_USER: {
|
|
94
|
+
name: string;
|
|
95
|
+
uid: number;
|
|
96
|
+
home: string;
|
|
97
|
+
};
|
|
98
|
+
/** Prefix that drops a root command to the runner user, keeping its env. */
|
|
99
|
+
export declare const RUNNER_SETPRIV: string[];
|
|
100
|
+
/** The guest platform a VM on this host runs. */
|
|
101
|
+
export declare function hostRunnerPlatform(arch?: string): RunnerPlatform;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { VmConfig } from '../isolation';
|
|
2
|
+
import { type RunnerDefinition, type RunnerPlatform, type RunnerStep } from './define';
|
|
3
|
+
import { type Fetcher, type ResolvedTool } from './toolsets';
|
|
4
|
+
export declare const runnerImagesDir: (configDir: string) => string;
|
|
5
|
+
export interface RunnerImageMeta {
|
|
6
|
+
key: string;
|
|
7
|
+
platform: RunnerPlatform;
|
|
8
|
+
forge: {
|
|
9
|
+
name: string;
|
|
10
|
+
key: string;
|
|
11
|
+
};
|
|
12
|
+
tools: ResolvedTool[];
|
|
13
|
+
packages: string[];
|
|
14
|
+
diskGiB: number;
|
|
15
|
+
/** The toolsets' env, and PATH entries to put first, for every job. */
|
|
16
|
+
env: Record<string, string>;
|
|
17
|
+
path: string[];
|
|
18
|
+
builtAt: string;
|
|
19
|
+
}
|
|
20
|
+
export interface RunnerImage {
|
|
21
|
+
rootfs: string;
|
|
22
|
+
meta: RunnerImageMeta;
|
|
23
|
+
}
|
|
24
|
+
export interface RunnerRecipe {
|
|
25
|
+
platform: RunnerPlatform;
|
|
26
|
+
forge: {
|
|
27
|
+
name: string;
|
|
28
|
+
key: string;
|
|
29
|
+
steps: RunnerStep[];
|
|
30
|
+
egress: string[];
|
|
31
|
+
};
|
|
32
|
+
tools: ResolvedTool[];
|
|
33
|
+
packages: string[];
|
|
34
|
+
diskGiB: number;
|
|
35
|
+
}
|
|
36
|
+
/** Everything that decides an image, resolved: the key is the hash of it. */
|
|
37
|
+
export declare function runnerRecipe(def: RunnerDefinition, o?: {
|
|
38
|
+
platform?: RunnerPlatform;
|
|
39
|
+
fetch?: Fetcher;
|
|
40
|
+
}): Promise<RunnerRecipe>;
|
|
41
|
+
export declare function runnerImageKey(base: string, r: RunnerRecipe): string;
|
|
42
|
+
/** The build, as root in the build VM, in order. */
|
|
43
|
+
export declare function buildSteps(r: RunnerRecipe): RunnerStep[];
|
|
44
|
+
/** The cached image for `r`, if it was built on this base. */
|
|
45
|
+
export declare function cachedRunnerImage(configDir: string, key: string): Promise<RunnerImage | undefined>;
|
|
46
|
+
/** The runner's image: from the cache, or built now (a few minutes). */
|
|
47
|
+
export declare function runnerImage(o: {
|
|
48
|
+
configDir: string;
|
|
49
|
+
vm: VmConfig;
|
|
50
|
+
recipe: RunnerRecipe;
|
|
51
|
+
log?: (line: string) => void;
|
|
52
|
+
}): Promise<RunnerImage>;
|