@themoltnet/pi-runtime 0.3.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 +57 -20
  2. package/dist/index.js +514 -526
  3. package/package.json +7 -7
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"]
@@ -10046,7 +10091,11 @@ var ProblemDetailsSchema = _Object_({
10046
10091
  }),
10047
10092
  code: ProblemCodeSchema,
10048
10093
  detail: Optional(String$1()),
10049
- instance: Optional(String$1())
10094
+ instance: Optional(String$1()),
10095
+ retryAfter: Optional(Integer({
10096
+ minimum: 0,
10097
+ description: "Non-negative delay in seconds before retrying, matching the Retry-After response header when present."
10098
+ }))
10050
10099
  }, {
10051
10100
  $id: "ProblemDetails",
10052
10101
  additionalProperties: true
@@ -10892,7 +10941,7 @@ var VerificationResult = _Object_({
10892
10941
  var VerificationRecord = _Object_({
10893
10942
  inputCid: String$1({ minLength: 1 }),
10894
10943
  results: _Array_(VerificationResult),
10895
- 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\"." })
10896
10945
  }, {
10897
10946
  $id: "VerificationRecord",
10898
10947
  additionalProperties: false
@@ -14586,6 +14635,16 @@ function checkVerificationInputCid(value, runtime) {
14586
14635
  }];
14587
14636
  return [];
14588
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
+ }
14589
14648
  function validateTaskResult(taskType, value, input, runtime, submission = false) {
14590
14649
  const entry = getTaskTypeEntry(taskType);
14591
14650
  if (!entry) return [{
@@ -14601,7 +14660,7 @@ function validateTaskResult(taskType, value, input, runtime, submission = false)
14601
14660
  message: validationError
14602
14661
  }];
14603
14662
  }
14604
- return checkVerificationInputCid(value, runtime);
14663
+ return [...checkVerificationInputCid(value, runtime), ...checkVerificationPassedConsistency(value)];
14605
14664
  }
14606
14665
  function validateTaskOutput(taskType, output, input, runtime) {
14607
14666
  return validateTaskResult(taskType, output, input, runtime);
@@ -14991,6 +15050,10 @@ var TaskAttempt = _Object_({
14991
15050
  taskId: Uuid,
14992
15051
  attemptN: Number$1({ minimum: 1 }),
14993
15052
  claimedByAgentId: Uuid,
15053
+ leaseId: Union([Uuid, Null()]),
15054
+ runtimeProfileId: Union([Uuid, Null()]),
15055
+ runtimeProfileRevision: Union([Integer({ minimum: 1 }), Null()]),
15056
+ policySnapshotHash: Union([String$1({ pattern: "^sha256:[0-9a-f]{64}$" }), Null()]),
14994
15057
  runtimeId: Union([Uuid, Null()]),
14995
15058
  claimedAt: IsoTimestamp,
14996
15059
  startedAt: Union([IsoTimestamp, Null()]),
@@ -15147,69 +15210,6 @@ function assembleTaskPrompt(taskType, sections) {
15147
15210
  };
15148
15211
  }
15149
15212
  //#endregion
15150
- //#region ../agent-runtime/src/prompts/final-output.ts
15151
- function buildFinalOutputBlock(opts) {
15152
- const { taskType, outputSchemaName, shapeSketch, extraNotes } = opts;
15153
- const submitTool = submitOutputToolName(taskType);
15154
- const lines = [
15155
- "## Final output (read this carefully)",
15156
- "",
15157
- `Your VERY LAST action in this conversation MUST report the structured`,
15158
- `output matching \`${outputSchemaName}\`.`,
15159
- "",
15160
- `Call \`${submitTool}\` exactly once with the payload.`,
15161
- `The runtime captures the validated arguments for attempt completion.`,
15162
- `Do NOT emit the output as plain assistant text. Do NOT rely on a`,
15163
- `JSON-in-message fallback. If you do not call \`${submitTool}\`, the`,
15164
- `attempt is recorded as failing the promised submit-output criterion`,
15165
- `even if the underlying work succeeded.`,
15166
- "",
15167
- `Your final assistant text before that tool call may explain your work,`,
15168
- `but the submit-tool call itself must be your VERY LAST action.`,
15169
- "",
15170
- `Task artifacts: when you produce large files, binary files, logs, reports,`,
15171
- `screenshots, traces, bundles, or datasets, save them in the task workspace`,
15172
- `and call \`moltnet_upload_task_artifact\` before the submit-output tool.`,
15173
- `Put the returned artifact CID in the structured output where the schema`,
15174
- `allows artifact metadata (for example \`artifacts[].cid\`). Do not paste`,
15175
- `large bytes into structured output.`,
15176
- "",
15177
- `Referenced inputs: if this task depends on prior task artifacts, call`,
15178
- `\`moltnet_list_task_artifacts\` for the referenced task and download the`,
15179
- `specific CID you need with \`moltnet_download_task_artifact\` before judging`,
15180
- `or continuing that work.`,
15181
- `For a bound input artifact, omit \`attemptN\` because it has no producing`,
15182
- `attempt. Pass \`attemptN\` only for an artifact from one exact task attempt.`,
15183
- "",
15184
- `Output shape:`,
15185
- "",
15186
- "```json",
15187
- shapeSketch,
15188
- "```"
15189
- ];
15190
- if (extraNotes?.length) {
15191
- lines.push("");
15192
- for (const note of extraNotes) lines.push(note);
15193
- }
15194
- return lines.join("\n");
15195
- }
15196
- //#endregion
15197
- //#region ../agent-runtime/src/prompts/proactive-memory.ts
15198
- function buildProactiveMemoryWorkflowBlock() {
15199
- return [
15200
- "Before material work, apply the runtime instructor's proactive memory",
15201
- "rules instead of waiting for a human prompt. Start with constrained",
15202
- "diary context: inspect tags/list entries when task provenance or scope",
15203
- "tags are known, then use `moltnet_search_entries` with `taskFilter`,",
15204
- "`entryTypes`, and tags. Do not run broad unfiltered searches before",
15205
- "constrained searches miss.",
15206
- "",
15207
- "For incident capture, follow the runtime instructor exactly: search",
15208
- "for similar episodic/semantic entries first, reference close matches,",
15209
- "and create a recurrence entry only when the repeat is useful signal."
15210
- ].join("\n");
15211
- }
15212
- //#endregion
15213
15213
  //#region ../agent-runtime/src/prompts/rubric-common.ts
15214
15214
  function renderRubricCriteriaList(rubric) {
15215
15215
  return rubric.criteria.map((c, i) => `${i + 1}. **${c.id}** (weight ${c.weight}, scoring: \`${c.scoring}\`) — ${c.description}`).join("\n");
@@ -15289,9 +15289,7 @@ function buildAssessBriefUserPrompt(input, ctx) {
15289
15289
  const scoring = [
15290
15290
  "- `llm_score`: score 0..1 continuous. `rationale` REQUIRED (2–4 sentences).",
15291
15291
  "- `boolean`: score exactly 0 or 1. `rationale` optional.",
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`.",
15293
- "",
15294
- "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`."
15295
15293
  ].join("\n");
15296
15294
  return assembleTaskPrompt("assess_brief", [
15297
15295
  {
@@ -15311,12 +15309,6 @@ function buildAssessBriefUserPrompt(input, ctx) {
15311
15309
  header: "Querying the producer's diary entries",
15312
15310
  body: diaryQuery
15313
15311
  },
15314
- {
15315
- id: "assess_brief.proactive_memory",
15316
- source: "discipline",
15317
- header: "Proactive memory use",
15318
- body: buildProactiveMemoryWorkflowBlock()
15319
- },
15320
15312
  {
15321
15313
  id: "assess_brief.workspace",
15322
15314
  source: "workspace",
@@ -15339,88 +15331,10 @@ function buildAssessBriefUserPrompt(input, ctx) {
15339
15331
  source: "rubric_judge",
15340
15332
  header: "Scoring rules",
15341
15333
  body: scoring
15342
- },
15343
- {
15344
- id: "assess_brief.final_output",
15345
- source: "final_output",
15346
- body: buildFinalOutputBlock({
15347
- taskType: "assess_brief",
15348
- outputSchemaName: "AssessBriefOutput",
15349
- shapeSketch: [
15350
- "{",
15351
- " \"scores\": [",
15352
- " { \"criterionId\": \"...\", \"score\": 0.0, \"rationale\": \"...\", \"evidence\": {} }",
15353
- " ],",
15354
- " \"composite\": <sum>,",
15355
- " \"verdict\": \"<1-3 sentence overall>\",",
15356
- " \"judgeModel\": \"<provider:model>\"",
15357
- "}"
15358
- ].join("\n"),
15359
- extraNotes: ["`composite` = Σ(weight_i × score_i) recomputed. The runtime rejects a mismatch."]
15360
- })
15361
15334
  }
15362
15335
  ]);
15363
15336
  }
15364
15337
  //#endregion
15365
- //#region ../agent-runtime/src/prompts/self-verification.ts
15366
- function buildSelfVerificationBlock(taskId, criteriaField = "successCriteria") {
15367
- return [
15368
- "## Self-verification",
15369
- "",
15370
- `If \`input.${criteriaField}\` is set on this task, your final output MUST`,
15371
- "include a `verification` block. Treat every item in those criteria as",
15372
- "part of the promise you made when you claimed the task. That includes",
15373
- "the built-in submit-output gate when present. Do not call the submit",
15374
- "tool until you have computed the verification payload you can honestly",
15375
- "stand behind.",
15376
- "",
15377
- `Call \`moltnet_get_task\` with task id \`${taskId}\` and read \`input.${criteriaField}\`.`,
15378
- "",
15379
- `- If \`input.${criteriaField}\` is **absent**, omit \`verification\` from your`,
15380
- " final output entirely.",
15381
- `- If \`input.${criteriaField}\` is **present**, evaluate every applicable`,
15382
- " item — `gates`, `assertions`, `rubric` criteria, `sideEffects` — against",
15383
- " your produced work and emit one result per id. Be honest: a `fail` with",
15384
- " a one-line reason is more useful than a false `pass`. Use `skip` (with a",
15385
- " `detail`) when you genuinely could not determine a result. Compute",
15386
- " `passed = results.every(r => r.status !== 'fail')`.",
15387
- "- `verification` MUST be a JSON object. Never send a string, markdown",
15388
- " block, null, or an empty placeholder. The submit tool expects an object",
15389
- " with `inputCid`, `results`, and `passed` fields.",
15390
- "",
15391
- "Verification shape:",
15392
- "",
15393
- "```json",
15394
- "{",
15395
- " \"inputCid\": \"<the inputCid you saw on the task>\",",
15396
- " \"results\": [",
15397
- " { \"id\": \"<criterion id>\", \"kind\": \"assertion|gate|rubric|sideEffect\",",
15398
- " \"status\": \"pass|fail|skip\", \"detail\": \"<optional one-liner>\" }",
15399
- " ],",
15400
- " \"passed\": <boolean>",
15401
- "}",
15402
- "```",
15403
- "",
15404
- "Minimal valid example:",
15405
- "",
15406
- "```json",
15407
- "{",
15408
- " \"inputCid\": \"<task inputCid>\",",
15409
- " \"results\": [",
15410
- " {",
15411
- " \"id\": \"<criterion id>\",",
15412
- " \"kind\": \"rubric\",",
15413
- " \"status\": \"pass\",",
15414
- " \"detail\": \"one-line reason\"",
15415
- " }",
15416
- " ],",
15417
- " \"passed\": true",
15418
- "}",
15419
- "```",
15420
- ""
15421
- ].join("\n");
15422
- }
15423
- //#endregion
15424
15338
  //#region ../agent-runtime/src/prompts/curate-pack.ts
15425
15339
  /**
15426
15340
  * Build the first user-message prompt for a `curate_pack` task.
@@ -15578,34 +15492,6 @@ function buildCuratePackUserPrompt(input, ctx) {
15578
15492
  source: "static",
15579
15493
  header: "Hard constraints",
15580
15494
  body: hardConstraints
15581
- },
15582
- {
15583
- id: "curate_pack.verification",
15584
- source: "verification",
15585
- body: buildSelfVerificationBlock(ctx.taskId)
15586
- },
15587
- {
15588
- id: "curate_pack.final_output",
15589
- source: "final_output",
15590
- body: buildFinalOutputBlock({
15591
- taskType: "curate_pack",
15592
- outputSchemaName: "CuratePackOutput",
15593
- shapeSketch: [
15594
- "{",
15595
- " \"packId\": \"<uuid>\",",
15596
- " \"packCid\": \"<cid>\",",
15597
- " \"entries\": [",
15598
- " { \"entryId\": \"<uuid>\", \"rank\": 1, \"rationale\": \"<why>\" }",
15599
- " ],",
15600
- " \"recipeParams\": { \"recipe\": \"...\", \"prompt\": \"...\", ... },",
15601
- " \"checkpoints\": [",
15602
- " { \"phase\": \"recon\", \"candidateIds\": [...], \"droppedIds\": [...], \"notes\": \"...\" }",
15603
- " ],",
15604
- " \"summary\": \"<2-4 sentences: what you looked for, how you narrowed, what defines the final set>\",",
15605
- " \"verification\": <required iff input.successCriteria; see Self-verification>",
15606
- "}"
15607
- ].join("\n")
15608
- })
15609
15495
  }
15610
15496
  ]);
15611
15497
  }
@@ -15642,16 +15528,13 @@ function buildFreeformUserPrompt(input, ctx) {
15642
15528
  const expectedOutput = input.expectedOutput ?? "";
15643
15529
  const constraints = input.constraints?.length ? input.constraints.map((constraint) => `- ${constraint}`).join("\n") : "";
15644
15530
  const suggestedTaskType = input.suggestedTaskType ? [`The proposer suggested task type \`${input.suggestedTaskType}\`.`, "Use it as a hint, not as a contract."].join("\n") : "";
15645
- const workflow = [
15646
- "1. Clarify the real objective from the brief before acting.",
15647
- "2. Search MoltNet diary memory for prior decisions, incidents, and",
15648
- " recurring traps relevant to the brief.",
15649
- "3. Gather enough context to avoid guessing.",
15650
- "4. Complete the requested work when it is safe and bounded.",
15651
- "5. If the request reveals a recurring task shape, include a",
15652
- " `proposedTaskType` in the final output with a concise rationale.",
15653
- "6. If you changed code on a branch, include that branch in",
15654
- " `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."
15655
15538
  ].join("\n");
15656
15539
  const sections = [
15657
15540
  {
@@ -15684,43 +15567,14 @@ function buildFreeformUserPrompt(input, ctx) {
15684
15567
  body: suggestedTaskType
15685
15568
  },
15686
15569
  {
15687
- id: "freeform.workflow",
15570
+ id: "freeform.outcome_hints",
15688
15571
  source: "static",
15689
- header: "Workflow",
15690
- body: workflow
15691
- },
15692
- {
15693
- id: "freeform.proactive_memory",
15694
- source: "discipline",
15695
- header: "Proactive memory use",
15696
- body: buildProactiveMemoryWorkflowBlock()
15697
- },
15698
- {
15699
- id: "freeform.verification",
15700
- source: "verification",
15701
- body: buildSelfVerificationBlock(ctx.taskId)
15702
- },
15703
- {
15704
- id: "freeform.final_output",
15705
- source: "final_output",
15706
- body: buildFinalOutputBlock({
15707
- taskType: "freeform",
15708
- outputSchemaName: "FreeformOutput",
15709
- shapeSketch: [
15710
- "{",
15711
- " \"summary\": \"<2-5 sentence result>\",",
15712
- " \"branch\": \"<branch name when code changed; omit for prose-only work>\",",
15713
- " \"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>\" }],",
15714
- " \"proposedTaskType\": { \"name\": \"...\", \"rationale\": \"...\", \"inputShape\": {}, \"outputShape\": {} },",
15715
- " \"diaryEntryIds\": [\"...\"],",
15716
- " \"verification\": <required iff input.successCriteria; see Self-verification>",
15717
- "}"
15718
- ].join("\n")
15719
- })
15572
+ header: "Outcome hints",
15573
+ body: outcomeHints
15720
15574
  }
15721
15575
  ];
15722
15576
  const priorContextBody = buildPriorContextSection(ctx.priorContext);
15723
- if (priorContextBody) sections.splice(sections.findIndex((s) => s.id === "freeform.workflow") + 1, 0, {
15577
+ if (priorContextBody) sections.push({
15724
15578
  id: "freeform.prior_context",
15725
15579
  source: "task_input",
15726
15580
  body: priorContextBody
@@ -15737,22 +15591,18 @@ function buildFreeformUserPrompt(input, ctx) {
15737
15591
  * is told to inspect them itself.
15738
15592
  */
15739
15593
  function buildFulfillBriefUserPrompt(input, ctx) {
15740
- const { brief, seedFiles, scopeHint } = input;
15594
+ const { brief, seedFiles } = input;
15741
15595
  const header = [
15742
15596
  "# Fulfill Brief Agent",
15743
15597
  "",
15744
15598
  "You are a software engineering agent working in a sandboxed environment.",
15745
15599
  "Use the current working directory as the task workspace.",
15746
- "The MoltNet runtime instructor (above, in this system prompt) defines the",
15747
- "invariants for this task: identity, gh authentication, diary discipline,",
15748
- "and the accountable-commit shape. Follow it for every commit.",
15749
15600
  "",
15750
15601
  "## Task: Fulfill brief",
15751
15602
  "",
15752
15603
  `Task id: \`${ctx.taskId}\``
15753
15604
  ].join("\n");
15754
15605
  const seedFilesBody = seedFiles?.length ? ["Start by reading these files to ground yourself:", ...seedFiles.map((f) => `- \`${f}\``)].join("\n") : "";
15755
- const branchSlug = ctx.correlationId ? `moltnet/${ctx.correlationId}/` : scopeHint ? `feat/${scopeHint}-` : "feat/";
15756
15606
  const correlation = ctx.correlationId ? [
15757
15607
  `This task carries correlationId \`${ctx.correlationId}\`. You MUST:`,
15758
15608
  "",
@@ -15769,23 +15619,6 @@ function buildFulfillBriefUserPrompt(input, ctx) {
15769
15619
  "for this task. Do not repurpose or switch the primary checkout.",
15770
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."
15771
15621
  ].join("\n") : "";
15772
- const workflow = [
15773
- 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>\`).`,
15774
- "2. Search MoltNet diary memory for prior decisions, incidents, and",
15775
- " recurring traps relevant to the brief before changing code.",
15776
- "3. Understand the problem — read relevant code; do not speculate.",
15777
- "4. Implement the change. Keep commits small and coherent.",
15778
- "5. Add tests if applicable.",
15779
- "6. For every commit, create a signed diary entry first via",
15780
- " `moltnet_create_entry` and embed its id in the commit trailer",
15781
- " `MoltNet-Diary: <id>` (per the runtime instructor).",
15782
- "7. Push the branch and open a PR — run `git push` and `gh pr create`",
15783
- " IN the VM with your normal `bash` tool (use the",
15784
- " `GH_TOKEN=$(moltnet github token …) gh …` form from the runtime",
15785
- " instructor for writes; read-only `gh` commands may run bare). Do NOT",
15786
- " use `moltnet_host_exec` for this; it needs human",
15787
- " approval that is unavailable in a headless run."
15788
- ].join("\n");
15789
15622
  return assembleTaskPrompt("fulfill_brief", [
15790
15623
  {
15791
15624
  id: "fulfill_brief.header",
@@ -15815,41 +15648,6 @@ function buildFulfillBriefUserPrompt(input, ctx) {
15815
15648
  source: "workspace",
15816
15649
  header: "Workspace",
15817
15650
  body: workspace
15818
- },
15819
- {
15820
- id: "fulfill_brief.workflow",
15821
- source: "static",
15822
- header: "Workflow",
15823
- body: workflow
15824
- },
15825
- {
15826
- id: "fulfill_brief.proactive_memory",
15827
- source: "discipline",
15828
- header: "Proactive memory use",
15829
- body: buildProactiveMemoryWorkflowBlock()
15830
- },
15831
- {
15832
- id: "fulfill_brief.verification",
15833
- source: "verification",
15834
- body: buildSelfVerificationBlock(ctx.taskId)
15835
- },
15836
- {
15837
- id: "fulfill_brief.final_output",
15838
- source: "final_output",
15839
- body: buildFinalOutputBlock({
15840
- taskType: "fulfill_brief",
15841
- outputSchemaName: "FulfillBriefOutput",
15842
- shapeSketch: [
15843
- "{",
15844
- " \"branch\": \"<branch-name>\",",
15845
- " \"commits\": [{ \"sha\": \"...\", \"message\": \"...\", \"diaryEntryId\": \"...\" }],",
15846
- " \"pullRequestUrl\": \"<url-or-null>\",",
15847
- " \"diaryEntryIds\": [\"...\"],",
15848
- " \"summary\": \"<1-3 sentence recap>\",",
15849
- " \"verification\": <required iff input.successCriteria; see Self-verification>",
15850
- "}"
15851
- ].join("\n")
15852
- })
15853
15651
  }
15854
15652
  ]);
15855
15653
  }
@@ -15926,26 +15724,6 @@ function buildJudgeEvalAttemptUserPrompt(input, ctx) {
15926
15724
  source: "rubric_judge",
15927
15725
  header: "Composite arithmetic",
15928
15726
  body: composite
15929
- },
15930
- {
15931
- id: "judge_eval_attempt.final_output",
15932
- source: "final_output",
15933
- body: buildFinalOutputBlock({
15934
- taskType: "judge_eval_attempt",
15935
- outputSchemaName: "JudgeEvalAttemptOutput",
15936
- shapeSketch: [
15937
- "{",
15938
- ` "targetTaskId": "${input.targetTaskId}",`,
15939
- ` "targetAttemptN": ${input.targetAttemptN},`,
15940
- " \"variantLabel\": \"<from producer input>\",",
15941
- " \"scores\": [ { \"criterionId\": \"...\", \"score\": 0..1, \"rationale\": \"...\", \"assertions\": [...]?, \"evidence\": { \"text\": \"...\" } } ],",
15942
- " \"composite\": <Σ(weight × score), 0..1>,",
15943
- " \"verdict\": \"<1-3 sentences>\",",
15944
- " \"judgeModel\": \"<id>\", // optional",
15945
- " \"traceparent\": \"<from claim>\"",
15946
- "}"
15947
- ].join("\n")
15948
- })
15949
15727
  }
15950
15728
  ]);
15951
15729
  }
@@ -16027,9 +15805,7 @@ function buildJudgePackUserPrompt(input, ctx) {
16027
15805
  "- Do NOT call `moltnet_pack_create` or `moltnet_pack_render`.",
16028
15806
  "- Do NOT fetch the curator's or renderer's task output directly — they",
16029
15807
  " may leak guidance that biases judgment.",
16030
- "- Keep the session focused on scoring; no speculative exploration.",
16031
- "",
16032
- `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."
16033
15809
  ].join("\n");
16034
15810
  return assembleTaskPrompt("judge_pack", [
16035
15811
  {
@@ -16071,37 +15847,6 @@ function buildJudgePackUserPrompt(input, ctx) {
16071
15847
  source: "static",
16072
15848
  header: "Constraints",
16073
15849
  body: constraints
16074
- },
16075
- {
16076
- id: "judge_pack.final_output",
16077
- source: "final_output",
16078
- body: buildFinalOutputBlock({
16079
- taskType: "judge_pack",
16080
- outputSchemaName: "JudgePackOutput",
16081
- shapeSketch: [
16082
- "{",
16083
- " \"scores\": [",
16084
- " { \"criterionId\": \"...\", \"score\": 0.0, \"rationale\": \"...\", \"evidence\": {} },",
16085
- " {",
16086
- " \"criterionId\": \"<llm_checklist criterion>\",",
16087
- " \"score\": 0, // 1 iff every assertion passed",
16088
- " \"assertions\": [",
16089
- " { \"id\": \"claim-1\", \"text\": \"...\", \"passed\": false, \"evidence\": \"...\" }",
16090
- " ]",
16091
- " }",
16092
- " ],",
16093
- " \"composite\": <sum-of-weighted-scores>,",
16094
- " \"verdict\": \"<1-3 sentence overall>\",",
16095
- " \"judgeModel\": \"<provider:model>\",",
16096
- " \"rendererBinaryCid\": \"<cid-string-only-if-available>\"",
16097
- "}"
16098
- ].join("\n"),
16099
- extraNotes: [
16100
- "Omit `rendererBinaryCid` entirely when no binary CID is exposed by",
16101
- "`moltnet_rendered_pack_get`. Do NOT emit `null` — the field is",
16102
- "optional and absence is the correct representation when unavailable."
16103
- ]
16104
- })
16105
15850
  }
16106
15851
  ]);
16107
15852
  }
@@ -16144,20 +15889,6 @@ function buildPrReviewUserPrompt(input, ctx) {
16144
15889
  "(for example publishing the judgment somewhere), perform that action as",
16145
15890
  "part of the task before reporting structured output."
16146
15891
  ].join("\n");
16147
- const workflow = [
16148
- "1. Read the subject summary, resources, inspection hints, and any",
16149
- " task-specific instructions before scoring.",
16150
- "2. Search MoltNet diary memory for prior decisions, incidents, and",
16151
- " recurring review traps relevant to the subject.",
16152
- "3. Inspect the target artefact directly using the tools and resources the",
16153
- " task makes available.",
16154
- "4. If you are in a dedicated disposable worktree and need the review target",
16155
- " checked out locally, do that work inside this disposable workspace only.",
16156
- "5. Apply the rubric strictly. This task is about complexity and",
16157
- " reviewability, not correctness or feature desirability.",
16158
- "6. Perform any required outward action before emitting the final",
16159
- " structured output."
16160
- ].join("\n");
16161
15892
  const taskPromptSection = input.taskPrompt ?? "";
16162
15893
  const preamble = renderRubricPreambleSection(rubric) ?? "";
16163
15894
  const criteria = renderRubricCriteriaList(rubric);
@@ -16166,9 +15897,7 @@ function buildPrReviewUserPrompt(input, ctx) {
16166
15897
  "- Score `1` when the subject clearly clears the criterion.",
16167
15898
  "- Score `0` when it does not, or when the evidence is ambiguous.",
16168
15899
  "- `rationale` is REQUIRED for every score. Keep it concrete and audit-friendly.",
16169
- "- Compute `composite = Σ(weight_i × score_i)` exactly; the runtime rejects mismatches.",
16170
- "",
16171
- "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."
16172
15901
  ].join("\n");
16173
15902
  return assembleTaskPrompt("pr_review", [
16174
15903
  {
@@ -16206,18 +15935,6 @@ function buildPrReviewUserPrompt(input, ctx) {
16206
15935
  header: "Execution contract",
16207
15936
  body: executionContract
16208
15937
  },
16209
- {
16210
- id: "pr_review.workflow",
16211
- source: "static",
16212
- header: "Review workflow",
16213
- body: workflow
16214
- },
16215
- {
16216
- id: "pr_review.proactive_memory",
16217
- source: "discipline",
16218
- header: "Proactive memory use",
16219
- body: buildProactiveMemoryWorkflowBlock()
16220
- },
16221
15938
  {
16222
15939
  id: "pr_review.task_prompt",
16223
15940
  source: "task_input",
@@ -16240,24 +15957,6 @@ function buildPrReviewUserPrompt(input, ctx) {
16240
15957
  source: "rubric_judge",
16241
15958
  header: "Scoring rules",
16242
15959
  body: scoring
16243
- },
16244
- {
16245
- id: "pr_review.final_output",
16246
- source: "final_output",
16247
- body: buildFinalOutputBlock({
16248
- taskType: "pr_review",
16249
- outputSchemaName: "PrReviewOutput",
16250
- shapeSketch: [
16251
- "{",
16252
- " \"scores\": [",
16253
- " { \"criterionId\": \"...\", \"score\": 0, \"rationale\": \"...\" }",
16254
- " ],",
16255
- " \"composite\": <sum-of-weighted-binary-scores>,",
16256
- " \"verdict\": \"<1-3 sentence overall>\"",
16257
- "}"
16258
- ].join("\n"),
16259
- extraNotes: ["`scores` MUST stay in the same order as the rubric criteria.", "`score` MUST be exactly `0` or `1` for every criterion."]
16260
- })
16261
15960
  }
16262
15961
  ]);
16263
15962
  }
@@ -16350,30 +16049,6 @@ function buildRenderPackUserPrompt(input, ctx) {
16350
16049
  source: "static",
16351
16050
  header: "Fidelity Discipline",
16352
16051
  body: fidelity
16353
- },
16354
- {
16355
- id: "render_pack.verification",
16356
- source: "verification",
16357
- body: buildSelfVerificationBlock(ctx.taskId)
16358
- },
16359
- {
16360
- id: "render_pack.final_output",
16361
- source: "final_output",
16362
- body: buildFinalOutputBlock({
16363
- taskType: "render_pack",
16364
- outputSchemaName: "RenderPackOutput",
16365
- shapeSketch: [
16366
- "{",
16367
- " \"renderedPackId\": \"<uuid-or-null>\",",
16368
- " \"renderedCid\": \"<cid>\",",
16369
- " \"renderMethod\": \"<label>\",",
16370
- " \"byteSize\": <int>,",
16371
- " \"entriesRendered\": <int>,",
16372
- " \"summary\": \"<1-3 sentence recap>\",",
16373
- " \"verification\": <required iff input.successCriteria; see Self-verification>",
16374
- "}"
16375
- ].join("\n")
16376
- })
16377
16052
  }
16378
16053
  ]);
16379
16054
  }
@@ -16408,7 +16083,7 @@ function buildRenderPackUserPrompt(input, ctx) {
16408
16083
  * field. Quoting the constraint back is not following the task.
16409
16084
  */
16410
16085
  function buildRunEvalUserPrompt(input, ctx) {
16411
- const { scenario, variantLabel, successCriteria } = input;
16086
+ const { scenario, variantLabel } = input;
16412
16087
  const effectiveRuntimeContext = ctx.effectiveRuntimeContext ?? input.context;
16413
16088
  const hasContext = effectiveRuntimeContext.length > 0;
16414
16089
  const hasInlineContext = effectiveRuntimeContext.some((entry) => entry.binding === "context_inline");
@@ -16429,27 +16104,6 @@ function buildRunEvalUserPrompt(input, ctx) {
16429
16104
  "rules, those rules override your generic instincts."
16430
16105
  ].join("\n") : "";
16431
16106
  const inputFiles = scenario.inputFiles?.length ? scenario.inputFiles.map((f) => `- \`${f}\``).join("\n") : "";
16432
- const verification = successCriteria ? buildSelfVerificationBlock(ctx.taskId) : "";
16433
- const finalOutput = buildFinalOutputBlock({
16434
- taskType: "run_eval",
16435
- outputSchemaName: "RunEvalOutput",
16436
- shapeSketch: [
16437
- "{",
16438
- " \"response\": \"<your free-form answer>\",",
16439
- " \"artifacts\": [{ \"path\": \"...\", \"cid\": \"...\" }], // optional",
16440
- " \"totalTokens\": <int>,",
16441
- " \"durationMs\": <int>,",
16442
- " \"traceparent\": \"<from claim>\",",
16443
- " \"verification\": {",
16444
- " \"inputCid\": \"<task inputCid>\",",
16445
- " \"results\": [",
16446
- " { \"id\": \"<criterion id>\", \"kind\": \"rubric\", \"status\": \"pass|fail|skip\", \"detail\": \"<optional one-liner>\" }",
16447
- " ],",
16448
- " \"passed\": <boolean>",
16449
- " } // required iff input.successCriteria; must be an object, never a string",
16450
- "}"
16451
- ].join("\n")
16452
- });
16453
16107
  return assembleTaskPrompt("run_eval", [
16454
16108
  {
16455
16109
  id: "run_eval.header",
@@ -16473,16 +16127,6 @@ function buildRunEvalUserPrompt(input, ctx) {
16473
16127
  source: "task_input",
16474
16128
  header: "Input files",
16475
16129
  body: inputFiles
16476
- },
16477
- {
16478
- id: "run_eval.verification",
16479
- source: "verification",
16480
- body: verification
16481
- },
16482
- {
16483
- id: "run_eval.final_output",
16484
- source: "final_output",
16485
- body: finalOutput
16486
16130
  }
16487
16131
  ]);
16488
16132
  }
@@ -16497,14 +16141,17 @@ function submissionAcceptsVerification(taskType) {
16497
16141
  /**
16498
16142
  * Add only the dynamic contract facts that a producer cannot infer from its
16499
16143
  * task-specific prompt: the declared success criteria and the immutable input
16500
- * CID its verification must cite. This is deliberately not a workflow block;
16501
- * 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.
16502
16148
  */
16503
16149
  function appendTaskContractFacts(prompt, task) {
16504
16150
  if (!hasSuccessCriteria(task.input) || !submissionAcceptsVerification(task.taskType)) return prompt;
16505
16151
  const criteriaJson = JSON.stringify(task.input.successCriteria, null, 2);
16506
16152
  const body = [
16507
- `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}\``,
16508
16155
  "",
16509
16156
  "These typed criteria are task facts. Assess the completed work against",
16510
16157
  "them before calling the submit-output tool. Its `verification` payload",
@@ -31076,6 +30723,12 @@ var ShellCommandAnalyzer = class ShellCommandAnalyzer {
31076
30723
  }
31077
30724
  };
31078
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
31079
30732
  //#region src/snapshot.ts
31080
30733
  /**
31081
30734
  * Snapshot builder with auto-build and caching.
@@ -31456,15 +31109,19 @@ function filterModelVisibleTools(tools, policy) {
31456
31109
  }
31457
31110
  function enabledPiToolNames(input) {
31458
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) {
31459
31116
  return [...new Set([...input.tools.map((tool) => tool.name), ...(input.extensions ?? []).flatMap((extension) => extension.declaredTools.filter((name) => isToolVisible(name, input.policy)))])].sort();
31460
31117
  }
31461
31118
  function isToolVisible(name, policy) {
31462
31119
  if (!policy || policy.enforcement !== "enforce") return true;
31463
- if (name === "bash") return true;
31120
+ if (name === "bash") return policy.allowedShellCommands === void 0 || policy.allowedShellCommands.length > 0;
31464
31121
  return policy.allowedTools.has(name);
31465
31122
  }
31466
31123
  function isKernelTool(name) {
31467
- return name.startsWith("submit_");
31124
+ return name.startsWith("submit_") || name === "subagent";
31468
31125
  }
31469
31126
  function wrapExtensionFactory(factory, contribution, policy) {
31470
31127
  return (pi) => {
@@ -31482,7 +31139,7 @@ function wrapExtensionFactory(factory, contribution, policy) {
31482
31139
  };
31483
31140
  }
31484
31141
  function claimToolName(names, name, owner) {
31485
- 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`);
31486
31143
  const previous = names.get(name);
31487
31144
  if (previous) throw new Error(`Pi tool name "${name}" is declared by ${previous} and ${owner}`);
31488
31145
  names.set(name, owner);
@@ -31576,6 +31233,17 @@ async function delay(ms, signal, label) {
31576
31233
  * investigation and the alternatives we rejected.
31577
31234
  */
31578
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
+ }
31579
31247
  function shouldRunResumeCommand(entry, ctx) {
31580
31248
  if (typeof entry === "string") return true;
31581
31249
  const workspaceModes = entry.when?.workspaceMode;
@@ -31648,7 +31316,7 @@ function resolveVmAgentDir(config) {
31648
31316
  function loadCredentials(agentDir) {
31649
31317
  const moltnetJson = readFileSync(path.join(agentDir, "moltnet.json"), "utf8");
31650
31318
  const agentEnvRaw = readFileSync(path.join(agentDir, "env"), "utf8");
31651
- const piAgentDir = process.env.PI_CODING_AGENT_DIR ?? path.join(process.env.HOME ?? "", ".pi", "agent");
31319
+ const piAgentDir = resolvePiCodingAgentDir();
31652
31320
  const piAuthPath = path.join(piAgentDir, "auth.json");
31653
31321
  const piAuthJson = existsSync(piAuthPath) ? readFileSync(piAuthPath, "utf8") : null;
31654
31322
  const gitconfigPath = path.join(agentDir, "gitconfig");
@@ -31803,7 +31471,7 @@ async function resumeVm(config) {
31803
31471
  else vmAgentEnv[k] = v;
31804
31472
  }
31805
31473
  vmAgentEnv.MOLTNET_CREDENTIALS_PATH = `${vmAgentDir}/moltnet.json`;
31806
- const vfsConfig = config.sandboxConfig?.vfs;
31474
+ const vfsConfig = resolveVfsShadowConfig(config.sandboxConfig);
31807
31475
  let workspaceProvider = new RealFSProvider(config.mountPath);
31808
31476
  workspaceProvider = new ShadowProvider(workspaceProvider, {
31809
31477
  shouldShadow: ({ path: shadowPath }) => shouldShadowNodeModulesPath(shadowPath),
@@ -31811,11 +31479,11 @@ async function resumeVm(config) {
31811
31479
  tmpfs: new AutoParentMemoryProvider(),
31812
31480
  writeMode: "tmpfs"
31813
31481
  });
31814
- if (vfsConfig?.shadow?.length) {
31815
- const predicate = createShadowPathPredicate(vfsConfig.shadow);
31482
+ if (vfsConfig.mode !== "none") {
31483
+ const predicate = createShadowPathPredicate(vfsConfig.patterns);
31816
31484
  workspaceProvider = new ShadowProvider(workspaceProvider, {
31817
31485
  shouldShadow: predicate,
31818
- writeMode: vfsConfig.shadowMode ?? "tmpfs"
31486
+ writeMode: vfsConfig.mode
31819
31487
  });
31820
31488
  }
31821
31489
  const forwardedEnv = {};
@@ -32275,7 +31943,7 @@ async function executeGondolinGrep(vm, localCwd, guestWorkspace, params, signal)
32275
31943
  const onAbort = () => ac.abort();
32276
31944
  signal?.addEventListener("abort", onAbort, { once: true });
32277
31945
  try {
32278
- const proc = vm.exec(["/bin/rg", ...args], {
31946
+ const proc = vm.exec(["rg", ...args], {
32279
31947
  signal: ac.signal,
32280
31948
  stdout: "pipe",
32281
31949
  stderr: "pipe"
@@ -32428,6 +32096,7 @@ function createGondolinBashOps(vm, localCwd, guestWorkspace) {
32428
32096
  */
32429
32097
  function decideToolCall(input) {
32430
32098
  if (input.enforcement === "off") return { allow: true };
32099
+ if (input.toolName.startsWith("submit_") || input.toolName === "subagent") return { allow: true };
32431
32100
  const resolved = resolveNames(input);
32432
32101
  if (resolved.kind === "unresolvable") return fenced(input.enforcement, "shell command could not be statically authorized", "unresolvable shell command (watch)");
32433
32102
  const arbitraryCode = [...new Set(resolved.tools.filter((tool) => tool.risk === "arbitrary-code").map((tool) => tool.name))];
@@ -32559,6 +32228,10 @@ async function resolveSessionToolPolicy(input) {
32559
32228
  try {
32560
32229
  const resolved = await withTimeout$1(input.agent.runtimeProfiles.allowedTools(input.profileId, { teamId: input.teamId }), timeoutMs);
32561
32230
  const shellCommands = resolved.allowedShellCommands ?? [];
32231
+ if (resolved.runtimeKind !== input.runtimeKind) throw new RuntimeKindMismatchError({
32232
+ expectedRuntimeKind: input.runtimeKind,
32233
+ receivedRuntimeKind: resolved.runtimeKind
32234
+ });
32562
32235
  return {
32563
32236
  enforcement: resolved.enforcement,
32564
32237
  allowedTools: new Set(resolved.allowedTools),
@@ -32592,6 +32265,12 @@ var ToolPolicyResolveTimeoutError = class extends Error {
32592
32265
  this.name = "ToolPolicyResolveTimeoutError";
32593
32266
  }
32594
32267
  };
32268
+ var RuntimeKindMismatchError = class extends Error {
32269
+ constructor(input) {
32270
+ super(`runtime kind mismatch: expected ${input.expectedRuntimeKind}, received ${input.receivedRuntimeKind}`);
32271
+ this.name = "RuntimeKindMismatchError";
32272
+ }
32273
+ };
32595
32274
  /**
32596
32275
  * Reject with {@link ToolPolicyResolveTimeoutError} if `promise` does not settle
32597
32276
  * within `timeoutMs`. A non-positive `timeoutMs` disables the deadline. The
@@ -32669,6 +32348,105 @@ function createToolPolicyExtension(deps) {
32669
32348
  };
32670
32349
  }
32671
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
32672
32450
  //#region src/runtime/resolve-prior-context.ts
32673
32451
  /**
32674
32452
  * Fetch the named attempt's output and project it into the prompt's
@@ -32855,6 +32633,40 @@ async function withTimeout(promise, timeoutMs, onTimeout) {
32855
32633
  }
32856
32634
  }
32857
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
32858
32670
  //#region src/runtime/runtime-context.ts
32859
32671
  /**
32860
32672
  * Pi-specific runtime context handling.
@@ -32979,6 +32791,73 @@ function buildWorkspaceMountInstructions(guestWorkspace) {
32979
32791
  " non-existent checkout."
32980
32792
  ].join("\n");
32981
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
+ }
32982
32861
  /**
32983
32862
  * Build the minimal immutable system-prompt kernel. Runtime-profile context
32984
32863
  * carries operator-selected workflow guidance; this kernel stays last in the
@@ -33001,31 +32880,7 @@ function buildRuntimeKernel(ctx) {
33001
32880
  `- Diary id (for this task): \`${ctx.diaryId}\``,
33002
32881
  `- Agent name: \`${ctx.agentName}\``,
33003
32882
  "",
33004
- "## Identity & credentials",
33005
- "",
33006
- "- Your credentials live at `/home/agent/.moltnet/<agent>/moltnet.json`",
33007
- " with the gitconfig and SSH key alongside. Do not move, copy, or expose",
33008
- " these files outside the VM.",
33009
- "- The `moltnet` CLI is installed in the VM and is the only supported way",
33010
- " to mint short-lived tokens. Do not invoke `npx @themoltnet/cli` or any",
33011
- " cached path — use the `moltnet` binary on `PATH`.",
33012
- "- Interactive sessions use the canonical `moltnet github guard` policy,",
33013
- " documented in `docs/reference/agent-configuration.md`. This headless VM",
33014
- " has no editor hook and no human GitHub token to fall back to: read-only",
33015
- " `gh` commands may run bare, but every write must use the App token:",
33016
- "",
33017
- " ```bash",
33018
- " CREDS=\"$(cd \"$(dirname \"$GIT_CONFIG_GLOBAL\")\" && pwd)/moltnet.json\"",
33019
- " GH_TOKEN=$(moltnet github token --credentials \"$CREDS\") gh <command>",
33020
- " ```",
33021
- "",
33022
- "- `git push` uses the gitconfig-configured credential helper and is not",
33023
- " a `gh` call — it does not need `GH_TOKEN`.",
33024
- "- Run `git` and `gh` in the VM with your normal `bash` tool — your",
33025
- " credentials are injected here, so they work in the guest. The",
33026
- " `moltnet_host_exec` tool is a last-resort host escape-hatch that",
33027
- " requires human approval and is unavailable in headless task runs;",
33028
- " never use it for routine git/gh.",
32883
+ buildCredentialInstructions(ctx.toolPolicy, ctx.sandbox),
33029
32884
  "",
33030
32885
  "## Skill packs",
33031
32886
  "",
@@ -33038,6 +32893,10 @@ function buildRuntimeKernel(ctx) {
33038
32893
  "",
33039
32894
  buildWorkspaceMountInstructions(ctx.guestWorkspace),
33040
32895
  "",
32896
+ buildSandboxCapabilityInstructions(ctx.sandbox, ctx.toolPolicy),
32897
+ ...ctx.sandbox ? [""] : [],
32898
+ buildToolPolicyInstructions(ctx.toolPolicy),
32899
+ "",
33041
32900
  "## Structured completion",
33042
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."
33043
32902
  ].join("\n");
@@ -33149,6 +33008,7 @@ function createSubagentTool(args) {
33149
33008
  cwdPath: args.cwdPath ?? args.mountPath,
33150
33009
  piAuthDir: args.piAuthDir,
33151
33010
  modelHandle: args.modelHandle,
33011
+ modelRegistry: args.modelRegistry,
33152
33012
  thinkingLevel: args.thinkingLevel,
33153
33013
  temperature: args.temperature,
33154
33014
  topP: args.topP,
@@ -33276,6 +33136,39 @@ function toolError(text, details = { captured: false }) {
33276
33136
  };
33277
33137
  }
33278
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
33279
33172
  //#region src/runtime/task-output.ts
33280
33173
  var METER_NAME = "@themoltnet/pi-extension/task-output";
33281
33174
  var parseResultCounter = null;
@@ -33445,7 +33338,8 @@ function submitOutputRepairHint(taskType, errors) {
33445
33338
  const hints = ["Tool args must be the output object directly, not wrapped in { output: ... }."];
33446
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\": \"...\" }.");
33447
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.");
33448
- 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 }.`);
33449
33343
  if (hints.length === 1) hints.push("Fix every listed field before re-calling this same tool.");
33450
33344
  return hints.join(" ");
33451
33345
  }
@@ -33463,7 +33357,18 @@ function onlySubmitOutputGate(input) {
33463
33357
  if (Array.isArray(assertions) && assertions.length > 0) return false;
33464
33358
  return criteria.rubric === void 0 && criteria.sideEffects === void 0 && criteria.minComposite === void 0;
33465
33359
  }
33466
- 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) {
33467
33372
  if (!isRecord(params) || !opts.inputCid || !onlySubmitOutputGate(opts.input)) return null;
33468
33373
  const repaired = { ...params };
33469
33374
  if ("artifacts" in repaired && !Array.isArray(repaired.artifacts)) if (isRecord(repaired.artifacts)) repaired.artifacts = [repaired.artifacts];
@@ -33479,15 +33384,14 @@ function repairFreeformSubmitOutput(params, opts) {
33479
33384
  id: SUBMIT_OUTPUT_GATE_ID,
33480
33385
  kind: "gate",
33481
33386
  status: "pass",
33482
- detail: "submit_freeform_output accepted valid args"
33387
+ detail: `submit_${taskType}_output accepted valid args`
33483
33388
  }],
33484
33389
  passed: true
33485
33390
  };
33486
33391
  return repaired;
33487
33392
  }
33488
33393
  function maybeRepairSubmitOutput(taskType, params, opts) {
33489
- if (taskType !== "freeform") return null;
33490
- const repaired = repairFreeformSubmitOutput(params, opts);
33394
+ const repaired = repairProducerSubmitOutput(taskType, params, opts);
33491
33395
  if (!repaired) return null;
33492
33396
  return validateTaskSubmission(taskType, repaired, opts.input, { inputCid: opts.inputCid }).length === 0 ? repaired : null;
33493
33397
  }
@@ -33511,10 +33415,24 @@ Agent submission schema:\n\`\`\`json\n${contract.parametersSchemaJson}\n\`\`\``,
33511
33415
  promptGuidelines: [
33512
33416
  `Call \`${contract.toolName}\` with the exact ${taskType} agent submission shape shown above.`,
33513
33417
  "The transport accepts malformed objects only so validation errors can be recovered in-session; the schema shown above is authoritative.",
33514
- "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."
33515
33420
  ],
33516
33421
  parameters: RecoverableSubmitToolParameters,
33517
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
+ };
33518
33436
  if (exhaustedValidationFailure) return {
33519
33437
  content: [{
33520
33438
  type: "text",
@@ -33565,6 +33483,7 @@ Agent submission schema:\n\`\`\`json\n${contract.parametersSchemaJson}\n\`\`\``,
33565
33483
  }
33566
33484
  captured = candidateParams;
33567
33485
  callCount += 1;
33486
+ await opts.onValidCapture?.();
33568
33487
  return {
33569
33488
  content: [{
33570
33489
  type: "text",
@@ -34005,7 +33924,7 @@ async function executePiTask(claimedTask, reporter, opts) {
34005
33924
  let subagentHandle = null;
34006
33925
  const finalUsage = emptyUsage(opts.provider, opts.model);
34007
33926
  let cancelListener = null;
34008
- const makeFailedOutput = (code, message, usage = finalUsage) => ({
33927
+ const makeFailedOutput = (code, message, usage = finalUsage, retryable = false) => ({
34009
33928
  taskId: task.id,
34010
33929
  attemptN,
34011
33930
  status: "failed",
@@ -34016,7 +33935,7 @@ async function executePiTask(claimedTask, reporter, opts) {
34016
33935
  error: {
34017
33936
  code,
34018
33937
  message,
34019
- retryable: false
33938
+ retryable
34020
33939
  }
34021
33940
  });
34022
33941
  const makeCancelledOutput = (message) => ({
@@ -34069,6 +33988,7 @@ async function executePiTask(claimedTask, reporter, opts) {
34069
33988
  reporterOpen = true;
34070
33989
  let checkpointPath;
34071
33990
  let resolvedVmTemplate = opts.resolvedVmTemplate;
33991
+ let effectiveSandboxConfig;
34072
33992
  try {
34073
33993
  if (!resolvedVmTemplate && opts.runtimeDefinition) resolvedVmTemplate = opts.resolveVmTemplate ? await opts.resolveVmTemplate() : await opts.runtimeDefinition.vm.resolve({ onProgress: opts.onSnapshotProgress });
34074
33994
  checkpointPath = resolvedVmTemplate?.checkpointPath ?? opts.checkpointPath ?? (opts.resolveCheckpointPath ? await opts.resolveCheckpointPath() : await ensureSnapshot({
@@ -34092,7 +34012,7 @@ async function executePiTask(claimedTask, reporter, opts) {
34092
34012
  return makeFailedOutput("worktree_setup_failed", message);
34093
34013
  }
34094
34014
  try {
34095
- const sandboxConfig = applyExecutionPlanSandboxOverrides(resolvedVmTemplate ? {
34015
+ effectiveSandboxConfig = applyExecutionPlanSandboxOverrides(resolvedVmTemplate ? {
34096
34016
  ...opts.sandboxConfig,
34097
34017
  snapshot: void 0,
34098
34018
  resumeCommands: [...resolvedVmTemplate.resumeCommands]
@@ -34104,7 +34024,7 @@ async function executePiTask(claimedTask, reporter, opts) {
34104
34024
  mountPath,
34105
34025
  workspaceMode: workspace.mode,
34106
34026
  extraAllowedHosts: opts.extraAllowedHosts,
34107
- sandboxConfig,
34027
+ sandboxConfig: effectiveSandboxConfig,
34108
34028
  forwardEnv: opts.forwardEnv,
34109
34029
  signal: reporter.cancelSignal
34110
34030
  });
@@ -34234,11 +34154,20 @@ async function executePiTask(claimedTask, reporter, opts) {
34234
34154
  cwdPath,
34235
34155
  guestWorkspace: managed.guestWorkspace
34236
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
+ });
34237
34163
  const { handle: submitToolHandle, tools: submitToolDefs } = resolveSubmitTools(task.taskType, {
34238
34164
  model: opts.model,
34239
34165
  input: task.input,
34240
34166
  inputCid: task.inputCid,
34241
- maxSubmitValidationRetries: opts.maxSubmitValidationRetries
34167
+ maxSubmitValidationRetries: opts.maxSubmitValidationRetries,
34168
+ onValidCapture: () => {
34169
+ submitCompletion.requestCompletion();
34170
+ }
34242
34171
  });
34243
34172
  const submitTools = submitToolDefs;
34244
34173
  try {
@@ -34266,48 +34195,45 @@ async function executePiTask(claimedTask, reporter, opts) {
34266
34195
  correlationId: task.correlationId ?? null
34267
34196
  })
34268
34197
  });
34269
- const piAuthDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
34270
- const modelHandle = getModel(opts.provider, opts.model);
34271
- const runtimeKernel = buildRuntimeKernel({
34272
- taskId: task.id,
34273
- taskType: task.taskType,
34274
- attemptN,
34275
- diaryId,
34276
- agentName: opts.agentName,
34277
- guestWorkspace: managed.guestWorkspace,
34278
- correlationId: task.correlationId ?? null
34279
- });
34280
- const appendSystemPrompt = composeRuntimeSystemPrompt({
34281
- profilePromptPrefix: injectedContext.systemPromptPrefix,
34282
- kernel: runtimeKernel
34283
- });
34198
+ const piAuthDir = resolvePiCodingAgentDir();
34199
+ const { modelHandle, modelRegistry } = resolveRuntimeProfileModel(piAuthDir, opts.provider, opts.model, opts.runtimeProfileId);
34284
34200
  const injectedSkills = injectedContext.skills;
34285
34201
  const toolPolicyExtensions = [];
34286
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
+ };
34287
34218
  if (opts.runtimeProfileId && opts.toolEnforcement && opts.toolEnforcement !== "off") {
34288
- const toolPolicyLogger = opts.toolPolicyLogger ?? {
34289
- debug: () => {},
34290
- info: (obj, msg) => console.error(JSON.stringify({
34291
- level: "info",
34292
- msg,
34293
- ...obj
34294
- })),
34295
- warn: (obj, msg) => console.error(JSON.stringify({
34296
- level: "warn",
34297
- msg,
34298
- ...obj
34299
- }))
34300
- };
34301
34219
  const [analyzer, policy] = await Promise.all([ShellCommandAnalyzer.create(), resolveSessionToolPolicy({
34302
34220
  agent: moltnetAgent,
34303
34221
  profileId: opts.runtimeProfileId,
34304
34222
  teamId: taskTeamId,
34223
+ runtimeKind: opts.runtimeDefinition?.runtimeKind ?? "gondolin_pi",
34305
34224
  enforcement: opts.toolEnforcement,
34306
34225
  logger: toolPolicyLogger
34307
34226
  })]);
34308
- 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
+ };
34309
34235
  toolPolicyExtensions.push(createToolPolicyExtension({
34310
- policy,
34236
+ policy: resolvedToolPolicy,
34311
34237
  analyzer,
34312
34238
  logger: toolPolicyLogger
34313
34239
  }));
@@ -34345,13 +34271,67 @@ async function executePiTask(claimedTask, reporter, opts) {
34345
34271
  policy: resolvedToolPolicy
34346
34272
  }) : [];
34347
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
+ });
34348
34327
  const parentSubagentTools = [];
34349
- if (taskTypeUsesSubagents(task.taskType)) {
34328
+ if (taskHasSubagents) {
34350
34329
  subagentHandle = createSubagentTool({
34351
34330
  mountPath,
34352
34331
  cwdPath,
34353
34332
  piAuthDir,
34354
34333
  modelHandle,
34334
+ modelRegistry,
34355
34335
  thinkingLevel: opts.thinkingLevel,
34356
34336
  temperature: opts.temperature,
34357
34337
  topP: opts.topP,
@@ -34385,6 +34365,7 @@ async function executePiTask(claimedTask, reporter, opts) {
34385
34365
  cwdPath,
34386
34366
  piAuthDir,
34387
34367
  modelHandle,
34368
+ modelRegistry,
34388
34369
  thinkingLevel: opts.thinkingLevel,
34389
34370
  temperature: opts.temperature,
34390
34371
  topP: opts.topP,
@@ -34408,7 +34389,11 @@ async function executePiTask(claimedTask, reporter, opts) {
34408
34389
  "moltnet.task.type": task.taskType
34409
34390
  },
34410
34391
  sessionPersistence: executionPlan?.sessionPersistence ?? void 0,
34411
- extraExtensionFactories: [...runtimeParentExtensions, ...toolPolicyExtensions]
34392
+ extraExtensionFactories: [
34393
+ ...runtimeParentExtensions,
34394
+ ...toolPolicyExtensions,
34395
+ submitCompletion.extension
34396
+ ]
34412
34397
  });
34413
34398
  } catch (err) {
34414
34399
  const message = err instanceof Error ? err.message : String(err);
@@ -34416,6 +34401,9 @@ async function executePiTask(claimedTask, reporter, opts) {
34416
34401
  message,
34417
34402
  phase: "session_setup"
34418
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);
34419
34407
  return makeFailedOutput("session_setup_failed", message);
34420
34408
  }
34421
34409
  const turnState = createSessionTurnState();
@@ -35249,4 +35237,4 @@ function describeToolErrorMessage(result) {
35249
35237
  }
35250
35238
  }
35251
35239
  //#endregion
35252
- 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 };