@themoltnet/pi-runtime 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.
Files changed (3) hide show
  1. package/dist/index.d.ts +54 -20
  2. package/dist/index.js +494 -525
  3. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -1,15 +1,15 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
4
- import { readFile, realpath, stat } from "node:fs/promises";
4
+ import { mkdir, readFile, realpath, stat } from "node:fs/promises";
5
5
  import path, { isAbsolute, join, relative, resolve, sep } from "node:path";
6
6
  import { pipeline } from "node:stream/promises";
7
- import { Type, getModel } from "@earendil-works/pi-ai";
8
- import { DEFAULT_MAX_BYTES, DefaultResourceLoader, SessionManager, createAgentSession, createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createSyntheticSourceInfo, createWriteToolDefinition, defineTool, formatSize, parseFrontmatter, truncateHead, truncateLine } from "@earendil-works/pi-coding-agent";
7
+ import { Type } from "@earendil-works/pi-ai";
8
+ import { AuthStorage, DEFAULT_MAX_BYTES, DefaultResourceLoader, ModelRegistry, SessionManager, createAgentSession, createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createSyntheticSourceInfo, createWriteToolDefinition, defineTool, formatSize, parseFrontmatter, truncateHead, truncateLine } from "@earendil-works/pi-coding-agent";
9
9
  import { SpanStatusCode, context, metrics, trace } from "@opentelemetry/api";
10
- import { homedir } from "node:os";
11
10
  import crypto, { createHash } from "crypto";
12
11
  import { createHash as createHash$1 } from "node:crypto";
12
+ import { homedir } from "node:os";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import { Readable } from "node:stream";
15
15
  import { MemoryProvider, RealFSProvider, ShadowProvider, VM, VmCheckpoint, createHttpHooks, createShadowPathPredicate, ensureImageSelector, isWriteFlag, loadGuestAssets } from "@earendil-works/gondolin";
@@ -43,6 +43,20 @@ var __copyProps = (to, from, except, desc) => {
43
43
  var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp$1({}, "__esModule", { value: true }), mod);
44
44
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
45
45
  //#endregion
46
+ //#region src/path-containment.ts
47
+ /**
48
+ * Check containment for already-resolved lexical or real paths.
49
+ *
50
+ * Callers that accept untrusted paths must resolve/realpath at their I/O
51
+ * boundary first; keeping the platform-specific relative-path rule here avoids
52
+ * subtly different `..` and absolute-path handling across runtime cleanup,
53
+ * session sync, and artifact staging.
54
+ */
55
+ function isResolvedPathInsideRoot(path, root) {
56
+ const rel = relative(root, path);
57
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
58
+ }
59
+ //#endregion
46
60
  //#region src/moltnet/render-phase6.ts
47
61
  function slugToTitle(value) {
48
62
  return value.split(/[:/_-]+/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ");
@@ -255,11 +269,32 @@ async function openWorkspaceArtifactInput(config, cwd, filePath) {
255
269
  }
256
270
  async function resolveWorkspaceOutputPath(cwd, filePath) {
257
271
  const resolved = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(cwd, filePath);
258
- const [realCwd, realParent] = await Promise.all([realpath(cwd), realpath(path.dirname(resolved))]);
259
- const rel = path.relative(realCwd, realParent);
260
- if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`task artifact output path escapes workspace: ${filePath}`);
272
+ const workspaceRoot = path.resolve(cwd);
273
+ const lexicalRel = path.relative(workspaceRoot, resolved);
274
+ if (lexicalRel === "" || lexicalRel.startsWith("..") || path.isAbsolute(lexicalRel)) throw new Error(`task artifact output path escapes workspace: ${filePath}`);
275
+ const realCwd = await realpath(cwd);
276
+ const parent = path.dirname(resolved);
277
+ assertPathInsideWorkspace(realCwd, await findExistingAncestor(parent), filePath);
278
+ await mkdir(parent, { recursive: true });
279
+ assertPathInsideWorkspace(realCwd, await realpath(parent), filePath);
261
280
  return resolved;
262
281
  }
282
+ async function findExistingAncestor(candidate) {
283
+ let current = candidate;
284
+ for (;;) {
285
+ try {
286
+ return await realpath(current);
287
+ } catch (err) {
288
+ if (!err || typeof err !== "object" || !("code" in err) || err.code !== "ENOENT") throw err;
289
+ }
290
+ const parent = path.dirname(current);
291
+ if (parent === current) throw new Error(`task artifact output has no existing ancestor: ${candidate}`);
292
+ current = parent;
293
+ }
294
+ }
295
+ function assertPathInsideWorkspace(realCwd, realPath, displayPath) {
296
+ if (!isResolvedPathInsideRoot(realPath, realCwd)) throw new Error(`task artifact output path escapes workspace: ${displayPath}`);
297
+ }
263
298
  /**
264
299
  * Expand the `taskFilter` shorthand on the diary list/search tools into
265
300
  * the matching `task:*` provenance tags emitted by `moltnet_create_entry`
@@ -1258,6 +1293,7 @@ async function buildAgentSession(args) {
1258
1293
  agentDir: args.piAuthDir,
1259
1294
  cwd: args.cwdPath,
1260
1295
  model: args.modelHandle,
1296
+ ...args.modelRegistry ? { modelRegistry: args.modelRegistry } : {},
1261
1297
  thinkingLevel: args.thinkingLevel ?? void 0,
1262
1298
  tools: args.tools,
1263
1299
  customTools: args.customTools,
@@ -9377,6 +9413,11 @@ var RuntimeModel = _Object_({
9377
9413
  var RUNTIME_PROFILE_CONTEXT_CATALOGUE = {
9378
9414
  version: 1,
9379
9415
  fragments: {
9416
+ "artifact-planner-v1": {
9417
+ binding: "prompt_prefix",
9418
+ content: "# Bounded artifact planner\n\n- The typed task facts, embedded bounded manifest, exact bound artifact references, registered tools, and runtime capability section are the complete contract. Do not search diaries, inspect a mounted repository, enumerate unrelated tasks or artifacts, use shell commands, modify files, commit, branch, push, or contact GitHub.\n- Read only the exact artifact CIDs named by the task, and only when the embedded manifest does not provide enough evidence. Never paginate or discover artifacts speculatively.\n- Perform semantic classification and planning from supplied content and producer/consumer evidence. Do not substitute filename, directory, language, ecosystem, or repository-specific exclusion rules for evidence.\n- Return exactly the requested versioned structured plan through the registered submit-output tool. Do not emit a second prose or JSON representation.",
9419
+ slug: "artifact-planner-v1"
9420
+ },
9380
9421
  "accountable-delivery-v1": {
9381
9422
  binding: "prompt_prefix",
9382
9423
  content: "# Accountable delivery\n\n- Pair every commit made during this task with a signed diary entry created by the `moltnet_create_entry` custom tool. Put the returned id in a `MoltNet-Diary: <id>` commit trailer.\n- Keep commit signing enabled; do not bypass the agent git configuration.\n- Push a branch and open or update a pull request only when the task asks for it. For GitHub mutations, use the credential-bound `GH_TOKEN` command form required by the runtime kernel.\n- Keep changes, commits, and any requested pull request coherent enough to review independently.",
@@ -9409,6 +9450,10 @@ var RUNTIME_PROFILE_CONTEXT_CATALOGUE = {
9409
9450
  }
9410
9451
  },
9411
9452
  recipes: {
9453
+ "artifact-planner@v1": {
9454
+ description: "Minimal artifact-only context for bounded semantic classification and planning.",
9455
+ fragments: ["artifact-planner-v1"]
9456
+ },
9412
9457
  "run-eval-direct@v1": {
9413
9458
  description: "Minimal direct context for a short, isolated evaluation run.",
9414
9459
  fragments: ["run-eval-direct-v1"]
@@ -10896,7 +10941,7 @@ var VerificationResult = _Object_({
10896
10941
  var VerificationRecord = _Object_({
10897
10942
  inputCid: String$1({ minLength: 1 }),
10898
10943
  results: _Array_(VerificationResult),
10899
- passed: Boolean$1()
10944
+ passed: Boolean$1({ description: "True iff every verification result has status \"pass\" or \"skip\"; false when any result has status \"fail\"." })
10900
10945
  }, {
10901
10946
  $id: "VerificationRecord",
10902
10947
  additionalProperties: false
@@ -14590,6 +14635,16 @@ function checkVerificationInputCid(value, runtime) {
14590
14635
  }];
14591
14636
  return [];
14592
14637
  }
14638
+ function checkVerificationPassedConsistency(value) {
14639
+ const verification = value !== null && typeof value === "object" ? value.verification : void 0;
14640
+ if (verification === void 0 || !Array.isArray(verification.results) || typeof verification.passed !== "boolean") return [];
14641
+ const expectedPassed = verification.results.every((result) => result.status !== "fail");
14642
+ if (verification.passed !== expectedPassed) return [{
14643
+ field: "output/verification/passed",
14644
+ message: "must be true iff no verification result has status \"fail\""
14645
+ }];
14646
+ return [];
14647
+ }
14593
14648
  function validateTaskResult(taskType, value, input, runtime, submission = false) {
14594
14649
  const entry = getTaskTypeEntry(taskType);
14595
14650
  if (!entry) return [{
@@ -14605,7 +14660,7 @@ function validateTaskResult(taskType, value, input, runtime, submission = false)
14605
14660
  message: validationError
14606
14661
  }];
14607
14662
  }
14608
- return checkVerificationInputCid(value, runtime);
14663
+ return [...checkVerificationInputCid(value, runtime), ...checkVerificationPassedConsistency(value)];
14609
14664
  }
14610
14665
  function validateTaskOutput(taskType, output, input, runtime) {
14611
14666
  return validateTaskResult(taskType, output, input, runtime);
@@ -15155,69 +15210,6 @@ function assembleTaskPrompt(taskType, sections) {
15155
15210
  };
15156
15211
  }
15157
15212
  //#endregion
15158
- //#region ../agent-runtime/src/prompts/final-output.ts
15159
- function buildFinalOutputBlock(opts) {
15160
- const { taskType, outputSchemaName, shapeSketch, extraNotes } = opts;
15161
- const submitTool = submitOutputToolName(taskType);
15162
- const lines = [
15163
- "## Final output (read this carefully)",
15164
- "",
15165
- `Your VERY LAST action in this conversation MUST report the structured`,
15166
- `output matching \`${outputSchemaName}\`.`,
15167
- "",
15168
- `Call \`${submitTool}\` exactly once with the payload.`,
15169
- `The runtime captures the validated arguments for attempt completion.`,
15170
- `Do NOT emit the output as plain assistant text. Do NOT rely on a`,
15171
- `JSON-in-message fallback. If you do not call \`${submitTool}\`, the`,
15172
- `attempt is recorded as failing the promised submit-output criterion`,
15173
- `even if the underlying work succeeded.`,
15174
- "",
15175
- `Your final assistant text before that tool call may explain your work,`,
15176
- `but the submit-tool call itself must be your VERY LAST action.`,
15177
- "",
15178
- `Task artifacts: when you produce large files, binary files, logs, reports,`,
15179
- `screenshots, traces, bundles, or datasets, save them in the task workspace`,
15180
- `and call \`moltnet_upload_task_artifact\` before the submit-output tool.`,
15181
- `Put the returned artifact CID in the structured output where the schema`,
15182
- `allows artifact metadata (for example \`artifacts[].cid\`). Do not paste`,
15183
- `large bytes into structured output.`,
15184
- "",
15185
- `Referenced inputs: if this task depends on prior task artifacts, call`,
15186
- `\`moltnet_list_task_artifacts\` for the referenced task and download the`,
15187
- `specific CID you need with \`moltnet_download_task_artifact\` before judging`,
15188
- `or continuing that work.`,
15189
- `For a bound input artifact, omit \`attemptN\` because it has no producing`,
15190
- `attempt. Pass \`attemptN\` only for an artifact from one exact task attempt.`,
15191
- "",
15192
- `Output shape:`,
15193
- "",
15194
- "```json",
15195
- shapeSketch,
15196
- "```"
15197
- ];
15198
- if (extraNotes?.length) {
15199
- lines.push("");
15200
- for (const note of extraNotes) lines.push(note);
15201
- }
15202
- return lines.join("\n");
15203
- }
15204
- //#endregion
15205
- //#region ../agent-runtime/src/prompts/proactive-memory.ts
15206
- function buildProactiveMemoryWorkflowBlock() {
15207
- return [
15208
- "Before material work, apply the runtime instructor's proactive memory",
15209
- "rules instead of waiting for a human prompt. Start with constrained",
15210
- "diary context: inspect tags/list entries when task provenance or scope",
15211
- "tags are known, then use `moltnet_search_entries` with `taskFilter`,",
15212
- "`entryTypes`, and tags. Do not run broad unfiltered searches before",
15213
- "constrained searches miss.",
15214
- "",
15215
- "For incident capture, follow the runtime instructor exactly: search",
15216
- "for similar episodic/semantic entries first, reference close matches,",
15217
- "and create a recurrence entry only when the repeat is useful signal."
15218
- ].join("\n");
15219
- }
15220
- //#endregion
15221
15213
  //#region ../agent-runtime/src/prompts/rubric-common.ts
15222
15214
  function renderRubricCriteriaList(rubric) {
15223
15215
  return rubric.criteria.map((c, i) => `${i + 1}. **${c.id}** (weight ${c.weight}, scoring: \`${c.scoring}\`) — ${c.description}`).join("\n");
@@ -15297,9 +15289,7 @@ function buildAssessBriefUserPrompt(input, ctx) {
15297
15289
  const scoring = [
15298
15290
  "- `llm_score`: score 0..1 continuous. `rationale` REQUIRED (2–4 sentences).",
15299
15291
  "- `boolean`: score exactly 0 or 1. `rationale` optional.",
15300
- "- `deterministic_signature_check`: run `moltnet entry verify` on every diary entry returned by step 3 above AND `git verify-commit` on every commit. Score 1 iff ALL signatures are valid; otherwise 0. Populate `evidence.commitsVerified`, `evidence.commitsTotal`, `evidence.signatureFailures`.",
15301
- "",
15302
- "Write a signed diary entry (tags: \"judgment\", \"assess_brief\") capturing the rationale before reporting structured output."
15292
+ "- `deterministic_signature_check`: run `moltnet entry verify` on every diary entry returned by step 3 above AND `git verify-commit` on every commit. Score 1 iff ALL signatures are valid; otherwise 0. Populate `evidence.commitsVerified`, `evidence.commitsTotal`, `evidence.signatureFailures`."
15303
15293
  ].join("\n");
15304
15294
  return assembleTaskPrompt("assess_brief", [
15305
15295
  {
@@ -15319,12 +15309,6 @@ function buildAssessBriefUserPrompt(input, ctx) {
15319
15309
  header: "Querying the producer's diary entries",
15320
15310
  body: diaryQuery
15321
15311
  },
15322
- {
15323
- id: "assess_brief.proactive_memory",
15324
- source: "discipline",
15325
- header: "Proactive memory use",
15326
- body: buildProactiveMemoryWorkflowBlock()
15327
- },
15328
15312
  {
15329
15313
  id: "assess_brief.workspace",
15330
15314
  source: "workspace",
@@ -15347,88 +15331,10 @@ function buildAssessBriefUserPrompt(input, ctx) {
15347
15331
  source: "rubric_judge",
15348
15332
  header: "Scoring rules",
15349
15333
  body: scoring
15350
- },
15351
- {
15352
- id: "assess_brief.final_output",
15353
- source: "final_output",
15354
- body: buildFinalOutputBlock({
15355
- taskType: "assess_brief",
15356
- outputSchemaName: "AssessBriefOutput",
15357
- shapeSketch: [
15358
- "{",
15359
- " \"scores\": [",
15360
- " { \"criterionId\": \"...\", \"score\": 0.0, \"rationale\": \"...\", \"evidence\": {} }",
15361
- " ],",
15362
- " \"composite\": <sum>,",
15363
- " \"verdict\": \"<1-3 sentence overall>\",",
15364
- " \"judgeModel\": \"<provider:model>\"",
15365
- "}"
15366
- ].join("\n"),
15367
- extraNotes: ["`composite` = Σ(weight_i × score_i) recomputed. The runtime rejects a mismatch."]
15368
- })
15369
15334
  }
15370
15335
  ]);
15371
15336
  }
15372
15337
  //#endregion
15373
- //#region ../agent-runtime/src/prompts/self-verification.ts
15374
- function buildSelfVerificationBlock(taskId, criteriaField = "successCriteria") {
15375
- return [
15376
- "## Self-verification",
15377
- "",
15378
- `If \`input.${criteriaField}\` is set on this task, your final output MUST`,
15379
- "include a `verification` block. Treat every item in those criteria as",
15380
- "part of the promise you made when you claimed the task. That includes",
15381
- "the built-in submit-output gate when present. Do not call the submit",
15382
- "tool until you have computed the verification payload you can honestly",
15383
- "stand behind.",
15384
- "",
15385
- `Call \`moltnet_get_task\` with task id \`${taskId}\` and read \`input.${criteriaField}\`.`,
15386
- "",
15387
- `- If \`input.${criteriaField}\` is **absent**, omit \`verification\` from your`,
15388
- " final output entirely.",
15389
- `- If \`input.${criteriaField}\` is **present**, evaluate every applicable`,
15390
- " item — `gates`, `assertions`, `rubric` criteria, `sideEffects` — against",
15391
- " your produced work and emit one result per id. Be honest: a `fail` with",
15392
- " a one-line reason is more useful than a false `pass`. Use `skip` (with a",
15393
- " `detail`) when you genuinely could not determine a result. Compute",
15394
- " `passed = results.every(r => r.status !== 'fail')`.",
15395
- "- `verification` MUST be a JSON object. Never send a string, markdown",
15396
- " block, null, or an empty placeholder. The submit tool expects an object",
15397
- " with `inputCid`, `results`, and `passed` fields.",
15398
- "",
15399
- "Verification shape:",
15400
- "",
15401
- "```json",
15402
- "{",
15403
- " \"inputCid\": \"<the inputCid you saw on the task>\",",
15404
- " \"results\": [",
15405
- " { \"id\": \"<criterion id>\", \"kind\": \"assertion|gate|rubric|sideEffect\",",
15406
- " \"status\": \"pass|fail|skip\", \"detail\": \"<optional one-liner>\" }",
15407
- " ],",
15408
- " \"passed\": <boolean>",
15409
- "}",
15410
- "```",
15411
- "",
15412
- "Minimal valid example:",
15413
- "",
15414
- "```json",
15415
- "{",
15416
- " \"inputCid\": \"<task inputCid>\",",
15417
- " \"results\": [",
15418
- " {",
15419
- " \"id\": \"<criterion id>\",",
15420
- " \"kind\": \"rubric\",",
15421
- " \"status\": \"pass\",",
15422
- " \"detail\": \"one-line reason\"",
15423
- " }",
15424
- " ],",
15425
- " \"passed\": true",
15426
- "}",
15427
- "```",
15428
- ""
15429
- ].join("\n");
15430
- }
15431
- //#endregion
15432
15338
  //#region ../agent-runtime/src/prompts/curate-pack.ts
15433
15339
  /**
15434
15340
  * Build the first user-message prompt for a `curate_pack` task.
@@ -15586,34 +15492,6 @@ function buildCuratePackUserPrompt(input, ctx) {
15586
15492
  source: "static",
15587
15493
  header: "Hard constraints",
15588
15494
  body: hardConstraints
15589
- },
15590
- {
15591
- id: "curate_pack.verification",
15592
- source: "verification",
15593
- body: buildSelfVerificationBlock(ctx.taskId)
15594
- },
15595
- {
15596
- id: "curate_pack.final_output",
15597
- source: "final_output",
15598
- body: buildFinalOutputBlock({
15599
- taskType: "curate_pack",
15600
- outputSchemaName: "CuratePackOutput",
15601
- shapeSketch: [
15602
- "{",
15603
- " \"packId\": \"<uuid>\",",
15604
- " \"packCid\": \"<cid>\",",
15605
- " \"entries\": [",
15606
- " { \"entryId\": \"<uuid>\", \"rank\": 1, \"rationale\": \"<why>\" }",
15607
- " ],",
15608
- " \"recipeParams\": { \"recipe\": \"...\", \"prompt\": \"...\", ... },",
15609
- " \"checkpoints\": [",
15610
- " { \"phase\": \"recon\", \"candidateIds\": [...], \"droppedIds\": [...], \"notes\": \"...\" }",
15611
- " ],",
15612
- " \"summary\": \"<2-4 sentences: what you looked for, how you narrowed, what defines the final set>\",",
15613
- " \"verification\": <required iff input.successCriteria; see Self-verification>",
15614
- "}"
15615
- ].join("\n")
15616
- })
15617
15495
  }
15618
15496
  ]);
15619
15497
  }
@@ -15650,16 +15528,13 @@ function buildFreeformUserPrompt(input, ctx) {
15650
15528
  const expectedOutput = input.expectedOutput ?? "";
15651
15529
  const constraints = input.constraints?.length ? input.constraints.map((constraint) => `- ${constraint}`).join("\n") : "";
15652
15530
  const suggestedTaskType = input.suggestedTaskType ? [`The proposer suggested task type \`${input.suggestedTaskType}\`.`, "Use it as a hint, not as a contract."].join("\n") : "";
15653
- const workflow = [
15654
- "1. Clarify the real objective from the brief before acting.",
15655
- "2. Search MoltNet diary memory for prior decisions, incidents, and",
15656
- " recurring traps relevant to the brief.",
15657
- "3. Gather enough context to avoid guessing.",
15658
- "4. Complete the requested work when it is safe and bounded.",
15659
- "5. If the request reveals a recurring task shape, include a",
15660
- " `proposedTaskType` in the final output with a concise rationale.",
15661
- "6. If you changed code on a branch, include that branch in",
15662
- " `branch` so future continuations can recover git context."
15531
+ const outcomeHints = [
15532
+ "Complete the brief using the supplied task facts, context, and effective",
15533
+ "runtime capabilities.",
15534
+ "If the request reveals a recurring task shape, populate the",
15535
+ "`proposedTaskType` field exposed by the registered submit-output tool.",
15536
+ "If you changed code on a branch, populate its `branch` field so a",
15537
+ "continuation can recover that git context."
15663
15538
  ].join("\n");
15664
15539
  const sections = [
15665
15540
  {
@@ -15692,43 +15567,14 @@ function buildFreeformUserPrompt(input, ctx) {
15692
15567
  body: suggestedTaskType
15693
15568
  },
15694
15569
  {
15695
- id: "freeform.workflow",
15570
+ id: "freeform.outcome_hints",
15696
15571
  source: "static",
15697
- header: "Workflow",
15698
- body: workflow
15699
- },
15700
- {
15701
- id: "freeform.proactive_memory",
15702
- source: "discipline",
15703
- header: "Proactive memory use",
15704
- body: buildProactiveMemoryWorkflowBlock()
15705
- },
15706
- {
15707
- id: "freeform.verification",
15708
- source: "verification",
15709
- body: buildSelfVerificationBlock(ctx.taskId)
15710
- },
15711
- {
15712
- id: "freeform.final_output",
15713
- source: "final_output",
15714
- body: buildFinalOutputBlock({
15715
- taskType: "freeform",
15716
- outputSchemaName: "FreeformOutput",
15717
- shapeSketch: [
15718
- "{",
15719
- " \"summary\": \"<2-5 sentence result>\",",
15720
- " \"branch\": \"<branch name when code changed; omit for prose-only work>\",",
15721
- " \"artifacts\": [{ \"kind\": \"...\", \"title\": \"...\", \"description\": \"...\", \"body\": \"<inline content up to 64 KiB; preferred for textual output so it persists with the task>\", \"url\": \"...\", \"path\": \"<worktree-ephemeral; not persisted after completion>\" }],",
15722
- " \"proposedTaskType\": { \"name\": \"...\", \"rationale\": \"...\", \"inputShape\": {}, \"outputShape\": {} },",
15723
- " \"diaryEntryIds\": [\"...\"],",
15724
- " \"verification\": <required iff input.successCriteria; see Self-verification>",
15725
- "}"
15726
- ].join("\n")
15727
- })
15572
+ header: "Outcome hints",
15573
+ body: outcomeHints
15728
15574
  }
15729
15575
  ];
15730
15576
  const priorContextBody = buildPriorContextSection(ctx.priorContext);
15731
- if (priorContextBody) sections.splice(sections.findIndex((s) => s.id === "freeform.workflow") + 1, 0, {
15577
+ if (priorContextBody) sections.push({
15732
15578
  id: "freeform.prior_context",
15733
15579
  source: "task_input",
15734
15580
  body: priorContextBody
@@ -15745,22 +15591,18 @@ function buildFreeformUserPrompt(input, ctx) {
15745
15591
  * is told to inspect them itself.
15746
15592
  */
15747
15593
  function buildFulfillBriefUserPrompt(input, ctx) {
15748
- const { brief, seedFiles, scopeHint } = input;
15594
+ const { brief, seedFiles } = input;
15749
15595
  const header = [
15750
15596
  "# Fulfill Brief Agent",
15751
15597
  "",
15752
15598
  "You are a software engineering agent working in a sandboxed environment.",
15753
15599
  "Use the current working directory as the task workspace.",
15754
- "The MoltNet runtime instructor (above, in this system prompt) defines the",
15755
- "invariants for this task: identity, gh authentication, diary discipline,",
15756
- "and the accountable-commit shape. Follow it for every commit.",
15757
15600
  "",
15758
15601
  "## Task: Fulfill brief",
15759
15602
  "",
15760
15603
  `Task id: \`${ctx.taskId}\``
15761
15604
  ].join("\n");
15762
15605
  const seedFilesBody = seedFiles?.length ? ["Start by reading these files to ground yourself:", ...seedFiles.map((f) => `- \`${f}\``)].join("\n") : "";
15763
- const branchSlug = ctx.correlationId ? `moltnet/${ctx.correlationId}/` : scopeHint ? `feat/${scopeHint}-` : "feat/";
15764
15606
  const correlation = ctx.correlationId ? [
15765
15607
  `This task carries correlationId \`${ctx.correlationId}\`. You MUST:`,
15766
15608
  "",
@@ -15777,23 +15619,6 @@ function buildFulfillBriefUserPrompt(input, ctx) {
15777
15619
  "for this task. Do not repurpose or switch the primary checkout.",
15778
15620
  ctx.workspace.branch ? `The current branch is \`${ctx.workspace.branch}\`. Stay on this branch unless the runtime instructor explicitly tells you otherwise.` : "Stay on the branch that was pre-provisioned for this task."
15779
15621
  ].join("\n") : "";
15780
- const workflow = [
15781
- ctx.workspace?.mode === "dedicated_worktree" ? `1. Use the already-provisioned dedicated worktree branch${ctx.workspace.branch ? ` (\`${ctx.workspace.branch}\`)` : ""}; do not create or switch the primary checkout.` : `1. Create a feature branch (starting prefix suggestion: \`${branchSlug}<short-slug>\`).`,
15782
- "2. Search MoltNet diary memory for prior decisions, incidents, and",
15783
- " recurring traps relevant to the brief before changing code.",
15784
- "3. Understand the problem — read relevant code; do not speculate.",
15785
- "4. Implement the change. Keep commits small and coherent.",
15786
- "5. Add tests if applicable.",
15787
- "6. For every commit, create a signed diary entry first via",
15788
- " `moltnet_create_entry` and embed its id in the commit trailer",
15789
- " `MoltNet-Diary: <id>` (per the runtime instructor).",
15790
- "7. Push the branch and open a PR — run `git push` and `gh pr create`",
15791
- " IN the VM with your normal `bash` tool (use the",
15792
- " `GH_TOKEN=$(moltnet github token …) gh …` form from the runtime",
15793
- " instructor for writes; read-only `gh` commands may run bare). Do NOT",
15794
- " use `moltnet_host_exec` for this; it needs human",
15795
- " approval that is unavailable in a headless run."
15796
- ].join("\n");
15797
15622
  return assembleTaskPrompt("fulfill_brief", [
15798
15623
  {
15799
15624
  id: "fulfill_brief.header",
@@ -15823,41 +15648,6 @@ function buildFulfillBriefUserPrompt(input, ctx) {
15823
15648
  source: "workspace",
15824
15649
  header: "Workspace",
15825
15650
  body: workspace
15826
- },
15827
- {
15828
- id: "fulfill_brief.workflow",
15829
- source: "static",
15830
- header: "Workflow",
15831
- body: workflow
15832
- },
15833
- {
15834
- id: "fulfill_brief.proactive_memory",
15835
- source: "discipline",
15836
- header: "Proactive memory use",
15837
- body: buildProactiveMemoryWorkflowBlock()
15838
- },
15839
- {
15840
- id: "fulfill_brief.verification",
15841
- source: "verification",
15842
- body: buildSelfVerificationBlock(ctx.taskId)
15843
- },
15844
- {
15845
- id: "fulfill_brief.final_output",
15846
- source: "final_output",
15847
- body: buildFinalOutputBlock({
15848
- taskType: "fulfill_brief",
15849
- outputSchemaName: "FulfillBriefOutput",
15850
- shapeSketch: [
15851
- "{",
15852
- " \"branch\": \"<branch-name>\",",
15853
- " \"commits\": [{ \"sha\": \"...\", \"message\": \"...\", \"diaryEntryId\": \"...\" }],",
15854
- " \"pullRequestUrl\": \"<url-or-null>\",",
15855
- " \"diaryEntryIds\": [\"...\"],",
15856
- " \"summary\": \"<1-3 sentence recap>\",",
15857
- " \"verification\": <required iff input.successCriteria; see Self-verification>",
15858
- "}"
15859
- ].join("\n")
15860
- })
15861
15651
  }
15862
15652
  ]);
15863
15653
  }
@@ -15934,26 +15724,6 @@ function buildJudgeEvalAttemptUserPrompt(input, ctx) {
15934
15724
  source: "rubric_judge",
15935
15725
  header: "Composite arithmetic",
15936
15726
  body: composite
15937
- },
15938
- {
15939
- id: "judge_eval_attempt.final_output",
15940
- source: "final_output",
15941
- body: buildFinalOutputBlock({
15942
- taskType: "judge_eval_attempt",
15943
- outputSchemaName: "JudgeEvalAttemptOutput",
15944
- shapeSketch: [
15945
- "{",
15946
- ` "targetTaskId": "${input.targetTaskId}",`,
15947
- ` "targetAttemptN": ${input.targetAttemptN},`,
15948
- " \"variantLabel\": \"<from producer input>\",",
15949
- " \"scores\": [ { \"criterionId\": \"...\", \"score\": 0..1, \"rationale\": \"...\", \"assertions\": [...]?, \"evidence\": { \"text\": \"...\" } } ],",
15950
- " \"composite\": <Σ(weight × score), 0..1>,",
15951
- " \"verdict\": \"<1-3 sentences>\",",
15952
- " \"judgeModel\": \"<id>\", // optional",
15953
- " \"traceparent\": \"<from claim>\"",
15954
- "}"
15955
- ].join("\n")
15956
- })
15957
15727
  }
15958
15728
  ]);
15959
15729
  }
@@ -16035,9 +15805,7 @@ function buildJudgePackUserPrompt(input, ctx) {
16035
15805
  "- Do NOT call `moltnet_pack_create` or `moltnet_pack_render`.",
16036
15806
  "- Do NOT fetch the curator's or renderer's task output directly — they",
16037
15807
  " may leak guidance that biases judgment.",
16038
- "- Keep the session focused on scoring; no speculative exploration.",
16039
- "",
16040
- `Write a signed diary entry (tags: \`judgment\`, \`judge_pack\`, \`rubric:${rubric.rubricId}\`) capturing the rationale before reporting structured output.`
15808
+ "- Keep the session focused on scoring; no speculative exploration."
16041
15809
  ].join("\n");
16042
15810
  return assembleTaskPrompt("judge_pack", [
16043
15811
  {
@@ -16079,37 +15847,6 @@ function buildJudgePackUserPrompt(input, ctx) {
16079
15847
  source: "static",
16080
15848
  header: "Constraints",
16081
15849
  body: constraints
16082
- },
16083
- {
16084
- id: "judge_pack.final_output",
16085
- source: "final_output",
16086
- body: buildFinalOutputBlock({
16087
- taskType: "judge_pack",
16088
- outputSchemaName: "JudgePackOutput",
16089
- shapeSketch: [
16090
- "{",
16091
- " \"scores\": [",
16092
- " { \"criterionId\": \"...\", \"score\": 0.0, \"rationale\": \"...\", \"evidence\": {} },",
16093
- " {",
16094
- " \"criterionId\": \"<llm_checklist criterion>\",",
16095
- " \"score\": 0, // 1 iff every assertion passed",
16096
- " \"assertions\": [",
16097
- " { \"id\": \"claim-1\", \"text\": \"...\", \"passed\": false, \"evidence\": \"...\" }",
16098
- " ]",
16099
- " }",
16100
- " ],",
16101
- " \"composite\": <sum-of-weighted-scores>,",
16102
- " \"verdict\": \"<1-3 sentence overall>\",",
16103
- " \"judgeModel\": \"<provider:model>\",",
16104
- " \"rendererBinaryCid\": \"<cid-string-only-if-available>\"",
16105
- "}"
16106
- ].join("\n"),
16107
- extraNotes: [
16108
- "Omit `rendererBinaryCid` entirely when no binary CID is exposed by",
16109
- "`moltnet_rendered_pack_get`. Do NOT emit `null` — the field is",
16110
- "optional and absence is the correct representation when unavailable."
16111
- ]
16112
- })
16113
15850
  }
16114
15851
  ]);
16115
15852
  }
@@ -16152,20 +15889,6 @@ function buildPrReviewUserPrompt(input, ctx) {
16152
15889
  "(for example publishing the judgment somewhere), perform that action as",
16153
15890
  "part of the task before reporting structured output."
16154
15891
  ].join("\n");
16155
- const workflow = [
16156
- "1. Read the subject summary, resources, inspection hints, and any",
16157
- " task-specific instructions before scoring.",
16158
- "2. Search MoltNet diary memory for prior decisions, incidents, and",
16159
- " recurring review traps relevant to the subject.",
16160
- "3. Inspect the target artefact directly using the tools and resources the",
16161
- " task makes available.",
16162
- "4. If you are in a dedicated disposable worktree and need the review target",
16163
- " checked out locally, do that work inside this disposable workspace only.",
16164
- "5. Apply the rubric strictly. This task is about complexity and",
16165
- " reviewability, not correctness or feature desirability.",
16166
- "6. Perform any required outward action before emitting the final",
16167
- " structured output."
16168
- ].join("\n");
16169
15892
  const taskPromptSection = input.taskPrompt ?? "";
16170
15893
  const preamble = renderRubricPreambleSection(rubric) ?? "";
16171
15894
  const criteria = renderRubricCriteriaList(rubric);
@@ -16174,9 +15897,7 @@ function buildPrReviewUserPrompt(input, ctx) {
16174
15897
  "- Score `1` when the subject clearly clears the criterion.",
16175
15898
  "- Score `0` when it does not, or when the evidence is ambiguous.",
16176
15899
  "- `rationale` is REQUIRED for every score. Keep it concrete and audit-friendly.",
16177
- "- Compute `composite = Σ(weight_i × score_i)` exactly; the runtime rejects mismatches.",
16178
- "",
16179
- "Write a signed diary entry (tags: `judgment`, `pr_review`) capturing the rationale before reporting structured output."
15900
+ "- Compute `composite = Σ(weight_i × score_i)` exactly; the runtime rejects mismatches."
16180
15901
  ].join("\n");
16181
15902
  return assembleTaskPrompt("pr_review", [
16182
15903
  {
@@ -16214,18 +15935,6 @@ function buildPrReviewUserPrompt(input, ctx) {
16214
15935
  header: "Execution contract",
16215
15936
  body: executionContract
16216
15937
  },
16217
- {
16218
- id: "pr_review.workflow",
16219
- source: "static",
16220
- header: "Review workflow",
16221
- body: workflow
16222
- },
16223
- {
16224
- id: "pr_review.proactive_memory",
16225
- source: "discipline",
16226
- header: "Proactive memory use",
16227
- body: buildProactiveMemoryWorkflowBlock()
16228
- },
16229
15938
  {
16230
15939
  id: "pr_review.task_prompt",
16231
15940
  source: "task_input",
@@ -16248,24 +15957,6 @@ function buildPrReviewUserPrompt(input, ctx) {
16248
15957
  source: "rubric_judge",
16249
15958
  header: "Scoring rules",
16250
15959
  body: scoring
16251
- },
16252
- {
16253
- id: "pr_review.final_output",
16254
- source: "final_output",
16255
- body: buildFinalOutputBlock({
16256
- taskType: "pr_review",
16257
- outputSchemaName: "PrReviewOutput",
16258
- shapeSketch: [
16259
- "{",
16260
- " \"scores\": [",
16261
- " { \"criterionId\": \"...\", \"score\": 0, \"rationale\": \"...\" }",
16262
- " ],",
16263
- " \"composite\": <sum-of-weighted-binary-scores>,",
16264
- " \"verdict\": \"<1-3 sentence overall>\"",
16265
- "}"
16266
- ].join("\n"),
16267
- extraNotes: ["`scores` MUST stay in the same order as the rubric criteria.", "`score` MUST be exactly `0` or `1` for every criterion."]
16268
- })
16269
15960
  }
16270
15961
  ]);
16271
15962
  }
@@ -16358,30 +16049,6 @@ function buildRenderPackUserPrompt(input, ctx) {
16358
16049
  source: "static",
16359
16050
  header: "Fidelity Discipline",
16360
16051
  body: fidelity
16361
- },
16362
- {
16363
- id: "render_pack.verification",
16364
- source: "verification",
16365
- body: buildSelfVerificationBlock(ctx.taskId)
16366
- },
16367
- {
16368
- id: "render_pack.final_output",
16369
- source: "final_output",
16370
- body: buildFinalOutputBlock({
16371
- taskType: "render_pack",
16372
- outputSchemaName: "RenderPackOutput",
16373
- shapeSketch: [
16374
- "{",
16375
- " \"renderedPackId\": \"<uuid-or-null>\",",
16376
- " \"renderedCid\": \"<cid>\",",
16377
- " \"renderMethod\": \"<label>\",",
16378
- " \"byteSize\": <int>,",
16379
- " \"entriesRendered\": <int>,",
16380
- " \"summary\": \"<1-3 sentence recap>\",",
16381
- " \"verification\": <required iff input.successCriteria; see Self-verification>",
16382
- "}"
16383
- ].join("\n")
16384
- })
16385
16052
  }
16386
16053
  ]);
16387
16054
  }
@@ -16416,7 +16083,7 @@ function buildRenderPackUserPrompt(input, ctx) {
16416
16083
  * field. Quoting the constraint back is not following the task.
16417
16084
  */
16418
16085
  function buildRunEvalUserPrompt(input, ctx) {
16419
- const { scenario, variantLabel, successCriteria } = input;
16086
+ const { scenario, variantLabel } = input;
16420
16087
  const effectiveRuntimeContext = ctx.effectiveRuntimeContext ?? input.context;
16421
16088
  const hasContext = effectiveRuntimeContext.length > 0;
16422
16089
  const hasInlineContext = effectiveRuntimeContext.some((entry) => entry.binding === "context_inline");
@@ -16437,27 +16104,6 @@ function buildRunEvalUserPrompt(input, ctx) {
16437
16104
  "rules, those rules override your generic instincts."
16438
16105
  ].join("\n") : "";
16439
16106
  const inputFiles = scenario.inputFiles?.length ? scenario.inputFiles.map((f) => `- \`${f}\``).join("\n") : "";
16440
- const verification = successCriteria ? buildSelfVerificationBlock(ctx.taskId) : "";
16441
- const finalOutput = buildFinalOutputBlock({
16442
- taskType: "run_eval",
16443
- outputSchemaName: "RunEvalOutput",
16444
- shapeSketch: [
16445
- "{",
16446
- " \"response\": \"<your free-form answer>\",",
16447
- " \"artifacts\": [{ \"path\": \"...\", \"cid\": \"...\" }], // optional",
16448
- " \"totalTokens\": <int>,",
16449
- " \"durationMs\": <int>,",
16450
- " \"traceparent\": \"<from claim>\",",
16451
- " \"verification\": {",
16452
- " \"inputCid\": \"<task inputCid>\",",
16453
- " \"results\": [",
16454
- " { \"id\": \"<criterion id>\", \"kind\": \"rubric\", \"status\": \"pass|fail|skip\", \"detail\": \"<optional one-liner>\" }",
16455
- " ],",
16456
- " \"passed\": <boolean>",
16457
- " } // required iff input.successCriteria; must be an object, never a string",
16458
- "}"
16459
- ].join("\n")
16460
- });
16461
16107
  return assembleTaskPrompt("run_eval", [
16462
16108
  {
16463
16109
  id: "run_eval.header",
@@ -16481,16 +16127,6 @@ function buildRunEvalUserPrompt(input, ctx) {
16481
16127
  source: "task_input",
16482
16128
  header: "Input files",
16483
16129
  body: inputFiles
16484
- },
16485
- {
16486
- id: "run_eval.verification",
16487
- source: "verification",
16488
- body: verification
16489
- },
16490
- {
16491
- id: "run_eval.final_output",
16492
- source: "final_output",
16493
- body: finalOutput
16494
16130
  }
16495
16131
  ]);
16496
16132
  }
@@ -16505,14 +16141,17 @@ function submissionAcceptsVerification(taskType) {
16505
16141
  /**
16506
16142
  * Add only the dynamic contract facts that a producer cannot infer from its
16507
16143
  * task-specific prompt: the declared success criteria and the immutable input
16508
- * CID its verification must cite. This is deliberately not a workflow block;
16509
- * the submit tool owns the output shape and profiles own optional behavior.
16144
+ * provenance CID its verification must cite. The CID is not a task artifact,
16145
+ * so the prompt states that it cannot be downloaded. This is deliberately not
16146
+ * a workflow block; the submit tool owns the output shape and profiles own
16147
+ * optional behavior.
16510
16148
  */
16511
16149
  function appendTaskContractFacts(prompt, task) {
16512
16150
  if (!hasSuccessCriteria(task.input) || !submissionAcceptsVerification(task.taskType)) return prompt;
16513
16151
  const criteriaJson = JSON.stringify(task.input.successCriteria, null, 2);
16514
16152
  const body = [
16515
- `Task input CID: \`${task.inputCid}\``,
16153
+ "Task input verification CID (provenance only; this is not a task",
16154
+ `artifact and cannot be downloaded): \`${task.inputCid}\``,
16516
16155
  "",
16517
16156
  "These typed criteria are task facts. Assess the completed work against",
16518
16157
  "them before calling the submit-output tool. Its `verification` payload",
@@ -31084,6 +30723,12 @@ var ShellCommandAnalyzer = class ShellCommandAnalyzer {
31084
30723
  }
31085
30724
  };
31086
30725
  //#endregion
30726
+ //#region src/config.ts
30727
+ /** Resolve Pi's host-side auth/config directory from process configuration. */
30728
+ function resolvePiCodingAgentDir() {
30729
+ return process.env["PI_CODING_AGENT_DIR"] ?? path.join(homedir(), ".pi", "agent");
30730
+ }
30731
+ //#endregion
31087
30732
  //#region src/snapshot.ts
31088
30733
  /**
31089
30734
  * Snapshot builder with auto-build and caching.
@@ -31464,15 +31109,19 @@ function filterModelVisibleTools(tools, policy) {
31464
31109
  }
31465
31110
  function enabledPiToolNames(input) {
31466
31111
  if (!input.policy || input.policy.enforcement !== "enforce") return void 0;
31112
+ return modelVisiblePiToolNames(input);
31113
+ }
31114
+ /** Exact tool names the selected runtime and effective policy expose. */
31115
+ function modelVisiblePiToolNames(input) {
31467
31116
  return [...new Set([...input.tools.map((tool) => tool.name), ...(input.extensions ?? []).flatMap((extension) => extension.declaredTools.filter((name) => isToolVisible(name, input.policy)))])].sort();
31468
31117
  }
31469
31118
  function isToolVisible(name, policy) {
31470
31119
  if (!policy || policy.enforcement !== "enforce") return true;
31471
- if (name === "bash") return true;
31120
+ if (name === "bash") return policy.allowedShellCommands === void 0 || policy.allowedShellCommands.length > 0;
31472
31121
  return policy.allowedTools.has(name);
31473
31122
  }
31474
31123
  function isKernelTool(name) {
31475
- return name.startsWith("submit_");
31124
+ return name.startsWith("submit_") || name === "subagent";
31476
31125
  }
31477
31126
  function wrapExtensionFactory(factory, contribution, policy) {
31478
31127
  return (pi) => {
@@ -31490,7 +31139,7 @@ function wrapExtensionFactory(factory, contribution, policy) {
31490
31139
  };
31491
31140
  }
31492
31141
  function claimToolName(names, name, owner) {
31493
- if (isKernelTool(name) || name === "subagent") throw new Error(`Pi tool name "${name}" is reserved by the runtime kernel`);
31142
+ if (isKernelTool(name)) throw new Error(`Pi tool name "${name}" is reserved by the runtime kernel`);
31494
31143
  const previous = names.get(name);
31495
31144
  if (previous) throw new Error(`Pi tool name "${name}" is declared by ${previous} and ${owner}`);
31496
31145
  names.set(name, owner);
@@ -31584,6 +31233,17 @@ async function delay(ms, signal, label) {
31584
31233
  * investigation and the alternatives we rejected.
31585
31234
  */
31586
31235
  var GUEST_TASK_CONTEXT_MOUNT = "/moltnet-task-context";
31236
+ function resolveVfsShadowConfig(config) {
31237
+ const patterns = config?.vfs?.shadow ?? [];
31238
+ if (patterns.length === 0) return {
31239
+ mode: "none",
31240
+ patterns: []
31241
+ };
31242
+ return {
31243
+ mode: config?.vfs?.shadowMode ?? "tmpfs",
31244
+ patterns
31245
+ };
31246
+ }
31587
31247
  function shouldRunResumeCommand(entry, ctx) {
31588
31248
  if (typeof entry === "string") return true;
31589
31249
  const workspaceModes = entry.when?.workspaceMode;
@@ -31656,7 +31316,7 @@ function resolveVmAgentDir(config) {
31656
31316
  function loadCredentials(agentDir) {
31657
31317
  const moltnetJson = readFileSync(path.join(agentDir, "moltnet.json"), "utf8");
31658
31318
  const agentEnvRaw = readFileSync(path.join(agentDir, "env"), "utf8");
31659
- const piAgentDir = process.env.PI_CODING_AGENT_DIR ?? path.join(process.env.HOME ?? "", ".pi", "agent");
31319
+ const piAgentDir = resolvePiCodingAgentDir();
31660
31320
  const piAuthPath = path.join(piAgentDir, "auth.json");
31661
31321
  const piAuthJson = existsSync(piAuthPath) ? readFileSync(piAuthPath, "utf8") : null;
31662
31322
  const gitconfigPath = path.join(agentDir, "gitconfig");
@@ -31811,7 +31471,7 @@ async function resumeVm(config) {
31811
31471
  else vmAgentEnv[k] = v;
31812
31472
  }
31813
31473
  vmAgentEnv.MOLTNET_CREDENTIALS_PATH = `${vmAgentDir}/moltnet.json`;
31814
- const vfsConfig = config.sandboxConfig?.vfs;
31474
+ const vfsConfig = resolveVfsShadowConfig(config.sandboxConfig);
31815
31475
  let workspaceProvider = new RealFSProvider(config.mountPath);
31816
31476
  workspaceProvider = new ShadowProvider(workspaceProvider, {
31817
31477
  shouldShadow: ({ path: shadowPath }) => shouldShadowNodeModulesPath(shadowPath),
@@ -31819,11 +31479,11 @@ async function resumeVm(config) {
31819
31479
  tmpfs: new AutoParentMemoryProvider(),
31820
31480
  writeMode: "tmpfs"
31821
31481
  });
31822
- if (vfsConfig?.shadow?.length) {
31823
- const predicate = createShadowPathPredicate(vfsConfig.shadow);
31482
+ if (vfsConfig.mode !== "none") {
31483
+ const predicate = createShadowPathPredicate(vfsConfig.patterns);
31824
31484
  workspaceProvider = new ShadowProvider(workspaceProvider, {
31825
31485
  shouldShadow: predicate,
31826
- writeMode: vfsConfig.shadowMode ?? "tmpfs"
31486
+ writeMode: vfsConfig.mode
31827
31487
  });
31828
31488
  }
31829
31489
  const forwardedEnv = {};
@@ -32283,7 +31943,7 @@ async function executeGondolinGrep(vm, localCwd, guestWorkspace, params, signal)
32283
31943
  const onAbort = () => ac.abort();
32284
31944
  signal?.addEventListener("abort", onAbort, { once: true });
32285
31945
  try {
32286
- const proc = vm.exec(["/bin/rg", ...args], {
31946
+ const proc = vm.exec(["rg", ...args], {
32287
31947
  signal: ac.signal,
32288
31948
  stdout: "pipe",
32289
31949
  stderr: "pipe"
@@ -32436,6 +32096,7 @@ function createGondolinBashOps(vm, localCwd, guestWorkspace) {
32436
32096
  */
32437
32097
  function decideToolCall(input) {
32438
32098
  if (input.enforcement === "off") return { allow: true };
32099
+ if (input.toolName.startsWith("submit_") || input.toolName === "subagent") return { allow: true };
32439
32100
  const resolved = resolveNames(input);
32440
32101
  if (resolved.kind === "unresolvable") return fenced(input.enforcement, "shell command could not be statically authorized", "unresolvable shell command (watch)");
32441
32102
  const arbitraryCode = [...new Set(resolved.tools.filter((tool) => tool.risk === "arbitrary-code").map((tool) => tool.name))];
@@ -32687,6 +32348,105 @@ function createToolPolicyExtension(deps) {
32687
32348
  };
32688
32349
  }
32689
32350
  //#endregion
32351
+ //#region src/runtime/capability-discovery.ts
32352
+ var GuestExecutableProbeError = class extends Error {
32353
+ code;
32354
+ constructor(code, message, options) {
32355
+ super(message, options);
32356
+ this.name = "GuestExecutableProbeError";
32357
+ this.code = code;
32358
+ }
32359
+ };
32360
+ var DEFAULT_PROBE_TIMEOUT_MS = 5e3;
32361
+ var MAX_STDERR_DETAIL_LENGTH = 1e3;
32362
+ /**
32363
+ * Verify only executables relevant to the resolved session policy.
32364
+ * Candidate names are positional arguments, never interpolated shell source.
32365
+ */
32366
+ async function discoverGuestExecutables(vm, candidates, options = {}) {
32367
+ const unique = [...new Set(candidates)].sort();
32368
+ if (unique.length === 0) return {
32369
+ available: [],
32370
+ unavailable: []
32371
+ };
32372
+ const timeoutMs = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
32373
+ const controller = new AbortController();
32374
+ let timedOut = false;
32375
+ const timeout = setTimeout(() => {
32376
+ timedOut = true;
32377
+ controller.abort();
32378
+ }, timeoutMs);
32379
+ const abortFromCaller = () => controller.abort(options.signal?.reason);
32380
+ options.signal?.addEventListener("abort", abortFromCaller, { once: true });
32381
+ if (options.signal?.aborted) abortFromCaller();
32382
+ let result;
32383
+ try {
32384
+ result = await vm.exec([
32385
+ "/bin/sh",
32386
+ "-lc",
32387
+ [
32388
+ "index=0",
32389
+ "for executable in \"$@\"; do",
32390
+ " if command -v \"$executable\" >/dev/null 2>&1; then",
32391
+ " printf \"%s\\n\" \"$index\"",
32392
+ " fi",
32393
+ " index=$((index + 1))",
32394
+ "done"
32395
+ ].join("\n"),
32396
+ "moltnet-capability-probe",
32397
+ ...unique
32398
+ ], { signal: controller.signal });
32399
+ } catch (error) {
32400
+ if (options.signal?.aborted) throw error;
32401
+ if (timedOut) throw new GuestExecutableProbeError("capability_probe_timeout", `Guest executable capability probe timed out after ${timeoutMs}ms`, { cause: error });
32402
+ throw error;
32403
+ } finally {
32404
+ clearTimeout(timeout);
32405
+ options.signal?.removeEventListener("abort", abortFromCaller);
32406
+ }
32407
+ if (result.exitCode !== 0) {
32408
+ const stderr = result.stderr.trim().slice(-MAX_STDERR_DETAIL_LENGTH);
32409
+ throw new GuestExecutableProbeError("capability_probe_failed", `Guest executable capability probe failed (exit ${result.exitCode})${stderr ? `: ${stderr}` : ""}`);
32410
+ }
32411
+ const availableIndexes = new Set(result.stdout.split("\n").filter(Boolean).map((value) => Number.parseInt(value, 10)).filter((value) => Number.isSafeInteger(value) && value >= 0));
32412
+ return {
32413
+ available: unique.filter((_, index) => availableIndexes.has(index)),
32414
+ unavailable: unique.filter((_, index) => !availableIndexes.has(index))
32415
+ };
32416
+ }
32417
+ //#endregion
32418
+ //#region src/runtime/model-selection.ts
32419
+ var RuntimeProfileModelResolutionError = class extends Error {
32420
+ constructor(message) {
32421
+ super(message);
32422
+ this.name = "RuntimeProfileModelResolutionError";
32423
+ }
32424
+ };
32425
+ /**
32426
+ * Resolve the exact runtime-profile model through Pi's custom-model registry.
32427
+ *
32428
+ * `getModel()` from pi-ai only knows generated built-in models. Runtime
32429
+ * profiles commonly select providers declared in the active Pi directory's
32430
+ * models.json, so an unresolved lookup must fail closed rather than letting
32431
+ * createAgentSession silently choose the default from settings.json.
32432
+ */
32433
+ function resolveRuntimeProfileModel(piAuthDir, provider, modelId, runtimeProfileId) {
32434
+ const authStorage = AuthStorage.create(join(piAuthDir, "auth.json"));
32435
+ const modelsPath = join(piAuthDir, "models.json");
32436
+ const modelRegistry = ModelRegistry.create(authStorage, modelsPath);
32437
+ const modelHandle = modelRegistry.find(provider, modelId);
32438
+ if (!modelHandle) {
32439
+ const registryError = modelRegistry.getError();
32440
+ const detail = registryError ? ` Registry error: ${registryError}` : "";
32441
+ const alternatives = modelRegistry.getAvailable().slice(0, 8).map((model) => `${model.provider}/${model.id}`);
32442
+ throw new RuntimeProfileModelResolutionError(`${runtimeProfileId ? `Runtime profile "${runtimeProfileId}"` : "Runtime profile"} model "${provider}/${modelId}" was not found in ${modelsPath}; refusing Pi default-model fallback.` + (alternatives.length > 0 ? ` Available models include: ${alternatives.join(", ")}.` : "") + detail);
32443
+ }
32444
+ return {
32445
+ modelHandle,
32446
+ modelRegistry
32447
+ };
32448
+ }
32449
+ //#endregion
32690
32450
  //#region src/runtime/resolve-prior-context.ts
32691
32451
  /**
32692
32452
  * Fetch the named attempt's output and project it into the prompt's
@@ -32873,6 +32633,40 @@ async function withTimeout(promise, timeoutMs, onTimeout) {
32873
32633
  }
32874
32634
  }
32875
32635
  //#endregion
32636
+ //#region src/runtime/runtime-capability-projection.ts
32637
+ /**
32638
+ * Project trusted policy resolution and runtime inventory into the exact
32639
+ * model-visible policy plus host-side degradation diagnostics.
32640
+ */
32641
+ function projectRuntimeCapabilities(input) {
32642
+ const visibleOptionalTools = input.visibleToolNames.filter((name) => !name.startsWith("submit_"));
32643
+ if (!input.policy) return {
32644
+ instructorPolicy: {
32645
+ enforcement: "off",
32646
+ allowedTools: visibleOptionalTools,
32647
+ allowedShellCommands: [],
32648
+ degraded: false
32649
+ },
32650
+ unavailableTools: [],
32651
+ unavailableExecutables: [],
32652
+ droppedShellCommandCount: 0
32653
+ };
32654
+ const visible = new Set(input.visibleToolNames);
32655
+ const unavailableTools = [...input.policy.allowedTools].filter((name) => !visible.has(name)).sort();
32656
+ const unavailableShellCommands = input.unavailableShellCommands ?? [];
32657
+ return {
32658
+ instructorPolicy: {
32659
+ enforcement: input.policy.enforcement,
32660
+ allowedTools: visibleOptionalTools,
32661
+ allowedShellCommands: input.policy.allowedShellCommands,
32662
+ degraded: input.policy.degraded
32663
+ },
32664
+ unavailableTools,
32665
+ unavailableExecutables: [...new Set(unavailableShellCommands.map(({ argvPrefix }) => argvPrefix[0]))].sort(),
32666
+ droppedShellCommandCount: unavailableShellCommands.length
32667
+ };
32668
+ }
32669
+ //#endregion
32876
32670
  //#region src/runtime/runtime-context.ts
32877
32671
  /**
32878
32672
  * Pi-specific runtime context handling.
@@ -32997,6 +32791,73 @@ function buildWorkspaceMountInstructions(guestWorkspace) {
32997
32791
  " non-existent checkout."
32998
32792
  ].join("\n");
32999
32793
  }
32794
+ function buildToolPolicyInstructions(policy) {
32795
+ const lines = [
32796
+ "## Effective runtime tool policy",
32797
+ "",
32798
+ "- The registered submit-output tool is always the completion protocol."
32799
+ ];
32800
+ if (!policy || policy.enforcement === "off") {
32801
+ lines.push("- Enforcement is off: runtime policy does not restrict visible tools or", " shell commands. The live sandbox still determines what is installed.");
32802
+ return lines.join("\n");
32803
+ }
32804
+ lines.push(`- Enforcement mode: \`${policy.enforcement}\`.`);
32805
+ if (policy.degraded) lines.push("- Policy resolution degraded. Enforce mode is fail-closed; only the", " kernel submit-output tool is available.");
32806
+ lines.push(policy.allowedTools.length > 0 ? "- The visible structured-tool definitions are the authorized surface." : policy.enforcement === "enforce" ? "- No optional structured tools are authorized." : "- No optional structured tools are registered.");
32807
+ if (policy.enforcement === "watch") lines.push("- Watch mode records policy decisions but does not block tool calls.");
32808
+ else if (policy.allowedShellCommands.length === 0) lines.push("- No shell commands are authorized. `bash` is not available; do not", " attempt shell, filesystem, git, GitHub CLI, or MoltNet CLI commands.");
32809
+ else lines.push("- Shell commands are restricted to these authorized argv prefixes:", ...renderShellCommandPrefixes(policy.allowedShellCommands), "- A visible `bash` tool does not grant broader shell authority. Do not", " attempt commands outside those prefixes.");
32810
+ lines.push("- Tools and commands absent from this effective policy are unavailable,", " even if advisory context mentions them.");
32811
+ return lines.join("\n");
32812
+ }
32813
+ function renderShellCommandPrefixes(commands) {
32814
+ const visible = commands.slice(0, 12).map(({ argvPrefix }) => ` - \`${argvPrefix.join(" ")}\``);
32815
+ const omitted = commands.length - visible.length;
32816
+ if (omitted > 0) visible.push(` - …and ${omitted} more authorized prefixes.`);
32817
+ return visible;
32818
+ }
32819
+ function buildSandboxCapabilityInstructions(sandbox, policy) {
32820
+ if (!sandbox) return "";
32821
+ const executableLines = !policy || policy.enforcement === "off" ? ["- Executables are discovered through the visible shell when needed."] : [sandbox.verifiedExecutables.length > 0 ? `- Session-verified policy executables: ${sandbox.verifiedExecutables.map((name) => `\`${name}\``).join(", ")}.` : "- No policy-relevant guest executables were verified for this session.", ...policy.enforcement === "watch" ? ["- Watch-mode probes are diagnostic, not an executable", " allowlist; other installed commands remain policy-permitted."] : []];
32822
+ const lines = [
32823
+ "## Effective sandbox capabilities",
32824
+ "",
32825
+ `- Workspace mode: \`${sandbox.workspaceMode}\`.`,
32826
+ `- VFS shadow mode: \`${sandbox.vfsShadowMode}\`${sandbox.vfsShadowPatterns.length > 0 ? ` for ${sandbox.vfsShadowPatterns.map((pattern) => `\`${pattern}\``).join(", ")}` : ""}.`,
32827
+ ...sandbox.nodeModulesWriteMode ? ["- `node_modules` writes use session-local tmpfs and do not persist", " to the mounted workspace."] : [],
32828
+ ...executableLines
32829
+ ];
32830
+ const externalHosts = [...sandbox.allowedHosts].sort();
32831
+ const internalHosts = [...sandbox.allowedInternalHosts].sort();
32832
+ lines.push(...externalHosts.length > 0 ? [`- Additional external egress hosts: ${externalHosts.map((host) => `\`${host}\``).join(", ")}.`] : [], ...internalHosts.length > 0 ? [`- Additional internal egress hosts: ${internalHosts.map((host) => `\`${host}\``).join(", ")}.`] : [], "- Runtime service endpoints required for task execution may be available", " in addition to the operator-configured hosts above.");
32833
+ return lines.join("\n");
32834
+ }
32835
+ function shellExecutableIsAvailable(policy, sandbox, executable) {
32836
+ if (!policy || policy.enforcement === "off") return true;
32837
+ if (sandbox && !sandbox.verifiedExecutables.includes(executable)) return false;
32838
+ if (policy.enforcement !== "enforce") return true;
32839
+ return policy.allowedShellCommands.some(({ argvPrefix }) => argvPrefix[0] === executable);
32840
+ }
32841
+ function buildCredentialInstructions(policy, sandbox) {
32842
+ const lines = [
32843
+ "## Identity & credentials",
32844
+ "",
32845
+ "- Your credentials live at `/home/agent/.moltnet/<agent>/moltnet.json`",
32846
+ " with the gitconfig and SSH key alongside. Do not move, copy, or expose",
32847
+ " these files outside the VM."
32848
+ ];
32849
+ const moltnetAvailable = shellExecutableIsAvailable(policy, sandbox, "moltnet");
32850
+ const ghAvailable = shellExecutableIsAvailable(policy, sandbox, "gh");
32851
+ const gitAvailable = shellExecutableIsAvailable(policy, sandbox, "git");
32852
+ if (moltnetAvailable) lines.push("- When authorized by the effective shell policy, use the installed", " `moltnet` binary on `PATH`; never invoke a cached or `npx` copy.");
32853
+ if (ghAvailable) {
32854
+ lines.push("- This headless VM has no human GitHub token fallback. Every authorized", " `gh` write must use an inline App token.");
32855
+ if (moltnetAvailable) lines.push("", " ```bash", " CREDS=\"$(cd \"$(dirname \"$GIT_CONFIG_GLOBAL\")\" && pwd)/moltnet.json\"", " GH_TOKEN=$(moltnet github token --credentials \"$CREDS\") gh <command>", " ```");
32856
+ else lines.push("- The effective policy does not authorize the `moltnet` token-minting", " command, so do not attempt a GitHub write.");
32857
+ }
32858
+ if (gitAvailable) lines.push("- An authorized `git push` uses the injected credential helper and does", " not need `GH_TOKEN`.");
32859
+ return lines.join("\n");
32860
+ }
33000
32861
  /**
33001
32862
  * Build the minimal immutable system-prompt kernel. Runtime-profile context
33002
32863
  * carries operator-selected workflow guidance; this kernel stays last in the
@@ -33019,31 +32880,7 @@ function buildRuntimeKernel(ctx) {
33019
32880
  `- Diary id (for this task): \`${ctx.diaryId}\``,
33020
32881
  `- Agent name: \`${ctx.agentName}\``,
33021
32882
  "",
33022
- "## Identity & credentials",
33023
- "",
33024
- "- Your credentials live at `/home/agent/.moltnet/<agent>/moltnet.json`",
33025
- " with the gitconfig and SSH key alongside. Do not move, copy, or expose",
33026
- " these files outside the VM.",
33027
- "- The `moltnet` CLI is installed in the VM and is the only supported way",
33028
- " to mint short-lived tokens. Do not invoke `npx @themoltnet/cli` or any",
33029
- " cached path — use the `moltnet` binary on `PATH`.",
33030
- "- Interactive sessions use the canonical `moltnet github guard` policy,",
33031
- " documented in `docs/reference/agent-configuration.md`. This headless VM",
33032
- " has no editor hook and no human GitHub token to fall back to: read-only",
33033
- " `gh` commands may run bare, but every write must use the App token:",
33034
- "",
33035
- " ```bash",
33036
- " CREDS=\"$(cd \"$(dirname \"$GIT_CONFIG_GLOBAL\")\" && pwd)/moltnet.json\"",
33037
- " GH_TOKEN=$(moltnet github token --credentials \"$CREDS\") gh <command>",
33038
- " ```",
33039
- "",
33040
- "- `git push` uses the gitconfig-configured credential helper and is not",
33041
- " a `gh` call — it does not need `GH_TOKEN`.",
33042
- "- Run `git` and `gh` in the VM with your normal `bash` tool — your",
33043
- " credentials are injected here, so they work in the guest. The",
33044
- " `moltnet_host_exec` tool is a last-resort host escape-hatch that",
33045
- " requires human approval and is unavailable in headless task runs;",
33046
- " never use it for routine git/gh.",
32883
+ buildCredentialInstructions(ctx.toolPolicy, ctx.sandbox),
33047
32884
  "",
33048
32885
  "## Skill packs",
33049
32886
  "",
@@ -33056,6 +32893,10 @@ function buildRuntimeKernel(ctx) {
33056
32893
  "",
33057
32894
  buildWorkspaceMountInstructions(ctx.guestWorkspace),
33058
32895
  "",
32896
+ buildSandboxCapabilityInstructions(ctx.sandbox, ctx.toolPolicy),
32897
+ ...ctx.sandbox ? [""] : [],
32898
+ buildToolPolicyInstructions(ctx.toolPolicy),
32899
+ "",
33059
32900
  "## Structured completion",
33060
32901
  "- The registered submit-output tool is the only completion wire protocol. Submit its typed payload when work is complete; prose is not a substitute."
33061
32902
  ].join("\n");
@@ -33167,6 +33008,7 @@ function createSubagentTool(args) {
33167
33008
  cwdPath: args.cwdPath ?? args.mountPath,
33168
33009
  piAuthDir: args.piAuthDir,
33169
33010
  modelHandle: args.modelHandle,
33011
+ modelRegistry: args.modelRegistry,
33170
33012
  thinkingLevel: args.thinkingLevel,
33171
33013
  temperature: args.temperature,
33172
33014
  topP: args.topP,
@@ -33294,6 +33136,39 @@ function toolError(text, details = { captured: false }) {
33294
33136
  };
33295
33137
  }
33296
33138
  //#endregion
33139
+ //#region src/runtime/submit-completion-coordinator.ts
33140
+ /**
33141
+ * Delay the post-submit session abort until every tool call in Pi's current
33142
+ * parallel batch has emitted `tool_execution_end`. A valid submit call can
33143
+ * finish before a sibling write or artifact upload; aborting immediately would
33144
+ * cancel that sibling after the runtime had already accepted the output.
33145
+ */
33146
+ function createSubmitCompletionCoordinator(options) {
33147
+ const activeToolCalls = /* @__PURE__ */ new Set();
33148
+ let completionRequested = false;
33149
+ let completionStarted = false;
33150
+ const drain = () => {
33151
+ if (!completionRequested || completionStarted || activeToolCalls.size > 0) return;
33152
+ completionStarted = true;
33153
+ Promise.resolve(options.onDrained()).catch(options.onError);
33154
+ };
33155
+ return {
33156
+ extension: (pi) => {
33157
+ pi.on("tool_execution_start", (event) => {
33158
+ activeToolCalls.add(event.toolCallId);
33159
+ });
33160
+ pi.on("tool_execution_end", (event) => {
33161
+ activeToolCalls.delete(event.toolCallId);
33162
+ drain();
33163
+ });
33164
+ },
33165
+ requestCompletion: () => {
33166
+ completionRequested = true;
33167
+ drain();
33168
+ }
33169
+ };
33170
+ }
33171
+ //#endregion
33297
33172
  //#region src/runtime/task-output.ts
33298
33173
  var METER_NAME = "@themoltnet/pi-extension/task-output";
33299
33174
  var parseResultCounter = null;
@@ -33463,7 +33338,8 @@ function submitOutputRepairHint(taskType, errors) {
33463
33338
  const hints = ["Tool args must be the output object directly, not wrapped in { output: ... }."];
33464
33339
  if (fields.has("output/artifacts")) hints.push("`artifacts` must be an array; omit it when there are no artifacts, use [], or use objects like { \"kind\": \"note\", \"title\": \"Result\", \"body\": \"...\" }.");
33465
33340
  if (fields.has("output/verification")) hints.push("`verification` must be an object with inputCid, results[], and passed; do not send it as text or an array.");
33466
- if (taskType === "freeform" && (fields.has("output/artifacts") || fields.has("output/verification"))) hints.push("Minimal valid freeform retry: { \"summary\": \"completed\", \"artifacts\": [], \"verification\": { \"inputCid\": \"<task inputCid>\", \"results\": [{ \"id\": \"submit-output\", \"kind\": \"gate\", \"status\": \"pass\", \"detail\": \"submit_freeform_output accepted valid args\" }], \"passed\": true } }.");
33341
+ if (fields.has("output/artifacts") || fields.has("output/verification")) if (taskType === "freeform") hints.push("Minimal valid freeform retry: { \"summary\": \"completed\", \"artifacts\": [], \"verification\": { \"inputCid\": \"<task inputCid>\", \"results\": [{ \"id\": \"submit-output\", \"kind\": \"gate\", \"status\": \"pass\", \"detail\": \"submit_freeform_output accepted valid args\" }], \"passed\": true } }.");
33342
+ else hints.push(`\`verification\` is a stamp when the only gate is submit-output: { "inputCid": "<task inputCid>", "results": [{ "id": "submit-output", "kind": "gate", "status": "pass", "detail": "submit_${taskType}_output accepted valid args" }], "passed": true }.`);
33467
33343
  if (hints.length === 1) hints.push("Fix every listed field before re-calling this same tool.");
33468
33344
  return hints.join(" ");
33469
33345
  }
@@ -33481,7 +33357,18 @@ function onlySubmitOutputGate(input) {
33481
33357
  if (Array.isArray(assertions) && assertions.length > 0) return false;
33482
33358
  return criteria.rubric === void 0 && criteria.sideEffects === void 0 && criteria.minComposite === void 0;
33483
33359
  }
33484
- function repairFreeformSubmitOutput(params, opts) {
33360
+ /**
33361
+ * Repair a producer submit-output payload when the task's ONLY success gate is
33362
+ * the auto-injected submit-output gate — i.e. there is nothing substantive to
33363
+ * self-assess, so the `verification` record is a mechanical stamp. Applies to
33364
+ * any producer task type (freeform, run_eval, …), not just freeform: weaker
33365
+ * models mis-type or omit the nested `verification` object identically across
33366
+ * types, and previously only freeform was repaired, so run_eval attempts failed
33367
+ * `output_validation_failed` on verification alone. The freeform-only field
33368
+ * coercions below are guarded by field presence, so they no-op for other types,
33369
+ * and the caller re-validates the repaired payload against the type's schema.
33370
+ */
33371
+ function repairProducerSubmitOutput(taskType, params, opts) {
33485
33372
  if (!isRecord(params) || !opts.inputCid || !onlySubmitOutputGate(opts.input)) return null;
33486
33373
  const repaired = { ...params };
33487
33374
  if ("artifacts" in repaired && !Array.isArray(repaired.artifacts)) if (isRecord(repaired.artifacts)) repaired.artifacts = [repaired.artifacts];
@@ -33497,15 +33384,14 @@ function repairFreeformSubmitOutput(params, opts) {
33497
33384
  id: SUBMIT_OUTPUT_GATE_ID,
33498
33385
  kind: "gate",
33499
33386
  status: "pass",
33500
- detail: "submit_freeform_output accepted valid args"
33387
+ detail: `submit_${taskType}_output accepted valid args`
33501
33388
  }],
33502
33389
  passed: true
33503
33390
  };
33504
33391
  return repaired;
33505
33392
  }
33506
33393
  function maybeRepairSubmitOutput(taskType, params, opts) {
33507
- if (taskType !== "freeform") return null;
33508
- const repaired = repairFreeformSubmitOutput(params, opts);
33394
+ const repaired = repairProducerSubmitOutput(taskType, params, opts);
33509
33395
  if (!repaired) return null;
33510
33396
  return validateTaskSubmission(taskType, repaired, opts.input, { inputCid: opts.inputCid }).length === 0 ? repaired : null;
33511
33397
  }
@@ -33529,10 +33415,24 @@ Agent submission schema:\n\`\`\`json\n${contract.parametersSchemaJson}\n\`\`\``,
33529
33415
  promptGuidelines: [
33530
33416
  `Call \`${contract.toolName}\` with the exact ${taskType} agent submission shape shown above.`,
33531
33417
  "The transport accepts malformed objects only so validation errors can be recovered in-session; the schema shown above is authoritative.",
33532
- "If the submit tool returns a validation error, fix every listed field and call the same tool again."
33418
+ "If the submit tool returns a validation error, fix every listed field and call the same tool again.",
33419
+ "The first valid submission is final and immediately ends the session."
33533
33420
  ],
33534
33421
  parameters: RecoverableSubmitToolParameters,
33535
33422
  async execute(_id, params) {
33423
+ if (captured) return {
33424
+ content: [{
33425
+ type: "text",
33426
+ text: "Output was already captured. The first valid payload remains final; this duplicate submission was ignored."
33427
+ }],
33428
+ details: {
33429
+ captured: true,
33430
+ callCount,
33431
+ invalidCallCount,
33432
+ maxSubmitValidationRetries,
33433
+ error: null
33434
+ }
33435
+ };
33536
33436
  if (exhaustedValidationFailure) return {
33537
33437
  content: [{
33538
33438
  type: "text",
@@ -33583,6 +33483,7 @@ Agent submission schema:\n\`\`\`json\n${contract.parametersSchemaJson}\n\`\`\``,
33583
33483
  }
33584
33484
  captured = candidateParams;
33585
33485
  callCount += 1;
33486
+ await opts.onValidCapture?.();
33586
33487
  return {
33587
33488
  content: [{
33588
33489
  type: "text",
@@ -34023,7 +33924,7 @@ async function executePiTask(claimedTask, reporter, opts) {
34023
33924
  let subagentHandle = null;
34024
33925
  const finalUsage = emptyUsage(opts.provider, opts.model);
34025
33926
  let cancelListener = null;
34026
- const makeFailedOutput = (code, message, usage = finalUsage) => ({
33927
+ const makeFailedOutput = (code, message, usage = finalUsage, retryable = false) => ({
34027
33928
  taskId: task.id,
34028
33929
  attemptN,
34029
33930
  status: "failed",
@@ -34034,7 +33935,7 @@ async function executePiTask(claimedTask, reporter, opts) {
34034
33935
  error: {
34035
33936
  code,
34036
33937
  message,
34037
- retryable: false
33938
+ retryable
34038
33939
  }
34039
33940
  });
34040
33941
  const makeCancelledOutput = (message) => ({
@@ -34087,6 +33988,7 @@ async function executePiTask(claimedTask, reporter, opts) {
34087
33988
  reporterOpen = true;
34088
33989
  let checkpointPath;
34089
33990
  let resolvedVmTemplate = opts.resolvedVmTemplate;
33991
+ let effectiveSandboxConfig;
34090
33992
  try {
34091
33993
  if (!resolvedVmTemplate && opts.runtimeDefinition) resolvedVmTemplate = opts.resolveVmTemplate ? await opts.resolveVmTemplate() : await opts.runtimeDefinition.vm.resolve({ onProgress: opts.onSnapshotProgress });
34092
33994
  checkpointPath = resolvedVmTemplate?.checkpointPath ?? opts.checkpointPath ?? (opts.resolveCheckpointPath ? await opts.resolveCheckpointPath() : await ensureSnapshot({
@@ -34110,7 +34012,7 @@ async function executePiTask(claimedTask, reporter, opts) {
34110
34012
  return makeFailedOutput("worktree_setup_failed", message);
34111
34013
  }
34112
34014
  try {
34113
- const sandboxConfig = applyExecutionPlanSandboxOverrides(resolvedVmTemplate ? {
34015
+ effectiveSandboxConfig = applyExecutionPlanSandboxOverrides(resolvedVmTemplate ? {
34114
34016
  ...opts.sandboxConfig,
34115
34017
  snapshot: void 0,
34116
34018
  resumeCommands: [...resolvedVmTemplate.resumeCommands]
@@ -34122,7 +34024,7 @@ async function executePiTask(claimedTask, reporter, opts) {
34122
34024
  mountPath,
34123
34025
  workspaceMode: workspace.mode,
34124
34026
  extraAllowedHosts: opts.extraAllowedHosts,
34125
- sandboxConfig,
34027
+ sandboxConfig: effectiveSandboxConfig,
34126
34028
  forwardEnv: opts.forwardEnv,
34127
34029
  signal: reporter.cancelSignal
34128
34030
  });
@@ -34252,11 +34154,20 @@ async function executePiTask(claimedTask, reporter, opts) {
34252
34154
  cwdPath,
34253
34155
  guestWorkspace: managed.guestWorkspace
34254
34156
  });
34157
+ const submitCompletion = createSubmitCompletionCoordinator({
34158
+ onDrained: () => session?.abort(),
34159
+ onError: async (err) => {
34160
+ await emitError("submit_output_abort", err instanceof Error ? err.message : String(err), { event: "submit_output_abort_failed" });
34161
+ }
34162
+ });
34255
34163
  const { handle: submitToolHandle, tools: submitToolDefs } = resolveSubmitTools(task.taskType, {
34256
34164
  model: opts.model,
34257
34165
  input: task.input,
34258
34166
  inputCid: task.inputCid,
34259
- maxSubmitValidationRetries: opts.maxSubmitValidationRetries
34167
+ maxSubmitValidationRetries: opts.maxSubmitValidationRetries,
34168
+ onValidCapture: () => {
34169
+ submitCompletion.requestCompletion();
34170
+ }
34260
34171
  });
34261
34172
  const submitTools = submitToolDefs;
34262
34173
  try {
@@ -34284,38 +34195,27 @@ async function executePiTask(claimedTask, reporter, opts) {
34284
34195
  correlationId: task.correlationId ?? null
34285
34196
  })
34286
34197
  });
34287
- const piAuthDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
34288
- const modelHandle = getModel(opts.provider, opts.model);
34289
- const runtimeKernel = buildRuntimeKernel({
34290
- taskId: task.id,
34291
- taskType: task.taskType,
34292
- attemptN,
34293
- diaryId,
34294
- agentName: opts.agentName,
34295
- guestWorkspace: managed.guestWorkspace,
34296
- correlationId: task.correlationId ?? null
34297
- });
34298
- const appendSystemPrompt = composeRuntimeSystemPrompt({
34299
- profilePromptPrefix: injectedContext.systemPromptPrefix,
34300
- kernel: runtimeKernel
34301
- });
34198
+ const piAuthDir = resolvePiCodingAgentDir();
34199
+ const { modelHandle, modelRegistry } = resolveRuntimeProfileModel(piAuthDir, opts.provider, opts.model, opts.runtimeProfileId);
34302
34200
  const injectedSkills = injectedContext.skills;
34303
34201
  const toolPolicyExtensions = [];
34304
34202
  let resolvedToolPolicy;
34203
+ let unavailableRuntimeShellCommands = [];
34204
+ let verifiedGuestExecutables = [];
34205
+ const toolPolicyLogger = opts.toolPolicyLogger ?? {
34206
+ debug: () => {},
34207
+ info: (obj, msg) => console.error(JSON.stringify({
34208
+ level: "info",
34209
+ msg,
34210
+ ...obj
34211
+ })),
34212
+ warn: (obj, msg) => console.error(JSON.stringify({
34213
+ level: "warn",
34214
+ msg,
34215
+ ...obj
34216
+ }))
34217
+ };
34305
34218
  if (opts.runtimeProfileId && opts.toolEnforcement && opts.toolEnforcement !== "off") {
34306
- const toolPolicyLogger = opts.toolPolicyLogger ?? {
34307
- debug: () => {},
34308
- info: (obj, msg) => console.error(JSON.stringify({
34309
- level: "info",
34310
- msg,
34311
- ...obj
34312
- })),
34313
- warn: (obj, msg) => console.error(JSON.stringify({
34314
- level: "warn",
34315
- msg,
34316
- ...obj
34317
- }))
34318
- };
34319
34219
  const [analyzer, policy] = await Promise.all([ShellCommandAnalyzer.create(), resolveSessionToolPolicy({
34320
34220
  agent: moltnetAgent,
34321
34221
  profileId: opts.runtimeProfileId,
@@ -34324,9 +34224,16 @@ async function executePiTask(claimedTask, reporter, opts) {
34324
34224
  enforcement: opts.toolEnforcement,
34325
34225
  logger: toolPolicyLogger
34326
34226
  })]);
34327
- resolvedToolPolicy = policy;
34227
+ verifiedGuestExecutables = (await discoverGuestExecutables(managed.vm, [...policy.allowedShellCommands.map(({ argvPrefix }) => argvPrefix[0]), ...policy.enforcement === "watch" ? resolvedVmTemplate?.executables ?? [] : []], { signal: reporter.cancelSignal })).available;
34228
+ const availableExecutables = new Set(verifiedGuestExecutables);
34229
+ const allowedShellCommands = policy.allowedShellCommands.filter(({ argvPrefix }) => availableExecutables.has(argvPrefix[0]));
34230
+ unavailableRuntimeShellCommands = policy.allowedShellCommands.filter(({ argvPrefix }) => !availableExecutables.has(argvPrefix[0]));
34231
+ resolvedToolPolicy = {
34232
+ ...policy,
34233
+ allowedShellCommands
34234
+ };
34328
34235
  toolPolicyExtensions.push(createToolPolicyExtension({
34329
- policy,
34236
+ policy: resolvedToolPolicy,
34330
34237
  analyzer,
34331
34238
  logger: toolPolicyLogger
34332
34239
  }));
@@ -34364,13 +34271,67 @@ async function executePiTask(claimedTask, reporter, opts) {
34364
34271
  policy: resolvedToolPolicy
34365
34272
  }) : [];
34366
34273
  const visibleBaseTools = filterModelVisibleTools([...gondolinCustomTools, ...moltnetTools], resolvedToolPolicy);
34274
+ const taskHasSubagents = taskTypeUsesSubagents(task.taskType);
34275
+ const visibleParentToolNames = modelVisiblePiToolNames({
34276
+ tools: [
34277
+ ...visibleBaseTools,
34278
+ ...runtimeParentTools,
34279
+ ...submitTools,
34280
+ ...taskHasSubagents ? [{ name: "subagent" }] : []
34281
+ ],
34282
+ extensions: opts.runtimeDefinition?.extensions,
34283
+ policy: resolvedToolPolicy
34284
+ });
34285
+ const capabilityProjection = projectRuntimeCapabilities({
34286
+ policy: resolvedToolPolicy,
34287
+ visibleToolNames: visibleParentToolNames,
34288
+ unavailableShellCommands: unavailableRuntimeShellCommands
34289
+ });
34290
+ if (capabilityProjection.unavailableTools.length > 0 || capabilityProjection.unavailableExecutables.length > 0) {
34291
+ const diagnostics = {
34292
+ runtimeProfileId: opts.runtimeProfileId,
34293
+ unavailableTools: capabilityProjection.unavailableTools,
34294
+ unavailableExecutables: capabilityProjection.unavailableExecutables,
34295
+ droppedShellCommandCount: capabilityProjection.droppedShellCommandCount
34296
+ };
34297
+ toolPolicyLogger.warn(diagnostics, "Runtime policy grants unavailable capabilities");
34298
+ await emit("info", {
34299
+ event: "runtime_capabilities_unavailable",
34300
+ ...diagnostics
34301
+ });
34302
+ }
34303
+ const vfsShadow = resolveVfsShadowConfig(effectiveSandboxConfig);
34304
+ const runtimeKernel = buildRuntimeKernel({
34305
+ taskId: task.id,
34306
+ taskType: task.taskType,
34307
+ attemptN,
34308
+ diaryId,
34309
+ agentName: opts.agentName,
34310
+ guestWorkspace: managed.guestWorkspace,
34311
+ correlationId: task.correlationId ?? null,
34312
+ sandbox: {
34313
+ workspaceMode: activeWorkspace.mode,
34314
+ vfsShadowMode: vfsShadow.mode,
34315
+ vfsShadowPatterns: vfsShadow.patterns,
34316
+ nodeModulesWriteMode: "tmpfs",
34317
+ verifiedExecutables: verifiedGuestExecutables,
34318
+ allowedHosts: [...effectiveSandboxConfig?.network?.allowedHosts ?? [], ...opts.extraAllowedHosts ?? []],
34319
+ allowedInternalHosts: effectiveSandboxConfig?.network?.allowedInternalHosts ?? []
34320
+ },
34321
+ toolPolicy: capabilityProjection.instructorPolicy
34322
+ });
34323
+ const appendSystemPrompt = composeRuntimeSystemPrompt({
34324
+ profilePromptPrefix: injectedContext.systemPromptPrefix,
34325
+ kernel: runtimeKernel
34326
+ });
34367
34327
  const parentSubagentTools = [];
34368
- if (taskTypeUsesSubagents(task.taskType)) {
34328
+ if (taskHasSubagents) {
34369
34329
  subagentHandle = createSubagentTool({
34370
34330
  mountPath,
34371
34331
  cwdPath,
34372
34332
  piAuthDir,
34373
34333
  modelHandle,
34334
+ modelRegistry,
34374
34335
  thinkingLevel: opts.thinkingLevel,
34375
34336
  temperature: opts.temperature,
34376
34337
  topP: opts.topP,
@@ -34404,6 +34365,7 @@ async function executePiTask(claimedTask, reporter, opts) {
34404
34365
  cwdPath,
34405
34366
  piAuthDir,
34406
34367
  modelHandle,
34368
+ modelRegistry,
34407
34369
  thinkingLevel: opts.thinkingLevel,
34408
34370
  temperature: opts.temperature,
34409
34371
  topP: opts.topP,
@@ -34427,7 +34389,11 @@ async function executePiTask(claimedTask, reporter, opts) {
34427
34389
  "moltnet.task.type": task.taskType
34428
34390
  },
34429
34391
  sessionPersistence: executionPlan?.sessionPersistence ?? void 0,
34430
- extraExtensionFactories: [...runtimeParentExtensions, ...toolPolicyExtensions]
34392
+ extraExtensionFactories: [
34393
+ ...runtimeParentExtensions,
34394
+ ...toolPolicyExtensions,
34395
+ submitCompletion.extension
34396
+ ]
34431
34397
  });
34432
34398
  } catch (err) {
34433
34399
  const message = err instanceof Error ? err.message : String(err);
@@ -34435,6 +34401,9 @@ async function executePiTask(claimedTask, reporter, opts) {
34435
34401
  message,
34436
34402
  phase: "session_setup"
34437
34403
  });
34404
+ if (reporter.cancelSignal.aborted) return makeCancelledOutput(reporter.cancelReason ?? "Task cancelled during session setup.");
34405
+ if (err instanceof RuntimeProfileModelResolutionError) return makeFailedOutput("invalid_model", message);
34406
+ if (err instanceof GuestExecutableProbeError) return makeFailedOutput(err.code, message, finalUsage, true);
34438
34407
  return makeFailedOutput("session_setup_failed", message);
34439
34408
  }
34440
34409
  const turnState = createSessionTurnState();
@@ -35268,4 +35237,4 @@ function describeToolErrorMessage(result) {
35268
35237
  }
35269
35238
  }
35270
35239
  //#endregion
35271
- export { GONDOLIN_TOOL_NAMES, HOST_EXEC_DEFAULT_BASE_ENV, MOLTNET_TOOL_NAMES, PI_EXECUTOR_MANIFEST_VERSION, PI_RUNTIME_DEFINITION_VERSION, activateAgentEnv, buildAgentSession, buildPiExecutorManifest, buildRuntimeKernel, buildWorkspaceMountInstructions, createGondolinBashOps, createGondolinEditOps, createGondolinFindOps, createGondolinLsOps, createGondolinReadOps, createGondolinToolDefinitions, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiRetryTriage, createPiTaskExecutor, createSubagentTool, createToolPolicyExtension, decideForEvent, decideToolCall, defineGondolinTemplate, definePiExtension, definePiRuntime, definePiTool, enabledPiToolNames, ensureSnapshot, executeGondolinGrep, executePiTask, filterModelVisibleTools, findMainWorktree, injectRuntimeContext as injectTaskContext, isKernelTool, isToolVisible, loadCredentials, materializePiExtensions, materializePiTools, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveSessionToolPolicy, resolveTaskWorktreePath, resumeVm, toGuestPath };
35240
+ export { GONDOLIN_TOOL_NAMES, HOST_EXEC_DEFAULT_BASE_ENV, MOLTNET_TOOL_NAMES, PI_EXECUTOR_MANIFEST_VERSION, PI_RUNTIME_DEFINITION_VERSION, activateAgentEnv, buildAgentSession, buildPiExecutorManifest, buildRuntimeKernel, buildWorkspaceMountInstructions, createGondolinBashOps, createGondolinEditOps, createGondolinFindOps, createGondolinLsOps, createGondolinReadOps, createGondolinToolDefinitions, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiRetryTriage, createPiTaskExecutor, createSubagentTool, createToolPolicyExtension, decideForEvent, decideToolCall, defineGondolinTemplate, definePiExtension, definePiRuntime, definePiTool, enabledPiToolNames, ensureSnapshot, executeGondolinGrep, executePiTask, filterModelVisibleTools, findMainWorktree, injectRuntimeContext as injectTaskContext, isKernelTool, isResolvedPathInsideRoot, isToolVisible, loadCredentials, materializePiExtensions, materializePiTools, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveSessionToolPolicy, resolveTaskWorktreePath, resumeVm, toGuestPath };