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,103 @@
|
|
|
1
|
+
// Generated by gild-site/scripts/generate-api-client.mjs. Edit the site source.
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
export const EVENT_NAMES = [
|
|
4
|
+
'channel.mention',
|
|
5
|
+
'session',
|
|
6
|
+
'runner',
|
|
7
|
+
'push',
|
|
8
|
+
'pull_request',
|
|
9
|
+
'pull_request_review',
|
|
10
|
+
'pull_request_review_comment',
|
|
11
|
+
'issues',
|
|
12
|
+
'issue_comment',
|
|
13
|
+
'workflow_run',
|
|
14
|
+
'workflow_job',
|
|
15
|
+
'ping',
|
|
16
|
+
'merge_queue',
|
|
17
|
+
'agent_request',
|
|
18
|
+
'health_changed',
|
|
19
|
+
'webhook_disabled',
|
|
20
|
+
] as const
|
|
21
|
+
export const eventName = z.enum(EVENT_NAMES)
|
|
22
|
+
export const hookInput = z.object({
|
|
23
|
+
name: z.literal('web').default('web'),
|
|
24
|
+
config: z.object({
|
|
25
|
+
url: z.string().url(),
|
|
26
|
+
content_type: z.enum(['json', 'form']).default('json'),
|
|
27
|
+
secret: z.string().min(1).max(256),
|
|
28
|
+
insecure_ssl: z.enum(['0']).default('0'),
|
|
29
|
+
}),
|
|
30
|
+
events: z.array(eventName).min(1).max(20).default(['push']),
|
|
31
|
+
active: z.boolean().default(true),
|
|
32
|
+
})
|
|
33
|
+
export const hookPatch = z.object({
|
|
34
|
+
config: hookInput.shape.config.partial().optional(),
|
|
35
|
+
events: z.array(eventName).min(1).max(20).optional(),
|
|
36
|
+
active: z.boolean().optional(),
|
|
37
|
+
})
|
|
38
|
+
export const hookSchema = z.object({
|
|
39
|
+
id: z.number().int(),
|
|
40
|
+
name: z.literal('web'),
|
|
41
|
+
type: z.enum(['Repository', 'Organization']),
|
|
42
|
+
active: z.boolean(),
|
|
43
|
+
events: z.array(eventName),
|
|
44
|
+
config: z.object({
|
|
45
|
+
url: z.string(),
|
|
46
|
+
content_type: z.enum(['json', 'form']),
|
|
47
|
+
insecure_ssl: z.literal('0'),
|
|
48
|
+
}),
|
|
49
|
+
created_at: z.string(),
|
|
50
|
+
updated_at: z.string(),
|
|
51
|
+
url: z.string(),
|
|
52
|
+
ping_url: z.string(),
|
|
53
|
+
deliveries_url: z.string(),
|
|
54
|
+
disabled_reason: z.string().nullable(),
|
|
55
|
+
})
|
|
56
|
+
export const streamEvent = z.object({
|
|
57
|
+
id: z.string(),
|
|
58
|
+
cursor: z.string(),
|
|
59
|
+
event: eventName,
|
|
60
|
+
repository: z.string().nullable(),
|
|
61
|
+
created_at: z.string(),
|
|
62
|
+
payload: z.record(z.string(), z.unknown()),
|
|
63
|
+
})
|
|
64
|
+
export const eventPage = z.object({
|
|
65
|
+
events: z.array(streamEvent),
|
|
66
|
+
cursor: z.string(),
|
|
67
|
+
})
|
|
68
|
+
export const deliverySchema = z.object({
|
|
69
|
+
id: z.number().int().positive(),
|
|
70
|
+
guid: z.string(),
|
|
71
|
+
delivered_at: z.string().nullable(),
|
|
72
|
+
redelivery: z.boolean(),
|
|
73
|
+
duration: z.number(),
|
|
74
|
+
status: z.string(),
|
|
75
|
+
status_code: z.number().nullable(),
|
|
76
|
+
event: eventName,
|
|
77
|
+
action: z.string().nullable(),
|
|
78
|
+
attempts: z.number().int(),
|
|
79
|
+
request: z.object({
|
|
80
|
+
headers: z.record(z.string(), z.string()),
|
|
81
|
+
payload: z.record(z.string(), z.unknown()),
|
|
82
|
+
wire: z.string(),
|
|
83
|
+
}),
|
|
84
|
+
response: z.object({
|
|
85
|
+
headers: z.record(z.string(), z.string()),
|
|
86
|
+
payload: z.string(),
|
|
87
|
+
truncated: z.boolean(),
|
|
88
|
+
}),
|
|
89
|
+
error: z.string().nullable(),
|
|
90
|
+
})
|
|
91
|
+
export type HookInput = z.output<typeof hookInput>
|
|
92
|
+
export type HookPatch = z.output<typeof hookPatch>
|
|
93
|
+
export type Hook = z.output<typeof hookSchema>
|
|
94
|
+
export type StreamEvent = z.output<typeof streamEvent>
|
|
95
|
+
export type Delivery = z.output<typeof deliverySchema>
|
|
96
|
+
export interface Emission {
|
|
97
|
+
created_at?: string
|
|
98
|
+
id: string
|
|
99
|
+
event: (typeof EVENT_NAMES)[number]
|
|
100
|
+
repository: string | null
|
|
101
|
+
audience?: string
|
|
102
|
+
payload: Record<string, unknown>
|
|
103
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Generated by gild-site/scripts/generate-api-client.mjs. Edit the site source.
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
const text = z.string().max(65536), id = z.string().regex(/^[A-Za-z0-9_.:-]{1,160}$/)
|
|
4
|
+
const at = z.string().datetime(), number = z.number().int().positive().max(1_000_000_000)
|
|
5
|
+
export const provenance = z.object({ forge: z.enum(['github','gitlab']), url: z.string().url(), author: z.string().max(200), id, updatedAt: at, state:z.string().optional(),closedAt:at.nullable().optional(),mergedAt:at.nullable().optional() })
|
|
6
|
+
const base = { number, title: text, body: text, bodyParts:z.number().int().positive().max(128).optional(), createdAt: at, imported: provenance }
|
|
7
|
+
export const importRecord = z.discriminatedUnion('kind', [
|
|
8
|
+
z.object({ kind:z.literal('issue'), ...base, state:z.enum(['open','closed']), labels:z.array(z.string().max(200)).max(100) }),
|
|
9
|
+
z.object({ kind:z.literal('pull'), ...base, state:z.enum(['open','closed','merged']), head:z.string().max(300), base:z.string().max(300), sourceRef:z.string().max(300), labels:z.array(z.string().max(200)).max(100).optional(),sourceHead:z.object({ref:z.string(),sha:z.string().nullable(),repository:z.string().nullable()}).optional(), patchParts:z.number().int().nonnegative().optional(), merged:z.object({ by:text, at, commit:z.string().regex(/^[a-f0-9]{40}$/) }).optional() }),
|
|
10
|
+
z.object({ kind:z.enum(['comment','review']), number, target:z.enum(['issue','pr']), id, body:text, bodyParts:z.number().int().positive().max(128).optional(), createdAt:at, imported:provenance, verdict:z.enum(['approve','changes','comment']).optional(), headOid:z.string().nullable().optional(), path:text.optional(), line:z.number().int().optional(), side:z.enum(['LEFT','RIGHT']).optional() }),
|
|
11
|
+
z.object({ kind:z.literal('label'), id, name:z.string().max(200), color:z.string().max(10), description:text }),
|
|
12
|
+
z.object({kind:z.literal('body'),number,target:z.enum(['issue','pr']),record:z.enum(['issue','pull','comment','review']),id:id.optional(),createdAt:at,version:at,part:z.number().int().nonnegative().max(127),parts:z.number().int().positive().max(128),text:z.string().max(8192)}),
|
|
13
|
+
z.object({ kind:z.literal('patch'), number, part:z.number().int().nonnegative().max(100000), text:z.string().max(16384) }),
|
|
14
|
+
])
|
|
15
|
+
export type ImportRecord = z.infer<typeof importRecord>
|
|
16
|
+
export const importInput = z.object({ url:z.string().min(1).max(2048), name:z.string().regex(/^[a-z0-9][a-z0-9._-]{0,62}$/i).optional(), private:z.boolean().optional(), mirror:z.boolean().default(false), source_token:z.string().max(16384).optional(), forge:z.enum(['github','gitlab','git']).optional() })
|
|
17
|
+
export const importProgress = z.object({ phase:z.enum(['git','metadata','workflows']), completed:z.number().int().nonnegative(), message:z.string().max(200), checkpoint:z.string().max(2048).optional(),default_branch:z.string().max(300).optional(),internal_prefix:z.string().regex(/^import_*\/pr$/).optional() })
|
|
18
|
+
export const importSummary=z.object({branches:z.number().int().nonnegative(),tags:z.number().int().nonnegative(),commits:z.number().int().nonnegative(),records:z.number().int().nonnegative(),elapsed_ms:z.number().nonnegative()})
|
|
19
|
+
export const importStatus = z.object({ repository:z.string(), source:z.string(), mirror:z.boolean(), state:z.enum(['queued','running','complete','mirroring','failed','cutover']), progress:importProgress, warnings:z.array(z.string()), error:z.string().nullable(), updated_at:z.string(), next_sync:z.string().nullable(),summary:importSummary.optional(),/** Too large for gild's servers: `gild clone` imports it locally. */local:z.boolean().optional() })
|
|
20
|
+
export const importAssignment = z.object({ repository:z.string(), source:z.string(), forge:z.enum(['github','gitlab','git']), token:z.string(), source_token:z.string().optional(), mirror:z.boolean(), checkpoint:z.string() })
|
|
21
|
+
export const importCredentials = z.object({ url:z.string(), token:z.string() })
|
|
22
|
+
export const importBatch = z.object({ records:z.array(importRecord).max(40), progress:importProgress })
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Generated by gild-site/scripts/generate-api-client.mjs. Edit the site source.
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
// Orchestrator agents: a grant on a repo or org lets an agent spawn child
|
|
4
|
+
// agents there, suspend and resume them, and read every agent's mentions.
|
|
5
|
+
const text = z.string()
|
|
6
|
+
export const agentGrant = z.enum(['pr', 'review', 'merge', 'coordinate', 'queue'])
|
|
7
|
+
export const orchestratorGrant = z.object({
|
|
8
|
+
agent: text,
|
|
9
|
+
/** Where the grant lives: this repo, or the org that owns it. */
|
|
10
|
+
scope: z.enum(['repo', 'org']),
|
|
11
|
+
child_limit: z.number().int(),
|
|
12
|
+
/** Live (not revoked) children it holds, across every repo. */
|
|
13
|
+
children: z.number().int(),
|
|
14
|
+
granted_by: text,
|
|
15
|
+
granted_at: text,
|
|
16
|
+
})
|
|
17
|
+
export const orchestratorList = z.object({
|
|
18
|
+
orchestrators: z.array(orchestratorGrant),
|
|
19
|
+
})
|
|
20
|
+
export const orchestratorInput = z.object({
|
|
21
|
+
/** sponsor/label of an existing agent (an unknown one answers 404). */
|
|
22
|
+
agent: text.min(1).max(80),
|
|
23
|
+
child_limit: z.number().int().min(1).max(100).default(10),
|
|
24
|
+
})
|
|
25
|
+
export const orchestratorRevoked = z.object({
|
|
26
|
+
ok: z.literal(true),
|
|
27
|
+
agent: text,
|
|
28
|
+
/** Every descendant revoked with it. */
|
|
29
|
+
revoked: z.array(text),
|
|
30
|
+
})
|
|
31
|
+
export const childInput = z.object({
|
|
32
|
+
label: text.regex(/^[a-z0-9][a-z0-9-]{0,30}$/),
|
|
33
|
+
publicKey: text.regex(/^ed25519:[A-Za-z0-9+/=]{1,200}$/),
|
|
34
|
+
grants: z.array(agentGrant).max(3).default([]),
|
|
35
|
+
note: text.max(280).optional(),
|
|
36
|
+
})
|
|
37
|
+
export const childAgent = z.object({
|
|
38
|
+
name: text,
|
|
39
|
+
label: text,
|
|
40
|
+
/** The orchestrator that spawned it. */
|
|
41
|
+
sponsor: text,
|
|
42
|
+
/** The person behind the orchestrator, and so behind the child. */
|
|
43
|
+
human: text,
|
|
44
|
+
repo: text,
|
|
45
|
+
grants: z.array(agentGrant),
|
|
46
|
+
suspended: z.boolean(),
|
|
47
|
+
suspended_at: text.nullable(),
|
|
48
|
+
suspended_by: text.nullable(),
|
|
49
|
+
created_at: text,
|
|
50
|
+
revoked_at: text.nullable(),
|
|
51
|
+
})
|
|
52
|
+
export const spawnedChild = childAgent.extend({
|
|
53
|
+
request_id: text,
|
|
54
|
+
scopes: z.array(text),
|
|
55
|
+
/** Returned once; the server keeps only its hash. */
|
|
56
|
+
token: text,
|
|
57
|
+
})
|
|
58
|
+
export const suspension = z.object({
|
|
59
|
+
agent: text,
|
|
60
|
+
suspended: z.boolean(),
|
|
61
|
+
suspended_at: text.nullable(),
|
|
62
|
+
suspended_by: text.nullable(),
|
|
63
|
+
})
|
|
64
|
+
export type ChildAgent = z.output<typeof childAgent>
|
|
65
|
+
/** Agent-facing operations: no token scope, the orchestrator grant (or a
|
|
66
|
+
* person's repo admin) is checked by the orchestrator service itself. */
|
|
67
|
+
export const ORCHESTRATED_OPERATIONS = ['childAgents', 'createChildAgent', 'suspendAgent', 'resumeAgent'] as const
|
|
68
|
+
/** Every operation the orchestrator service executes. */
|
|
69
|
+
export const ORCHESTRATOR_OPERATIONS = [
|
|
70
|
+
...ORCHESTRATED_OPERATIONS,
|
|
71
|
+
'repoOrchestrators', 'setRepoOrchestrator', 'deleteRepoOrchestrator',
|
|
72
|
+
'orgOrchestrators', 'setOrgOrchestrator', 'deleteOrgOrchestrator',
|
|
73
|
+
] as const
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Generated by gild-site/scripts/generate-api-client.mjs. Edit the site source.
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
export const runnerRegistration = z.object({
|
|
4
|
+
name: z.string().regex(/^[a-zA-Z0-9][\w.-]{0,63}$/),
|
|
5
|
+
os: z.enum(['darwin', 'macos', 'linux', 'windows', 'win32']),
|
|
6
|
+
arch: z.enum(['x64', 'arm64', 'aarch64', 'amd64']),
|
|
7
|
+
labels: z
|
|
8
|
+
.array(z.string().regex(/^[\w.-]{1,64}$/))
|
|
9
|
+
.max(32)
|
|
10
|
+
.default([]),
|
|
11
|
+
group: z.string().min(1).default('default'),
|
|
12
|
+
})
|
|
13
|
+
export const runnerGroup = z.object({
|
|
14
|
+
id: z.string().min(1),
|
|
15
|
+
name: z.string().trim().min(1).max(64),
|
|
16
|
+
visibility: z.enum(['all', 'selected']),
|
|
17
|
+
allow_public_repositories: z.boolean(),
|
|
18
|
+
repositories: z
|
|
19
|
+
.array(z.string().regex(/^[a-zA-Z0-9][\w.-]*\/[a-zA-Z0-9][\w.-]*$/))
|
|
20
|
+
.max(1000),
|
|
21
|
+
})
|
|
22
|
+
export const groupInput = runnerGroup
|
|
23
|
+
.omit({ id: true })
|
|
24
|
+
.partial()
|
|
25
|
+
.extend({ name: runnerGroup.shape.name })
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Generated by gild-site/scripts/generate-api-client.mjs. Edit the site source.
|
|
2
|
+
export const SCOPES = [
|
|
3
|
+
'repo:read',
|
|
4
|
+
'repo:write',
|
|
5
|
+
'channel:write',
|
|
6
|
+
'issues:write',
|
|
7
|
+
'pulls:write',
|
|
8
|
+
'actions',
|
|
9
|
+
'hooks',
|
|
10
|
+
'agents',
|
|
11
|
+
'admin',
|
|
12
|
+
] as const
|
|
13
|
+
export type Scope = (typeof SCOPES)[number]
|
|
14
|
+
export interface Principal {
|
|
15
|
+
anonymous?: boolean
|
|
16
|
+
id: string
|
|
17
|
+
name: string
|
|
18
|
+
publicKey: string
|
|
19
|
+
scopes: readonly string[] | null
|
|
20
|
+
repos: readonly string[] | null
|
|
21
|
+
}
|
|
22
|
+
export function hasScope(p: Principal, scope: Scope) {
|
|
23
|
+
return p.scopes === null || p.scopes.includes(scope)
|
|
24
|
+
}
|
|
25
|
+
export function permitsRepo(p: Principal, owner: string, repo: string) {
|
|
26
|
+
return (
|
|
27
|
+
p.repos === null ||
|
|
28
|
+
p.repos.some((r) => r.toLowerCase() === `${owner}/${repo}`.toLowerCase())
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
export function grantScopes(grants: readonly string[]): Scope[] {
|
|
32
|
+
return [
|
|
33
|
+
'repo:read',
|
|
34
|
+
...(grants.includes('pr')
|
|
35
|
+
? (['repo:write', 'issues:write', 'pulls:write'] as const)
|
|
36
|
+
: []),
|
|
37
|
+
...(grants.includes('review') || grants.includes('queue')
|
|
38
|
+
? (['pulls:write'] as const)
|
|
39
|
+
: []),
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Generated by gild-site/scripts/generate-api-client.mjs. Edit the site source.
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
const path = z
|
|
4
|
+
.string()
|
|
5
|
+
.min(1)
|
|
6
|
+
.max(1024)
|
|
7
|
+
.refine(
|
|
8
|
+
(p) =>
|
|
9
|
+
!p.startsWith('/') &&
|
|
10
|
+
!p.split('/').includes('..') &&
|
|
11
|
+
!/[\x00-\x1f]/.test(p),
|
|
12
|
+
'Use a repository-relative path',
|
|
13
|
+
)
|
|
14
|
+
/** Optional live state; no prompts, tool arguments or hook payloads. */
|
|
15
|
+
export const sessionState = z.object({
|
|
16
|
+
status: z.enum(['unknown', 'busy', 'idle', 'waiting', 'tool_start', 'tool_end', 'ended']),
|
|
17
|
+
tool: z.string().regex(/^[a-zA-Z0-9_.:/-]{1,160}$/).optional(),
|
|
18
|
+
last_activity: z.iso.datetime(),
|
|
19
|
+
}).strict()
|
|
20
|
+
export const sessionInput = z
|
|
21
|
+
.object({
|
|
22
|
+
id: z.string().uuid().optional(),
|
|
23
|
+
agent: z.string().max(100).optional(),
|
|
24
|
+
started_at: z.iso.datetime(),
|
|
25
|
+
ended_at: z.iso.datetime().nullable().default(null),
|
|
26
|
+
state: sessionState.optional(),
|
|
27
|
+
model: z.string().min(1).max(120),
|
|
28
|
+
tokens_in: z.number().int().nonnegative().default(0),
|
|
29
|
+
tokens_out: z.number().int().nonnegative().default(0),
|
|
30
|
+
cost: z.number().nonnegative().optional(),
|
|
31
|
+
currency: z
|
|
32
|
+
.string()
|
|
33
|
+
.regex(/^[A-Z]{3}$/)
|
|
34
|
+
.default('USD'),
|
|
35
|
+
commands: z
|
|
36
|
+
.array(
|
|
37
|
+
z.object({
|
|
38
|
+
name: z.string().min(1).max(4096),
|
|
39
|
+
exit_code: z.number().int().nullable(),
|
|
40
|
+
duration_ms: z.number().nonnegative(),
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
43
|
+
.max(200)
|
|
44
|
+
.default([]),
|
|
45
|
+
files: z
|
|
46
|
+
.object({
|
|
47
|
+
read: z.array(path).max(1000).default([]),
|
|
48
|
+
written: z.array(path).max(1000).default([]),
|
|
49
|
+
})
|
|
50
|
+
.default({ read: [], written: [] }),
|
|
51
|
+
notes: z.string().max(16000).default(''),
|
|
52
|
+
})
|
|
53
|
+
.refine(
|
|
54
|
+
(s) => !s.ended_at || Date.parse(s.ended_at) >= Date.parse(s.started_at),
|
|
55
|
+
'ended_at must follow started_at',
|
|
56
|
+
)
|
|
57
|
+
export const receiptPayload = sessionInput.safeExtend({
|
|
58
|
+
id: z.string().uuid(),
|
|
59
|
+
agent: z.string(),
|
|
60
|
+
repository: z.string(),
|
|
61
|
+
target: z.string(),
|
|
62
|
+
created_at: z.iso.datetime(),
|
|
63
|
+
updated_at: z.iso.datetime(),
|
|
64
|
+
version: z.number().int().positive(),
|
|
65
|
+
closed: z.boolean(),
|
|
66
|
+
})
|
|
67
|
+
export const receiptSchema = receiptPayload.safeExtend({
|
|
68
|
+
reported_by: z.literal('agent').default('agent'),
|
|
69
|
+
})
|
|
70
|
+
export type SessionInput = z.output<typeof sessionInput>
|
|
71
|
+
export type Receipt = z.output<typeof receiptSchema>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Generated by gild-site/scripts/generate-api-client.mjs. Edit the site source.
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
const link = z.union([
|
|
4
|
+
z.literal(''),
|
|
5
|
+
z
|
|
6
|
+
.url()
|
|
7
|
+
.max(2048)
|
|
8
|
+
.refine((s) => /^https?:\/\//.test(s), 'Use an http or https URL'),
|
|
9
|
+
])
|
|
10
|
+
const email = z.union([z.literal(''), z.email().max(254)])
|
|
11
|
+
export const profileSchema = z.object({
|
|
12
|
+
displayName: z.string().max(100),
|
|
13
|
+
publicEmail: email,
|
|
14
|
+
description: z.string().max(1000),
|
|
15
|
+
url: link,
|
|
16
|
+
socialLinks: z.array(link).max(4),
|
|
17
|
+
location: z.string().max(100),
|
|
18
|
+
picture: z.string().nullable(),
|
|
19
|
+
})
|
|
20
|
+
export const profileInput = profileSchema
|
|
21
|
+
.omit({ picture: true })
|
|
22
|
+
.partial()
|
|
23
|
+
.strict()
|
|
24
|
+
|
|
25
|
+
export const renameStatus = z.object({
|
|
26
|
+
job: z
|
|
27
|
+
.object({
|
|
28
|
+
id: z.string(),
|
|
29
|
+
from: z.string(),
|
|
30
|
+
to: z.string(),
|
|
31
|
+
org: z.boolean(),
|
|
32
|
+
orgOwner: z.boolean(),
|
|
33
|
+
phase: z.enum([
|
|
34
|
+
'waiting',
|
|
35
|
+
'copying',
|
|
36
|
+
'publishing',
|
|
37
|
+
'rebinding',
|
|
38
|
+
'rollback',
|
|
39
|
+
'complete',
|
|
40
|
+
'failed',
|
|
41
|
+
]),
|
|
42
|
+
repos: z.array(
|
|
43
|
+
z.object({ from: z.string(), to: z.string(), id: z.string() }),
|
|
44
|
+
),
|
|
45
|
+
copied: z.number(),
|
|
46
|
+
rebound: z.number(),
|
|
47
|
+
error: z.string().nullable(),
|
|
48
|
+
})
|
|
49
|
+
.nullable(),
|
|
50
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// The station's end of an agent's channel: what other members say goes into
|
|
2
|
+
// the agent's VM over its pty, and what the agent says there comes back as
|
|
3
|
+
// the agent's messages (wire.ts). The VM is untrusted: only well-formed say
|
|
4
|
+
// lines count, they are posted as the agent and nobody else, and an answer
|
|
5
|
+
// may only point at a message the agent was given.
|
|
6
|
+
import type { PtyHandle } from '../isolation/session'
|
|
7
|
+
import type { LocalChannel } from './local'
|
|
8
|
+
import {
|
|
9
|
+
decodeFromAgent,
|
|
10
|
+
decodeToAgent,
|
|
11
|
+
encodeToAgent,
|
|
12
|
+
lineSplitter,
|
|
13
|
+
printable,
|
|
14
|
+
} from './wire'
|
|
15
|
+
|
|
16
|
+
/** Lines of the VM's other output shown per chat. */
|
|
17
|
+
const VM_LINES = 200
|
|
18
|
+
|
|
19
|
+
export function bridgeAgent(o: {
|
|
20
|
+
channel: LocalChannel
|
|
21
|
+
/** The agent's member name. */
|
|
22
|
+
agent: string
|
|
23
|
+
pty: Pick<PtyHandle, 'write' | 'onData'>
|
|
24
|
+
/** Whatever else the VM printed (its errors), for the person's log. */
|
|
25
|
+
log?: (line: string) => void
|
|
26
|
+
}): () => void {
|
|
27
|
+
const given = new Set<string>()
|
|
28
|
+
let off = false
|
|
29
|
+
// Nothing goes in until the guest says it is here: by then its pty is raw,
|
|
30
|
+
// so a message is neither echoed nor cut at the line-editing limit.
|
|
31
|
+
let ready = false
|
|
32
|
+
const queued: string[] = []
|
|
33
|
+
let refused = 0
|
|
34
|
+
let shown = 0
|
|
35
|
+
const post = (
|
|
36
|
+
text: string,
|
|
37
|
+
options: { note?: boolean; answers?: string },
|
|
38
|
+
) => {
|
|
39
|
+
try {
|
|
40
|
+
o.channel.post(o.agent, text, options)
|
|
41
|
+
if (options.answers) given.delete(options.answers)
|
|
42
|
+
return true
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// Said once, not once per message a flooding VM sends.
|
|
45
|
+
if (refused++ === 0)
|
|
46
|
+
o.log?.(`the agent's messages aren't taken: ${(e as Error).message}`)
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
o.pty.onData(
|
|
51
|
+
lineSplitter((line) => {
|
|
52
|
+
if (off) return
|
|
53
|
+
const said = decodeFromAgent(line)
|
|
54
|
+
if (!said) {
|
|
55
|
+
// The pty echoing a message is neither the agent speaking nor worth
|
|
56
|
+
// showing; anything else is, made safe for a terminal.
|
|
57
|
+
if (line.trim() && !decodeToAgent(line)) {
|
|
58
|
+
// Enough to see what went wrong; never a firehose.
|
|
59
|
+
shown++
|
|
60
|
+
if (shown <= VM_LINES)
|
|
61
|
+
o.log?.(`(vm) ${printable(line.slice(0, 500))}`)
|
|
62
|
+
else if (shown === VM_LINES + 1)
|
|
63
|
+
o.log?.('(vm) … more output from the VM is not shown')
|
|
64
|
+
}
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
const answers =
|
|
68
|
+
said.answers && given.has(said.answers) ? said.answers : undefined
|
|
69
|
+
if (!post(said.text, { note: said.note, answers }) && answers)
|
|
70
|
+
// An answer that couldn't be kept still finishes its message, so the
|
|
71
|
+
// chat can end; the station says why, not the VM.
|
|
72
|
+
try {
|
|
73
|
+
o.channel.post(o.agent, "the agent's answer couldn't be kept", {
|
|
74
|
+
note: true,
|
|
75
|
+
answers,
|
|
76
|
+
reserved: true,
|
|
77
|
+
})
|
|
78
|
+
given.delete(answers)
|
|
79
|
+
} catch {}
|
|
80
|
+
if (!ready && said.note) {
|
|
81
|
+
ready = true
|
|
82
|
+
for (const line of queued.splice(0)) o.pty.write(line)
|
|
83
|
+
}
|
|
84
|
+
}),
|
|
85
|
+
)
|
|
86
|
+
const stop = o.channel.onMessage((m) => {
|
|
87
|
+
if (m.from === o.agent || m.note) return
|
|
88
|
+
given.add(m.id)
|
|
89
|
+
const line = encodeToAgent({ id: m.id, from: m.from, text: m.text })
|
|
90
|
+
if (ready) o.pty.write(line)
|
|
91
|
+
else queued.push(line)
|
|
92
|
+
})
|
|
93
|
+
return () => {
|
|
94
|
+
off = true
|
|
95
|
+
stop()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// A channel the station hosts (docs/architecture.md section 4; harar section
|
|
2
|
+
// 3b): the chat in `mirab dev` between the person at this terminal and the
|
|
3
|
+
// agent in its VM. The station is the app here: it holds the messages, one
|
|
4
|
+
// JSON line each, in a file only this machine's user can read, and never lets
|
|
5
|
+
// them leave the machine. Membership is fixed when the channel opens; a
|
|
6
|
+
// message is untrusted input from whoever wrote it, and being a member grants
|
|
7
|
+
// nothing else. A channel id is never reused, so a new chat never shows an
|
|
8
|
+
// old one's messages.
|
|
9
|
+
import { randomBytes } from 'node:crypto'
|
|
10
|
+
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
|
|
13
|
+
/** The longest message a channel takes; a longer one is refused, never cut. */
|
|
14
|
+
export const MAX_MESSAGE = 64 * 1024
|
|
15
|
+
/** The most a station keeps for one channel, so a VM that floods it can't
|
|
16
|
+
* fill the disk. */
|
|
17
|
+
export const MAX_CHANNEL = 64 * 1024 * 1024
|
|
18
|
+
|
|
19
|
+
export interface Member {
|
|
20
|
+
/** An actor's harar name, e.g. `gg.addis.human.sami`, `gg.addis.agent.ops`. */
|
|
21
|
+
name: string
|
|
22
|
+
can: 'speak' | 'listen'
|
|
23
|
+
/** Named by this machine alone: nothing (no harar token) vouched for it. */
|
|
24
|
+
unverified?: true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Message {
|
|
28
|
+
/** Never reused, and ordered by the millisecond: a ULID. */
|
|
29
|
+
id: string
|
|
30
|
+
from: string
|
|
31
|
+
at: string
|
|
32
|
+
text: string
|
|
33
|
+
/** Said about the chat (the agent reloaded, a turn failed), not in it. */
|
|
34
|
+
note?: true
|
|
35
|
+
/** The message this finishes: its reply, or why there is none. */
|
|
36
|
+
answers?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface LocalChannel {
|
|
40
|
+
/** `<org>.channel.ch-<ulid>` */
|
|
41
|
+
readonly id: string
|
|
42
|
+
readonly members: readonly Member[]
|
|
43
|
+
/** Where the messages are kept. */
|
|
44
|
+
readonly path: string
|
|
45
|
+
post(
|
|
46
|
+
from: string,
|
|
47
|
+
text: string,
|
|
48
|
+
options?: {
|
|
49
|
+
note?: boolean
|
|
50
|
+
answers?: string
|
|
51
|
+
/** The station's own note: it may use the room kept back for it, so
|
|
52
|
+
* whatever fills the channel, a message can still be answered. */
|
|
53
|
+
reserved?: boolean
|
|
54
|
+
},
|
|
55
|
+
): Message
|
|
56
|
+
messages(): Message[]
|
|
57
|
+
/** Every message posted from now on, in order. */
|
|
58
|
+
onMessage(listener: (message: Message) => void): () => void
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
|
|
62
|
+
|
|
63
|
+
/** 48 bits of milliseconds and 80 random bits, in Crockford base32. */
|
|
64
|
+
export function ulid(now = Date.now()): string {
|
|
65
|
+
let time = ''
|
|
66
|
+
for (let t = now, i = 0; i < 10; i++, t = Math.floor(t / 32))
|
|
67
|
+
time = CROCKFORD[t % 32] + time
|
|
68
|
+
let random = ''
|
|
69
|
+
const bytes = randomBytes(10)
|
|
70
|
+
let bits = 0
|
|
71
|
+
let value = 0
|
|
72
|
+
for (const byte of bytes) {
|
|
73
|
+
value = (value << 8) | byte
|
|
74
|
+
bits += 8
|
|
75
|
+
while (bits >= 5) {
|
|
76
|
+
bits -= 5
|
|
77
|
+
random += CROCKFORD[(value >> bits) & 31]
|
|
78
|
+
}
|
|
79
|
+
value &= (1 << bits) - 1
|
|
80
|
+
}
|
|
81
|
+
return time + random
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function openChannel(o: {
|
|
85
|
+
/** The directory the station keeps channels in (private to this user). */
|
|
86
|
+
dir: string
|
|
87
|
+
/** The org whose channel it is: the hosting agent's. */
|
|
88
|
+
org: string
|
|
89
|
+
members: Member[]
|
|
90
|
+
now?: () => Date
|
|
91
|
+
/** Tests make it small. */
|
|
92
|
+
maxBytes?: number
|
|
93
|
+
}): LocalChannel {
|
|
94
|
+
const now = o.now ?? (() => new Date())
|
|
95
|
+
const id = `${o.org}.channel.ch-${ulid(now().getTime()).toLowerCase()}`
|
|
96
|
+
const path = join(o.dir, `${id}.jsonl`)
|
|
97
|
+
const members = o.members.map((m) => ({ ...m }))
|
|
98
|
+
const byName = new Map(members.map((m) => [m.name, m]))
|
|
99
|
+
if (byName.size !== members.length)
|
|
100
|
+
throw new Error('a channel lists each member once')
|
|
101
|
+
mkdirSync(o.dir, { recursive: true, mode: 0o700 })
|
|
102
|
+
// The first line says what the channel is; `flag: 'wx'` because an id is
|
|
103
|
+
// never reused.
|
|
104
|
+
appendFileSync(
|
|
105
|
+
path,
|
|
106
|
+
JSON.stringify({ channel: id, members, opened: now().toISOString() }) +
|
|
107
|
+
'\n',
|
|
108
|
+
{ mode: 0o600, flag: 'wx' },
|
|
109
|
+
)
|
|
110
|
+
const listeners = new Set<(message: Message) => void>()
|
|
111
|
+
let kept = 0
|
|
112
|
+
return {
|
|
113
|
+
id,
|
|
114
|
+
members,
|
|
115
|
+
path,
|
|
116
|
+
post(from, text, options) {
|
|
117
|
+
const member = byName.get(from)
|
|
118
|
+
if (!member) throw new Error(`${from} isn't a member of ${id}`)
|
|
119
|
+
if (member.can !== 'speak')
|
|
120
|
+
throw new Error(`${from} only listens in ${id}`)
|
|
121
|
+
if (Buffer.byteLength(text) > MAX_MESSAGE)
|
|
122
|
+
throw new Error(`a message is at most ${MAX_MESSAGE} bytes`)
|
|
123
|
+
const at = now()
|
|
124
|
+
const message: Message = {
|
|
125
|
+
id: ulid(at.getTime()),
|
|
126
|
+
from,
|
|
127
|
+
at: at.toISOString(),
|
|
128
|
+
text,
|
|
129
|
+
...(options?.note ? { note: true as const } : {}),
|
|
130
|
+
...(options?.answers ? { answers: options.answers } : {}),
|
|
131
|
+
}
|
|
132
|
+
const line = JSON.stringify(message) + '\n'
|
|
133
|
+
const max = o.maxBytes ?? MAX_CHANNEL
|
|
134
|
+
const room = options?.reserved ? max : max - Math.min(max / 8, 1 << 20)
|
|
135
|
+
if (kept + Buffer.byteLength(line) > room)
|
|
136
|
+
throw new Error(`${id} is full; open a new chat`)
|
|
137
|
+
appendFileSync(path, line)
|
|
138
|
+
kept += Buffer.byteLength(line)
|
|
139
|
+
for (const listener of listeners) listener(message)
|
|
140
|
+
return message
|
|
141
|
+
},
|
|
142
|
+
messages() {
|
|
143
|
+
return readFileSync(path, 'utf8')
|
|
144
|
+
.split('\n')
|
|
145
|
+
.slice(1)
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
.map((line) => JSON.parse(line) as Message)
|
|
148
|
+
},
|
|
149
|
+
onMessage(listener) {
|
|
150
|
+
listeners.add(listener)
|
|
151
|
+
return () => void listeners.delete(listener)
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Generated by gild-site/scripts/generate-api-client.mjs. Edit the site source.
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { sessionState } from '../api/sessions-contract.ts'
|
|
4
|
+
export const channelState = sessionState
|
|
5
|
+
export type ChannelState = z.output<typeof channelState>
|
|
6
|
+
export function stateLabel(state: ChannelState) {
|
|
7
|
+
if (state.status === 'waiting') return '◐ waiting'
|
|
8
|
+
if (state.status === 'busy' || state.status === 'tool_start') return '● busy' + (state.tool ? ` · ${state.tool}` : '')
|
|
9
|
+
if (['idle', 'tool_end', 'ended'].includes(state.status)) return '○ idle'
|
|
10
|
+
return '○ unknown'
|
|
11
|
+
}
|