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,96 @@
1
+ // harar as the gateway's vault, on its local socket: one JSON request, the
2
+ // write side closed, one JSON response. The same client npm `harar` ships as
3
+ // `import { vault } from 'harar'` (addiscorp/harar, js/); mirab uses its own
4
+ // copy until that package is published.
5
+ import { connect } from 'node:net'
6
+ import type { Vault } from 'rixse'
7
+
8
+ /** harar answers are small; anything longer is refused. */
9
+ const MAX_RESPONSE = 1024 * 1024
10
+
11
+ /** One request to harar (the per-user agent or a primary: the same shape),
12
+ * and its answer if it says ok; otherwise why not. */
13
+ function ask(
14
+ socket: string,
15
+ request: Record<string, string>,
16
+ what: string,
17
+ ): Promise<{ value?: unknown; hosts?: unknown }> {
18
+ return new Promise((done, fail) => {
19
+ const conn = connect(socket)
20
+ const chunks: Buffer[] = []
21
+ let size = 0
22
+ conn.setTimeout(10_000, () =>
23
+ conn.destroy(new Error('harar did not answer in 10 s')),
24
+ )
25
+ conn.on('connect', () => conn.end(JSON.stringify(request)))
26
+ conn.on('data', (chunk: Buffer) => {
27
+ size += chunk.length
28
+ if (size > MAX_RESPONSE)
29
+ conn.destroy(new Error('harar sent too long an answer'))
30
+ else chunks.push(chunk)
31
+ })
32
+ conn.on('error', (e) => fail(new Error(`harar at ${socket}: ${e.message}`)))
33
+ conn.on('end', () => {
34
+ let answer: {
35
+ ok?: boolean
36
+ value?: unknown
37
+ hosts?: unknown
38
+ error?: unknown
39
+ }
40
+ try {
41
+ answer = JSON.parse(Buffer.concat(chunks).toString('utf8'))
42
+ } catch {
43
+ return fail(new Error(`harar at ${socket} did not answer with JSON`))
44
+ }
45
+ if (
46
+ typeof answer !== 'object' ||
47
+ answer === null ||
48
+ Array.isArray(answer)
49
+ )
50
+ return fail(
51
+ new Error(`harar at ${socket} did not answer with an object`),
52
+ )
53
+ if (answer.ok === true) return done(answer)
54
+ fail(
55
+ new Error(
56
+ `harar refused ${what}: ${typeof answer.error === 'string' ? answer.error : 'no answer'}`,
57
+ ),
58
+ )
59
+ })
60
+ })
61
+ }
62
+
63
+ /** The hosts harar lets `key` go to (`*.` for any name under one). */
64
+ export async function hararHosts(
65
+ socket: string,
66
+ key: string,
67
+ ): Promise<string[]> {
68
+ const { hosts } = await ask(socket, { op: 'hosts', key }, key)
69
+ if (!Array.isArray(hosts) || !hosts.every((h) => typeof h === 'string'))
70
+ throw new Error(`harar sent no hosts for ${key}`)
71
+ return hosts
72
+ }
73
+
74
+ /** `key`'s value for one request to `host`: harar decides, and records the host. */
75
+ export async function hararSend(
76
+ socket: string,
77
+ key: string,
78
+ host: string,
79
+ ): Promise<string> {
80
+ const { value } = await ask(
81
+ socket,
82
+ { op: 'send', key, host },
83
+ `${key} to ${host}`,
84
+ )
85
+ if (typeof value !== 'string')
86
+ throw new Error(`harar refused ${key} to ${host}: no value`)
87
+ return value
88
+ }
89
+
90
+ /** harar at `socket`, as the gateway's vault. */
91
+ export function hararVault(socket: string): Vault {
92
+ return {
93
+ hosts: (secret) => hararHosts(socket, secret),
94
+ send: (secret, host) => hararSend(socket, secret, host),
95
+ }
96
+ }
@@ -0,0 +1,90 @@
1
+ import { generateKeyPairSync } from 'node:crypto'
2
+ import { mkdir, mkdtemp, writeFile, rm } from 'node:fs/promises'
3
+ import { join, resolve } from 'node:path'
4
+
5
+ export function testIdentity(apiToken: unknown = null) {
6
+ const keys = generateKeyPairSync('ed25519')
7
+ return {
8
+ schema: 1,
9
+ name: 'alice',
10
+ device: 'test',
11
+ apiToken,
12
+ publicKey:
13
+ 'ed25519:' +
14
+ keys.publicKey.export({ format: 'der', type: 'spki' }).toString('base64'),
15
+ secretKey: keys.privateKey
16
+ .export({ format: 'der', type: 'pkcs8' })
17
+ .toString('base64'),
18
+ createdAt: new Date().toISOString(),
19
+ }
20
+ }
21
+ export async function fixture(
22
+ handler: (request: Request) => Response | Promise<Response>,
23
+ ) {
24
+ await mkdir(resolve('.tmp'), { recursive: true })
25
+ const root = await mkdtemp(join(resolve('.tmp'), 'cli-review-'))
26
+ const server = Bun.serve({ hostname: '127.0.0.1', port: 0, fetch: handler })
27
+ const origin = server.url.origin
28
+ return {
29
+ root,
30
+ origin,
31
+ async identity(
32
+ token: unknown = { server: origin, token: 'gf_fixturetoken' },
33
+ ) {
34
+ const identity = testIdentity(token)
35
+ await writeFile(join(root, 'identity.json'), JSON.stringify(identity), {
36
+ mode: 0o600,
37
+ })
38
+ return identity
39
+ },
40
+ async agent(token: string | null = 'gf_agentfixture') {
41
+ await mkdir(join(root, 'agents'), { recursive: true })
42
+ const agent = {
43
+ ...testIdentity(),
44
+ name: 'alice/test',
45
+ server: origin,
46
+ token,
47
+ requestId: 'request-1',
48
+ }
49
+ await writeFile(join(root, 'agents/test.json'), JSON.stringify(agent), {
50
+ mode: 0o600,
51
+ })
52
+ return agent
53
+ },
54
+ async close() {
55
+ server.stop(true)
56
+ await rm(root, { recursive: true, force: true })
57
+ },
58
+ }
59
+ }
60
+ export function startCLI(root: string, args: string[], stdin?: string) {
61
+ return Bun.spawn(
62
+ [
63
+ process.execPath,
64
+ 'run',
65
+ resolve('src/mirab.ts'),
66
+ '--config-dir',
67
+ root,
68
+ ...args,
69
+ ],
70
+ {
71
+ stdin: stdin === undefined ? 'ignore' : new Blob([stdin]),
72
+ stdout: 'pipe',
73
+ stderr: 'pipe',
74
+ },
75
+ )
76
+ }
77
+ export async function cli(root: string, args: string[], stdin?: string) {
78
+ const proc = startCLI(root, args, stdin)
79
+ const timeout = setTimeout(() => proc.kill('SIGTERM'), 10000)
80
+ try {
81
+ const [code, out, err] = await Promise.all([
82
+ proc.exited,
83
+ new Response(proc.stdout).text(),
84
+ new Response(proc.stderr).text(),
85
+ ])
86
+ return { code, out, err }
87
+ } finally {
88
+ clearTimeout(timeout)
89
+ }
90
+ }
package/src/tools.ts ADDED
@@ -0,0 +1,3 @@
1
+ // `import { defineTool } from 'mirab/tools'`, as eve's tools do.
2
+ export { defineTool } from './agent/define'
3
+ export type { Tool } from './agent/define'
@@ -0,0 +1,76 @@
1
+ import { z } from 'zod';
2
+ /** Why `name` isn't a harar name of `kind` (`<org>.<kind>.<local>`), or null. */
3
+ export declare function nameProblem(name: string, kind: 'agent' | 'channel' | 'human', example: string): string | null;
4
+ /** Why `name` isn't an agent name (`<org>.agent.<local>`, harar's rules), or null. */
5
+ export declare function agentNameProblem(name: string): string | null;
6
+ /** Why `name` isn't a human's harar name (`<org>.human.<local>`), or null. */
7
+ export declare function humanNameProblem(name: string): string | null;
8
+ /** `provider/model`, e.g. `anthropic/claude-sonnet-5-5`. */
9
+ export declare const MODEL: RegExp;
10
+ /** A tool's name is its file's name, or its key in `tools`; the model sees exactly that. */
11
+ export declare const TOOL_NAME: RegExp;
12
+ /** Why `name` can't be a tool's name, or null. `__proto__` is refused: as a
13
+ * key in plain data it is dropped or changes the object, never a tool. */
14
+ export declare function toolNameProblem(name: string): string | null;
15
+ /** A tool's input: a JSON Schema for an object, which is what models take. */
16
+ export declare const toolInput: z.ZodRecord<z.ZodString, z.ZodUnknown>;
17
+ /** A tool that is an executable in the VM: the model's input as JSON on its
18
+ * stdin (checked against `input` first), its answer as JSON on stdout. */
19
+ export declare const commandToolSchema: z.ZodObject<{
20
+ description: z.ZodString;
21
+ command: z.ZodString;
22
+ args: z.ZodOptional<z.ZodArray<z.ZodString>>;
23
+ input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
24
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
25
+ }, z.core.$strict>;
26
+ export type CommandTool = z.infer<typeof commandToolSchema>;
27
+ /** Tools by name. zod's record drops a `__proto__` key before checking it,
28
+ * so the raw data is checked for one first. */
29
+ export declare function toolRecord<T extends z.ZodType>(tool: T): z.ZodPipe<z.ZodUnknown, z.ZodRecord<z.ZodString, T>>;
30
+ /** A whitelist entry as the station matches it: `api.github.com`, or
31
+ * `*.github.com` for every name under it. Names only, never an address. */
32
+ export declare function domainProblem(domain: string): string | null;
33
+ /** Characters that can make a terminal show something other than the text:
34
+ * controls, C1 controls, line separators and bidirectional overrides. A
35
+ * person approves rules by reading them, so none may appear in one. */
36
+ export declare const UNSHOWABLE: RegExp;
37
+ /** What agent/agent.ts declares. TypeScript tools are files in agent/tools/;
38
+ * `tools` here holds command tools. */
39
+ export declare const agentSchema: z.ZodObject<{
40
+ name: z.ZodString;
41
+ model: z.ZodString;
42
+ tools: z.ZodOptional<z.ZodPipe<z.ZodUnknown, z.ZodRecord<z.ZodString, z.ZodObject<{
43
+ description: z.ZodString;
44
+ command: z.ZodString;
45
+ args: z.ZodOptional<z.ZodArray<z.ZodString>>;
46
+ input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
47
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
48
+ }, z.core.$strict>>>>;
49
+ packages: z.ZodOptional<z.ZodArray<z.ZodString>>;
50
+ domains: z.ZodOptional<z.ZodArray<z.ZodString>>;
51
+ channels: z.ZodOptional<z.ZodArray<z.ZodString>>;
52
+ gateway: z.ZodOptional<z.ZodObject<{
53
+ rules: z.ZodOptional<z.ZodArray<z.ZodObject<{
54
+ host: z.ZodString;
55
+ method: z.ZodOptional<z.ZodString>;
56
+ path: z.ZodOptional<z.ZodString>;
57
+ effect: z.ZodEnum<{
58
+ apply: "apply";
59
+ forbidden: "forbidden";
60
+ request: "request";
61
+ }>;
62
+ }, z.core.$strict>>>;
63
+ secrets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
64
+ agent: z.ZodString;
65
+ approved: z.ZodOptional<z.ZodString>;
66
+ }, z.core.$strict>>>;
67
+ }, z.core.$strict>>;
68
+ }, z.core.$strict>;
69
+ export type Agent = z.infer<typeof agentSchema>;
70
+ export declare function defineAgent(agent: Agent): Agent;
71
+ export interface Tool<Input extends z.ZodType> {
72
+ description: string;
73
+ inputSchema: Input;
74
+ execute(input: z.infer<Input>): unknown | Promise<unknown>;
75
+ }
76
+ export declare function defineTool<Input extends z.ZodType>(tool: Tool<Input>): Tool<Input>;
@@ -0,0 +1,53 @@
1
+ import { z } from 'zod';
2
+ import { type Tool } from './define';
3
+ /** A TypeScript tool, `agent/<file>`, run by the agent loop. */
4
+ export declare const typescriptToolSchema: z.ZodObject<{
5
+ description: z.ZodString;
6
+ file: z.ZodString;
7
+ input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
8
+ }, z.core.$strict>;
9
+ export declare const manifestSchema: z.ZodObject<{
10
+ name: z.ZodString;
11
+ model: z.ZodString;
12
+ instructions: z.ZodString;
13
+ tools: z.ZodPipe<z.ZodUnknown, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
14
+ description: z.ZodString;
15
+ file: z.ZodString;
16
+ input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
17
+ }, z.core.$strict>, z.ZodObject<{
18
+ description: z.ZodString;
19
+ command: z.ZodString;
20
+ args: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
+ input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
22
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
23
+ }, z.core.$strict>]>>>;
24
+ packages: z.ZodArray<z.ZodString>;
25
+ domains: z.ZodArray<z.ZodString>;
26
+ channels: z.ZodArray<z.ZodString>;
27
+ gateway: z.ZodDefault<z.ZodObject<{
28
+ rules: z.ZodOptional<z.ZodArray<z.ZodObject<{
29
+ host: z.ZodString;
30
+ method: z.ZodOptional<z.ZodString>;
31
+ path: z.ZodOptional<z.ZodString>;
32
+ effect: z.ZodEnum<{
33
+ apply: "apply";
34
+ forbidden: "forbidden";
35
+ request: "request";
36
+ }>;
37
+ }, z.core.$strict>>>;
38
+ secrets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
39
+ agent: z.ZodString;
40
+ approved: z.ZodOptional<z.ZodString>;
41
+ }, z.core.$strict>>>;
42
+ }, z.core.$strict>>;
43
+ }, z.core.$strict>;
44
+ export type Manifest = z.infer<typeof manifestSchema>;
45
+ /** Checks manifest data from anywhere (agent.ts, `nix eval --json`). */
46
+ export declare function parseManifest(data: unknown, from: string): Manifest;
47
+ export interface LoadedManifest {
48
+ manifest: Manifest;
49
+ /** The TypeScript tools as their files export them, by name. */
50
+ typescript: Record<string, Tool<z.ZodType>>;
51
+ }
52
+ /** Reads `<project>/agent` (agent.ts, instructions.md, tools/*.ts) into a manifest. */
53
+ export declare function loadManifest(project: string): Promise<LoadedManifest>;
@@ -0,0 +1 @@
1
+ export declare function registerAgentModules(): void;
@@ -0,0 +1,124 @@
1
+ import { type VmConfig } from './isolation';
2
+ import type { Isolation } from './isolation/session';
3
+ /** Where apt goes, over HTTPS: the station lets nothing else out. */
4
+ export declare const UBUNTU_MIRRORS: string[];
5
+ export interface ImageMeta {
6
+ key: string;
7
+ packages: string[];
8
+ base: string;
9
+ builtAt: string;
10
+ updatedAt: string;
11
+ /** What the last update upgraded. */
12
+ upgraded: string[];
13
+ }
14
+ export declare const imagesDir: (configDir: string) => string;
15
+ /** The base image: where it is, its size, when it was written, and a hash of
16
+ * its first 4 MB (the filesystem's superblock and early blocks), without
17
+ * reading all 3 GB. A new base is a new key. */
18
+ export declare function baseId(rootfs: string): Promise<string>;
19
+ /** A cache key for any build recipe: the base plus the recipe as canonical
20
+ * JSON (object keys sorted at every level), so equal recipes share a key. */
21
+ export declare function recipeKey(base: string, recipe: unknown): string;
22
+ /** The cache key: the base and the package list, order and repeats aside. */
23
+ export declare function imageKey(base: string, packages: string[]): string;
24
+ export interface CachedImage {
25
+ rootfs: string;
26
+ meta: ImageMeta;
27
+ }
28
+ export declare function cachedImage(configDir: string, key: string, base?: string): Promise<CachedImage | undefined>;
29
+ /** In a build VM: tools other than apt (curl, rustup) reach the station too. */
30
+ export declare const BUILD_ENV: {
31
+ PATH: string;
32
+ HOME: string;
33
+ DEBIAN_FRONTEND: string;
34
+ LANG: string;
35
+ HTTP_PROXY: string;
36
+ HTTPS_PROXY: string;
37
+ http_proxy: string;
38
+ https_proxy: string;
39
+ NO_PROXY: string;
40
+ };
41
+ /** Runs one command as root in the VM; a failure carries the end of its output. */
42
+ export declare function run(iso: Pick<Isolation, 'exec'>, argv: string[], log?: (l: string) => void, env?: Record<string, string>): Promise<string[]>;
43
+ /** A build VM on `rootfs` whose only way out is the station, to the hosts in
44
+ * `allow` (default: Ubuntu's mirrors, for apt). `rootGiB` grows its root
45
+ * disk first; the build then grows the filesystem (resize2fs). */
46
+ export declare function bootForBuild(o: {
47
+ configDir: string;
48
+ vm: VmConfig;
49
+ rootfs: string;
50
+ key: string;
51
+ allow?: string[];
52
+ rootGiB?: number;
53
+ log?: (line: string) => void;
54
+ }): Promise<{
55
+ iso: Isolation;
56
+ cleanup: () => Promise<void>;
57
+ }>;
58
+ /** Grows the running guest's root filesystem to fill its (grown) disk, while
59
+ * mounted. resize2fs can't: the kernel mounts the root as /dev/root, so it
60
+ * doesn't see /dev/vda as mounted and refuses an exclusive open. This is the
61
+ * ioctl resize2fs itself uses online (EXT4_IOC_RESIZE_FS). */
62
+ export declare const GROW_ROOT_FS: string;
63
+ /** Stops the VM and keeps its root disk as the image in `dir`, moving any
64
+ * image there now aside as the previous one. */
65
+ export declare function keep(iso: Pick<Isolation, 'exec' | 'close'>, dir: string, meta: object, opts?: {
66
+ previous?: boolean;
67
+ }): Promise<string>;
68
+ /** The agent's image: from the cache, or built now. */
69
+ export declare function agentImage(o: {
70
+ configDir: string;
71
+ vm: VmConfig;
72
+ packages: string[];
73
+ log?: (line: string) => void;
74
+ }): Promise<string>;
75
+ /** The packages a dry-run `apt-get -s upgrade` would upgrade. */
76
+ export declare function upgradesIn(lines: string[]): string[];
77
+ /** Boots the agent's image and upgrades it in place when apt has anything
78
+ * newer; the image before stays as the previous one. */
79
+ export declare function updateImage(o: {
80
+ configDir: string;
81
+ vm: VmConfig;
82
+ packages: string[];
83
+ log?: (line: string) => void;
84
+ }): Promise<{
85
+ rootfs: string;
86
+ upgraded: string[];
87
+ }>;
88
+ export interface ListedImage {
89
+ key: string;
90
+ packages: string[];
91
+ /** Built on the base this mirab boots now; otherwise it is never used again. */
92
+ current: boolean;
93
+ /** When it was built, updated or booted last. */
94
+ usedAt: Date;
95
+ bytes: number;
96
+ }
97
+ /** Every cached agent image, newest use first. */
98
+ export declare function listImages(configDir: string, currentBase: string): Promise<ListedImage[]>;
99
+ /**
100
+ * What `mirab image prune` removes: images built on a base this mirab no
101
+ * longer boots (after an upgrade they are never used again), images unused
102
+ * for `days` (at least one), and what a build left behind a day or more ago:
103
+ * scratch and an image directory that never got as far as its meta.json. An
104
+ * image used in the last hour always stays, and a VM boots from its own copy,
105
+ * so a running one is never touched. One that can't be removed is reported,
106
+ * and the rest go on.
107
+ */
108
+ export declare function pruneImages(o: {
109
+ configDir: string;
110
+ currentBase: string;
111
+ days: number;
112
+ dryRun?: boolean;
113
+ now?: Date;
114
+ }): Promise<{
115
+ removed: {
116
+ key: string;
117
+ why: string;
118
+ bytes: number;
119
+ }[];
120
+ failed: {
121
+ key: string;
122
+ error: string;
123
+ }[];
124
+ }>;
@@ -0,0 +1,6 @@
1
+ export { agentNameProblem, defineAgent, defineTool } from './agent/define';
2
+ export type { Agent, Tool } from './agent/define';
3
+ export { loadManifest, manifestSchema, parseManifest } from './agent/manifest';
4
+ export type { Manifest } from './agent/manifest';
5
+ export { defineRunner, RUNNER_PLATFORMS, RUNNER_SETPRIV, RUNNER_USER, TOOLSETS, } from './runner/define';
6
+ export type { Forge, RunnerDefinition, RunnerFile, RunnerInput, RunnerPlatform, RunnerStep, RunnerTicket, } from './runner/define';
@@ -0,0 +1,37 @@
1
+ export declare const COLIMA_INSTALL = "brew install colima docker && colima start --vm-type vz --mount-type virtiofs";
2
+ export interface ColimaMount {
3
+ location: string;
4
+ writable: boolean;
5
+ }
6
+ export type ColimaState = {
7
+ running: true;
8
+ /** `unix://…/docker.sock`, passed to docker as `--host`. */
9
+ socket: string;
10
+ vmType: string;
11
+ arch: string;
12
+ mountType: string;
13
+ mounts: ColimaMount[];
14
+ } | {
15
+ running: false;
16
+ reason: string;
17
+ fix: string;
18
+ };
19
+ export interface ColimaProbe {
20
+ /** stdout of `colima status --json`, or undefined if it failed or is not installed. */
21
+ status(): string | undefined;
22
+ installed(): boolean;
23
+ /** ~/.colima/default/colima.yaml, if present. */
24
+ config(): string | undefined;
25
+ home: string;
26
+ exists(path: string): boolean;
27
+ }
28
+ export declare const systemColimaProbe: (profile?: string) => ColimaProbe;
29
+ /** The `mounts:` list of colima.yaml; an empty list means Colima's default ($HOME, writable). */
30
+ export declare function colimaMounts(yaml: string | undefined, home: string): {
31
+ location: string;
32
+ writable: boolean;
33
+ }[];
34
+ export declare function detectColima(p: ColimaProbe): ColimaState;
35
+ /** The mount that makes `path` visible in Colima's VM, if any (and writable when asked). */
36
+ export declare function colimaMountFor(path: string, mounts: ColimaMount[], writable: boolean): ColimaMount | undefined;
37
+ export declare function colimaStatusLine(c: ColimaState): string;
@@ -0,0 +1,25 @@
1
+ import { type Isolation } from './session';
2
+ export interface OciConfig {
3
+ /** `docker` or `podman` (anything with the same CLI). */
4
+ engine: string;
5
+ image: string;
6
+ /** Static linux guest agent binary, mounted read-only into the container. */
7
+ agent: string;
8
+ memory: string;
9
+ cpus: string;
10
+ pids: number;
11
+ egress: 'auto' | 'block' | 'allow';
12
+ /** Engine endpoint (`docker --host`), e.g. Colima's socket on macOS. */
13
+ host?: string;
14
+ /** Where the engine runs, for labels: `Colima vz VM`. */
15
+ where?: string;
16
+ }
17
+ export declare const EGRESS_NETWORK = "mirab-egress";
18
+ /** Network the container joins: the filtered bridge, or none. `allow` without setup is an error. */
19
+ export declare function containerNetwork(mode: OciConfig['egress'], ready: boolean, setup?: string): "mirab-egress" | "none";
20
+ export declare const GUEST_WORK = "/workspace";
21
+ export declare const NETWORK_SETUP = "sudo scripts/vm-network-setup.sh";
22
+ /** The same script, run inside Colima's VM, where the containers' network lives. */
23
+ export declare const COLIMA_NETWORK_SETUP = "colima ssh -- sudo sh -s up < scripts/vm-network-setup.sh";
24
+ export declare function ociAvailable(engine: string, agent: string, host?: string): boolean;
25
+ export declare function startOci(cfg: OciConfig, work: string, log?: (line: string) => void): Isolation;
@@ -0,0 +1,55 @@
1
+ import { type Server } from 'node:net';
2
+ import { type Channel, type Isolation, type Opener } from './session';
3
+ export interface FirecrackerConfig {
4
+ firecracker: string;
5
+ kernel: string;
6
+ /** Read-only base image containing mirab-guest-agent and /init-mirab.sh. */
7
+ rootfs: string;
8
+ memoryMiB: number;
9
+ vcpus: number;
10
+ /** Guest vsock port the agent listens on. */
11
+ port: number;
12
+ /** `block` = no network device; `allow` = tap or refuse; `auto` = tap when set up, else none. */
13
+ egress: 'auto' | 'block' | 'allow';
14
+ }
15
+ export declare const GUEST_WORK = "/workspace";
16
+ /**
17
+ * The Firecracker to run when isolation.json names none: the one shipped next
18
+ * to the mirab executable (where mirab's packaging will put it), else
19
+ * `firecracker` on PATH. `vm.firecracker` in isolation.json wins over both.
20
+ */
21
+ export declare function defaultFirecracker(execPath?: string): string;
22
+ /** /dev/kvm, Firecracker and mke2fs: what this host needs besides the guest image. */
23
+ export declare function firecrackerHostReady(firecracker: string): boolean;
24
+ export declare function firecrackerAvailable(c: Pick<FirecrackerConfig, 'firecracker' | 'kernel' | 'rootfs'>): boolean;
25
+ export declare function vsockChannel(uds: string, port: number): Promise<Channel>;
26
+ /**
27
+ * The Isolation members every vsock backend shares (Firecracker, vz): the
28
+ * work directory (when there is one) appears at /workspace, and guest connections to host port N
29
+ * arrive on the unix socket `<uds>_N` (Firecracker's convention, which the vz
30
+ * helper follows too).
31
+ */
32
+ export declare function vsockMembers(open: Opener, uds: string, work: string | null, listeners: Server[]): Pick<Isolation, 'guestPath' | 'put' | 'exec' | 'pty' | 'files' | 'onGuestMessage' | 'onGuestConnect'>;
33
+ /** What a VM start may add to the backend's config. */
34
+ export interface VmStartOptions {
35
+ /** Grow this VM's root disk clone to this size before boot (sparse; never
36
+ * shrinks). The guest sees a bigger /dev/vda; growing the filesystem on it
37
+ * (resize2fs) is the guest's job. */
38
+ rootGiB?: number;
39
+ /** vz: guest-to-host vsock ports to forward (default GUEST_TO_HOST_PORTS);
40
+ * [] for a VM that must not reach the host at all. Firecracker forwards a
41
+ * port only when someone serves it (onGuestConnect), so it needs no list. */
42
+ listen?: number[];
43
+ }
44
+ /** Sparse-grows a disk image file to `gib` GiB; refuses to shrink one. */
45
+ export declare function growDisk(path: string, gib: number): Promise<void>;
46
+ /** Checked bytes, not a signed 32-bit shift: large work images stay positive. */
47
+ export declare function workImageBytes(usedKiB: number): number;
48
+ export interface BootTimings {
49
+ imageMs: number;
50
+ bootToAgentMs: number;
51
+ }
52
+ export declare function startFirecracker(cfg: FirecrackerConfig, work: string | null, vmDir: string, log?: (line: string) => void, opts?: VmStartOptions): Promise<Isolation & {
53
+ timings: BootTimings;
54
+ pid: number;
55
+ }>;
@@ -0,0 +1,47 @@
1
+ import { type Isolation } from './session';
2
+ export declare const MARKER_DIR = "/etc/mirab/host-users";
3
+ /** What scripts/host-user-setup.sh wrote to /etc/mirab/host-users/<user>.json. */
4
+ export interface Marker {
5
+ version: 1;
6
+ user: string;
7
+ uid: number;
8
+ gid: number;
9
+ owner: string;
10
+ agent: string;
11
+ workRoot: string;
12
+ network?: string;
13
+ }
14
+ /** Everything detection looks at, so tests can describe any machine. */
15
+ export interface HostProbe {
16
+ marker(user: string): string | undefined;
17
+ uidOf(user: string): number | undefined;
18
+ me: {
19
+ uid: number;
20
+ name: string;
21
+ groups: number[];
22
+ };
23
+ stat(path: string): {
24
+ uid: number;
25
+ gid: number;
26
+ mode: number;
27
+ dir: boolean;
28
+ } | undefined;
29
+ }
30
+ export declare const systemProbe: (markerDir?: string) => HostProbe;
31
+ export type HostUserState = {
32
+ ok: true;
33
+ marker: Marker;
34
+ } | {
35
+ ok: false;
36
+ reason: string;
37
+ fix: string;
38
+ };
39
+ export declare const setupCommand: (user: string, owner: string) => string;
40
+ /** Is the host tier for `user` set up, and set up safely? Never runs sudo. */
41
+ export declare function hostUserState(user: string, probe: HostProbe): HostUserState;
42
+ /**
43
+ * Stage the job directory under the shared work root, then run the agent as
44
+ * the dedicated user. `runAs` is the privilege switch: the sudo rule from the
45
+ * setup script; tests pass [] to exercise the same path as themselves.
46
+ */
47
+ export declare function startHostUser(marker: Marker, work: string, log?: (line: string) => void, runAs?: string[]): Promise<Isolation>;