@themoltnet/pi-extension 0.15.2 → 0.16.1
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 +49 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +148 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,6 +126,16 @@ the base snapshot is used (Alpine + git + gh + MoltNet CLI + agent user).
|
|
|
126
126
|
"GOPATH": "/home/agent/go",
|
|
127
127
|
"GOROOT": "/usr/lib/go"
|
|
128
128
|
},
|
|
129
|
+
"hostExec": {
|
|
130
|
+
"autoApprove": [
|
|
131
|
+
{
|
|
132
|
+
"argsExcludes": ["--mirror", "--all"],
|
|
133
|
+
"argsPrefix": ["push"],
|
|
134
|
+
"executable": "git"
|
|
135
|
+
},
|
|
136
|
+
{ "argsPrefix": ["pr", "create"], "executable": "gh" }
|
|
137
|
+
]
|
|
138
|
+
},
|
|
129
139
|
"resources": {
|
|
130
140
|
"cpus": 2,
|
|
131
141
|
"memory": "6G"
|
|
@@ -183,6 +193,45 @@ Environment variable overrides applied to the guest VM. Use this to fix host
|
|
|
183
193
|
env pollution (e.g. `GOROOT` from mise/asdf pointing at a macOS path leaking
|
|
184
194
|
into the Linux guest).
|
|
185
195
|
|
|
196
|
+
### `hostExec`
|
|
197
|
+
|
|
198
|
+
Host-side escape hatch policy for `moltnet_host_exec`. The executable must
|
|
199
|
+
still be in the built-in host-exec allowlist (`git`, `gh`, `moltnet`); this
|
|
200
|
+
setting only controls whether the per-call UI approval dialog is skipped.
|
|
201
|
+
|
|
202
|
+
`autoApprove: true` skips the dialog for every allowed host command. Use that
|
|
203
|
+
only on isolated hosts or disposable machines.
|
|
204
|
+
|
|
205
|
+
For local daemon runs, prefer rule-based approval:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"hostExec": {
|
|
210
|
+
"autoApprove": [
|
|
211
|
+
{
|
|
212
|
+
"argsExcludes": ["--mirror", "--all"],
|
|
213
|
+
"argsPrefix": ["push"],
|
|
214
|
+
"executable": "git"
|
|
215
|
+
},
|
|
216
|
+
{ "argsPrefix": ["pr", "create"], "executable": "gh" },
|
|
217
|
+
{ "argsPrefix": ["pr", "view"], "executable": "gh" }
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Each rule matches an exact executable plus optional argument constraints:
|
|
224
|
+
|
|
225
|
+
| Field | Description |
|
|
226
|
+
| -------------- | ----------------------------------------------------------- |
|
|
227
|
+
| `executable` | Exact executable name |
|
|
228
|
+
| `argsPrefix` | Ordered argument prefix; later flags/args are still allowed |
|
|
229
|
+
| `argsContains` | Tokens that must appear anywhere in the args |
|
|
230
|
+
| `argsExcludes` | Tokens that block auto-approval when present |
|
|
231
|
+
|
|
232
|
+
If a rule only sets `executable`, all argument lists for that executable are
|
|
233
|
+
auto-approved after the built-in executable allowlist check.
|
|
234
|
+
|
|
186
235
|
## Base snapshot
|
|
187
236
|
|
|
188
237
|
Every snapshot includes:
|
package/dist/index.d.ts
CHANGED
|
@@ -249,6 +249,34 @@ export declare interface ExecutePiTaskOptions {
|
|
|
249
249
|
* process. See #1078.
|
|
250
250
|
*/
|
|
251
251
|
makeOnTurnEvent?: TurnEventHandlerFactory;
|
|
252
|
+
/**
|
|
253
|
+
* Cap the number of tool-use turns per attempt. When the limit is
|
|
254
|
+
* reached, the pi session is aborted and the attempt finalizes with
|
|
255
|
+
* `error.code: max_turns_exceeded`. A tool-use turn = any `turn_end`
|
|
256
|
+
* whose `stopReason !== 'end_turn'` (matches the Anthropic SDK
|
|
257
|
+
* `max_turns` semantics: the model's final text-only response doesn't
|
|
258
|
+
* count). Default `0` = disabled. Recommended `30` for `fulfill_brief`.
|
|
259
|
+
* Closes part of #1094.
|
|
260
|
+
*/
|
|
261
|
+
maxTurns?: number;
|
|
262
|
+
/**
|
|
263
|
+
* Cap the number of `bash` tool timeouts per attempt. A timeout is a
|
|
264
|
+
* `tool_execution_end` for `bash` whose result text contains
|
|
265
|
+
* "Command timed out after" (pi's stable error wrapper from
|
|
266
|
+
* `@earendil-works/pi-coding-agent`'s bash tool). When the limit is
|
|
267
|
+
* reached, the pi session is aborted and the attempt finalizes with
|
|
268
|
+
* `error.code: max_bash_timeouts_exceeded`. Catches the death-spiral
|
|
269
|
+
* pattern from task `a3762f44` where the model kept retrying
|
|
270
|
+
* long-blocking shell commands until the host job timeout fired.
|
|
271
|
+
* Default `3`. Set to `0` to disable. Closes part of #1094.
|
|
272
|
+
*/
|
|
273
|
+
maxBashTimeouts?: number;
|
|
274
|
+
/**
|
|
275
|
+
* Skip per-call UI approval for matching `moltnet_host_exec` commands.
|
|
276
|
+
* Keep false/undefined for interactive consumers. `true` skips every dialog
|
|
277
|
+
* after HOST_EXEC_ALLOWED; an array limits auto-approval to matching rules.
|
|
278
|
+
*/
|
|
279
|
+
hostExecAutoApprove?: HostExecAutoApproveConfig;
|
|
252
280
|
}
|
|
253
281
|
|
|
254
282
|
/**
|
|
@@ -263,6 +291,19 @@ export declare function findMainWorktree(): string;
|
|
|
263
291
|
*/
|
|
264
292
|
export declare const HOST_EXEC_DEFAULT_BASE_ENV: ReadonlySet<string>;
|
|
265
293
|
|
|
294
|
+
declare type HostExecAutoApproveConfig = boolean | readonly HostExecAutoApproveRule[];
|
|
295
|
+
|
|
296
|
+
declare interface HostExecAutoApproveRule {
|
|
297
|
+
/** Exact executable name. Must still pass HOST_EXEC_ALLOWED. */
|
|
298
|
+
executable: string;
|
|
299
|
+
/** Optional ordered argument prefix; flags after the prefix are allowed. */
|
|
300
|
+
argsPrefix?: readonly string[];
|
|
301
|
+
/** Optional unordered argument tokens that must appear somewhere. */
|
|
302
|
+
argsContains?: readonly string[];
|
|
303
|
+
/** Optional argument tokens that prevent auto-approval when present. */
|
|
304
|
+
argsExcludes?: readonly string[];
|
|
305
|
+
}
|
|
306
|
+
|
|
266
307
|
export declare interface InjectedTaskContext {
|
|
267
308
|
/** Refs that were delivered, in declared order, for audit. */
|
|
268
309
|
injected: ContextRef[];
|
|
@@ -341,6 +382,19 @@ declare interface MoltNetToolsConfig {
|
|
|
341
382
|
* Defaults to HOST_EXEC_DEFAULT_BASE_ENV when omitted.
|
|
342
383
|
*/
|
|
343
384
|
hostExecBaseEnv?: ReadonlySet<string>;
|
|
385
|
+
/**
|
|
386
|
+
* When true, `moltnet_host_exec` skips the per-call UI approval dialog.
|
|
387
|
+
* Intended for non-interactive daemon automation only; interactive
|
|
388
|
+
* consumers should keep the default false behavior.
|
|
389
|
+
*/
|
|
390
|
+
autoApproveHostExec?: boolean;
|
|
391
|
+
/**
|
|
392
|
+
* Host-exec auto-approval policy. `true` skips all dialogs after the
|
|
393
|
+
* executable allowlist check. An array skips only commands matching one of
|
|
394
|
+
* the supplied executable/argument rules. Omitted/false preserves the
|
|
395
|
+
* interactive approval flow.
|
|
396
|
+
*/
|
|
397
|
+
hostExecAutoApprove?: HostExecAutoApproveConfig;
|
|
344
398
|
/**
|
|
345
399
|
* Active-task context, populated by the agent-daemon path. When set,
|
|
346
400
|
* `moltnet_create_entry` enforces `diaryId === taskContext.diaryId` and
|
|
@@ -397,6 +451,19 @@ export declare interface SandboxConfig {
|
|
|
397
451
|
};
|
|
398
452
|
/** Environment variable overrides for the guest VM (applied on top of defaults). */
|
|
399
453
|
env?: Record<string, string>;
|
|
454
|
+
/** Host-side escape hatch policy. Applies only to `moltnet_host_exec`. */
|
|
455
|
+
hostExec?: {
|
|
456
|
+
/**
|
|
457
|
+
* `true` auto-approves every allowed executable. An array auto-approves
|
|
458
|
+
* only commands matching one of the executable/argument rules.
|
|
459
|
+
*/
|
|
460
|
+
autoApprove?: boolean | {
|
|
461
|
+
executable: string;
|
|
462
|
+
argsPrefix?: string[];
|
|
463
|
+
argsContains?: string[];
|
|
464
|
+
argsExcludes?: string[];
|
|
465
|
+
}[];
|
|
466
|
+
};
|
|
400
467
|
/** VM resource allocation. */
|
|
401
468
|
resources?: {
|
|
402
469
|
/** Memory size in qemu syntax (default '1G'). */
|
package/dist/index.js
CHANGED
|
@@ -7274,6 +7274,7 @@ function renderPhase6Markdown(pack) {
|
|
|
7274
7274
|
* These tools run on the host (not in the VM) via the MoltNet SDK,
|
|
7275
7275
|
* so agent credentials never touch the VM filesystem.
|
|
7276
7276
|
*/
|
|
7277
|
+
var DIARY_TAG_MAX_LENGTH = 128;
|
|
7277
7278
|
/**
|
|
7278
7279
|
* Baseline env keys forwarded to host-exec child processes.
|
|
7279
7280
|
* Callers can extend this set at sandbox startup via `MoltNetToolsConfig.hostExecBaseEnv`.
|
|
@@ -7302,6 +7303,19 @@ function ensureConnected(config) {
|
|
|
7302
7303
|
teamId: config.getTeamId() ?? ""
|
|
7303
7304
|
};
|
|
7304
7305
|
}
|
|
7306
|
+
function hostExecMatchesAutoApproveRule(params, rule) {
|
|
7307
|
+
if (params.executable !== rule.executable) return false;
|
|
7308
|
+
if (rule.argsExcludes?.some((arg) => params.args.includes(arg))) return false;
|
|
7309
|
+
if (rule.argsPrefix && !rule.argsPrefix.every((arg, index) => params.args[index] === arg)) return false;
|
|
7310
|
+
if (rule.argsContains && !rule.argsContains.every((arg) => params.args.includes(arg))) return false;
|
|
7311
|
+
return true;
|
|
7312
|
+
}
|
|
7313
|
+
function shouldAutoApproveHostExec(params, config) {
|
|
7314
|
+
const policy = config.autoApproveHostExec === true ? true : config.hostExecAutoApprove ?? false;
|
|
7315
|
+
if (policy === true) return true;
|
|
7316
|
+
if (!Array.isArray(policy)) return false;
|
|
7317
|
+
return policy.some((rule) => hostExecMatchesAutoApproveRule(params, rule));
|
|
7318
|
+
}
|
|
7305
7319
|
/**
|
|
7306
7320
|
* Expand the `taskFilter` shorthand on the diary list/search tools into
|
|
7307
7321
|
* the matching `task:*` provenance tags emitted by `moltnet_create_entry`
|
|
@@ -7520,14 +7534,14 @@ function createMoltNetTools(config) {
|
|
|
7520
7534
|
limit: Type.Optional(Type.Number({ description: "Max entries to return (default 10)" })),
|
|
7521
7535
|
tags: Type.Optional(Type.Array(Type.String({
|
|
7522
7536
|
minLength: 1,
|
|
7523
|
-
maxLength:
|
|
7537
|
+
maxLength: DIARY_TAG_MAX_LENGTH
|
|
7524
7538
|
}), {
|
|
7525
7539
|
description: "Tags filter — entry must have ALL listed tags (AND). Max 20.",
|
|
7526
7540
|
maxItems: 20
|
|
7527
7541
|
})),
|
|
7528
7542
|
excludeTags: Type.Optional(Type.Array(Type.String({
|
|
7529
7543
|
minLength: 1,
|
|
7530
|
-
maxLength:
|
|
7544
|
+
maxLength: DIARY_TAG_MAX_LENGTH
|
|
7531
7545
|
}), {
|
|
7532
7546
|
description: "Tags to exclude — entry must have NONE of these. Max 20.",
|
|
7533
7547
|
maxItems: 20
|
|
@@ -7620,14 +7634,14 @@ function createMoltNetTools(config) {
|
|
|
7620
7634
|
limit: Type.Optional(Type.Number({ description: "Max results (default 5)" })),
|
|
7621
7635
|
tags: Type.Optional(Type.Array(Type.String({
|
|
7622
7636
|
minLength: 1,
|
|
7623
|
-
maxLength:
|
|
7637
|
+
maxLength: DIARY_TAG_MAX_LENGTH
|
|
7624
7638
|
}), {
|
|
7625
7639
|
description: "Entry must have ALL listed tags (AND). Max 20.",
|
|
7626
7640
|
maxItems: 20
|
|
7627
7641
|
})),
|
|
7628
7642
|
excludeTags: Type.Optional(Type.Array(Type.String({
|
|
7629
7643
|
minLength: 1,
|
|
7630
|
-
maxLength:
|
|
7644
|
+
maxLength: DIARY_TAG_MAX_LENGTH
|
|
7631
7645
|
}), {
|
|
7632
7646
|
description: "Entry must have NONE of these tags. Max 20.",
|
|
7633
7647
|
maxItems: 20
|
|
@@ -7673,7 +7687,7 @@ function createMoltNetTools(config) {
|
|
|
7673
7687
|
const createEntry = defineTool({
|
|
7674
7688
|
name: "moltnet_create_entry",
|
|
7675
7689
|
label: "Create MoltNet Diary Entry",
|
|
7676
|
-
description: "Create a new diary entry to record decisions, findings, incidents, or reflections. During an active task, the entry is forced into the task diary and tagged with the task:* provenance namespace (task:id:<id>, task:type:<type>, task:attempt:<n>, plus task:correlation:<id> when set); an explicit diaryId mismatching the task diary is rejected.",
|
|
7690
|
+
description: "Create a new diary entry to record decisions, findings, incidents, or reflections. During an active task, the entry is forced into the task diary and tagged with the task:* provenance namespace (task:id:<id>, task:type:<type>, task:attempt:<n>, plus task:correlation:<id> when set); an explicit diaryId mismatching the task diary is rejected. Use this tool — NOT `moltnet entry create` / `moltnet entry create-signed` via bash. The CLI path bypasses task-tag auto-injection and leaves entries invisible to taskFilter queries.",
|
|
7677
7691
|
parameters: Type.Object({
|
|
7678
7692
|
title: Type.String({ description: "Entry title (concise, descriptive)" }),
|
|
7679
7693
|
content: Type.String({ description: "Entry content (markdown)" }),
|
|
@@ -7813,7 +7827,7 @@ function createMoltNetTools(config) {
|
|
|
7813
7827
|
}),
|
|
7814
7828
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
7815
7829
|
if (!HOST_EXEC_ALLOWED.has(params.executable)) throw new Error(`host_exec: '${params.executable}' is not in the allowed list (${[...HOST_EXEC_ALLOWED].join(", ")}). Extend HOST_EXEC_ALLOWED only after explicit security review.`);
|
|
7816
|
-
if (ctx?.ui) {
|
|
7830
|
+
if (ctx?.ui && !shouldAutoApproveHostExec(params, config)) {
|
|
7817
7831
|
const cmdDisplay = [params.executable, ...params.args].join(" ");
|
|
7818
7832
|
if (!await ctx.ui.confirm("Allow host command?", `The agent wants to run on your machine:\n\n ${cmdDisplay}\n\nAllow?`)) throw new Error(`host_exec: user declined approval for: ${cmdDisplay}`);
|
|
7819
7833
|
}
|
|
@@ -8253,38 +8267,48 @@ async function resumeVm(config) {
|
|
|
8253
8267
|
[GUEST_TASK_SKILLS_MOUNT]: new MemoryProvider()
|
|
8254
8268
|
} }
|
|
8255
8269
|
});
|
|
8256
|
-
|
|
8270
|
+
try {
|
|
8271
|
+
await vm.exec(`sh -c '
|
|
8257
8272
|
cp /etc/gondolin/mitm/ca.crt /usr/local/share/ca-certificates/gondolin-mitm.crt
|
|
8258
8273
|
update-ca-certificates 2>/dev/null
|
|
8259
8274
|
cat /etc/gondolin/mitm/ca.crt >> /etc/ssl/certs/ca-certificates.crt
|
|
8260
8275
|
'`);
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8276
|
+
await vmRun(vm, "DNS resolvers", `printf 'nameserver 8.8.8.8\\nnameserver 1.1.1.1\\n' > /etc/resolv.conf`);
|
|
8277
|
+
await vmRun(vm, "git safe.directory", `git config --system --add safe.directory '*'`);
|
|
8278
|
+
for (const [i, cmd] of (config.sandboxConfig?.resumeCommands ?? []).entries()) await vmRun(vm, `resumeCommands[${i}]`, cmd);
|
|
8279
|
+
const vmSshDir = `${vmAgentDir}/ssh`;
|
|
8280
|
+
await vm.exec(`mkdir -p ${vmAgentDir}/ssh /home/agent/.pi/agent`);
|
|
8281
|
+
if (creds.piAuthJson !== null) await vm.fs.writeFile("/home/agent/.pi/agent/auth.json", creds.piAuthJson, { mode: 384 });
|
|
8282
|
+
const vmMoltnetJson = rewriteMoltnetJsonPaths(creds.moltnetJson, vmAgentDir, vmSshDir, creds.githubAppPemFilename);
|
|
8283
|
+
await vm.fs.writeFile(`${vmAgentDir}/moltnet.json`, vmMoltnetJson, { mode: 384 });
|
|
8284
|
+
await vm.fs.writeFile(`${vmAgentDir}/env`, creds.agentEnvRaw, { mode: 384 });
|
|
8285
|
+
if (creds.gitconfig) {
|
|
8286
|
+
const vmSigningKey = `${vmSshDir}/id_ed25519`;
|
|
8287
|
+
let vmGitconfig = creds.gitconfig.replace(/signingKey\s*=\s*.+/g, `signingKey = ${vmSigningKey}`);
|
|
8288
|
+
vmGitconfig = ensureRelativeWorktreePaths(vmGitconfig);
|
|
8289
|
+
await vm.fs.writeFile(`${vmAgentDir}/gitconfig`, vmGitconfig, { mode: 420 });
|
|
8290
|
+
}
|
|
8291
|
+
if (creds.sshPrivateKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519`, creds.sshPrivateKey, { mode: 384 });
|
|
8292
|
+
if (creds.sshPublicKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519.pub`, creds.sshPublicKey, { mode: 420 });
|
|
8293
|
+
if (creds.allowedSigners) await vm.fs.writeFile(`${vmSshDir}/allowed_signers`, creds.allowedSigners, { mode: 420 });
|
|
8294
|
+
if (creds.githubAppPem && creds.githubAppPemFilename) await vm.fs.writeFile(`${vmAgentDir}/${creds.githubAppPemFilename}`, creds.githubAppPem, { mode: 384 });
|
|
8295
|
+
await vm.exec("chown -R agent:agent /home/agent/.pi /home/agent/.moltnet");
|
|
8296
|
+
return {
|
|
8297
|
+
vm,
|
|
8298
|
+
credentials: creds,
|
|
8299
|
+
mountPath: config.mountPath,
|
|
8300
|
+
guestWorkspace: GUEST_WORKSPACE$2,
|
|
8301
|
+
agentDir
|
|
8302
|
+
};
|
|
8303
|
+
} catch (err) {
|
|
8304
|
+
try {
|
|
8305
|
+
await vm.close();
|
|
8306
|
+
} catch (closeErr) {
|
|
8307
|
+
const m = closeErr instanceof Error ? closeErr.message : String(closeErr);
|
|
8308
|
+
process.stderr.write(`[vm] post-throw vm.close() failed: ${m}\n`);
|
|
8309
|
+
}
|
|
8310
|
+
throw err;
|
|
8311
|
+
}
|
|
8288
8312
|
}
|
|
8289
8313
|
/**
|
|
8290
8314
|
* Rewrite host-absolute paths inside moltnet.json to VM-local equivalents.
|
|
@@ -14706,8 +14730,8 @@ function buildRuntimeInstructor(ctx) {
|
|
|
14706
14730
|
"## Diary discipline",
|
|
14707
14731
|
"",
|
|
14708
14732
|
`- During this task, every diary entry MUST land in \`${ctx.diaryId}\``,
|
|
14709
|
-
" (the task diary). The
|
|
14710
|
-
" and rejects mismatched explicit `diaryId` parameters.",
|
|
14733
|
+
" (the task diary). The `moltnet_create_entry` custom tool enforces",
|
|
14734
|
+
" this and rejects mismatched explicit `diaryId` parameters.",
|
|
14711
14735
|
`- Provenance tags \`task:id:${ctx.taskId}\`, \`task:type:${ctx.taskType}\`,`,
|
|
14712
14736
|
` and \`task:attempt:${ctx.attemptN}\`${ctx.correlationId ? `, plus \`task:correlation:${ctx.correlationId}\`` : ""} are auto-injected on every entry.`,
|
|
14713
14737
|
" These share the `task:` namespace so `moltnet_diary_tags` with",
|
|
@@ -14715,12 +14739,23 @@ function buildRuntimeInstructor(ctx) {
|
|
|
14715
14739
|
" `taskFilter` shorthand on `moltnet_list_entries` /",
|
|
14716
14740
|
" `moltnet_search_entries` expands into them. You may add additional",
|
|
14717
14741
|
" tags but you cannot remove the auto-injected ones.",
|
|
14742
|
+
"- **DO NOT shell out to `moltnet entry create` / `moltnet entry",
|
|
14743
|
+
" create-signed` / any other `moltnet entry` subcommand via bash.**",
|
|
14744
|
+
" Those CLI paths hit the REST API directly and bypass the",
|
|
14745
|
+
" custom tool's task-tag auto-injection, leaving you with",
|
|
14746
|
+
" untagged entries that `moltnet_list_entries` with a",
|
|
14747
|
+
" `taskFilter: { taskId: ... }` cannot find. The legreffier skill",
|
|
14748
|
+
" recommends `moltnet entry *` for normal interactive sessions —",
|
|
14749
|
+
" inside a running task that advice does not apply. Use the",
|
|
14750
|
+
" `moltnet_create_entry` custom tool only.",
|
|
14718
14751
|
"",
|
|
14719
14752
|
"## Accountable commits",
|
|
14720
14753
|
"",
|
|
14721
14754
|
"- Every commit you make during this task MUST be paired with a signed",
|
|
14722
|
-
" diary entry created via `moltnet_create_entry
|
|
14723
|
-
" entry
|
|
14755
|
+
" diary entry created via the `moltnet_create_entry` custom tool",
|
|
14756
|
+
" (NOT via `moltnet entry create-signed` from bash — see Diary",
|
|
14757
|
+
" discipline above). Embed the returned entry id in the commit",
|
|
14758
|
+
" trailer `MoltNet-Diary: <id>`.",
|
|
14724
14759
|
"- Commits must be signed with the agent credentials (gitconfig is",
|
|
14725
14760
|
" pre-configured). Do not bypass signing.",
|
|
14726
14761
|
"",
|
|
@@ -15345,6 +15380,7 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15345
15380
|
clearSessionErrors: () => {},
|
|
15346
15381
|
getHostCwd: () => mountPath,
|
|
15347
15382
|
hostExecBaseEnv: new Set([...HOST_EXEC_DEFAULT_BASE_ENV, ...Object.keys(managed.credentials.agentEnv)]),
|
|
15383
|
+
hostExecAutoApprove: opts.hostExecAutoApprove ?? opts.sandboxConfig?.hostExec?.autoApprove ?? false,
|
|
15348
15384
|
getTaskContext: () => ({
|
|
15349
15385
|
taskId: task.id,
|
|
15350
15386
|
taskType: task.taskType,
|
|
@@ -15417,6 +15453,11 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15417
15453
|
let assistantText = "";
|
|
15418
15454
|
let reporterError = null;
|
|
15419
15455
|
const usage = finalUsage;
|
|
15456
|
+
let capAbort = null;
|
|
15457
|
+
let toolUseTurnCount = 0;
|
|
15458
|
+
let bashTimeoutCount = 0;
|
|
15459
|
+
const maxTurns = opts.maxTurns ?? 0;
|
|
15460
|
+
const maxBashTimeouts = opts.maxBashTimeouts ?? 3;
|
|
15420
15461
|
cancelListener = wireSessionAbort(reporter.cancelSignal, session);
|
|
15421
15462
|
const recordingPromise = [];
|
|
15422
15463
|
const track = (p) => {
|
|
@@ -15431,6 +15472,23 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15431
15472
|
}
|
|
15432
15473
|
}));
|
|
15433
15474
|
};
|
|
15475
|
+
const liveSession = session;
|
|
15476
|
+
const triggerCapAbort = (code, message) => {
|
|
15477
|
+
if (capAbort) return;
|
|
15478
|
+
capAbort = {
|
|
15479
|
+
code,
|
|
15480
|
+
message
|
|
15481
|
+
};
|
|
15482
|
+
liveSession.abort().catch((err) => {
|
|
15483
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
15484
|
+
process.stderr.write(`[cap] session.abort() failed: ${m}\n`);
|
|
15485
|
+
});
|
|
15486
|
+
track(emit("info", {
|
|
15487
|
+
event: "cap_abort",
|
|
15488
|
+
code,
|
|
15489
|
+
message
|
|
15490
|
+
}));
|
|
15491
|
+
};
|
|
15434
15492
|
session.subscribe((event) => {
|
|
15435
15493
|
if (event.type === "message_update") {
|
|
15436
15494
|
const ae = event.assistantMessageEvent;
|
|
@@ -15439,12 +15497,17 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15439
15497
|
track(emit("text_delta", { delta: ae.delta }));
|
|
15440
15498
|
}
|
|
15441
15499
|
} else if (event.type === "tool_execution_start") track(emit("tool_call_start", { tool_name: event.toolName }));
|
|
15442
|
-
else if (event.type === "tool_execution_end")
|
|
15443
|
-
|
|
15444
|
-
|
|
15445
|
-
|
|
15446
|
-
|
|
15447
|
-
|
|
15500
|
+
else if (event.type === "tool_execution_end") {
|
|
15501
|
+
track(emit("tool_call_end", {
|
|
15502
|
+
tool_name: event.toolName,
|
|
15503
|
+
is_error: event.isError,
|
|
15504
|
+
result: event.isError ? truncateForWire(event.result) : void 0
|
|
15505
|
+
}));
|
|
15506
|
+
if (maxBashTimeouts > 0 && event.toolName === "bash" && event.isError && isBashTimeoutResult(event.result)) {
|
|
15507
|
+
bashTimeoutCount += 1;
|
|
15508
|
+
if (bashTimeoutCount >= maxBashTimeouts) triggerCapAbort("max_bash_timeouts_exceeded", `Aborted after ${bashTimeoutCount} bash timeouts in this attempt (cap ${maxBashTimeouts}).`);
|
|
15509
|
+
}
|
|
15510
|
+
} else if (event.type === "turn_end") {
|
|
15448
15511
|
const msg = event.message;
|
|
15449
15512
|
if (msg?.role === "assistant" && msg.usage) {
|
|
15450
15513
|
usage.inputTokens += Math.max(0, msg.usage.input ?? 0);
|
|
@@ -15454,7 +15517,12 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15454
15517
|
if (cr) usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + cr;
|
|
15455
15518
|
if (cw) usage.cacheWriteTokens = (usage.cacheWriteTokens ?? 0) + cw;
|
|
15456
15519
|
}
|
|
15457
|
-
|
|
15520
|
+
const stopReason = msg?.stopReason ?? "end_turn";
|
|
15521
|
+
track(emit("turn_end", { stop_reason: stopReason }));
|
|
15522
|
+
if (maxTurns > 0 && stopReason !== "end_turn" && stopReason !== "aborted" && stopReason !== "error") {
|
|
15523
|
+
toolUseTurnCount += 1;
|
|
15524
|
+
if (toolUseTurnCount >= maxTurns) triggerCapAbort("max_turns_exceeded", `Aborted after ${toolUseTurnCount} tool-use turns (cap ${maxTurns}).`);
|
|
15525
|
+
}
|
|
15458
15526
|
llmAbort = msg?.stopReason === "error";
|
|
15459
15527
|
if (msg?.stopReason === "error") llmErrorMessage = typeof msg.errorMessage === "string" && msg.errorMessage.length > 0 ? msg.errorMessage : null;
|
|
15460
15528
|
else llmErrorMessage = null;
|
|
@@ -15483,7 +15551,7 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15483
15551
|
let parsedOutput = null;
|
|
15484
15552
|
let parsedOutputCid = null;
|
|
15485
15553
|
let parseError = null;
|
|
15486
|
-
if (!runError && !llmAbort && !cancelled) {
|
|
15554
|
+
if (!runError && !llmAbort && !cancelled && !capAbort) {
|
|
15487
15555
|
const captured = submitToolHandle?.getCaptured() ?? null;
|
|
15488
15556
|
if (captured) try {
|
|
15489
15557
|
parsedOutput = captured;
|
|
@@ -15536,6 +15604,21 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15536
15604
|
retryable: false
|
|
15537
15605
|
}
|
|
15538
15606
|
};
|
|
15607
|
+
const capAbortSnapshot = capAbort;
|
|
15608
|
+
if (capAbortSnapshot) return {
|
|
15609
|
+
taskId: task.id,
|
|
15610
|
+
attemptN,
|
|
15611
|
+
status: "failed",
|
|
15612
|
+
output: null,
|
|
15613
|
+
outputCid: null,
|
|
15614
|
+
usage,
|
|
15615
|
+
durationMs: Date.now() - startTime,
|
|
15616
|
+
error: {
|
|
15617
|
+
code: capAbortSnapshot.code,
|
|
15618
|
+
message: capAbortSnapshot.message,
|
|
15619
|
+
retryable: false
|
|
15620
|
+
}
|
|
15621
|
+
};
|
|
15539
15622
|
const status = runError || llmAbort || parseError || reporterError ? "failed" : "completed";
|
|
15540
15623
|
const errorCode = runError?.code ?? parseError?.code ?? reporterError?.code ?? (llmAbort ? "llm_api_error" : void 0);
|
|
15541
15624
|
const errorMessage = runError?.message ?? parseError?.message ?? reporterError?.message ?? (llmAbort ? llmErrorMessage ?? "LLM API error during turn" : void 0);
|
|
@@ -15637,6 +15720,25 @@ function summarizePayloadForLog(kind, payload) {
|
|
|
15637
15720
|
default: return payload;
|
|
15638
15721
|
}
|
|
15639
15722
|
}
|
|
15723
|
+
/**
|
|
15724
|
+
* Detect pi's bash-timeout error wrapper in a `tool_execution_end`
|
|
15725
|
+
* result. The bash tool surfaces a timeout as a structured tool result
|
|
15726
|
+
* `{ content: [{ type: 'text', text: '… Command timed out after N
|
|
15727
|
+
* seconds' }] }` (see `@earendil-works/pi-coding-agent`'s bash.js).
|
|
15728
|
+
* Substring-match against the stable wrapper string is the only
|
|
15729
|
+
* mechanism short of patching pi; the string is part of pi's external
|
|
15730
|
+
* tool-error API and changing it would break agents that read tool
|
|
15731
|
+
* errors.
|
|
15732
|
+
*/
|
|
15733
|
+
function isBashTimeoutResult(result) {
|
|
15734
|
+
if (result === null || result === void 0) return false;
|
|
15735
|
+
if (typeof result === "string") return result.includes("Command timed out after");
|
|
15736
|
+
if (typeof result !== "object") return false;
|
|
15737
|
+
const content = result.content;
|
|
15738
|
+
if (!Array.isArray(content)) return false;
|
|
15739
|
+
for (const part of content) if (typeof part === "object" && part !== null && typeof part.text === "string" && part.text.includes("Command timed out after")) return true;
|
|
15740
|
+
return false;
|
|
15741
|
+
}
|
|
15640
15742
|
var TRUNCATE_LIMIT = 4 * 1024;
|
|
15641
15743
|
function truncateForWire(value) {
|
|
15642
15744
|
if (value === null || value === void 0) return value;
|
package/package.json
CHANGED