@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
@@ -15,7 +15,7 @@ import type { Prepared } from "./contracts.js";
15
15
  * end-of-task lane's deliberate ABSENCE of `signal`) is a legitimate per-lane difference and stays at
16
16
  * the call site, where its reason is written down.
17
17
  *
18
- * Every export is a free function: the turn-boundary lane assembles its call inside `makeTurnBoundary`
18
+ * Every export is a free function: the turn-boundary lane assembles its call inside `createTurnBoundary`
19
19
  * (a module-level function with no `this`), so a method would not be reachable from all three sites.
20
20
  * Nothing here reads or writes runner state — pure assembly over `spec` + `Prepared`.
21
21
  */
@@ -315,6 +315,14 @@ export interface Prepared {
315
315
  /** design/99 §E6 — the DENY-NARROWING layers (session rules + skill scope, deny-only). Re-checked on RESUME
316
316
  * before an approved pending tool executes, so a rule tightened during the suspend still applies. */
317
317
  denyNarrowingPolicy?: ToolPolicy;
318
+ /** #632 — the person's own persisted-rule lane (deny/ask rows), re-adjudicated on RESUME at the same
319
+ * station as the deny-narrowing layers: a deny row added while the call was parked vetoes the execution
320
+ * (`deniedBy: "persisted_rule"`); an ask row or an unreadable store fails closed the same way (the approval
321
+ * answered a question the person has since asked to be asked again — re-issue and re-approve). Absent when
322
+ * no rule store is wired. */
323
+ persistedRuleLane?: {
324
+ adjudicate: (req: ToolCallRequest) => Promise<import("../hooks.js").PersistedRuleAnswer>;
325
+ };
318
326
  /** RB-63: the deployment's own caller policy, re-checked on a durable resume ONLY when the approver
319
327
  * rewrote the pending call's args (see the composition site for why the edit case is special). */
320
328
  basePolicyForResumeEdit?: ToolPolicy;
@@ -718,7 +726,10 @@ export interface Prepared {
718
726
  * the human already adjudicated it). Same wrapping (offload + ctx) the harness runs with.
719
727
  *
720
728
  * ONE array, mutated in place; identity is the contract (every closure that resolves a name at run time
721
- * reads this exact array). WRITER TABLE (design/238 R-3) — the only stations that write it, in prepare order:
729
+ * reads this exact array). Since design/388 the array is PRIVATE to the `RosterBuilder` (tool-roster.ts)
730
+ * every station below writes through its verbs (`mount` / `mountAll` / `unmountWhere` / `replaceAt` /
731
+ * `replaceDomain`) and this field is the builder's read face. WRITER TABLE (design/238 R-3) — the only
732
+ * stations that write it, in prepare order:
722
733
  * · caps-and-workflow — MINTS it (`spec.tools` through the ctx wrap + the large-result wrapper), then pushes
723
734
  * ReportBlocked (enableBlockedReport ≠ false), ReportFindings (no caller tool of that name/alias),
724
735
  * ExitPlanMode + EnterPlanMode (enablePlanMode ∧ the plan-review face), Workflow (self-orchestration active
@@ -740,7 +751,28 @@ export interface Prepared {
740
751
  * memory session or a recorder channel); the refresh seam re-runs the same pass through `contentOriginWrapRef`
741
752
  * after a re-splice (identity-tracked, so a wrapper is never wrapped again).
742
753
  * tool-disclosure and listings only READ it (`harnessTools` is a derived list; placeholders never enter here). */
743
- tools: AgentTool[];
754
+ tools: readonly AgentTool[];
755
+ /**
756
+ * design/388 §2.5 — this leg's minted TOOL ROSTER (the same object `wiringManifest.tools` carries):
757
+ * one row per mounted instance in wire order. Minted ONCE at the prompt-assembly station (the same
758
+ * pass that projects the prompt manifest's tool rows, so the two agree by construction); the park
759
+ * station reads the gated call's identity off it, the exemption probe hands it to the host.
760
+ */
761
+ toolRoster: import("../tool-roster.js").ToolRoster;
762
+ /**
763
+ * design/388 §2.5 — the run-time delta seat: every roster change after prepare (the RefreshMcpTools
764
+ * seam) re-mints the roster here and publishes a `ToolRosterDelta`. The run loop subscribes at the
765
+ * point it emits `wiring_manifest` and forwards each delta as a `tool_roster_delta` event; until it
766
+ * does, deltas are buffered in order.
767
+ */
768
+ toolRosterDeltas: import("../tool-roster.js").ToolRosterDeltaSeat;
769
+ /**
770
+ * design/388 L4 — the leg's `tool_end.structured` projector: the engine card set ∪ the caller rows' declared
771
+ * `renderHints.resultCards`; an undeclared `details.type` is dropped and announced ONCE per leg
772
+ * (`config.tool_card_undeclared`). The ONE card port: every tool_end body projection (the live loop, the resumed
773
+ * batch, the reconcile-recovered orphan) takes it as a parameter; no module-level card set exists to fall back to.
774
+ */
775
+ structuredProjector: (result: unknown) => unknown;
744
776
  /** Name→effect map for every tool this task can call (design/44 §3). Used by the abort-path orphan
745
777
  * reconcile (design/64 §9) to make interrupted tool_results effect-aware (read/idempotent = safe to
746
778
  * repeat; write/unknown = outcome unknown). Unknown names default to `write` (conservative). */
@@ -1985,7 +2017,7 @@ export interface ParkAsk {
1985
2017
  * Grouped by lifecycle semantics per the B15 pre-recon partition. Function members start as inert
1986
2018
  * placeholders and are assigned at their ORIGINAL declaration sites (the original code could not
1987
2019
  * call them earlier either — TDZ proved it). Not on the npm face; this extraction is what enabled
1988
- * makeTurnBoundary / makeHarnessHandlers (三期, both in runtask.ts).
2020
+ * createTurnBoundary / createHarnessHandlers (三期, both in runtask.ts).
1989
2021
  */
1990
2022
  export interface RunState {
1991
2023
  telemetry: {
@@ -2331,6 +2363,17 @@ export interface HarnessHandlersDeps {
2331
2363
  * Signatures are spelled out (not `Pick<Runner, …>`), because naming the class from here would be the
2332
2364
  * very edge this seat exists to remove.
2333
2365
  */
2366
+ /**
2367
+ * The Runner's deployment deps as a LIVE read for the lanes that read them after an await (the resume ladder's
2368
+ * rungs). `Runner.deps` is not a constant: `swapModels` replaces the object, so a lane handed the object at its
2369
+ * call would read a stale catalog — and a stale anything — where the one-function ladder read `this.deps.x` at the
2370
+ * instant of the read. The seat is the Runner's own view (a getter over the private field), so `runner.deps.x`
2371
+ * in a lane is exactly `this.deps.x` in the method it came from.
2372
+ */
2373
+ export interface RunnerDepsSeat {
2374
+ /** The deps object the Runner holds RIGHT NOW — never cache it across an await. */
2375
+ readonly deps: RunnerDeps;
2376
+ }
2334
2377
  export interface RunnerSelfSeat {
2335
2378
  /** The session store this Runner was constructed over — the delegation tools acquire / release / fork through it. */
2336
2379
  readonly sessions: SessionStore;
@@ -9,13 +9,15 @@
9
9
  *
10
10
  * ## One read per adjudicated call
11
11
  *
12
- * The gate consults the org lane (`orgRules.adjudicate`) on every decision and the personal lane
13
- * (`persistedRules.admits`) only on a surviving ask, in that order. The org lane's read is therefore
12
+ * The gate consults the org lane (`orgRules.adjudicate`) and then the personal lane
13
+ * (`persistedRules.adjudicate`) on every decision, in that order (the personal lane's deny/ask rows must
14
+ * be able to tighten an allow, so it can no longer wait for a surviving ask). The org lane's read is
14
15
  * HANDED to the personal lane by `toolCallId` (taken once, then forgotten), so a governed deployment pays
15
16
  * one store query — one org-source fetch — per call, exactly as it did with two seams. An entry the
16
- * personal lane never claims (an allow, a deny) is evicted first-in-first-out past a small bound; an
17
- * evicted or never-handed entry simply costs the personal lane its own read. The handoff is a cost
18
- * device, not a semantic one: both lanes judge one snapshot, which is the honest shape for one decision.
17
+ * personal lane never claims (a call on a tool it does not speak for) is evicted first-in-first-out past
18
+ * a small bound; an evicted or never-handed entry simply costs the personal lane its own read. The
19
+ * handoff is a cost device, not a semantic one: both lanes judge one snapshot, which is the honest shape
20
+ * for one decision.
19
21
  *
20
22
  * ## Where the lanes do NOT go
21
23
  *
@@ -25,14 +27,12 @@
25
27
  */
26
28
  import type { AskRuleEvidence, ToolCallRequest } from "../tool-policy.js";
27
29
  import { persistedRuleMandateOf, type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit } from "../hooks.js";
28
- import type { PersistedAllowRule, RuleOffer, SegmentCoverage } from "../permission-rule-model.js";
30
+ import type { PersistedRule, PersistedRuleVerdict, RuleOffer, SegmentCoverage } from "../permission-rule-model.js";
29
31
  import { type OrgRuleResolution } from "../permission-rule-org.js";
30
32
  import type { PermissionRuleStoreProvider } from "../permission-rule-provider.js";
31
- /** The one SHELL tool the persisted-rule lane speaks for. */
33
+ /** The one SHELL tool the persisted-rule lane's COMMAND grammar speaks for (the rule-offer face mints for
34
+ * it alone). The PATH grammar's tools are derived from the catalog (`ruleToolGrammarOf`). */
32
35
  export declare const PERSISTED_RULE_TOOL = "Bash";
33
- /** design/382 §2.5 — the READ tool the directory-rule family speaks for (its one other admission
34
- * point, the cd-segment notch, lives inside the model's own conjunction/coverage arms). */
35
- export declare const DIRECTORY_RULE_TOOL = "Read";
36
36
  /**
37
37
  * design/252 G-2 — the evidence an INHERITED-lane ask carries.
38
38
  *
@@ -67,28 +67,35 @@ export declare function orgRevisionEvidenceOf(resolution: OrgRuleResolution, onD
67
67
  revision?: number;
68
68
  };
69
69
  /**
70
- * design/252 G-2 / design/375 — project the admitting COVERAGE SET onto the gate's rule-lane answer:
71
- * each member's canonical text plus its own add DOTS. A single rule admitting the whole command is a
72
- * one-element set; a compound admitted by the per-segment conjunction is the set of covering rules in
73
- * segment order (the model's own reporting-order contract).
70
+ * design/252 G-2 / design/375 — project the model's verdict onto the gate's rule-lane answer: the
71
+ * behavior that spoke, and each member's canonical text plus its own add DOTS. For an allow the set is
72
+ * the admitting COVERAGE SET (a single rule admitting the whole command is a one-element set; a compound
73
+ * admitted by the per-segment conjunction is the set of covering rules in segment order the model's
74
+ * own reporting-order contract); for a deny/ask it is the one reaching rule the precedence walk chose.
74
75
  *
75
76
  * `adds` on a listed rule are the LIVE adds — the store's read already dropped tombstoned dots — so
76
- * this is the set that actually admitted the call, which is the identity a later audit reconciles a
77
+ * this is the set that actually decided the call, which is the identity a later audit reconciles a
77
78
  * shadowed ask against. The sanitized display text deliberately is not one. Copied rather than
78
79
  * aliased: the evidence must not change under a store that reuses its row objects.
79
80
  */
80
- export declare function persistedRuleHitOf(admitting: readonly PersistedAllowRule[] | undefined): PersistedRuleHit | undefined;
81
+ export declare function persistedRuleHitOf(verdict: PersistedRuleVerdict | undefined): PersistedRuleHit | undefined;
81
82
  /**
82
- * design/382 §2.5-1 — the persisted-rule lane's READ arm: the call's target path in the SAME lexical
83
- * identity the rule family is defined over — absolute as spelled, relative resolved against the LIVE
84
- * tracked working directory (the very base the Read tool itself resolves against; task root when no
85
- * tracker moved), then normalized lexically, zero IO. One admission predicate
86
- * ({@link directoryRuleAdmits}), the one eligibility predicate for scope/liveness, first table-order hit
87
- * reported. Everything this lane does NOT do is structural: it clears ASKS only the read fences
88
- * (workspace containment, the sensitive-read deny face) judge inside the tool exactly as they would with
89
- * no rule anywhere, and an unresolvable path is simply no admission (fail toward asking).
83
+ * design/382 §2.5-1, widened to every path-targeting tool — the persisted-rule lane's PATH arm: the call's
84
+ * target path in the SAME lexical identity the rule family is defined over — absolute as spelled, relative
85
+ * resolved against the LIVE tracked working directory (the very base the file tools themselves resolve
86
+ * against; task root when no tracker moved), then normalized lexically, zero IO.
87
+ *
88
+ * TWO readings of the call's path slot, one per direction (design/388 B6/B17): the tightening arms judge
89
+ * the PROTECTIVE target (the object face's declaration, the catalog's for the name as the floor — reading
90
+ * the catalog for an undeclared same-name tool can only tighten a deny); the allow arm judges the DECLARED
91
+ * target (the face's OWN declaration only — a same-name tool that declares no path target is unconfirmable
92
+ * and no grant admits it). The precedence walk, the admission predicates and the Read-only allow are the
93
+ * model's (`adjudicatePersistedPathRules`). An ALLOW here clears ASKS only — the read fences (workspace
94
+ * containment, the sensitive-read deny face) judge inside the tool exactly as they would with no rule
95
+ * anywhere — and an unresolvable path is simply no verdict (fail toward asking on the allow side, no
96
+ * refusal on the tightening side: a rule about a path speaks about no other shape).
90
97
  */
91
- export declare function directoryRuleLaneAnswer(table: readonly PersistedAllowRule[], args: unknown, ctx: {
98
+ export declare function pathRuleLaneAnswer(table: readonly PersistedRule[], req: Pick<ToolCallRequest, "toolName" | "args" | "face">, ctx: {
92
99
  root: string | undefined;
93
100
  sessionId: string | undefined;
94
101
  liveCwd: string | undefined;
@@ -97,7 +104,7 @@ export declare function directoryRuleLaneAnswer(table: readonly PersistedAllowRu
97
104
  * a deployment without one keeps a decision path byte-identical to a build without the feature. */
98
105
  export interface PermissionRuleLanes {
99
106
  personal: {
100
- admits: (req: ToolCallRequest) => Promise<PersistedRuleAnswer>;
107
+ adjudicate: (req: ToolCallRequest) => Promise<PersistedRuleAnswer>;
101
108
  } | undefined;
102
109
  org: {
103
110
  adjudicate: (req: ToolCallRequest) => Promise<OrgGateVerdict>;
@@ -1,8 +1,8 @@
1
1
  import { persistedRuleMandateOf } from "../hooks.js";
2
- import { directoryRuleAdmits, eligiblePersisted, findAdmittingRule, lexicalNormalAbsolutePathOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
2
+ import { adjudicatePersistedPathRules, adjudicatePersistedRules, lexicalNormalAbsolutePathOf, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
3
+ import { declaredPathTargetOf, pathTargetValue, protectivePathTargetOf } from "../tool-registry.js";
3
4
  import { orgRuleVerdictFor } from "../permission-rule-org.js";
4
5
  export const PERSISTED_RULE_TOOL = "Bash";
5
- export const DIRECTORY_RULE_TOOL = "Read";
6
6
  const HANDOFF_BOUND = 256;
7
7
  export function inheritedAskRuleEvidence(provider) {
8
8
  const org = provider?.partitions.org === true ? "not_adjudicated" : "not_wired";
@@ -19,21 +19,26 @@ export function orgRevisionEvidenceOf(resolution, onDefect) {
19
19
  `the adjudication stands, but the ask carries no revision evidence for this call`);
20
20
  return {};
21
21
  }
22
- export function persistedRuleHitOf(admitting) {
23
- return admitting === undefined
22
+ export function persistedRuleHitOf(verdict) {
23
+ return verdict === undefined
24
24
  ? undefined
25
- : { rules: admitting.map((r) => ({ rule: r.rule, dots: r.adds.map((a) => ({ actor: a.dot.actor, counter: a.dot.counter })) })) };
25
+ : { behavior: verdict.behavior, rules: verdict.rules.map((r) => ({ rule: r.rule, dots: r.adds.map((a) => ({ actor: a.dot.actor, counter: a.dot.counter })) })) };
26
26
  }
27
- export function directoryRuleLaneAnswer(table, args, ctx) {
28
- const filePath = args?.file_path;
29
- if (typeof filePath !== "string" || filePath === "")
30
- return undefined;
31
- const spelled = filePath.startsWith("/") ? filePath : `${(ctx.liveCwd ?? ctx.root ?? "").replace(/\/+$/, "")}/${filePath}`;
32
- const target = lexicalNormalAbsolutePathOf(spelled);
33
- if (target === undefined)
27
+ export function pathRuleLaneAnswer(table, req, ctx) {
28
+ const base = (ctx.liveCwd ?? ctx.root ?? "").replace(/\/+$/, "");
29
+ const resolve = (pt) => {
30
+ if (pt === undefined)
31
+ return undefined;
32
+ const spelled = pathTargetValue(req.args, pt);
33
+ if (spelled === undefined)
34
+ return undefined;
35
+ return lexicalNormalAbsolutePathOf(spelled.startsWith("/") ? spelled : `${base}/${spelled}`);
36
+ };
37
+ const tighten = resolve(protectivePathTargetOf(req));
38
+ const allow = resolve(declaredPathTargetOf(req));
39
+ if (tighten === undefined && allow === undefined)
34
40
  return undefined;
35
- const hit = table.find((r) => eligiblePersisted(r, { tool: DIRECTORY_RULE_TOOL, cwd: ctx.root, sessionId: ctx.sessionId }) && directoryRuleAdmits(r, target));
36
- return hit !== undefined ? persistedRuleHitOf([hit]) : undefined;
41
+ return persistedRuleHitOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) }, { ...(ctx.root !== undefined ? { root: ctx.root } : {}), ...(ctx.liveCwd !== undefined ? { cwd: ctx.liveCwd } : {}) }));
37
42
  }
38
43
  export function createPermissionRuleLanes(cfg) {
39
44
  const provider = cfg.provider;
@@ -103,10 +108,11 @@ export function createPermissionRuleLanes(cfg) {
103
108
  : undefined;
104
109
  const personal = provider.partitions.durable
105
110
  ? {
106
- admits: async (req) => {
111
+ adjudicate: async (req) => {
107
112
  if (anonymous && !cfg.localOwnerDeclared)
108
113
  return undefined;
109
- if (req.toolName !== PERSISTED_RULE_TOOL && req.toolName !== DIRECTORY_RULE_TOOL)
114
+ const grammar = ruleToolGrammarOf(req.toolName);
115
+ if (grammar === undefined)
110
116
  return undefined;
111
117
  let view;
112
118
  try {
@@ -122,16 +128,16 @@ export function createPermissionRuleLanes(cfg) {
122
128
  }
123
129
  const table = view.rules;
124
130
  const liveCwd = cfg.liveCwd();
125
- if (req.toolName === DIRECTORY_RULE_TOOL) {
126
- return directoryRuleLaneAnswer(table, req.args, { root: cfg.root, sessionId: cfg.sessionId, liveCwd });
131
+ if (grammar === "path") {
132
+ return pathRuleLaneAnswer(table, req, { root: cfg.root, sessionId: cfg.sessionId, liveCwd });
127
133
  }
128
134
  const command = req.args?.command;
129
135
  if (typeof command !== "string")
130
136
  return undefined;
131
137
  const execCwd = liveCwd !== undefined ? { execCwd: liveCwd } : {};
132
- const admitting = findAdmittingRule(table, { tool: req.toolName, command, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
133
- if (admitting !== undefined)
134
- return persistedRuleHitOf(admitting);
138
+ const verdict = adjudicatePersistedRules(table, { tool: req.toolName, command, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
139
+ if (verdict !== undefined)
140
+ return persistedRuleHitOf(verdict);
135
141
  const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
136
142
  return coverage !== undefined ? { segmentCoverage: coverage } : undefined;
137
143
  },
@@ -46,7 +46,7 @@ export interface PrepareAskLaneInput {
46
46
  handsCwdRef: CwdRef | undefined;
47
47
  /** borrowed-readonly — the shared mount roster; read at RUN time by the approval-preview projection (alias-aware
48
48
  * lookup over whatever the roster holds when the ask is minted). Never mutated here. */
49
- tools: AgentTool[];
49
+ tools: readonly AgentTool[];
50
50
  /** borrowed-mutable — the inherited-unavailable marker set. Writer here: `resolveAskBound` consumes (`delete`) a
51
51
  * marked call at its fail-closed intercept; the policy-chain phase records, the gate station sweeps. */
52
52
  inheritedUnavailableAsks: Set<string>;
@@ -34,6 +34,7 @@ import type { AutoModeArmReason } from "../wiring-manifest.js";
34
34
  import type { ReadFace } from "../../tools/fs/index.js";
35
35
  import type { InheritedGate, Prepared, RunInternals, RunnerSelfSeat, ToolFaceSnapshot } from "./contracts.js";
36
36
  import { type BlockedRef } from "./synthetic-tools.js";
37
+ import { RosterBuilder } from "../tool-roster.js";
37
38
  export interface PrepareCapsAndWorkflowInput {
38
39
  /** borrowed-readonly — the REBOUND spec, whole: the roster map reads `tools`; the mounts read `enableBlockedReport`,
39
40
  * `enablePlanMode`, `interactiveTools`, `checkpointStore`; the caps/compliance stations read `principal`, `selfOrchestration`,
@@ -124,10 +125,10 @@ export interface PrepareCapsAndWorkflowInput {
124
125
  }>;
125
126
  }
126
127
  export interface PrepareCapsAndWorkflowResult {
127
- /** owned — THE run's roster, minted here from the caller's tools; every later mount pushes into this same array
128
- * (`Prepared.tools`, whose JSDoc carries the writer table). Writers here: the map that mints it, then the ReportBlocked,
129
- * ReportFindings, ExitPlanMode, EnterPlanMode and Workflow pushes. */
130
- tools: AgentTool[];
128
+ /** owned — THE run's roster BUILDER (design/388: the mount array is private to it; `Prepared.tools` is its read face,
129
+ * whose JSDoc carries the writer table). Minted here from the caller's tools; every later mount goes through its verbs.
130
+ * Writers here: the caller mounts, then the ReportBlocked, ReportFindings, ExitPlanMode, EnterPlanMode and Workflow mounts. */
131
+ roster: RosterBuilder;
131
132
  /** borrowed-mutable — the ReportBlocked tool's write seat (`Prepared.blockedRef`). Writer: the tool at run time; read at
132
133
  * result assembly. */
133
134
  blockedRef: BlockedRef;
@@ -2,7 +2,7 @@ import { isSyntheticApiErrorMessage } from "../../internal/harness.js";
2
2
  import { createHash } from "node:crypto";
3
3
  import { forkGovernanceDenial } from "../../agents/subagent.js";
4
4
  import { CROSS_SESSION_CLASSIFIER_RULE } from "../../agents/cross-session-envelope.js";
5
- import { createRunWorkflowTool, RUN_WORKFLOW_TOOL_NAME } from "../../orchestration/run-workflow-tool.js";
5
+ import { createRunWorkflowTool } from "../../orchestration/run-workflow-tool.js";
6
6
  import { isSelfOrchestrationActive } from "../../orchestration/workflow-script-runner.js";
7
7
  import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
8
8
  import { thinkingOffExpressible } from "../../brain/reasoning.js";
@@ -20,6 +20,7 @@ import { emitTrace } from "../trace.js";
20
20
  import { defineTool, isDefineToolProduct } from "../tools.js";
21
21
  import { derivedRouteFallsBack } from "./derived-route-fallback.js";
22
22
  import { REPORT_FINDINGS_TOOL_NAME, createReportBlockedTool, createReportFindingsTool } from "./synthetic-tools.js";
23
+ import { RosterBuilder, callerMountSource } from "../tool-roster.js";
23
24
  function assembleParentCaptureState(o, i, ctl, ancestors) {
24
25
  const build = (optedOut, indeterminate) => ({
25
26
  optedOut: optedOut === true,
@@ -65,7 +66,12 @@ function autoModeArmReasonOf(intent, facePresent, capsAutoMode, capsFaulted) {
65
66
  }
66
67
  export async function prepareCapsAndWorkflow(input) {
67
68
  const { spec, deps, internals, session, sessionId, runId, hostTaskId, taskScope, taskRootFinal, model, thinking, lockedPreflight, toolEffects, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, harnessRef, frozenOnAsk, carrierReadFace, forwardEvent, inheritedGateForChildren, enrichSpecToolCtx, maybeOffload, requestReview, enterPlanMode, autoModeIntent, peerLaneActive, peerSendMessageBuiltIn, runnerSelf } = input;
68
- const tools = (spec.tools ?? []).map((t) => {
69
+ const roster = new RosterBuilder(toolEffects);
70
+ for (const t of spec.tools ?? []) {
71
+ const mounted = mountCallerTool(t);
72
+ roster.mount(mounted, { source: callerMountSource(mounted), mountedBy: "scenario" });
73
+ }
74
+ function mountCallerTool(t) {
69
75
  if (isDefineToolProduct(t)) {
70
76
  return maybeOffload(t, t);
71
77
  }
@@ -73,20 +79,20 @@ export async function prepareCapsAndWorkflow(input) {
73
79
  ...t,
74
80
  execute: (args, ctx) => t.execute(args, enrichSpecToolCtx(ctx)),
75
81
  }), t);
76
- });
82
+ }
77
83
  const blockedRef = {};
78
84
  if (spec.enableBlockedReport !== false) {
79
- tools.push(createReportBlockedTool(blockedRef));
85
+ roster.mount(createReportBlockedTool(blockedRef), { source: "builtin", mountedBy: "always" });
80
86
  }
81
87
  if (!(spec.tools ?? []).some((t) => t.name === REPORT_FINDINGS_TOOL_NAME || t.aliases?.includes(REPORT_FINDINGS_TOOL_NAME))) {
82
- tools.push(createReportFindingsTool());
88
+ roster.mount(createReportFindingsTool(), { source: "builtin", mountedBy: "always" });
83
89
  }
84
90
  if (spec.enablePlanMode === true && spec.interactiveTools !== false) {
85
91
  const planReviewFace = resolveCheckpointStore(spec, deps) !== undefined;
86
92
  if (spec.interactiveTools === true || planReviewFace) {
87
- tools.push(defineTool(createPresentPlanTool(requestReview)));
93
+ roster.mount(defineTool(createPresentPlanTool(requestReview)), { source: "builtin", mountedBy: "optional" });
88
94
  if (planReviewFace) {
89
- tools.push(defineTool(createEnterPlanModeTool(enterPlanMode)));
95
+ roster.mount(defineTool(createEnterPlanModeTool(enterPlanMode)), { source: "builtin", mountedBy: "optional" });
90
96
  }
91
97
  }
92
98
  }
@@ -210,8 +216,7 @@ export async function prepareCapsAndWorkflow(input) {
210
216
  workflowToolsActive = true;
211
217
  const currentSizeGuideline = () => resolveWorkflowSizeGuideline(deps.workflowLimits?.sizeGuideline).size;
212
218
  workflowSizeGuideline = { legGuideline: currentSizeGuideline(), current: currentSizeGuideline };
213
- toolEffects.set(RUN_WORKFLOW_TOOL_NAME, "write");
214
- tools.push(await createRunWorkflowTool({
219
+ roster.mount(await createRunWorkflowTool({
215
220
  runner: runnerSelf,
216
221
  scriptRunner: deps.workflowScriptRunner,
217
222
  governanceBaseline: deps.workflowGovernanceBaseline,
@@ -266,7 +271,7 @@ export async function prepareCapsAndWorkflow(input) {
266
271
  parentCenterSourceRevision: () => input.centerAdoption()?.sourceRevision,
267
272
  ...(forwardEvent ? { forwardEvent } : {}),
268
273
  inheritedGateForChildren,
269
- }));
274
+ }), { source: "builtin", mountedBy: "workflow" });
270
275
  }
271
- return { tools, blockedRef, runtimeCaps, runtimeCapsFaulted, complianceDenies, complianceDegraded, agentForkDenial, observersActive, autoModeArmReason, autoModeDecider, autoModeDenialTracking, autoModeArming, selfOrchestrationActive, workflowToolsActive, workflowSizeGuideline };
276
+ return { roster, blockedRef, runtimeCaps, runtimeCapsFaulted, complianceDenies, complianceDegraded, agentForkDenial, observersActive, autoModeArmReason, autoModeDecider, autoModeDenialTracking, autoModeArming, selfOrchestrationActive, workflowToolsActive, workflowSizeGuideline };
272
277
  }
@@ -1,43 +1,22 @@
1
- /**
2
- * design/390 M11 — prepareTask's TOOL-FACE FOLD + DEFERRED CLASSIFICATION phase: the exclusion valve's
3
- * true unmount (in place, on the shared mount array), the full-shell anti-drift invariant, the classic-
4
- * profile description swap, `classifyDeferredOverFace` (the one classifier both counterfactual passes
5
- * run), the built-in memory pairs' support-name pre-check (sole-cause counterfactual, whole-group
6
- * retraction, the engine pair's recall paragraph stripped back out of the memory block) and the final
7
- * `deferred` set. The body is the driver's stretch moved verbatim (one exception, named below); the
8
- * interface is the dependency list it had implicitly (design/238 R-1).
9
- *
10
- * SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await, so the phase adds no yield between
11
- * the git-status probe before it and the content-origin wrap after it (design/390 §1.5 S1: a stretch
12
- * with no await is extracted as a sync function — an `async` wrapper would open a microtask window).
13
- *
14
- * The one non-move: the stretch used to write `sharedMemoryPairMounted = false` /
15
- * `memoryEnginePairMounted = false` on the retraction arm. Nothing reads either flag after this stretch
16
- * (the driver's next reader was this stretch's own pair derivation, which runs first), so the two dead
17
- * writes are not carried into the Result — the flags arrive as borrowed-readonly inputs.
18
- *
19
- * Throws pass through unchanged: `internal.full_shell_reachable_mismatch` (an engine invariant, never a
20
- * configuration error). The reserved-name refusal for a standing ToolSearch collision is NOT here — it
21
- * belongs to the disclosure mount downstream, which reads the `deferred` set this phase returns.
22
- */
23
- import type { AgentTool } from "../../internal/harness.js";
24
1
  import type { Model } from "../../internal/llm.js";
25
2
  import type { MaterializedA2a } from "../a2a.js";
26
3
  import type { MaterializedMcp } from "../mcp.js";
27
4
  import type { RunnerDeps, TaskSpec } from "../types.js";
28
5
  import type { AnnounceOnceSink, ToolFaceSnapshot } from "./contracts.js";
6
+ import { RosterBuilder } from "../tool-roster.js";
29
7
  export interface PrepareDeferClassifyInput {
30
8
  /** borrowed-readonly — the REBOUND spec (`spec′`); only `tools` is read (the caller roster the
31
9
  * classifier judges: names for the full-schema filter, specs for the `defer` declarations). */
32
- spec: Pick<TaskSpec, "tools">;
10
+ spec: Pick<TaskSpec, "tools" | "excludeAllTools">;
33
11
  /** borrowed-readonly — deployment deps; only `deferMode` is read (the classifier's mode knob). */
34
12
  deps: Pick<RunnerDeps, "deferMode">;
35
13
  /** borrowed-mutable — the shared mount array (later refs alias it; in-place contract, never
36
14
  * re-created). Mutated here, in order: `splice` of every excluded name; element REPLACEMENT with a
37
15
  * shallow copy for the classic-profile description swap (the caller's ToolSpec object is never
38
16
  * mutated); `splice` of a retracted memory group. After this phase: the content-origin wrap
39
- * (element replacement with the wrapped object), the disclosure mount, the MCP refresh re-splice. */
40
- tools: AgentTool[];
17
+ * (element replacement with the wrapped object), the disclosure mount, the MCP refresh re-splice. Since
18
+ * design/388 every one of those writes is a builder verb (`unmountWhere` / `replaceAt`). */
19
+ roster: RosterBuilder;
41
20
  /** borrowed-readonly — the frozen task-start tool face: `exclude` (the valve), `defer` (operator
42
21
  * defers, exact wire names), `alwaysLoad` (the inline pins). */
43
22
  toolFaceSnapshot: Pick<ToolFaceSnapshot, "exclude" | "defer" | "alwaysLoad">;
@@ -4,6 +4,7 @@ import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js";
4
4
  import { MEMORY_ENGINE_TOOL_NAMES } from "../memory-engine/tools.js";
5
5
  import { SHARED_MEMORY_TOOL_NAMES } from "../shared-memory/types.js";
6
6
  import { TOOL_SEARCH_NAME, classifyDeferred } from "./tool-disclosure.js";
7
+ import { RosterBuilder } from "../tool-roster.js";
7
8
  function explicitlyDeferredMemoryTrio(mounted, roster, deferNames) {
8
9
  return mounted ? MEMORY_ENGINE_TOOL_NAMES.filter((n) => roster.some((t) => t.name === n) && (deferNames ?? []).includes(n)) : [];
9
10
  }
@@ -11,13 +12,15 @@ function memoryGroupRetractionSet(builtinDeferPairNames, engineTrioInPlay) {
11
12
  return new Set([...builtinDeferPairNames, ...(engineTrioInPlay ? MEMORY_ENGINE_TOOL_NAMES : [])]);
12
13
  }
13
14
  export function prepareDeferClassify(input) {
14
- const { spec, deps, tools, toolFaceSnapshot, handsEnabled, fullShellReachable, promptProfile, model, mcp, a2a, sharedMemoryPairMounted, memoryEnginePairMounted, memoryRecallSegment, onceLedger, sessionId } = input;
15
+ const { spec, deps, roster, toolFaceSnapshot, handsEnabled, fullShellReachable, promptProfile, model, mcp, a2a, sharedMemoryPairMounted, memoryEnginePairMounted, memoryRecallSegment, onceLedger, sessionId } = input;
16
+ const tools = roster.tools;
15
17
  let memoryBlock = input.memoryBlock;
16
- if (toolFaceSnapshot.exclude !== undefined && toolFaceSnapshot.exclude.length > 0) {
18
+ if (spec.excludeAllTools === true) {
19
+ roster.unmountWhere(() => true);
20
+ }
21
+ else if (toolFaceSnapshot.exclude !== undefined && toolFaceSnapshot.exclude.length > 0) {
17
22
  const excluded = new Set(toolFaceSnapshot.exclude);
18
- for (let i = tools.length - 1; i >= 0; i--)
19
- if (excluded.has(tools[i].name))
20
- tools.splice(i, 1);
23
+ roster.unmountWhere((t) => excluded.has(t.name));
21
24
  }
22
25
  if (handsEnabled) {
23
26
  const fullShellOnRoster = tools.some((t) => t.name === "Bash" && getToolContract(t, "core").contractId === FULL_SHELL_CONTRACT_ID);
@@ -33,7 +36,7 @@ export function prepareDeferClassify(input) {
33
36
  for (let i = 0; i < tools.length; i++) {
34
37
  const t = tools[i];
35
38
  if (t.descriptionClassic !== undefined)
36
- tools[i] = { ...t, description: t.descriptionClassic };
39
+ roster.replaceAt(i, { ...t, description: t.descriptionClassic });
37
40
  }
38
41
  }
39
42
  const userToolNames = (spec.tools ?? []).map((t) => t.name);
@@ -73,10 +76,7 @@ export function prepareDeferClassify(input) {
73
76
  const supportNameTaken = tools.some((t) => t.name === TOOL_SEARCH_NAME || (t.aliases ?? []).includes(TOOL_SEARCH_NAME));
74
77
  if (soleCause && supportNameTaken) {
75
78
  const retractNames = memoryGroupRetractionSet(builtinDeferPairNames, memoryEnginePair.length > 0);
76
- for (let i = tools.length - 1; i >= 0; i--) {
77
- if (retractNames.has(tools[i].name))
78
- tools.splice(i, 1);
79
- }
79
+ roster.unmountWhere((t) => retractNames.has(t.name));
80
80
  if (sharedMemoryPair.length > 0) {
81
81
  onceLedger.onError(new Error(`Shared memory tools ${SHARED_MEMORY_TOOL_NAMES.join("/")} were NOT mounted: mounting them would inject ` +
82
82
  `the "${TOOL_SEARCH_NAME}" tool, whose name this task already declares — the pair mounts together or not at all.`), { phase: "config", sessionId, classification: "shared-memory-not-mounted" });
@@ -6,6 +6,7 @@ import type { CwdRef } from "../../tools/fs/index.js";
6
6
  import { type ToolResultStore } from "../tool-result-store.js";
7
7
  import type { RunnerDeps, TaskSpec, ToolEffect, ToolExecuteContext } from "../types.js";
8
8
  import type { PrepareResume, RunInternals, RunnerSelfSeat, ToolFaceSnapshot } from "./contracts.js";
9
+ import { RosterBuilder } from "../tool-roster.js";
9
10
  export interface PrepareDelegationSurfaceInput {
10
11
  /** borrowed-readonly — the REBOUND spec. Read: `tools` (the shadow checks and the revival spawner's delegation-tool
11
12
  * lookup), `oneShot` (the read/kill faces' completion promises), `retainBackgroundProcesses` (the Monitor receipt),
@@ -44,10 +45,10 @@ export interface PrepareDelegationSurfaceInput {
44
45
  /** borrowed-mutable — the lane's late-bound facts; the SendMessage mount hands the SAME object to its peer seats (identity is
45
46
  * the contract: the wiring station fills `askEffective` later). Not written here. */
46
47
  peerLaneRefs: PeerLaneRefs;
47
- /** borrowed-mutable — the run's shared roster (`Prepared.tools`, whose JSDoc carries the writer table). Writers here: the
48
- * eight mounts (TaskOutput, TaskStop, SendMessage, AgentTranscript, ListAgents, Monitor, the worktree pair, the offload
49
- * reader), every one a push. */
50
- tools: AgentTool[];
48
+ /** borrowed-mutable — the run's roster builder (`Prepared.tools` is its read face; the writer table is on that field).
49
+ * Writers here: the eight mounts (TaskOutput, TaskStop, SendMessage, AgentTranscript, ListAgents, Monitor, the worktree
50
+ * pair, the offload reader), every one a `mount`. */
51
+ roster: RosterBuilder;
51
52
  /** borrowed-mutable — the effect collection (`Prepared.toolEffects`). Writers here: TaskOutput → read, TaskStop / SendMessage
52
53
  * (built-in mount only) / Monitor / EnterWorktree / ExitWorktree → write. */
53
54
  toolEffects: Map<string, ToolEffect>;
@@ -8,6 +8,7 @@ import { createTaskOutputTool, createTaskStopTool, defaultTaskRegistry } from ".
8
8
  import { createReadToolResultTool } from "../tool-result-store.js";
9
9
  import { emitTrace } from "../trace.js";
10
10
  import { deliverEngineNotice } from "../types.js";
11
+ import { RosterBuilder } from "../tool-roster.js";
11
12
  function buildTier3ReviveSpawn(delegationForRevive, enrichSpecToolCtx) {
12
13
  if (delegationForRevive === undefined)
13
14
  return undefined;
@@ -29,21 +30,21 @@ function buildTier3ReviveSpawn(delegationForRevive, enrichSpecToolCtx) {
29
30
  };
30
31
  }
31
32
  export function prepareDelegationSurface(input) {
32
- const { spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, executionEnv, runnerSelf, backgroundTaskToolsActive, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, tools, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle } = input;
33
+ const { spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, executionEnv, runnerSelf, backgroundTaskToolsActive, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, roster, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle } = input;
33
34
  let worktreeSessionRef;
34
35
  const delegationSurfaceActive = backgroundTaskToolsActive || workflowToolsActive;
35
36
  if (delegationSurfaceActive || internals?.parentNotify !== undefined || peerLaneActive) {
36
37
  if (delegationSurfaceActive) {
37
- toolEffects.set("TaskOutput", "read");
38
- toolEffects.set("TaskStop", "write");
39
- tools.push(firstPartyOffload(createTaskOutputTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore, notificationWired: internals?.onTaskNotification !== undefined, oneShot: spec.oneShot, toolResultStore: offloadStore })), firstPartyOffload(createTaskStopTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore })));
38
+ roster.mountAll([
39
+ firstPartyOffload(createTaskOutputTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore, notificationWired: internals?.onTaskNotification !== undefined, oneShot: spec.oneShot, toolResultStore: offloadStore })),
40
+ firstPartyOffload(createTaskStopTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore })),
41
+ ], { source: "builtin", mountedBy: "delegation" });
40
42
  }
41
43
  if (runnerSelf && !(spec.tools ?? []).some((t) => t.name === SEND_MESSAGE_TOOL_NAME)) {
42
- toolEffects.set(SEND_MESSAGE_TOOL_NAME, "write");
43
44
  axisExplicitNegatives.set(SEND_MESSAGE_TOOL_NAME, { ...axisExplicitNegatives.get(SEND_MESSAGE_TOOL_NAME), egress: false });
44
45
  const delegationForRevive = (spec.tools ?? []).find((t) => t.agentListing !== undefined);
45
46
  const reviveSpawn = deps.backgroundAgentStore !== undefined && deps.mailboxStore !== undefined ? buildTier3ReviveSpawn(delegationForRevive, enrichSpecToolCtx) : undefined;
46
- tools.push(firstPartyOffload(createSendMessageTool({
47
+ roster.mount(firstPartyOffload(createSendMessageTool({
47
48
  runner: runnerSelf,
48
49
  registry: defaultTaskRegistry,
49
50
  ...(subagentRetain ? { retain: subagentRetain } : {}),
@@ -76,9 +77,9 @@ export function prepareDelegationSurface(input) {
76
77
  message: `delegation transcript integrity: agent ${handle}'s durable row binds a transcript session the session store attests is gone — the declared transcript durability is being contradicted (check the session store wiring/retention)`,
77
78
  detail: { handle, ...(scope !== undefined ? { scope } : {}) },
78
79
  }),
79
- })));
80
+ })), { source: "builtin", mountedBy: delegationSurfaceActive ? "delegation" : internals?.parentNotify !== undefined ? "hostInternals" : "peerLane" });
80
81
  if (delegationSurfaceActive && !(spec.tools ?? []).some((t) => t.name === AGENT_TRANSCRIPT_TOOL_NAME)) {
81
- tools.push(firstPartyOffload(createAgentTranscriptTool({
82
+ roster.mount(firstPartyOffload(createAgentTranscriptTool({
82
83
  runner: runnerSelf,
83
84
  registry: defaultTaskRegistry,
84
85
  agentStore: deps.backgroundAgentStore,
@@ -91,17 +92,16 @@ export function prepareDelegationSurface(input) {
91
92
  message: `delegation transcript integrity: agent ${handle}'s durable row binds a transcript session the session store attests is gone — the declared transcript durability is being contradicted (check the session store wiring/retention)`,
92
93
  detail: { handle, ...(scope !== undefined ? { scope } : {}) },
93
94
  }),
94
- })));
95
+ })), { source: "builtin", mountedBy: "delegation" });
95
96
  }
96
97
  }
97
98
  }
98
99
  const listAgents = peerLaneActive && peerSendMessageBuiltIn && listAgentsMountable({ exclude: toolFaceSnapshot.exclude, specTools: spec.tools ?? [] }) ? mountListAgents({ peerDirectory: deps.peerDirectory, sessionId, scope: taskScope, hostTaskId, ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}), ...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}), registry: defaultTaskRegistry, ...(deps.rosterStore !== undefined ? { roster: deps.rosterStore } : {}), specTools: spec.tools ?? [], toolEffects }) : undefined;
99
100
  if (listAgents !== undefined)
100
- tools.push(firstPartyOffload(listAgents));
101
+ roster.mount(firstPartyOffload(listAgents), { source: "builtin", mountedBy: "peerLane" });
101
102
  if (backgroundTaskToolsActive) {
102
- toolEffects.set("Monitor", "write");
103
103
  envHandToolNames.add("Monitor");
104
- tools.push(firstPartyOffload(createMonitorTool(executionEnv, {
104
+ roster.mount(firstPartyOffload(createMonitorTool(executionEnv, {
105
105
  registry: defaultTaskRegistry,
106
106
  owner: hostTaskId,
107
107
  scope: taskScope,
@@ -110,11 +110,9 @@ export function prepareDelegationSurface(input) {
110
110
  ...(handsCwdRef !== undefined ? { cwdRef: handsCwdRef } : {}),
111
111
  ...(offloadStore !== undefined ? { toolResultStore: offloadStore } : {}),
112
112
  ...(spec.retainBackgroundProcesses === true ? { retainBackgroundProcesses: true } : {}),
113
- })));
113
+ })), { source: "builtin", mountedBy: "backgroundTasks" });
114
114
  }
115
115
  if (handsCwdRef !== undefined) {
116
- toolEffects.set("EnterWorktree", "write");
117
- toolEffects.set("ExitWorktree", "write");
118
116
  worktreeSessionRef = {
119
117
  ...(resume?.seed.activeWorktree
120
118
  ? {
@@ -136,9 +134,9 @@ export function prepareDelegationSurface(input) {
136
134
  };
137
135
  if (workspaceStateSettle !== undefined)
138
136
  workspaceStateSettle.restoredWorktreeDir = worktreeSessionRef.current?.worktreeDir;
139
- tools.push(...createWorktreeTools(executionEnv, { repoRoot: taskRootFinal, cwdRef: handsCwdRef, session: worktreeSessionRef }).map((t) => (envHandToolNames.add(t.name), firstPartyOffload(t))));
137
+ roster.mountAll(createWorktreeTools(executionEnv, { repoRoot: taskRootFinal, cwdRef: handsCwdRef, session: worktreeSessionRef }).map((t) => (envHandToolNames.add(t.name), firstPartyOffload(t))), { source: "builtin", mountedBy: "hands" });
140
138
  }
141
139
  if (offloadStore)
142
- tools.push(createReadToolResultTool(offloadStore));
140
+ roster.mount(createReadToolResultTool(offloadStore), { source: "builtin", mountedBy: "optional" });
143
141
  return { delegationSurfaceActive, worktreeSessionRef };
144
142
  }