@sema-agent/core 7.7.0 → 7.9.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 (186) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/agents/agent-transcript-tool.js +2 -2
  3. package/dist/agents/list-agents-tool.js +2 -3
  4. package/dist/agents/observer.js +2 -2
  5. package/dist/agents/send-message-tool.js +2 -1
  6. package/dist/agents/subagent-steps.js +5 -4
  7. package/dist/agents/subagent.d.ts +1 -1
  8. package/dist/agents/subagent.js +2 -3
  9. package/dist/core/ask-origin.d.ts +12 -1
  10. package/dist/core/ask-origin.js +5 -1
  11. package/dist/core/ask-question.js +2 -2
  12. package/dist/core/checkpoint-store.d.ts +64 -24
  13. package/dist/core/context-edit.d.ts +0 -22
  14. package/dist/core/context-edit.js +2 -11
  15. package/dist/core/engine-notice.d.ts +6 -0
  16. package/dist/core/fs-write-gate-policy.d.ts +7 -1
  17. package/dist/core/fs-write-gate-policy.js +7 -7
  18. package/dist/core/gate-fold.js +2 -2
  19. package/dist/core/gate-lanes.js +53 -26
  20. package/dist/core/gate-outcome.d.ts +8 -4
  21. package/dist/core/gate-outcome.js +2 -1
  22. package/dist/core/governance-codes.d.ts +1 -1
  23. package/dist/core/governance-codes.js +6 -0
  24. package/dist/core/hooks.d.ts +64 -33
  25. package/dist/core/hooks.js +1 -1
  26. package/dist/core/lsp.js +2 -2
  27. package/dist/core/mcp-server-spec.d.ts +17 -0
  28. package/dist/core/mcp.js +3 -1
  29. package/dist/core/memory-engine/tools.js +4 -12
  30. package/dist/core/permission-rule-consent.d.ts +29 -36
  31. package/dist/core/permission-rule-consent.js +110 -60
  32. package/dist/core/permission-rule-model.d.ts +254 -55
  33. package/dist/core/permission-rule-model.js +323 -43
  34. package/dist/core/permission-rule-org.d.ts +9 -8
  35. package/dist/core/permission-rule-org.js +6 -15
  36. package/dist/core/permission-rule-provider.d.ts +10 -6
  37. package/dist/core/permission-rule-provider.js +12 -8
  38. package/dist/core/permission-rule-session.d.ts +7 -6
  39. package/dist/core/permission-rule-session.js +29 -10
  40. package/dist/core/permission-rule-store.d.ts +40 -18
  41. package/dist/core/permission-rule-store.js +68 -42
  42. package/dist/core/permission-rule-sync.d.ts +9 -3
  43. package/dist/core/permission-rule-sync.js +29 -25
  44. package/dist/core/permission-rule-syntax.d.ts +30 -0
  45. package/dist/core/permission-rule-syntax.js +44 -0
  46. package/dist/core/permission-rules.d.ts +85 -33
  47. package/dist/core/permission-rules.js +104 -88
  48. package/dist/core/persisted-rule-arms.d.ts +56 -0
  49. package/dist/core/persisted-rule-arms.js +48 -0
  50. package/dist/core/present-plan-tool.js +3 -4
  51. package/dist/core/runner/active-skill-scope.js +7 -4
  52. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  53. package/dist/core/runner/contracts.d.ts +46 -3
  54. package/dist/core/runner/permission-rule-lanes.d.ts +33 -26
  55. package/dist/core/runner/permission-rule-lanes.js +27 -21
  56. package/dist/core/runner/prepare-ask-lane.d.ts +1 -1
  57. package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -4
  58. package/dist/core/runner/prepare-caps-and-workflow.js +16 -11
  59. package/dist/core/runner/prepare-defer-classify.d.ts +5 -26
  60. package/dist/core/runner/prepare-defer-classify.js +10 -10
  61. package/dist/core/runner/prepare-delegation-surface.d.ts +5 -4
  62. package/dist/core/runner/prepare-delegation-surface.js +15 -17
  63. package/dist/core/runner/prepare-gate-stations.d.ts +4 -1
  64. package/dist/core/runner/prepare-gate-stations.js +4 -2
  65. package/dist/core/runner/prepare-hands-readface.d.ts +3 -2
  66. package/dist/core/runner/prepare-hands-readface.js +3 -2
  67. package/dist/core/runner/prepare-inherited-gate.d.ts +1 -1
  68. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  69. package/dist/core/runner/prepare-lsp.d.ts +4 -3
  70. package/dist/core/runner/prepare-lsp.js +3 -2
  71. package/dist/core/runner/prepare-memory-engine-session.d.ts +4 -4
  72. package/dist/core/runner/prepare-memory-engine-session.js +4 -2
  73. package/dist/core/runner/prepare-offload-wrappers.js +7 -5
  74. package/dist/core/runner/prepare-park-ask.d.ts +4 -0
  75. package/dist/core/runner/prepare-park-ask.js +4 -1
  76. package/dist/core/runner/prepare-project-context.d.ts +5 -23
  77. package/dist/core/runner/prepare-project-context.js +7 -5
  78. package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
  79. package/dist/core/runner/prepare-protocol-tools.d.ts +5 -1
  80. package/dist/core/runner/prepare-protocol-tools.js +38 -21
  81. package/dist/core/runner/prepare-question-face.d.ts +4 -3
  82. package/dist/core/runner/prepare-question-face.js +5 -3
  83. package/dist/core/runner/prepare-run-refs.d.ts +16 -0
  84. package/dist/core/runner/prepare-run-refs.js +6 -0
  85. package/dist/core/runner/prepare-safety-scan.js +48 -2
  86. package/dist/core/runner/prepare-task.js +37 -19
  87. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -2
  88. package/dist/core/runner/prepare-tool-disclosure-mount.js +3 -1
  89. package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
  90. package/dist/core/runner/prepare-wiring-manifest.js +3 -8
  91. package/dist/core/runner/resume-admission.d.ts +53 -0
  92. package/dist/core/runner/resume-admission.js +83 -0
  93. package/dist/core/runner/resume-apply.d.ts +50 -0
  94. package/dist/core/runner/resume-apply.js +184 -0
  95. package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
  96. package/dist/core/runner/resume-checkpoint-screen.js +108 -0
  97. package/dist/core/runner/resume-claim.d.ts +32 -0
  98. package/dist/core/runner/resume-claim.js +27 -0
  99. package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
  100. package/dist/core/runner/resume-internals-and-config.js +50 -0
  101. package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
  102. package/dist/core/runner/resume-policy-outcome.js +127 -0
  103. package/dist/core/runner/resume-preflight.d.ts +40 -0
  104. package/dist/core/runner/resume-preflight.js +122 -0
  105. package/dist/core/runner/resume-review-outcome.d.ts +30 -0
  106. package/dist/core/runner/resume-review-outcome.js +88 -0
  107. package/dist/core/runner/run-harness-handlers.d.ts +46 -0
  108. package/dist/core/runner/run-harness-handlers.js +345 -0
  109. package/dist/core/runner/run-turn-boundary.d.ts +69 -0
  110. package/dist/core/runner/run-turn-boundary.js +694 -0
  111. package/dist/core/runner/runtask.d.ts +20 -0
  112. package/dist/core/runner/runtask.js +129 -1774
  113. package/dist/core/runner/session-rule-policy.js +9 -4
  114. package/dist/core/runner/synthetic-tools.js +5 -8
  115. package/dist/core/runner/tool-disclosure.d.ts +4 -0
  116. package/dist/core/runner/tool-disclosure.js +9 -5
  117. package/dist/core/runner/tool-end-body.d.ts +12 -5
  118. package/dist/core/runner/tool-end-body.js +8 -7
  119. package/dist/core/runner/tool-face-overlay.d.ts +40 -0
  120. package/dist/core/runner/tool-face-overlay.js +125 -0
  121. package/dist/core/runner/tool-output-projection.d.ts +14 -1
  122. package/dist/core/runner/tool-output-projection.js +24 -18
  123. package/dist/core/runner/turn-attachments.d.ts +2 -2
  124. package/dist/core/sensitive-path-policy.d.ts +3 -0
  125. package/dist/core/sensitive-path-policy.js +6 -5
  126. package/dist/core/shared-memory/tools.js +3 -6
  127. package/dist/core/store-contracts/checkpoint-store-contract.js +23 -0
  128. package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
  129. package/dist/core/task-event.d.ts +10 -0
  130. package/dist/core/task-registry.js +4 -5
  131. package/dist/core/task-spec.d.ts +8 -0
  132. package/dist/core/tool-catalog-entries.d.ts +48 -0
  133. package/dist/core/tool-catalog-entries.js +174 -0
  134. package/dist/core/tool-conformance.d.ts +22 -0
  135. package/dist/core/tool-conformance.js +37 -0
  136. package/dist/core/tool-face.d.ts +171 -0
  137. package/dist/core/tool-face.js +25 -0
  138. package/dist/core/tool-policy.d.ts +24 -0
  139. package/dist/core/tool-policy.js +4 -2
  140. package/dist/core/tool-registry.d.ts +114 -0
  141. package/dist/core/tool-registry.js +114 -0
  142. package/dist/core/tool-result-budget.d.ts +0 -3
  143. package/dist/core/tool-result-budget.js +4 -3
  144. package/dist/core/tool-result-store.d.ts +0 -18
  145. package/dist/core/tool-result-store.js +2 -18
  146. package/dist/core/tool-roster.d.ts +459 -0
  147. package/dist/core/tool-roster.js +507 -0
  148. package/dist/core/tool-spec.d.ts +54 -3
  149. package/dist/core/tools.js +7 -0
  150. package/dist/core/wiring-manifest.d.ts +13 -0
  151. package/dist/core/wiring-manifest.js +2 -1
  152. package/dist/core/write-protect.d.ts +2 -1
  153. package/dist/core/write-protect.js +5 -4
  154. package/dist/engine/compaction/utils.d.ts +6 -0
  155. package/dist/engine/compaction/utils.js +19 -19
  156. package/dist/engine/loop/types.d.ts +6 -0
  157. package/dist/index.d.ts +17 -6
  158. package/dist/index.js +17 -6
  159. package/dist/orchestration/goal.js +2 -1
  160. package/dist/orchestration/run-spec.js +3 -5
  161. package/dist/orchestration/run-workflow-tool.js +2 -2
  162. package/dist/prompt-assembly/event-registry.js +3 -3
  163. package/dist/prompt-assembly/tool-catalog.d.ts +2 -2
  164. package/dist/stores/file/checkpoint-store.js +1 -1
  165. package/dist/stores/file/permission-rule-store.d.ts +28 -23
  166. package/dist/stores/file/permission-rule-store.js +64 -16
  167. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  168. package/dist/tools/fs/fs-bash.d.ts +1 -1
  169. package/dist/tools/fs/fs-bash.js +7 -11
  170. package/dist/tools/fs/fs-pdf.d.ts +1 -1
  171. package/dist/tools/fs/fs-read.js +2 -2
  172. package/dist/tools/fs/fs-search-tools.d.ts +0 -7
  173. package/dist/tools/fs/fs-search-tools.js +3 -16
  174. package/dist/tools/fs/fs-write.js +4 -6
  175. package/dist/tools/fs/notebook.d.ts +1 -1
  176. package/dist/tools/fs/repo-map.js +2 -2
  177. package/dist/tools/fs/safety.d.ts +3 -2
  178. package/dist/tools/fs/safety.js +6 -5
  179. package/dist/tools/monitor.js +2 -2
  180. package/dist/tools/scheduler-tools.js +5 -8
  181. package/dist/tools/task-list.js +5 -8
  182. package/dist/tools/todo.js +2 -2
  183. package/dist/tools/web.js +3 -6
  184. package/dist/tools/worktree.js +3 -4
  185. package/package.json +4 -1
  186. package/test/export-surface.snapshot.json +189 -17
@@ -33,6 +33,7 @@ import type { CwdRef } from "../../tools/fs/fs-shared.js";
33
33
  import type { AskLane, IrreversibilityTier, ParkAsk, Prepared, ReversibilityProbes } from "./contracts.js";
34
34
  import type { StopForDenialLimit } from "./denial-limit-arms.js";
35
35
  import type { PermissionRuleLanes } from "./permission-rule-lanes.js";
36
+ import { type ToolRosterDeltaSeat } from "../tool-roster.js";
36
37
  export interface PrepareGateStationsInput {
37
38
  /** borrowed-readonly — the in-stream ask lane, or undefined when the gate machinery is inactive (then no `tool_call`
38
39
  * station is built). Read: `adjudicate`, `resolveAskBound`, `notifyPermissionDenied`, `notifyHookError`. */
@@ -41,7 +42,9 @@ export interface PrepareGateStationsInput {
41
42
  parkAsk: ParkAsk | undefined;
42
43
  /** borrowed-readonly — the FINAL mount roster: iterated once for the effect sync and the write-protection arming. Never
43
44
  * mutated here. */
44
- tools: AgentTool[];
45
+ tools: readonly AgentTool[];
46
+ /** borrowed-readonly — the leg's delta seat (design/388): its current roster is the face source for every gated call. */
47
+ toolRosterDeltas: ToolRosterDeltaSeat;
45
48
  /** borrowed-mutable — the effect collection. Writer here: the final-roster sync fills MISSING entries from each
46
49
  * first-party tool's declared effect (never overrides a scanned one); the plan-mode deny reads it per call. */
47
50
  toolEffects: Map<string, ToolEffect>;
@@ -7,6 +7,7 @@ import { directDeny } from "./gate-exit.js";
7
7
  import { emitTrace } from "../trace.js";
8
8
  import { createWriteProtectionCheck } from "../write-protect.js";
9
9
  import { PATH_CONFINABLE_WRITE_TOOLS } from "./session-rule-policy.js";
10
+ import { toolCallFaceOf } from "../tool-roster.js";
10
11
  function screenGateResult(result) {
11
12
  if (result.suspend !== undefined)
12
13
  return { defects: [] };
@@ -39,7 +40,7 @@ function maybeHumanRejectionHalt(input) {
39
40
  };
40
41
  }
41
42
  export function prepareGateStations(input) {
42
- const { askLane, parkAsk, tools, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, planModeRef, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, irreversibilityTier, reversibilityProbes, abortController, shellGatedBash, shellGatedMonitor, autoModeDecider, autoModeDenialTracking, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash } = input;
43
+ const { askLane, parkAsk, tools, toolRosterDeltas, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, planModeRef, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, irreversibilityTier, reversibilityProbes, abortController, shellGatedBash, shellGatedMonitor, autoModeDecider, autoModeDenialTracking, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash } = input;
43
44
  if (askLane !== undefined && parkAsk !== undefined) {
44
45
  const { adjudicate, resolveAskBound, notifyPermissionDenied, notifyHookError } = askLane;
45
46
  const { resolveContentAsk, suspendAsk } = parkAsk;
@@ -103,6 +104,7 @@ export function prepareGateStations(input) {
103
104
  hookTimeoutMs,
104
105
  ...(handsCwdRef !== undefined ? { trackedCwd: () => handsCwdRef.current } : {}),
105
106
  ...(hookEnvFace !== undefined ? { hookEnv: hookEnvFace } : {}),
107
+ toolFaceOf: (name) => toolCallFaceOf(toolRosterDeltas.current, name),
106
108
  adjudicate,
107
109
  resolveAsk: resolveAskBound,
108
110
  suspendAsk,
@@ -121,7 +123,7 @@ export function prepareGateStations(input) {
121
123
  ...(permissionRuleLane
122
124
  ? {
123
125
  persistedRules: {
124
- admits: permissionRuleLane.admits,
126
+ adjudicate: permissionRuleLane.adjudicate,
125
127
  onResolved: (info) => emitTrace(deps.tracer, () => ({
126
128
  kind: "permission.persisted_rule_allowed",
127
129
  version: 2,
@@ -5,6 +5,7 @@ import type { Model } from "../../internal/llm.js";
5
5
  import type { AgentTool, ExecutionEnv, WorkspaceState } from "../../internal/harness.js";
6
6
  import type { StoredSession } from "../session.js";
7
7
  import type { IrreversibilityTier, PrepareResume, ReversibilityProbes, RunInternals, ToolFaceSnapshot } from "./contracts.js";
8
+ import { RosterBuilder } from "../tool-roster.js";
8
9
  /** Test seam (mirrors `__resetMalformedNoticeSeatAnnouncement`): never called by production code.
9
10
  * Deliberately asymmetric — it resets only the console latch: the WeakSet arm needs no seam
10
11
  * because a test resets it by minting a fresh sink function (identity IS the ledger key), while
@@ -174,8 +175,8 @@ export interface PrepareHandsReadFaceInput {
174
175
  * contract, never map/copy). This phase pushes the hands band IN PLACE (after caller tools /
175
176
  * built-ins / MCP / A2A, before the background-task mounts — T22 order). Mutation owners after
176
177
  * this phase: P11 background/Monitor/worktree pushes, the driver's excludeTools splice, the
177
- * refresh seam's runtime splice/push. */
178
- tools: AgentTool[];
178
+ * refresh seam's runtime splice/push. Since design/388 the array is builder-private: this phase `mountAll`s the band. */
179
+ roster: RosterBuilder;
179
180
  /** borrowed-readonly HERE (census divergence #5 vs the design row): this phase only reads
180
181
  * `.has("Bash")`/`.has("Monitor")` for the provenance bits — its mutation owners are B-2/P8/P9
181
182
  * (+refresh seam)/P11/P22, per the B-2 Result's mutation-owner table. */
@@ -8,6 +8,7 @@ import { normalizeFileText } from "../../tools/fs/encoding.js";
8
8
  import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
9
9
  import { deliverEngineNotice } from "../types.js";
10
10
  import { rebaseWorkspacePath } from "./workspace-path.js";
11
+ import { RosterBuilder } from "../tool-roster.js";
11
12
  const readFaceClampAnnouncedSinks = new WeakSet();
12
13
  let readFaceClampConsoleAnnounced = false;
13
14
  export function __resetReadFaceClampAnnouncement() {
@@ -123,7 +124,7 @@ export function resolveHandsLessReadFace(input) {
123
124
  }
124
125
  export async function prepareHandsMount(input) {
125
126
  const envHandToolNames = new Set();
126
- const { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, firstPartyOffload, tools, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes } = input;
127
+ const { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, firstPartyOffload, roster, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes } = input;
127
128
  assertStringArraySeat(spec.additionalDirectories, "TaskSpec.additionalDirectories", "strings");
128
129
  assertStringArraySeat(spec.additionalReadDirectories, "TaskSpec.additionalReadDirectories", "strings");
129
130
  assertStringArraySeat(deps.readDenyPatterns, "RunnerDeps.readDenyPatterns", "compiler-owned");
@@ -351,7 +352,7 @@ export async function prepareHandsMount(input) {
351
352
  deps.onError?.(new Error(`spec.tools ${shadowed.length > 1 ? "entries" : "entry"} ${names} collide${shadowed.length > 1 ? "" : "s"} with the built-in hands tool band — the built-in wins (registered later; the harness tool Map is last-write-wins) and the caller tool never dispatches this task. Rename the caller tool, or don't inject executionEnv if you intend to replace the band.`), { phase: "config", sessionId });
352
353
  }
353
354
  }
354
- tools.push(...band.map((t) => (envHandToolNames.add(t.name), firstPartyOffload(t))));
355
+ roster.mountAll(band.map((t) => (envHandToolNames.add(t.name), firstPartyOffload(t))), { source: "builtin", mountedBy: "hands" });
355
356
  const shellGate = effectiveShellGate;
356
357
  if (shellGate === "off" && !(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
357
358
  deps.onError?.(new Error(`shell gate doctrine is "off" while a real writable shell (Bash) is mounted — no shell safety-axis ` +
@@ -53,7 +53,7 @@ export interface PrepareInheritedGateInput {
53
53
  * / `oneShot` / `clientContext` / `principal` / `additionalDirectories` / `additionalReadDirectories` / `envFacts` /
54
54
  * `memoryPersistenceCapable` / `getApiKeyAndHeaders` / `checkpointStore` / `backgroundScope` / `enablePlanMode` (the ctx
55
55
  * enrichment's trusted seats). Never mutated. */
56
- spec: Pick<TaskSpec, "shellGate" | "durableApproval" | "onAsk" | "onQuestion" | "handsReadOnly" | "interactiveTools" | "oneShot" | "clientContext" | "principal" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable" | "getApiKeyAndHeaders" | "checkpointStore" | "backgroundScope" | "enablePlanMode">;
56
+ spec: Pick<TaskSpec, "shellGate" | "durableApproval" | "onAsk" | "onQuestion" | "handsReadOnly" | "interactiveTools" | "oneShot" | "clientContext" | "principal" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable" | "getApiKeyAndHeaders" | "checkpointStore" | "backgroundScope" | "enablePlanMode" | "excludeAllTools">;
57
57
  /** borrowed-readonly — the deployment seats: `autoMode.denialLimit` (the rebuilt trackers' bounds), `memoryScopeAdmission` /
58
58
  * `deploymentMemoryScopes` / `compliancePostureResolver` (the governed-provenance bit), `onError` (the screening-face crash
59
59
  * lane), `onNotice` (the stop seat's notice), `executionEnv` (the hook-env identity predicate), `delegationEntryCaps` (resolved
@@ -162,6 +162,7 @@ export function prepareInheritedGate(input) {
162
162
  const fullShellReachable = handsEnabled &&
163
163
  !(executionEnv instanceof StubExecutionEnv) &&
164
164
  spec.handsReadOnly !== true &&
165
+ spec.excludeAllTools !== true &&
165
166
  !(toolFaceSnapshot.exclude?.includes("Bash") ?? false);
166
167
  const { gateStopRef, stopForDenialLimit } = createDenialLimitStop({ sessionId, runId, onNotice: deps.onNotice, abort: abortRun });
167
168
  const delegationEntryCapsResolved = resolveDelegationEntryCaps(deps.delegationEntryCaps);
@@ -11,10 +11,11 @@
11
11
  * with no await is extracted as a sync function — an `async` wrapper would open a microtask window).
12
12
  * Read-stability of the host handles for the call: {@link RunInternals} (`@contract prepare.deps-read-stable`).
13
13
  */
14
- import type { AgentTool, ExecutionEnv } from "../../internal/harness.js";
14
+ import type { ExecutionEnv } from "../../internal/harness.js";
15
15
  import type { LspDiagnosticsRegistry } from "../lsp-diagnostics.js";
16
16
  import type { RunnerDeps, TaskSpec } from "../types.js";
17
17
  import type { Prepared } from "./contracts.js";
18
+ import { RosterBuilder } from "../tool-roster.js";
18
19
  export interface PrepareLspInput {
19
20
  /** borrowed-readonly — the REBOUND spec. Read: `lspManager` (the per-task manager, outranks the deployment's),
20
21
  * `lspDiagnostics` (the lane's opt-out), `handsReadOnly` (a read-only run has no lane). Never mutated. */
@@ -34,9 +35,9 @@ export interface PrepareLspInput {
34
35
  /** borrowed-readonly — the tracked-cwd cell the hands band writes; the nudge READS `current` per call as its
35
36
  * resolution base (identity is the contract — the band's `cd` handling writes this exact cell). */
36
37
  handsCwdRef: Prepared["cwdRef"];
37
- /** borrowed-mutable — the run's shared roster (identity is the contract). Writer here: ONE push (the LSP tool)
38
+ /** borrowed-mutable — the run's roster builder (identity is the contract). Writer here: ONE mount (the LSP tool)
38
39
  * when a manager exists. */
39
- tools: AgentTool[];
40
+ roster: RosterBuilder;
40
41
  /** borrowed-mutable — the env-hand membership set the read-posture observer projects; writer here: `add("LSP")`
41
42
  * on the mount. */
42
43
  envHandToolNames: Set<string>;
@@ -1,11 +1,12 @@
1
1
  import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
2
2
  import { pathToUri } from "../lsp-protocol.js";
3
+ import { RosterBuilder } from "../tool-roster.js";
3
4
  export function prepareLsp(input) {
4
- const { spec, deps, executionEnv, taskRootFinal, handsEnabled, sessionId, handsCwdRef, tools, envHandToolNames } = input;
5
+ const { spec, deps, executionEnv, taskRootFinal, handsEnabled, sessionId, handsCwdRef, roster, envHandToolNames } = input;
5
6
  const lspManager = spec.lspManager ?? deps.lspManager;
6
7
  if (lspManager) {
7
8
  const lspRoot = taskRootFinal;
8
- tools.push(createLspTool(lspManager, { isPathIgnored: gitCheckIgnoreFilter(executionEnv, lspRoot), env: executionEnv }));
9
+ roster.mount(createLspTool(lspManager, { isPathIgnored: gitCheckIgnoreFilter(executionEnv, lspRoot), env: executionEnv }), { source: "builtin", mountedBy: "optional" });
9
10
  envHandToolNames.add("LSP");
10
11
  }
11
12
  const lspDiagnostics = spec.lspDiagnostics !== false && lspManager?.diagnostics !== undefined && handsEnabled && spec.handsReadOnly !== true
@@ -16,12 +16,12 @@
16
16
  * One read changed spelling and nothing else: the declared-class announcement read `mcp?.statuses` while `mcp` had
17
17
  * long been the protocol-tools phase's non-null handle; the optional chain was dead and is now the plain member read.
18
18
  */
19
- import type { AgentTool } from "../../internal/harness.js";
20
19
  import type { LockedPreflight } from "../locked-config.js";
21
20
  import type { MaterializedMcp } from "../mcp.js";
22
21
  import { type DelegationContentSafety } from "../memory-engine/delegation-provenance.js";
23
22
  import type { RunnerDeps, TaskSpec } from "../types.js";
24
23
  import type { Prepared, RunInternals } from "./contracts.js";
24
+ import { RosterBuilder } from "../tool-roster.js";
25
25
  export interface PrepareMemoryEngineSessionInput {
26
26
  /** borrowed-readonly — the mounted engine session, or undefined (the ARMING fact's first half). Read: `pollution`
27
27
  * (the mark face), `contentSafety` (the own half of the narrowing), `settlement` (the seat + the receipt ledger),
@@ -64,10 +64,10 @@ export interface PrepareMemoryEngineSessionInput {
64
64
  spec: Pick<TaskSpec, "tools">;
65
65
  /** borrowed-readonly — the env-hand roster (membership minted at the mount sites); read by the classification. */
66
66
  envHandToolNames: ReadonlySet<string>;
67
- /** borrowed-mutable — the run's ONE tool roster. Writer here: the wrap pass REPLACES entries in place
68
- * (`tools[i] = wrapped`, identity-tracked so a re-run never stacks wrappers); see the roster's writer table on
67
+ /** borrowed-mutable — the run's roster builder. Writer here: the wrap pass REPLACES entries in place
68
+ * (`roster.replaceAt(i, wrapped)`, identity-tracked so a re-run never stacks wrappers); see the roster's writer table on
69
69
  * {@link Prepared.tools}. */
70
- tools: AgentTool[];
70
+ roster: RosterBuilder;
71
71
  /** borrowed-mutable — the re-runnable wrap pass's seat (the protocol-tools phase mints it; the rebuild seam re-runs it
72
72
  * after a refresh re-splice). Writer: THIS phase installs the pass and runs it once. */
73
73
  contentOriginWrapRef: {
@@ -5,8 +5,10 @@ import { protocolOf } from "../protocol-table.js";
5
5
  import { deliverEngineNotice } from "../types.js";
6
6
  import { inlineUntrusted } from "../untrusted-text.js";
7
7
  import { fileArgPath } from "../../tools/fs/safety.js";
8
+ import { RosterBuilder } from "../tool-roster.js";
8
9
  export function prepareMemoryEngineSession(input) {
9
- const { memoryEngineSession, internals, memoryDelegationEvidence, externalContentTargetActive, lockedPreflight, mcp, sessionId, runId, deps, provenanceForChildrenRef, delegationSettlementRef, spec, envHandToolNames, tools, contentOriginWrapRef } = input;
10
+ const { memoryEngineSession, internals, memoryDelegationEvidence, externalContentTargetActive, lockedPreflight, mcp, sessionId, runId, deps, provenanceForChildrenRef, delegationSettlementRef, spec, envHandToolNames, roster, contentOriginWrapRef } = input;
11
+ const tools = roster.tools;
10
12
  const delegationProvenanceChannel = internals?.delegationProvenance;
11
13
  if (memoryEngineSession !== undefined || delegationProvenanceChannel !== undefined) {
12
14
  const pollution = memoryEngineSession?.pollution;
@@ -196,7 +198,7 @@ export function prepareMemoryEngineSession(input) {
196
198
  },
197
199
  };
198
200
  contentOriginWrapped.add(wrapped);
199
- tools[i] = wrapped;
201
+ roster.replaceAt(i, wrapped);
200
202
  }
201
203
  };
202
204
  contentOriginWrapRef.current();
@@ -1,4 +1,6 @@
1
- import { DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, firstPartyOffloadPolicy, InMemoryToolResultStore, RunnerSharedToolResultStore, ScopedToolResultStore, withToolResultOffload, } from "../tool-result-store.js";
1
+ import { DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, InMemoryToolResultStore, RunnerSharedToolResultStore, ScopedToolResultStore, withToolResultOffload, } from "../tool-result-store.js";
2
+ import { faceOf } from "../tool-roster.js";
3
+ const FIRST_PARTY_OFFLOAD_THRESHOLD_CHARS = 50_000;
2
4
  export function prepareOffloadWrappers(input) {
3
5
  const { spec, deps, resume, offloadScope, sessionId } = input;
4
6
  const nestedStats = { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0, anyUnpriced: false };
@@ -31,10 +33,10 @@ export function prepareOffloadWrappers(input) {
31
33
  return withToolResultOffload(tool, offloadStore, perTool?.offloadThresholdChars ?? offloadThreshold, sessionId, () => offloadReachableToolsRef.current?.());
32
34
  };
33
35
  const firstPartyOffload = (tool) => {
34
- const policy = firstPartyOffloadPolicy(tool.name);
35
- if (policy.offload === false)
36
- return maybeOffload(tool, policy);
37
- return maybeOffload(tool, explicitGlobalThreshold === undefined ? policy : undefined);
36
+ const face = faceOf(tool);
37
+ if (face.offload === false)
38
+ return maybeOffload(tool, { offload: false });
39
+ return maybeOffload(tool, explicitGlobalThreshold === undefined ? { offloadThresholdChars: face.offloadThresholdChars ?? FIRST_PARTY_OFFLOAD_THRESHOLD_CHARS } : undefined);
38
40
  };
39
41
  const remoteToolOffload = (tool) => {
40
42
  if (explicitGlobalThreshold !== undefined)
@@ -30,6 +30,7 @@ import type { CwdRef } from "../../tools/fs/fs-shared.js";
30
30
  import { type ContentAskBinding } from "./content-ask-bindings.js";
31
31
  import type { AskLane, ParkAsk, Prepared, SuspendSaga } from "./contracts.js";
32
32
  import type { createRuleOffersOf } from "./permission-rule-lanes.js";
33
+ import { type ToolRosterDeltaSeat } from "../tool-roster.js";
33
34
  export interface PrepareParkAskInput {
34
35
  /** borrowed-readonly — the in-stream ask lane, or undefined when the gate machinery is inactive (then both closures are
35
36
  * absent). Read: `composedCallSignal` (the fence and the delivery race), `approvalPreviewOf` (the row's preview). */
@@ -49,6 +50,9 @@ export interface PrepareParkAskInput {
49
50
  /** borrowed-readonly — the leg's checkpoint store, or undefined (then `suspendAsk` is absent — the park predicate's store
50
51
  * term). Read: the declared fidelity; the saga writes to it. */
51
52
  checkpointStore: CheckpointStore | undefined;
53
+ /** borrowed-readonly — the leg's delta seat (design/388 B18): the park reads the gated tool's identity off its
54
+ * CURRENT roster snapshot so the row carries who the action was minted against. */
55
+ toolRosterDeltas: ToolRosterDeltaSeat;
52
56
  /** borrowed-readonly — the wiring-manifest phase's park predicate (`parkLane.effective`): arms `suspendAsk`. */
53
57
  parkLaneArmed: boolean;
54
58
  /** borrowed-readonly — the question-face phase's routability predicate (the mounted question tool is the engine's own,
@@ -14,6 +14,7 @@ import { checkpointScopeOf } from "./checkpoint-scope.js";
14
14
  import { CONTENT_ASK_BINDING_CAP } from "./content-ask-bindings.js";
15
15
  import { DEFAULT_UNATTENDED_APPROVAL_TTL_MS, publishCommittedSuspend, sanitizedTtlMs } from "./park-commit.js";
16
16
  import { remoteEnvFailureNote } from "./remote-env-retry.js";
17
+ import { callFaceSeat, parkedToolIdentity, parkedToolFace } from "../tool-roster.js";
17
18
  class ParkRefusal extends Error {
18
19
  constructor(message, options) {
19
20
  super(message, options);
@@ -21,7 +22,7 @@ class ParkRefusal extends Error {
21
22
  }
22
23
  }
23
24
  export function prepareParkAsk(input) {
24
- const { askLane, saga, spec, deps, sessionId, checkpointStore, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedBash, shellGatedMonitor, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef } = input;
25
+ const { askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedBash, shellGatedMonitor, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef } = input;
25
26
  if (askLane === undefined || saga === undefined)
26
27
  return { parkAsk: undefined };
27
28
  const { composedCallSignal, approvalPreviewOf } = askLane;
@@ -157,6 +158,7 @@ export function prepareParkAsk(input) {
157
158
  args: presented.value,
158
159
  toolCallId: req.toolCallId,
159
160
  budget: budgetSnapshot,
161
+ ...callFaceSeat(req.face),
160
162
  ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}),
161
163
  }, cutSignal)), cutSignal);
162
164
  if (reprojectedRaced.tag === "aborted")
@@ -277,6 +279,7 @@ export function prepareParkAsk(input) {
277
279
  kind: "tool_approval",
278
280
  toolCallId: req.toolCallId,
279
281
  toolName: req.toolName,
282
+ ...parkedToolIdentity(toolRosterDeltas.current, req.toolName), ...parkedToolFace(toolRosterDeltas.current, req.toolName),
280
283
  args: parkedArgs,
281
284
  ...(() => {
282
285
  const p = approvalPreviewOf(req.toolName, parkedArgs);
@@ -1,27 +1,9 @@
1
- /**
2
- * design/390 M9 — prepareTask's PROJECT-CONTEXT phase: the deployment's project memory snapshot (the
3
- * `loadProjectMemory` seam: phase derivation, instruction sources, the framed + fenced project layer
4
- * appended to the memory block), the skills roster (declaration rank, normalization, the 1MB load gate,
5
- * the manifest-scope door, the `skill` tool mount), the two built-in memory pairs' mount-or-yield
6
- * (shared-memory pair, engine-memory pair), and the skills-listing projection the run loop announces.
7
- * The body is the driver's stretch moved verbatim; the interface below is the dependency list it had
8
- * implicitly (design/238 R-1: a new read is an Input field, i.e. a review event).
9
- *
10
- * ASYNC BY CONTENT: the stretch awaits the session branch read, the seam itself and the read-state
11
- * seeding (all inside the `loadProjectMemory` arm). It already sits past prepareTask's first await, so
12
- * the one extra microtask the driver's `await` adds on the seam-less path opens no window any caller
13
- * could reach (design/390 §1.5 S1 synchronicity rule: only pre-first-await stretches must stay sync).
14
- *
15
- * Throws pass through unchanged: `config.reserved_tool_name` (a caller tool named like the skill tool
16
- * while skills are present) is the only refusal here; the seam's own throw and the seeding's are
17
- * caught and forwarded to `deps.onError` (best-effort, never block the task).
18
- */
19
- import type { AgentTool } from "../../internal/harness.js";
20
1
  import { type StoredSession } from "../session.js";
21
2
  import type { RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
22
3
  import type { ActiveSkillScope } from "./active-skill-scope.js";
23
4
  import type { AnnounceOnceSink, Prepared, PrepareResume, RunInternals, ToolFaceSnapshot } from "./contracts.js";
24
5
  import { normalizeSkills } from "./synthetic-tools.js";
6
+ import { RosterBuilder } from "../tool-roster.js";
25
7
  export interface PrepareProjectContextInput {
26
8
  /** borrowed-readonly — the deployment seams this phase reads: `loadProjectMemory` (the snapshot),
27
9
  * `onError` (the operator lane for a throwing seam / seeding / oversize skill), `sharedMemoryStores`
@@ -64,10 +46,10 @@ export interface PrepareProjectContextInput {
64
46
  * and the `skill` tool alias this exact object). Mutated here: `push` of every inherited parent
65
47
  * frame; after this phase only the `skill` tool's execute pushes/pops. */
66
48
  skillScope: ActiveSkillScope;
67
- /** borrowed-mutable — the shared mount array (later refs alias it; in-place contract). Mutated
68
- * here: `push` of the `skill` tool, the shared-memory pair and the engine-memory pair. Occupancy
69
- * is judged on this ALREADY-MOUNTED roster (names + aliases). */
70
- tools: AgentTool[];
49
+ /** borrowed-mutable — the run's roster builder (later refs alias its read face; in-place contract). Mutated
50
+ * here: `mount` of the `skill` tool, the shared-memory pair and the engine-memory pair. Occupancy
51
+ * is judged on the ALREADY-MOUNTED read face (names + aliases). */
52
+ roster: RosterBuilder;
71
53
  /** borrowed-readonly — the frozen task-start tool face; only `exclude` is read (pair yield ①). */
72
54
  toolFaceSnapshot: Pick<ToolFaceSnapshot, "exclude">;
73
55
  /** borrowed-readonly — the once-per-session announcement ledger; only its deduplicated `onError`
@@ -6,8 +6,10 @@ import { createSharedMemoryTools } from "../shared-memory/tools.js";
6
6
  import { SHARED_MEMORY_TOOL_NAMES } from "../shared-memory/types.js";
7
7
  import { defineTool } from "../tools.js";
8
8
  import { SKILL_CONTENT_MAX_CHARS, SKILL_TOOL_NAME, createSkillTool, normalizeSkills } from "./synthetic-tools.js";
9
+ import { RosterBuilder } from "../tool-roster.js";
9
10
  export async function prepareProjectContext(input) {
10
- const { deps, spec, internals, session, sessionId, hostTaskId, taskRootFinal, handsEnabled, delegation, seedContextFiles, memoryBlockFromEngine, skillScope, tools, toolFaceSnapshot, onceLedger, memoryTools, memoryEngineSession, memorySearchToolsPlanned, memoryPairExcludedName, memoryPairOccupiedName, resume } = input;
11
+ const { deps, spec, internals, session, sessionId, hostTaskId, taskRootFinal, handsEnabled, delegation, seedContextFiles, memoryBlockFromEngine, skillScope, roster, toolFaceSnapshot, onceLedger, memoryTools, memoryEngineSession, memorySearchToolsPlanned, memoryPairExcludedName, memoryPairOccupiedName, resume } = input;
12
+ const tools = roster.tools;
11
13
  let memoryBlock = memoryBlockFromEngine;
12
14
  let instructionSources;
13
15
  let projectInstructionContent;
@@ -88,7 +90,7 @@ export async function prepareProjectContext(input) {
88
90
  e.code = "config.reserved_tool_name";
89
91
  throw e;
90
92
  }
91
- tools.push(createSkillTool(skillSpecs, skillScope));
93
+ roster.mount(createSkillTool(skillSpecs, skillScope), { source: "builtin", mountedBy: "optional" });
92
94
  }
93
95
  const sharedMemoryProvider = deps.sharedMemoryStores;
94
96
  let sharedMemoryPairMounted = false;
@@ -110,16 +112,16 @@ export async function prepareProjectContext(input) {
110
112
  " — the pair mounts together or not at all."), { phase: "config", sessionId, classification: "shared-memory-not-mounted" });
111
113
  }
112
114
  else {
113
- tools.push(...createSharedMemoryTools({
115
+ roster.mountAll(createSharedMemoryTools({
114
116
  provider: sharedMemoryProvider,
115
117
  context: { sessionId, taskId: hostTaskId, ...(spec.principal !== undefined ? { principal: spec.principal } : {}) },
116
- }).map((s) => defineTool(s)));
118
+ }).map((s) => defineTool(s)), { source: "builtin", mountedBy: "memory" });
117
119
  sharedMemoryPairMounted = true;
118
120
  }
119
121
  }
120
122
  let memoryEnginePairMounted = false;
121
123
  if (memoryTools !== undefined && memoryTools.length > 0) {
122
- tools.push(...memoryTools.map((s) => defineTool(s)));
124
+ roster.mountAll(memoryTools.map((s) => defineTool(s)), { source: "builtin", mountedBy: "memory" });
123
125
  memoryEnginePairMounted = true;
124
126
  }
125
127
  else if (memoryEngineSession !== undefined && !memorySearchToolsPlanned) {
@@ -48,7 +48,7 @@ export interface PreparePromptAssemblyInput {
48
48
  fableMitigations: boolean;
49
49
  /** borrowed-readonly — the run's ONE tool roster, read only: the assembly receives DETACHED copies, the audit and the
50
50
  * manifest read it in place. Never mutated here. */
51
- tools: AgentTool[];
51
+ tools: readonly AgentTool[];
52
52
  /** borrowed-readonly — the task model; only `promptGuidance` (the model-guidance section). */
53
53
  model: Pick<Model, "promptGuidance">;
54
54
  /** borrowed-readonly — the acquired session id (every operator line's attribution). */
@@ -4,6 +4,7 @@ import { type MaterializedA2a } from "../a2a.js";
4
4
  import type { LockedPreflight } from "../locked-config.js";
5
5
  import type { RunnerDeps, TaskSpec, ToolEffect } from "../types.js";
6
6
  import type { AnnounceOnceSink, IrreversibilityTier, Prepared, RollbackStack, ToolFaceSnapshot } from "./contracts.js";
7
+ import { RosterBuilder, type ToolRosterDeltaSeat } from "../tool-roster.js";
7
8
  export interface PrepareProtocolToolsInput {
8
9
  /** borrowed-readonly — the frozen preflight; only `mcp` (the declared server entries) is read: the
9
10
  * materialize input, the RefreshMcpTools mount predicate, and whether a real client was acquired. */
@@ -34,7 +35,10 @@ export interface PrepareProtocolToolsInput {
34
35
  * Writers here: three pushes (the MCP tools through the wrapper, RefreshMcpTools, the A2A tools through the
35
36
  * wrapper) and, at RUN time, the refresh closure's splice within a server's name-prefix domain. Later
36
37
  * phases push their own mounts; the roster's full writer table is the delegation-surface phase's. */
37
- tools: AgentTool[];
38
+ roster: RosterBuilder;
39
+ /** borrowed-mutable — the run-time delta seat (design/388 §2.5): the refresh closure re-mints the roster through it after
40
+ * every re-splice so the stream learns of the change. Armed by the driver after the roster is minted. */
41
+ rosterSeat: ToolRosterDeltaSeat;
38
42
  /** borrowed-readonly — the large-result wrapper for protocol tools (per-tool `_meta` bar, 50K default). */
39
43
  remoteToolOffload: (tool: AgentTool) => AgentTool;
40
44
  /** borrowed-readonly — the frozen tool face; `exclude` is read at REFRESH time so a deployment-excluded
@@ -2,8 +2,13 @@ import { Type } from "typebox";
2
2
  import { materializeMcpTools } from "../mcp.js";
3
3
  import { materializeA2aTools } from "../a2a.js";
4
4
  import { deliverEngineNotice } from "../types.js";
5
+ import { RosterBuilder } from "../tool-roster.js";
6
+ import { REFRESH_MCP_TOOLS_TOOL_NAME, toolFace } from "../tool-catalog-entries.js";
7
+ import { MCP_NAMESPACE, A2A_NAMESPACE } from "../protocol-table.js";
8
+ import { mintNamespacePrefix } from "../protocol-naming.js";
9
+ import { applyMcpToolFaces } from "./tool-face-overlay.js";
5
10
  export async function prepareProtocolTools(input) {
6
- const { lockedPreflight, spec, deps, reminderMark, reminderDisclosureCounts, runId, sessionId, onceLedger, tools, remoteToolOffload, toolFaceSnapshot, toolEffects, axisExplicitNegatives, irreversibilityTier, irreversibleTools, egressTools, abortController, rollback } = input;
11
+ const { lockedPreflight, spec, deps, reminderMark, reminderDisclosureCounts, runId, sessionId, onceLedger, roster, rosterSeat, remoteToolOffload, toolFaceSnapshot, toolEffects, axisExplicitNegatives, irreversibilityTier, irreversibleTools, egressTools, abortController, rollback } = input;
7
12
  const mcp = lockedPreflight.mcp?.length
8
13
  ? await materializeMcpTools(lockedPreflight.mcp, spec.principal, deps.onElicit, deps.mcpImageResizer, { reminderMark, counts: reminderDisclosureCounts }, mcpRevocationWiring(deps, runId))
9
14
  : { tools: [], toolAxes: [], warnings: [], serverInstructions: [], instructionsDelta: { pendingAdds: [], pendingRemovals: [] }, droppedTools: [], statuses: [], refresh: async () => [], dispose: async () => { } };
@@ -24,14 +29,35 @@ export async function prepareProtocolTools(input) {
24
29
  throw e;
25
30
  }
26
31
  }
27
- tools.push(...mcp.tools.map((t) => remoteToolOffload(t)));
32
+ const faceAnnounced = new Set();
33
+ const withFaces = (tools, serverName) => {
34
+ const server = (lockedPreflight.mcp ?? []).find((s) => s.name === serverName);
35
+ return server === undefined ? [...tools] : applyMcpToolFaces({ tools, server, onNotice: deps.onNotice, announced: faceAnnounced, sessionId: sessionId ?? "", runId });
36
+ };
37
+ {
38
+ const servers = (lockedPreflight.mcp ?? []).map((s) => ({ name: s.name, prefix: mintNamespacePrefix(MCP_NAMESPACE, s.name) }));
39
+ const faced = new Map();
40
+ for (const server of servers) {
41
+ const own = mcp.tools.filter((x) => x.name.startsWith(server.prefix));
42
+ for (const t of withFaces(own, server.name))
43
+ faced.set(t.name, t);
44
+ }
45
+ for (const t of mcp.tools) {
46
+ const server = servers.find((s) => t.name.startsWith(s.prefix));
47
+ const mounted = server === undefined ? t : faced.get(t.name);
48
+ if (mounted === undefined)
49
+ continue;
50
+ roster.mount(remoteToolOffload(mounted), { source: "mcp", mountedBy: "optional", origin: { peer: server?.name ?? MCP_NAMESPACE.parse(t.name)?.peer ?? t.name } });
51
+ }
52
+ }
28
53
  const rebuildHarnessToolsRef = {};
29
54
  const contentOriginWrapRef = {};
30
55
  const toolCallGateArmedRef = { armed: false };
31
56
  if (lockedPreflight.mcp?.length) {
32
- tools.push({
33
- name: "RefreshMcpTools",
34
- label: "RefreshMcpTools",
57
+ roster.mount({
58
+ ...toolFace("refresh-mcp-tools"),
59
+ name: REFRESH_MCP_TOOLS_TOOL_NAME,
60
+ label: REFRESH_MCP_TOOLS_TOOL_NAME,
35
61
  description: "Refresh the tool list of connected MCP servers. Never dials or re-dials connections — it only re-reads the tool list over the existing connection. The refreshed tools are available immediately; you can call them on your next step. Use when a server's expected tool is missing, or the tool list looks stale after a connection recovered. Omit `server` to refresh every connected server.",
36
62
  parameters: Type.Object({
37
63
  server: Type.Optional(Type.String({ description: "Name of a single MCP server to refresh; omit to refresh all connected servers." })),
@@ -76,19 +102,8 @@ export async function prepareProtocolTools(input) {
76
102
  anyActiveFailure = true;
77
103
  continue;
78
104
  }
79
- let domainAnchor = -1;
80
- for (let i = tools.length - 1; i >= 0; i--) {
81
- const t = tools[i];
82
- if (t.name.startsWith(r.prefix)) {
83
- domainAnchor = i;
84
- tools.splice(i, 1);
85
- }
86
- }
87
- const refreshedMounts = pushable.map((t) => remoteToolOffload(t));
88
- if (domainAnchor >= 0)
89
- tools.splice(domainAnchor, 0, ...refreshedMounts);
90
- else
91
- tools.push(...refreshedMounts);
105
+ const refreshedMounts = withFaces(pushable, r.server).map((t) => remoteToolOffload(t));
106
+ roster.replaceDomain(r.prefix, refreshedMounts, { source: "mcp", mountedBy: "optional", origin: { peer: r.server } });
92
107
  changed = true;
93
108
  const detail = [];
94
109
  const shownAdded = r.added.filter((n) => !excludedSet.has(n));
@@ -104,6 +119,8 @@ export async function prepareProtocolTools(input) {
104
119
  }
105
120
  if (changed)
106
121
  await rebuildHarnessToolsRef.current?.();
122
+ if (changed)
123
+ rosterSeat.republish();
107
124
  if (changed && !toolCallGateArmedRef.armed && (irreversibleTools.size > 0 || egressTools.size > 0)) {
108
125
  lines.push("WARNING: refreshed tools declare irreversible/egress safety hints, but this task started with no approval gate registered (no policy/hooks and no gated tools at start) — these hints cannot arm a gate mid-task (RB-46); the refreshed tools run ungated on this deployment shape.");
109
126
  }
@@ -115,8 +132,7 @@ export async function prepareProtocolTools(input) {
115
132
  ...(anyActiveFailure && !changed ? { isError: true } : {}),
116
133
  };
117
134
  },
118
- });
119
- toolEffects.set("RefreshMcpTools", "read");
135
+ }, { source: "builtin", mountedBy: "optional" });
120
136
  }
121
137
  const foldProtocolAxes = (axes, protocolLabel) => {
122
138
  for (const axis of axes) {
@@ -160,7 +176,8 @@ export async function prepareProtocolTools(input) {
160
176
  throw e;
161
177
  }
162
178
  }
163
- tools.push(...a2a.tools.map((t) => remoteToolOffload(t)));
179
+ for (const t of a2a.tools)
180
+ roster.mount(remoteToolOffload(t), { source: "a2a", mountedBy: "optional", origin: { peer: A2A_NAMESPACE.parse(t.name)?.peer ?? t.name } });
164
181
  foldProtocolAxes(a2a.toolAxes, "A2A");
165
182
  return { mcp, a2a, rebuildHarnessToolsRef, contentOriginWrapRef, toolCallGateArmedRef };
166
183
  }
@@ -22,6 +22,7 @@ import { type CheckpointStore } from "../checkpoint-store.js";
22
22
  import type { RunnerDeps, RuntimeCaps, TaskSpec } from "../types.js";
23
23
  import type { PrepareResume } from "./contracts.js";
24
24
  import { type ContentAskBinding } from "./content-ask-bindings.js";
25
+ import { RosterBuilder } from "../tool-roster.js";
25
26
  export { CONTENT_ASK_BINDING_CAP, type ContentAskBinding } from "./content-ask-bindings.js";
26
27
  export interface PrepareQuestionFaceInput {
27
28
  /** borrowed-readonly — the run's frozen question seat (spec > deps), resolved once at the top of prepare;
@@ -45,10 +46,10 @@ export interface PrepareQuestionFaceInput {
45
46
  resume: Pick<PrepareResume, "redeemedContentAskCallId" | "redeemedContentAskQuestionsHash"> | undefined;
46
47
  /** borrowed-readonly — the acquired session id (the tool's source task id; operator-line context). */
47
48
  sessionId: string;
48
- /** borrowed-mutable — the run's shared roster (identity is the contract). Writer here: ONE push (the question
49
+ /** borrowed-mutable — the run's roster builder (identity is the contract). Writer here: ONE mount (the question
49
50
  * tool) when the mount predicate holds; `contentAskRoutable` reads `includes` per call against the LIVE
50
- * array (the roster is edited in place after the mount). */
51
- tools: AgentTool[];
51
+ * read face (the roster is edited in place after the mount). */
52
+ roster: RosterBuilder;
52
53
  /** borrowed-readonly — LIVE GETTER over the inherited-unavailable marker set (declared by the driver's
53
54
  * gate-fold section after this phase; `has` only, per call at run time). See the module header. */
54
55
  inheritedUnavailableAsks: () => ReadonlySet<string>;
@@ -2,9 +2,11 @@ import { createAskUserQuestionTool, isLiveQuestionFace } from "../ask-question.j
2
2
  import { boundInputHashOf } from "../canonical-json.js";
3
3
  import { resolveCheckpointStore } from "../checkpoint-store.js";
4
4
  import { CONTENT_ASK_BINDING_CAP } from "./content-ask-bindings.js";
5
+ import { RosterBuilder } from "../tool-roster.js";
5
6
  export { CONTENT_ASK_BINDING_CAP } from "./content-ask-bindings.js";
6
7
  export function prepareQuestionFace(input) {
7
- const { frozenOnQuestion, spec, deps, runtimeCaps, resolvedInteractionPosture, resume, sessionId, tools, inheritedUnavailableAsks } = input;
8
+ const { frozenOnQuestion, spec, deps, runtimeCaps, resolvedInteractionPosture, resume, sessionId, roster, inheritedUnavailableAsks } = input;
9
+ const tools = roster.tools;
8
10
  const onQuestion = frozenOnQuestion;
9
11
  const liveQuestionFace = isLiveQuestionFace(onQuestion) ? onQuestion : undefined;
10
12
  const contentAskBindings = new Map();
@@ -60,7 +62,7 @@ export function prepareQuestionFace(input) {
60
62
  const questionToolMounted = spec.interactiveTools === true || (spec.interactiveTools !== false && (onQuestion !== undefined || durableQuestionFace));
61
63
  let mountedQuestionTool;
62
64
  if (questionToolMounted)
63
- tools.push((mountedQuestionTool =
65
+ roster.mount((mountedQuestionTool =
64
66
  createAskUserQuestionTool(mountedQuestionFace, { principal: spec.principal, sourceTaskId: sessionId }, {
65
67
  ...(resume?.redeemedContentAskCallId !== undefined
66
68
  ? {
@@ -79,6 +81,6 @@ export function prepareQuestionFace(input) {
79
81
  : "the wired question channel failed") +
80
82
  `) — the model was instructed to self-answer and the run CONTINUES (warning, not a failure).`), { phase: "degraded", sessionId, classification: "no-human-autoanswered" });
81
83
  },
82
- })));
84
+ })), { source: "builtin", mountedBy: "optional" });
83
85
  return { checkpointStore, liveQuestionFace, contentAskBindings, contentAskRoutable, lateStrandedAnswers, discloseStrandedAnswers, settleContentAskBindings, durableQuestionFace, mountedQuestionFace, questionToolMounted, mountedQuestionTool };
84
86
  }
@@ -86,4 +86,20 @@ export interface PrepareRunRefsResult {
86
86
  worktreeIsolation: SubagentWorktreeIsolation | undefined;
87
87
  }
88
88
  /** The M3a phase body — prepareTask's run-refs stretch, verbatim (see the module header). */
89
+ /**
90
+ * A run that opted into `forwardSubagentEvents` may have BACKGROUND children whose frames are forwarded to the
91
+ * deployment's sink from the child's own run. Those forwards are scheduled on later microtasks than the parent's
92
+ * turn, so "the parent stream ended" and "the child's already-emitted frames reached the sink" are two different
93
+ * moments — the gap is one microtask ladder deep and moves whenever the gate's station structure changes (design/393
94
+ * / #594: the three-layer gate added three async frames to a child's gated call, and a consumer that read the parent
95
+ * to `done` then inspected the sink lost the child's `tool_end`). The rule is not a tick count: **before the parent
96
+ * says `done`, every frame a child had already handed to the forward channel is delivered.** One macrotask turn is
97
+ * the smallest boundary that strictly orders after all pending microtasks; it is taken only on the opted-in run,
98
+ * only once, right before `done`.
99
+ */
100
+ export declare function forwardsSubagentEvents(spec: Pick<TaskSpec, "forwardSubagentEvents">): boolean;
101
+ /** The drain itself — one macrotask turn. Callers gate it with {@link forwardsSubagentEvents} SYNCHRONOUSLY so a
102
+ * non-forwarding run's terminal push is not one microtask later than it was (an `await` of an early-returning
103
+ * async function still yields once; the black-box round measured that tick). */
104
+ export declare function drainForwardedFramesBeforeDone(): Promise<void>;
89
105
  export declare function prepareRunRefs(input: PrepareRunRefsInput): PrepareRunRefsResult;
@@ -2,6 +2,12 @@ import { createSubagentWorktreeHelper } from "../../agents/subagent.js";
2
2
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
3
3
  import { ActiveSkillScope } from "./active-skill-scope.js";
4
4
  import { createEditedFilesLedger } from "./edited-files-ledger.js";
5
+ export function forwardsSubagentEvents(spec) {
6
+ return spec.forwardSubagentEvents === true;
7
+ }
8
+ export function drainForwardedFramesBeforeDone() {
9
+ return new Promise((resolve) => setImmediate(resolve));
10
+ }
5
11
  export function prepareRunRefs(input) {
6
12
  const { spec, internals, executionEnv, taskRootFinal } = input;
7
13
  const { note: noteFileEdited, snapshot: editedFilesSnapshot } = createEditedFilesLedger();