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,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
|
+
}
|
package/src/test-cli.ts
ADDED
|
@@ -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,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
|
+
}>;
|
package/types/index.d.ts
ADDED
|
@@ -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>;
|