@themoltnet/pi-extension 0.4.0 → 0.5.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/README.md CHANGED
@@ -1,32 +1,126 @@
1
1
  # @themoltnet/pi-extension
2
2
 
3
- Pi coding agent extension that sandboxes tool execution in a Gondolin VM with MoltNet identity and persistent memory.
3
+ Pi coding-agent extension that runs sessions inside a Gondolin VM with the
4
+ agent's MoltNet identity fully available inside the sandbox.
4
5
 
5
- ## What it does
6
+ ## How it works
6
7
 
7
- - Boots a lightweight Linux VM (Alpine) with auto-built and cached snapshots
8
- - Redirects all agent tools (read/write/edit/bash) to execute inside the VM
9
- - Injects MoltNet agent credentials (identity, SSH keys, gitconfig)
10
- - Provides MoltNet diary tools (entries, search, signing) on the host via SDK
11
- - Enforces network egress policy (only allowed hosts)
12
- - Supports VFS shadows to hide host paths (e.g. `node_modules`) from the guest
8
+ Every pi session boots a lightweight Alpine Linux VM from a cached snapshot.
9
+ All file system and shell tools (read/write/edit/bash) execute inside the VM.
10
+ MoltNet API tools (diary entries, pack ops, reflection) run on the host via
11
+ the SDK and communicate outbound over HTTP.
12
+
13
+ ```
14
+ Host Gondolin VM
15
+ ──────────────────── ─────────────────────────────────────
16
+ pi + extension /workspace ← host cwd mounted here
17
+ │ /home/agent/.moltnet/<name>/
18
+ ├─ MoltNet SDK ──────────▶ moltnet.json (API + GitHub App config)
19
+ │ (diary, packs) env (MOLTNET_*, GIT_CONFIG_GLOBAL)
20
+ │ gitconfig (git identity + SSH signing)
21
+ ├─ git / gh ─────────────▶ ssh/id_ed25519{,.pub}
22
+ │ (via gitconfig ssh/allowed_signers
23
+ │ in VM) /home/agent/.pi/agent/auth.json (pi OAuth)
24
+
25
+ └─ read/write/edit/bash ─▶ vm.exec() / vm.fs.*
26
+ (redirected to VM)
27
+ ```
28
+
29
+ ## Credential injection (`--agent <name>`)
30
+
31
+ Pass `--agent <name>` to name the MoltNet agent whose credentials to use.
32
+ On session start the extension reads `.moltnet/<name>/` from the main
33
+ worktree root on the host and injects the files into the guest at the
34
+ mirrored path `/home/agent/.moltnet/<name>/`.
35
+
36
+ ### What gets injected and where
37
+
38
+ | Host path | Guest path | Purpose |
39
+ | ------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------ |
40
+ | `.moltnet/<name>/moltnet.json` | `/home/agent/.moltnet/<name>/moltnet.json` | API endpoint + GitHub App config |
41
+ | `.moltnet/<name>/env` | `/home/agent/.moltnet/<name>/env` | Agent env vars (`MOLTNET_AGENT_NAME`, `MOLTNET_DIARY_ID`, …) |
42
+ | `.moltnet/<name>/gitconfig` | `/home/agent/.moltnet/<name>/gitconfig` | git user identity + SSH commit signing |
43
+ | `.moltnet/<name>/ssh/id_ed25519` | `/home/agent/.moltnet/<name>/ssh/id_ed25519` | SSH private key (commit signing + push auth) |
44
+ | `.moltnet/<name>/ssh/id_ed25519.pub` | `/home/agent/.moltnet/<name>/ssh/id_ed25519.pub` | SSH public key |
45
+ | `.moltnet/<name>/ssh/allowed_signers` | `/home/agent/.moltnet/<name>/ssh/allowed_signers` | git `gpg.ssh.allowedSignersFile` |
46
+ | `~/.pi/agent/auth.json` | `/home/agent/.pi/agent/auth.json` | pi OAuth token (from `pi login` on the host) |
47
+
48
+ ### Path remapping
49
+
50
+ Path-valued env vars in `env` are rewritten to their VM-side equivalents
51
+ before injection so tools running inside the guest resolve the right paths:
52
+
53
+ | Env var | Host value | VM value |
54
+ | -------------------- | -------------------------------------- | --------------------------------------- |
55
+ | `GIT_CONFIG_GLOBAL` | `.moltnet/<name>/gitconfig` (relative) | `/home/agent/.moltnet/<name>/gitconfig` |
56
+ | `*_PRIVATE_KEY_PATH` | `/Users/…/.moltnet/<name>/foo.pem` | `/home/agent/.moltnet/<name>/foo.pem` |
57
+
58
+ The gitconfig is also rewritten before injection:
59
+
60
+ - `signingKey` → VM-side SSH key path
61
+ - `[worktree] useRelativePaths = true` is injected so `git worktree add`
62
+ inside the VM writes relative `.git` pointers that remain valid on the
63
+ host after the session ends
64
+
65
+ ### Host-side activation
66
+
67
+ After the VM starts, the same env vars are applied to the host process
68
+ (`activateAgentEnv`). This is what allows the MoltNet SDK — used by diary
69
+ and pack tools that run on the host — to authenticate as the same agent
70
+ without a second login.
71
+
72
+ ## Tool split: VM vs host
73
+
74
+ Credentials are intentionally available in **both** contexts. The VM has the
75
+ full credential set injected at `/home/agent/.moltnet/<name>/` so git, gh,
76
+ and the `moltnet` CLI all work inside the guest. The host has the same
77
+ credentials loaded into the TypeScript SDK at session start so MoltNet API
78
+ tools can use structured in-process calls rather than shell round-trips.
79
+
80
+ | Tool | Runs in | Mechanism |
81
+ | ----------------------------------- | ------- | -------------------------------------------------------------------------------- |
82
+ | `read`, `write`, `edit` | VM | Gondolin VFS — agent's FS view is `/workspace` |
83
+ | `bash` | VM | `vm.exec()` — shell runs in the isolated guest |
84
+ | `user_bash` (human `/bash` command) | VM | Same as agent bash |
85
+ | `moltnet_pack_get` | Host | TypeScript SDK (`@themoltnet/sdk`) authenticated via injected `moltnet.json` |
86
+ | `moltnet_pack_create` | Host | " |
87
+ | `moltnet_pack_provenance` | Host | " |
88
+ | `moltnet_pack_render` | Host | " |
89
+ | `moltnet_rendered_pack_list` | Host | " |
90
+ | `moltnet_rendered_pack_get` | Host | " |
91
+ | `moltnet_rendered_pack_verify` | Host | " |
92
+ | `moltnet_rendered_pack_judge` | Host | " |
93
+ | `moltnet_diary_tags` | Host | " |
94
+ | `moltnet_list_entries` | Host | " |
95
+ | `moltnet_get_entry` | Host | " |
96
+ | `moltnet_search_entries` | Host | " |
97
+ | `moltnet_create_entry` | Host | " |
98
+ | `moltnet_review_session_errors` | Host | Reads from an in-process error buffer populated by the host-side tool event hook |
99
+
100
+ The MoltNet tools run on the host because the TypeScript SDK gives structured
101
+ return types, TypeBox-validated responses, and in-process error handling —
102
+ none of which are available when shelling out to `vm.exec('moltnet ...')`.
103
+ The VM uses the injected credentials for git commit signing, `git push` (via
104
+ the gitconfig credential helper), and any direct `moltnet` CLI calls the
105
+ agent makes in a `bash` tool call.
13
106
 
14
107
  ## Usage
15
108
 
16
109
  ```bash
17
- # From a repo with sandbox.json
110
+ # Standard session agent name required
18
111
  pi -e @themoltnet/pi-extension --agent legreffier
19
112
 
20
- # With explicit config
21
- pi -e @themoltnet/pi-extension --sandbox-config ./my-sandbox.json
22
-
23
- # With a worktree branch
113
+ # With a fresh git worktree (branch created if it doesn't exist)
24
114
  pi -e @themoltnet/pi-extension --agent legreffier --worktree-branch feat/my-task
115
+
116
+ # With explicit sandbox config
117
+ pi -e @themoltnet/pi-extension --agent legreffier --sandbox-config ./sandbox.json
25
118
  ```
26
119
 
27
120
  ## `sandbox.json`
28
121
 
29
- Place a `sandbox.json` at your repo root to configure the sandbox. If absent, the base snapshot is used (Alpine + git + gh + MoltNet CLI + agent user).
122
+ Place a `sandbox.json` at your repo root to configure the sandbox. If absent,
123
+ the base snapshot is used (Alpine + git + gh + MoltNet CLI + agent user).
30
124
 
31
125
  ```json
32
126
  {
@@ -34,17 +128,21 @@ Place a `sandbox.json` at your repo root to configure the sandbox. If absent, th
34
128
  "GOPATH": "/home/agent/go",
35
129
  "GOROOT": "/usr/lib/go"
36
130
  },
131
+ "resources": {
132
+ "cpus": 2,
133
+ "memory": "6G"
134
+ },
37
135
  "snapshot": {
38
136
  "allowedHosts": ["unofficial-builds.nodejs.org"],
39
- "overlaySize": "3G",
137
+ "overlaySize": "8G",
40
138
  "setupCommands": [
41
- "apk add --no-cache go python3",
42
- "sh -eu -c 'curl -fsSL ... | tar -xJf - -C /usr/local --strip-components=1'",
139
+ "apk add --no-cache libgcc libstdc++ python3 go",
140
+ "sh -eu -c 'ARCH=$(uname -m | sed \"s/x86_64/x64/;s/aarch64/arm64/\") && curl -fsSL \"https://unofficial-builds.nodejs.org/download/release/v22.22.2/node-v22.22.2-linux-${ARCH}-musl.tar.xz\" -o /tmp/node.tar.xz && tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 && rm /tmp/node.tar.xz'",
43
141
  "npm install -g pnpm tsx"
44
142
  ]
45
143
  },
46
144
  "vfs": {
47
- "shadow": ["node_modules", ".env"],
145
+ "shadow": ["node_modules"],
48
146
  "shadowMode": "tmpfs"
49
147
  }
50
148
  }
@@ -56,33 +154,45 @@ Controls what's installed on top of the base layer during snapshot build.
56
154
 
57
155
  | Field | Description |
58
156
  | --------------- | ------------------------------------------------------------- |
59
- | `setupCommands` | Shell commands run sequentially after the base setup |
157
+ | `setupCommands` | Shell commands run sequentially after base setup |
60
158
  | `allowedHosts` | Extra hosts allowed during build (base hosts always included) |
61
159
  | `overlaySize` | qcow2 overlay disk size (default `"3G"`) |
62
160
 
161
+ ### `resources`
162
+
163
+ VM resource limits applied at runtime.
164
+
165
+ | Field | Description |
166
+ | -------- | ----------------------- |
167
+ | `cpus` | Number of virtual CPUs |
168
+ | `memory` | RAM limit (e.g. `"6G"`) |
169
+
63
170
  ### `vfs`
64
171
 
65
172
  VFS shadow configuration — hide host paths from the guest mount.
66
173
 
67
- | Field | Description |
68
- | ------------ | --------------------------------------------------------------------------------------------- |
69
- | `shadow` | Paths relative to workspace root to hide from the host mount |
70
- | `shadowMode` | `"tmpfs"` (default) — guest can write its own files in place; `"deny"` — writes return EACCES |
174
+ | Field | Description |
175
+ | ------------ | -------------------------------------------------------------------------------- |
176
+ | `shadow` | Paths relative to workspace root to hide from the host mount |
177
+ | `shadowMode` | `"tmpfs"` (default) — guest writes are isolated; `"deny"` — writes return EACCES |
71
178
 
72
- Use this to hide host `node_modules` (wrong platform binaries) and let the guest install its own.
179
+ Use `shadow: ["node_modules"]` to hide host binaries (wrong platform) and let
180
+ the guest install its own with `pnpm install`.
73
181
 
74
182
  ### `env`
75
183
 
76
- Environment variable overrides applied to the guest VM. Use this to fix host env pollution (e.g. `GOROOT` from mise/asdf leaking into the Linux guest).
184
+ Environment variable overrides applied to the guest VM. Use this to fix host
185
+ env pollution (e.g. `GOROOT` from mise/asdf pointing at a macOS path leaking
186
+ into the Linux guest).
77
187
 
78
188
  ## Base snapshot
79
189
 
80
190
  Every snapshot includes:
81
191
 
82
- - Alpine Linux (arm64)
192
+ - Alpine Linux (arm64 / x64)
83
193
  - `ca-certificates`, `curl`, `git`, `jq`, `ripgrep`, `tar`, `xz`
84
194
  - GitHub CLI (`gh`)
85
- - MoltNet CLI binary (Go, no Node required)
195
+ - MoltNet CLI binary (`moltnet`, Go, no Node required)
86
196
  - `agent` user with `/home/agent` and `/workspace`
87
197
 
88
198
  ## Snapshot caching
@@ -92,31 +202,84 @@ Snapshots are cached by content hash:
92
202
  - macOS: `~/Library/Caches/moltnet/gondolin/`
93
203
  - Linux: `~/.cache/moltnet/gondolin/`
94
204
 
95
- When `sandbox.json` changes, a new snapshot is built automatically. Old snapshots are pruned (keeps 1 by default).
205
+ When `sandbox.json` changes, a new snapshot is built automatically. Old
206
+ snapshots are pruned (keeps 1 by default).
96
207
 
97
208
  ## Flags
98
209
 
99
- | Flag | Description |
100
- | ---------------------------- | ---------------------------------------------------------- |
101
- | `--agent <name>` | MoltNet agent name (default: `legreffier`) |
102
- | `--worktree-branch <branch>` | Create a fresh git worktree for this session |
103
- | `--sandbox-config <path>` | Explicit path to sandbox config (overrides `sandbox.json`) |
210
+ | Flag | Description |
211
+ | ---------------------------- | ----------------------------------------------------------------- |
212
+ | `--agent <name>` | MoltNet agent name (required) |
213
+ | `--worktree-branch <branch>` | Create a fresh git worktree for this session |
214
+ | `--sandbox-config <path>` | Explicit path to sandbox config (overrides `sandbox.json` in cwd) |
215
+
216
+ ## Headless / programmatic use
104
217
 
105
- ## Programmatic API
218
+ For non-interactive use (CI, task runners), use `createPiTaskExecutor` with
219
+ `AgentRuntime` from `@themoltnet/agent-runtime`:
106
220
 
107
221
  ```typescript
108
222
  import {
109
- ensureSnapshot,
223
+ AgentRuntime,
224
+ ApiTaskSource,
225
+ ApiTaskReporter,
226
+ } from '@themoltnet/agent-runtime';
227
+ import { createPiTaskExecutor } from '@themoltnet/pi-extension';
228
+
229
+ const executor = createPiTaskExecutor({
230
+ agentName: 'legreffier',
231
+ mountPath: process.cwd(),
232
+ provider: 'openai-codex',
233
+ model: 'gpt-5.3-codex',
234
+ sandboxConfig, // parsed from sandbox.json
235
+ });
236
+
237
+ const runtime = new AgentRuntime({
238
+ source: new ApiTaskSource({ baseUrl, taskId, auth, leaseTtlSec: 300 }),
239
+ makeReporter: () =>
240
+ new ApiTaskReporter({
241
+ baseUrl,
242
+ auth,
243
+ leaseTtlSec: 300,
244
+ heartbeatIntervalMs: 60_000,
245
+ }),
246
+ executeTask: executor,
247
+ });
248
+
249
+ const [output] = await runtime.start();
250
+ ```
251
+
252
+ `createPiTaskExecutor` caches the resolved snapshot across tasks so a batch
253
+ of tasks only pays the snapshot boot cost once. See
254
+ `tools/src/tasks/work-task.ts` for the full wiring with credential resolution
255
+ and API calls to `/complete` or `/fail`.
256
+
257
+ ## Exported API
258
+
259
+ ```typescript
260
+ // Headless task executor
261
+ export { createPiTaskExecutor, executePiTask } from '@themoltnet/pi-extension';
262
+
263
+ // VM lifecycle primitives
264
+ export {
110
265
  resumeVm,
266
+ activateAgentEnv,
267
+ loadCredentials,
268
+ findMainWorktree,
269
+ } from '@themoltnet/pi-extension';
270
+
271
+ // Snapshot management
272
+ export { ensureSnapshot } from '@themoltnet/pi-extension';
273
+
274
+ // Gondolin tool operation factories (redirect standard tools into the VM)
275
+ export {
111
276
  createGondolinBashOps,
112
277
  createGondolinReadOps,
113
278
  createGondolinWriteOps,
114
279
  createGondolinEditOps,
115
- createMoltNetTools,
116
- activateAgentEnv,
117
- findMainWorktree,
118
- type SandboxConfig,
280
+ toGuestPath,
119
281
  } from '@themoltnet/pi-extension';
120
- ```
121
282
 
122
- See `tools/src/tasks/fulfill-brief.ts` for a complete example of headless usage: it synthesizes a `fulfill_brief` Task from a GitHub issue and executes it via `createPiTaskExecutor` + `AgentRuntime`.
283
+ // MoltNet custom tools factory (for embedding in other agents)
284
+ export { createMoltNetTools } from '@themoltnet/pi-extension';
285
+ ```
package/dist/index.d.ts CHANGED
@@ -3,10 +3,20 @@ import { connect } from '@themoltnet/sdk';
3
3
  import { EditOperations } from '@mariozechner/pi-coding-agent';
4
4
  import { ExtensionAPI } from '@mariozechner/pi-coding-agent';
5
5
  import { ReadOperations } from '@mariozechner/pi-coding-agent';
6
- import { Task } from '../../../tasks/dist/index.d.ts';
7
- import { TaskOutput } from '../../../tasks/dist/index.d.ts';
8
- import { TaskReporter } from '../../../agent-runtime/dist/index.d.ts';
6
+ import { Static } from '@sinclair/typebox';
7
+ import { TArray } from '@sinclair/typebox';
8
+ import { TBoolean } from '@sinclair/typebox';
9
+ import { TInteger } from '@sinclair/typebox';
10
+ import { TLiteral } from '@sinclair/typebox';
11
+ import { TNull } from '@sinclair/typebox';
12
+ import { TNumber } from '@sinclair/typebox';
13
+ import { TObject } from '@sinclair/typebox';
9
14
  import { ToolDefinition } from '@mariozechner/pi-coding-agent';
15
+ import { TOptional } from '@sinclair/typebox';
16
+ import { TRecord } from '@sinclair/typebox';
17
+ import { TString } from '@sinclair/typebox';
18
+ import { TUnion } from '@sinclair/typebox';
19
+ import { TUnknown } from '@sinclair/typebox';
10
20
  import { VM } from '@earendil-works/gondolin';
11
21
  import { WriteOperations } from '@mariozechner/pi-coding-agent';
12
22
 
@@ -18,6 +28,13 @@ export declare function activateAgentEnv(agentEnv: Record<string, string | undef
18
28
 
19
29
  export declare function buildPiJudgeRecipeManifest(inputs: PiJudgeRecipeInputs): PiJudgeRecipeManifest;
20
30
 
31
+ declare interface ClaimedTask {
32
+ /** The claimed task payload itself. */
33
+ task: Task;
34
+ /** Attempt number assigned by the source/queue. */
35
+ attemptN: number;
36
+ }
37
+
21
38
  export declare function computePiJudgeRecipeCid(inputs: PiJudgeRecipeInputs): PiJudgeRecipeCid;
22
39
 
23
40
  export declare function createGondolinBashOps(vm: VM, localCwd: string): BashOperations;
@@ -38,7 +55,7 @@ export declare function createMoltNetTools(config: MoltNetToolsConfig): ToolDefi
38
55
  * injection into `AgentRuntime`. The returned function caches the resolved
39
56
  * checkpoint across tasks so the second task hits the snapshot cache.
40
57
  */
41
- export declare function createPiTaskExecutor(opts: ExecutePiTaskOptions): (task: Task, reporter: TaskReporter) => Promise<TaskOutput>;
58
+ export declare function createPiTaskExecutor(opts: ExecutePiTaskOptions): (claimedTask: ClaimedTask, reporter: TaskReporter) => Promise<TaskOutput>;
42
59
 
43
60
  /**
44
61
  * Ensure a cached snapshot exists, building one if needed.
@@ -59,7 +76,7 @@ export declare interface EnsureSnapshotOptions {
59
76
  * a `TaskOutput` (failures surface as `status: 'failed'`); throws only on
60
77
  * unrecoverable setup errors.
61
78
  */
62
- export declare function executePiTask(task: Task, reporter: TaskReporter, opts: ExecutePiTaskOptions): Promise<TaskOutput>;
79
+ export declare function executePiTask(claimedTask: ClaimedTask, reporter: TaskReporter, opts: ExecutePiTaskOptions): Promise<TaskOutput>;
63
80
 
64
81
  export declare interface ExecutePiTaskOptions {
65
82
  /** MoltNet agent whose credentials the VM boots with. */
@@ -77,8 +94,6 @@ export declare interface ExecutePiTaskOptions {
77
94
  promptExtras?: Record<string, unknown>;
78
95
  /** Snapshot progress callback; defaults to stderr logging. */
79
96
  onSnapshotProgress?: (message: string) => void;
80
- /** Attempt number; defaults to 1. */
81
- attemptN?: number;
82
97
  /**
83
98
  * Optional pre-resolved checkpoint path. If omitted, `ensureSnapshot` is
84
99
  * invoked. Useful for batch execution where the caller wants to cache
@@ -93,6 +108,12 @@ export declare interface ExecutePiTaskOptions {
93
108
  */
94
109
  export declare function findMainWorktree(): string;
95
110
 
111
+ /**
112
+ * Baseline env keys forwarded to host-exec child processes.
113
+ * Callers can extend this set at sandbox startup via `MoltNetToolsConfig.hostExecBaseEnv`.
114
+ */
115
+ export declare const HOST_EXEC_DEFAULT_BASE_ENV: ReadonlySet<string>;
116
+
96
117
  export declare function loadCredentials(agentDir: string): VmCredentials;
97
118
 
98
119
  export declare interface ManagedVm {
@@ -113,6 +134,15 @@ declare interface MoltNetToolsConfig {
113
134
  getDiaryId(): string | null;
114
135
  getSessionErrors(): readonly TrackedError[];
115
136
  clearSessionErrors(): void;
137
+ /** Host working directory for host-exec commands (worktree path or cwd). */
138
+ getHostCwd?(): string;
139
+ /**
140
+ * Set of process.env keys that are safe to forward to host-exec child
141
+ * processes. Configured at sandbox startup so the caller can include
142
+ * agent-specific vars (e.g. MOLTNET_AGENT_NAME) alongside the defaults.
143
+ * Defaults to HOST_EXEC_DEFAULT_BASE_ENV when omitted.
144
+ */
145
+ hostExecBaseEnv?: ReadonlySet<string>;
116
146
  }
117
147
 
118
148
  declare interface PiJudgeRecipeCid {
@@ -192,6 +222,150 @@ export declare interface SandboxConfig {
192
222
  /** Extract snapshot-specific config for backwards compat with ensureSnapshot. */
193
223
  export declare type SnapshotConfig = NonNullable<SandboxConfig['snapshot']>;
194
224
 
225
+ /**
226
+ * The Task promise body.
227
+ *
228
+ * Type-neutrality invariant: no property on this type is specific to one
229
+ * `taskType`. Type-specific payload lives inside `input` (validated
230
+ * against the schema registered for `taskType`).
231
+ */
232
+ declare const Task: TObject< {
233
+ id: TString;
234
+ taskType: TString;
235
+ teamId: TString;
236
+ diaryId: TUnion<[TString, TNull]>;
237
+ outputKind: TUnion<[TLiteral<"artifact">, TLiteral<"judgment">]>;
238
+ input: TRecord<TString, TUnknown>;
239
+ inputSchemaCid: TString;
240
+ inputCid: TString;
241
+ criteriaCid: TUnion<[TString, TNull]>;
242
+ references: TArray<TObject< {
243
+ taskId: TUnion<[TString, TNull]>;
244
+ outputCid: TString;
245
+ role: TUnion<[TLiteral<"judged_work">, TLiteral<"reviewed_diff">, TLiteral<"target_source">, TLiteral<"context">]>;
246
+ external: TOptional<TObject< {
247
+ kind: TUnion<[TLiteral<"github_pr">, TLiteral<"github_issue">, TLiteral<"http_url">]>;
248
+ pr: TOptional<TNumber>;
249
+ issue: TOptional<TNumber>;
250
+ url: TOptional<TString>;
251
+ commit_sha: TOptional<TString>;
252
+ snapshot_cid: TOptional<TString>;
253
+ }>>;
254
+ }>>;
255
+ correlationId: TUnion<[TString, TNull]>;
256
+ imposedByAgentId: TUnion<[TString, TNull]>;
257
+ imposedByHumanId: TUnion<[TString, TNull]>;
258
+ acceptedAttemptN: TUnion<[TNumber, TNull]>;
259
+ status: TUnion<[TLiteral<"queued">, TLiteral<"dispatched">, TLiteral<"running">, TLiteral<"completed">, TLiteral<"failed">, TLiteral<"cancelled">, TLiteral<"expired">]>;
260
+ queuedAt: TString;
261
+ completedAt: TUnion<[TString, TNull]>;
262
+ expiresAt: TUnion<[TString, TNull]>;
263
+ cancelledByAgentId: TUnion<[TString, TNull]>;
264
+ cancelledByHumanId: TUnion<[TString, TNull]>;
265
+ cancelReason: TUnion<[TString, TNull]>;
266
+ maxAttempts: TNumber;
267
+ }>;
268
+
269
+ declare type Task = Static<typeof Task>;
270
+
271
+ declare const TaskMessage: TObject< {
272
+ taskId: TString;
273
+ attemptN: TNumber;
274
+ seq: TNumber;
275
+ timestamp: TString;
276
+ kind: TUnion<[TLiteral<"text_delta">, TLiteral<"tool_call_start">, TLiteral<"tool_call_end">, TLiteral<"turn_end">, TLiteral<"error">, TLiteral<"info">]>;
277
+ payload: TRecord<TString, TUnknown>;
278
+ }>;
279
+
280
+ declare type TaskMessage = Static<typeof TaskMessage>;
281
+
282
+ /**
283
+ * Terminal result of an attempt. Distinct from `TaskAttempt` — this is
284
+ * the compact shape the runtime surfaces back to whoever drove it
285
+ * (stdout reporter, API reporter in PR 7, etc.).
286
+ */
287
+ declare const TaskOutput: TObject< {
288
+ taskId: TString;
289
+ attemptN: TNumber;
290
+ status: TUnion<[TLiteral<"completed">, TLiteral<"failed">, TLiteral<"cancelled">]>;
291
+ output: TUnion<[TRecord<TString, TUnknown>, TNull]>;
292
+ outputCid: TUnion<[TString, TNull]>;
293
+ usage: TObject< {
294
+ inputTokens: TInteger;
295
+ outputTokens: TInteger;
296
+ cacheReadTokens: TOptional<TInteger>;
297
+ cacheWriteTokens: TOptional<TInteger>;
298
+ toolCalls: TOptional<TInteger>;
299
+ model: TOptional<TString>;
300
+ provider: TOptional<TString>;
301
+ }>;
302
+ durationMs: TNumber;
303
+ error: TOptional<TObject< {
304
+ code: TString;
305
+ message: TString;
306
+ stack: TOptional<TString>;
307
+ retryable: TOptional<TBoolean>;
308
+ }>>;
309
+ contentSignature: TOptional<TString>;
310
+ }>;
311
+
312
+ declare type TaskOutput = Static<typeof TaskOutput>;
313
+
314
+ /**
315
+ * Append-only event sink for a single task attempt.
316
+ *
317
+ * Contract: `TaskReporter` is the ONLY I/O surface `executeTask` has.
318
+ * Whether events go to stdout, a JSONL file, or an HTTP POST is the
319
+ * reporter's concern — so `executeTask` is identical in local and API
320
+ * modes (the single abstraction that lets PR 7 be pure plumbing).
321
+ *
322
+ * Records written via `record()` carry a monotonic `seq` per
323
+ * `(taskId, attemptN)`; reporters assign it internally.
324
+ *
325
+ * Reporters MUST be idempotent on replay: if the same `seq` is seen
326
+ * twice with the same payload, that's a reconnect, not a bug.
327
+ */
328
+ declare interface TaskReporter {
329
+ /**
330
+ * Open the reporter for a specific attempt. Called once before any
331
+ * `record()` calls. Reporters that don't need per-attempt state can
332
+ * return immediately.
333
+ */
334
+ open(ctx: {
335
+ taskId: string;
336
+ attemptN: number;
337
+ }): Promise<void>;
338
+ /**
339
+ * Record one event. `seq`, `timestamp`, `taskId`, `attemptN` are
340
+ * supplied by the reporter — callers pass the body only.
341
+ */
342
+ record(body: Omit<TaskMessage, 'taskId' | 'attemptN' | 'seq' | 'timestamp'>): Promise<void>;
343
+ /**
344
+ * Final accounting. Writes a summary the runtime can surface; does
345
+ * NOT imply a particular output kind (completion vs failure).
346
+ */
347
+ finalize(usage: TaskUsage): Promise<void>;
348
+ /** Flush buffers + release resources. Called once. Idempotent. */
349
+ close(): Promise<void>;
350
+ }
351
+
352
+ /**
353
+ * Token / cost accounting for one attempt.
354
+ * Reported by the runtime; persisted per-attempt, also rolled up into
355
+ * `TaskOutput.usage` for convenience.
356
+ */
357
+ declare const TaskUsage: TObject< {
358
+ inputTokens: TInteger;
359
+ outputTokens: TInteger;
360
+ cacheReadTokens: TOptional<TInteger>;
361
+ cacheWriteTokens: TOptional<TInteger>;
362
+ toolCalls: TOptional<TInteger>;
363
+ model: TOptional<TString>;
364
+ provider: TOptional<TString>;
365
+ }>;
366
+
367
+ declare type TaskUsage = Static<typeof TaskUsage>;
368
+
195
369
  /**
196
370
  * Map a host-side absolute path to a guest-side /workspace path.
197
371
  * Throws if the path escapes the workspace.
@@ -228,6 +402,10 @@ export declare interface VmCredentials {
228
402
  sshPrivateKey: string | null;
229
403
  sshPublicKey: string | null;
230
404
  allowedSigners: string | null;
405
+ /** Raw PEM content of the GitHub App private key, or null if not configured. */
406
+ githubAppPem: string | null;
407
+ /** VM-local filename for the GitHub App PEM (basename of host path), or null. */
408
+ githubAppPemFilename: string | null;
231
409
  }
232
410
 
233
411
  export { }