@sema-agent/core 7.34.0 → 7.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +156 -3
  2. package/dist/brain/errors.d.ts +23 -1
  3. package/dist/brain/errors.js +19 -2
  4. package/dist/brain/status-sink.d.ts +7 -1
  5. package/dist/brain/stream-shared.d.ts +3 -1
  6. package/dist/brain/stream-shared.js +16 -4
  7. package/dist/core/ask-origin.d.ts +34 -6
  8. package/dist/core/ask-origin.js +24 -23
  9. package/dist/core/ask-question.js +2 -1
  10. package/dist/core/checkpoint-store.d.ts +4 -0
  11. package/dist/core/gate-fold.js +2 -2
  12. package/dist/core/gate-lanes.js +5 -4
  13. package/dist/core/governance-codes.d.ts +17 -1
  14. package/dist/core/governance-codes.js +11 -1
  15. package/dist/core/hooks.d.ts +3 -0
  16. package/dist/core/message-utils.d.ts +2 -1
  17. package/dist/core/message-utils.js +2 -1
  18. package/dist/core/mode-policy.d.ts +3 -0
  19. package/dist/core/mode-policy.js +5 -0
  20. package/dist/core/permission-rule-model.d.ts +4 -0
  21. package/dist/core/permission-rule-model.js +4 -0
  22. package/dist/core/permission-rules.js +21 -1
  23. package/dist/core/persisted-rule-arms.d.ts +4 -1
  24. package/dist/core/persisted-rule-arms.js +4 -0
  25. package/dist/core/retired-keys.js +1 -1
  26. package/dist/core/runner/active-skill-scope.js +5 -4
  27. package/dist/core/runner/assemble-result.js +2 -2
  28. package/dist/core/runner/contracts.d.ts +26 -4
  29. package/dist/core/runner/denial-limit-arms.d.ts +6 -1
  30. package/dist/core/runner/denial-limit-arms.js +4 -4
  31. package/dist/core/runner/file-history-request.d.ts +26 -0
  32. package/dist/core/runner/file-history-request.js +90 -0
  33. package/dist/core/runner/permission-rule-lanes.d.ts +2 -1
  34. package/dist/core/runner/permission-rule-lanes.js +14 -11
  35. package/dist/core/runner/prepare-ask-lane.d.ts +1 -1
  36. package/dist/core/runner/prepare-ask-lane.js +2 -2
  37. package/dist/core/runner/prepare-file-history.d.ts +9 -3
  38. package/dist/core/runner/prepare-file-history.js +4 -90
  39. package/dist/core/runner/prepare-gate-stations.d.ts +4 -3
  40. package/dist/core/runner/prepare-gate-stations.js +15 -11
  41. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  42. package/dist/core/runner/prepare-inherited-gate.js +10 -4
  43. package/dist/core/runner/prepare-park-ask.d.ts +4 -2
  44. package/dist/core/runner/prepare-park-ask.js +5 -4
  45. package/dist/core/runner/prepare-policy-chain.d.ts +3 -2
  46. package/dist/core/runner/prepare-policy-chain.js +171 -175
  47. package/dist/core/runner/prepare-task.d.ts +3 -1
  48. package/dist/core/runner/prepare-task.js +9 -5
  49. package/dist/core/runner/run-brain-sinks.js +3 -1
  50. package/dist/core/runner/run-leg.js +5 -3
  51. package/dist/core/runner/run-notification-lane.d.ts +5 -0
  52. package/dist/core/runner/run-notification-lane.js +3 -2
  53. package/dist/core/runner/runtask.d.ts +3 -1
  54. package/dist/core/runner/runtask.js +20 -16
  55. package/dist/core/runner/session-mode-seats.d.ts +48 -0
  56. package/dist/core/runner/session-mode-seats.js +73 -0
  57. package/dist/core/runner/session-rule-policy.js +3 -2
  58. package/dist/core/secret-env.d.ts +30 -58
  59. package/dist/core/secret-env.js +149 -586
  60. package/dist/core/sensitive-path-policy.js +2 -1
  61. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
  62. package/dist/core/tool-policy.d.ts +31 -6
  63. package/dist/core/tool-policy.js +35 -16
  64. package/dist/core/tool-registry.d.ts +36 -0
  65. package/dist/core/tool-registry.js +22 -1
  66. package/dist/core/tool-roster.d.ts +17 -2
  67. package/dist/core/tool-roster.js +11 -1
  68. package/dist/core/trace.d.ts +11 -0
  69. package/dist/engine/execution-env/node-execution-env.d.ts +3 -1
  70. package/dist/engine/execution-env/node-execution-env.js +5 -2
  71. package/dist/engine/harness/agent-harness.js +2 -1
  72. package/dist/engine/lsp/node-lsp-manager.js +1 -1
  73. package/dist/index.d.ts +4 -4
  74. package/dist/index.js +4 -4
  75. package/dist/stores/file/index.d.ts +5 -3
  76. package/dist/stores/file/session-store.d.ts +8 -2
  77. package/dist/stores/file/session-store.js +5 -5
  78. package/dist/tools/fs/file-read-check.d.ts +2 -1
  79. package/dist/tools/fs/file-read-check.js +15 -7
  80. package/dist/tools/fs/shell-read-check.d.ts +10 -2
  81. package/dist/tools/fs/shell-read-check.js +12 -8
  82. package/package.json +1 -1
  83. package/test/export-surface.snapshot.json +15 -1
  84. package/wire-changes/core@7.35.0.json +584 -0
  85. package/wire-changes/core@7.35.1.json +59 -0
@@ -1,5 +1,5 @@
1
1
  import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
2
- import { extractErrorCode, stripErrorCodePrefix } from "../../brain/errors.js";
2
+ import { extractErrorCode, isProviderEchoShape, stripErrorCodePrefix } from "../../brain/errors.js";
3
3
  import { terminalProjection } from "./terminal-projection.js";
4
4
  import { haltConsumed } from "./halt-attribution.js";
5
5
  import { providerReportedModel } from "../message-utils.js";
@@ -127,7 +127,7 @@ export function terminalCauseOf(final, stats, flags) {
127
127
  if (final.isApiErrorMessage === true) {
128
128
  apiFailure = {
129
129
  ...(final.apiErrorStatus !== undefined ? { status: final.apiErrorStatus } : {}),
130
- ...(final.requestId !== undefined ? { requestId: final.requestId } : {}),
130
+ ...(typeof final.requestId === "string" && isProviderEchoShape(final.requestId) ? { requestId: final.requestId } : {}),
131
131
  };
132
132
  }
133
133
  const lifted = extractErrorCode(message);
@@ -59,6 +59,7 @@ import type { ActiveSkillFrame } from "./active-skill-scope.js";
59
59
  import type { BudgetAxis } from "./assemble-result.js";
60
60
  import type { GitStatusLaneRef } from "./git-status-frame.js";
61
61
  import type { SessionReadFileStates } from "./session-read-file-states.js";
62
+ import type { LegModeCeiling, SessionModeSeats } from "./session-mode-seats.js";
62
63
  import type { BlockedRef, OutputRef, SkillListingEntry } from "./synthetic-tools.js";
63
64
  import type { createAttachmentState, WriteFamily } from "./turn-attachments.js";
64
65
  import type { cacheFamilyOf } from "./usage-accounting.js";
@@ -443,6 +444,8 @@ export interface Prepared {
443
444
  };
444
445
  /** Removes the `spec.signal` abort listener on task end (else a long-lived signal leaks listeners). */
445
446
  releaseSignal: () => void;
447
+ /** The leg has stopped: its children's mode ceiling moves to the session's current leg (called once its loop settled). */
448
+ endModeCeiling: () => void;
446
449
  /**
447
450
  * End-of-leg sweep for questions this leg's gate routed to a person. Called once by the
448
451
  * run loop at task end, on every exit path. A binding that is still present means a person ANSWERED and
@@ -1362,13 +1365,17 @@ export interface InheritedGate {
1362
1365
  */
1363
1366
  permissionMode?: PermissionMode;
1364
1367
  /**
1365
- * The parent's LIVE mode seat (opaque half; never serialized): the child's effective mode is DERIVED from it on
1366
- * every read, so a parent's steer reaches a running child on the child's next arbitration without a forwarding
1367
- * frame. `sessionId` is the parent's, the address of the user notice a folded child declaration is announced on.
1368
+ * The parent's live mode ceiling (opaque half; never serialized): the parent leg's mode while that leg runs, its
1369
+ * SESSION's current leg after it — the child's effective mode is DERIVED from it on every read, so a parent's steer
1370
+ * reaches a running child on the child's next arbitration without a forwarding frame, and the session's next leg
1371
+ * reaches a child that outlived the leg that spawned it (a session-scoped background child). `sessionId` is the
1372
+ * parent's, the address of the user notice a folded child declaration is announced on. `hold` (the engine's own
1373
+ * ceiling): the child is re-judged at each move of the ceiling, and a fold that produces is announced as at prepare.
1368
1374
  */
1369
1375
  permissionModeCeiling?: {
1370
1376
  readonly current: PermissionMode;
1371
1377
  readonly sessionId: string;
1378
+ readonly hold?: LegModeCeiling["hold"];
1372
1379
  };
1373
1380
  /**
1374
1381
  * Org-memory admission freeze (ruled 2026-08-05): the spawning chain's FROZEN admitted org-scope
@@ -1459,6 +1466,14 @@ export interface InheritedGate {
1459
1466
  * the one every gate consults, here and on every descendant.
1460
1467
  */
1461
1468
  arming?: AutoModeArmingRecipe;
1469
+ /**
1470
+ * The ancestor's live mode ceiling (opaque, never serialized; the chain's `permissionModeCeiling`): the classifier
1471
+ * answers the ancestor's asks only while this reads `auto` — else they go to the frozen approver. Absent (a
1472
+ * re-supplied entry) ⇒ the classifier answers as recorded.
1473
+ */
1474
+ liveMode?: {
1475
+ readonly current: PermissionMode;
1476
+ };
1462
1477
  };
1463
1478
  /**
1464
1479
  * Set ONLY on an entry whose `policy` is the folded form of an ancestor's PreToolUse screening face
@@ -1993,6 +2008,13 @@ export interface RunInternals {
1993
2008
  * channel, same posture as every other field here.
1994
2009
  */
1995
2010
  sessionReadStates?: SessionReadFileStates;
2011
+ /**
2012
+ * The Runner's per-session permission-mode seats ({@link SessionModeSeats}): the chain a leg hands its children
2013
+ * carries its SESSION's seat as the children's ceiling, and each leg binds itself to that seat once its prepare
2014
+ * commits — so a session-scoped background child follows the session's current leg. Always set by the Runner's own
2015
+ * prepare call (overriding any caller value); absent on a standalone prepareTask, which mints a seat of its own.
2016
+ */
2017
+ sessionModeSeats?: SessionModeSeats;
1996
2018
  /** Trusted system-injection sink (`Runner.runLocked` wires it to the live TaskStream queue plus the
1997
2019
  * current harness lanes; never a public TaskSpec field), ADDRESSED TO THE SESSION rather than to this
1998
2020
  * run: a producer wired here (a background shell, a watch, a delegation settle) keeps the closure long
@@ -2749,7 +2771,7 @@ export interface RunnerSelfSeat {
2749
2771
  */
2750
2772
  export type PrepareTaskFn = (spec: TaskSpec, deps: RunnerDeps, sessions: SessionStore, resume?: PrepareResume, internals?: RunInternals, runnerSelf?: RunnerSelfSeat, runIdSink?: {
2751
2773
  runId?: string;
2752
- }) => Promise<Prepared>;
2774
+ }, holdSessionLock?: (sessionId: string) => Promise<void>) => Promise<Prepared>;
2753
2775
  /**
2754
2776
  * The Result of an INSTALLING phase: a lane whose every product is installed on a seat it borrowed
2755
2777
  * (the harness's stop gate and recovery chain, the run state's counter groups, the stream's result setter, the claimed
@@ -27,6 +27,10 @@ export interface InheritedAutoMode {
27
27
  decider: AutoModeDecider;
28
28
  denialTracking?: AutoModeDenialTracker;
29
29
  arming?: AutoModeArmingRecipe;
30
+ /** The ancestor's live mode seat: the decider answers only while it reads `auto` (absent ⇒ as recorded). */
31
+ liveMode?: {
32
+ readonly current: import("../ask-origin.js").PermissionMode;
33
+ };
30
34
  }
31
35
  /**
32
36
  * A re-supplied chain entry (a cross-process redemption rebuilt its ancestor's classifier from the
@@ -179,7 +183,8 @@ export type InheritedClassifierJudgment<A extends InheritedAsk> =
179
183
  export declare function judgeInheritedClassifier<A extends InheritedAsk>(opts: {
180
184
  autoMode: InheritedAutoMode | undefined;
181
185
  /** The leg's live permission mode (the gate's seat): under `dontAsk` / `bypassPermissions` / `plan` the decider is not
182
- * dispatched (the mode's arbitration decides first). Absent ⇒ a decider on the chain is dispatched. */
186
+ * dispatched (the mode's arbitration decides first). Absent ⇒ a decider on the chain is dispatched (subject to the
187
+ * entry's own `autoMode.liveMode`). */
183
188
  liveMode?: () => import("../ask-origin.js").PermissionMode;
184
189
  ask: A;
185
190
  req: ToolCallRequest;
@@ -80,17 +80,17 @@ export function inheritedAskCarry(judged, liveApprover, tracker) {
80
80
  export async function judgeInheritedClassifier(opts) {
81
81
  const { autoMode, ask, req } = opts;
82
82
  const origin = askOriginOf(ask, INHERITED_STATION_FACTS);
83
- const modeDecidesFirst = () => {
83
+ const classifierStandsDown = () => {
84
84
  const live = opts.liveMode?.();
85
- return live === "dontAsk" || live === "bypassPermissions" || live === "plan";
85
+ return live === "dontAsk" || live === "bypassPermissions" || live === "plan" || (autoMode?.liveMode !== undefined && autoMode.liveMode.current !== "auto");
86
86
  };
87
- if (autoMode === undefined || modeDecidesFirst() || !classifyAsk(ask, INHERITED_STATION_FACTS).row.classifierEligible) {
87
+ if (autoMode === undefined || classifierStandsDown() || !classifyAsk(ask, INHERITED_STATION_FACTS).row.classifierEligible) {
88
88
  return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false, origin };
89
89
  }
90
90
  const verdict = await autoMode.decider
91
91
  .decide({ req, ...(ask.message !== undefined ? { askMessage: ask.message } : {}) }, opts.signal)
92
92
  .catch(() => ({ kind: "unavailable", cause: "error" }));
93
- if (verdict.kind === "allow" && modeDecidesFirst())
93
+ if (verdict.kind === "allow" && classifierStandsDown())
94
94
  return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false, origin };
95
95
  const attributed = (d) => (verdict.classifier !== undefined ? attestClassifierRound(d, verdict.classifier, req) : d);
96
96
  if (verdict.kind === "allow") {
@@ -0,0 +1,26 @@
1
+ /** The file-history REQUEST door and its bad-value doors — the pre-acquire half of the file-history phase (`prepare-file-history.ts`). */
2
+ import type { FileHistoryStore } from "../file-history-store.js";
3
+ import type { RunnerDeps, TaskSpec } from "../types.js";
4
+ /**
5
+ * The file-history REQUEST door: every refusal the request (`TaskSpec`) and the deployment's own seats decide
6
+ * without the session, the env or the store's contents — the three bad-value doors (`RunnerDeps.onTrackFailure`,
7
+ * `RunnerDeps.fileHistoryStore` / `fileHistoryCapture`, `TaskSpec.fileHistory`), the retired `rewindFiles` restore
8
+ * sense, the restoreFiles / resumeAt / rewindFilesTo contradictions, and a restore asked of a run that holds no
9
+ * history (`rewind.capture_disabled` / `rewind.store_unconfigured`). SYNCHRONOUS and side-effect free; the driver
10
+ * calls it BEFORE the session is acquired, so a request refused here leaves the session — its leaf included —
11
+ * exactly as it was. What it resolves rides into the file-history phase (`prepareFileHistory`) unchanged.
12
+ */
13
+ export declare function resolveFileHistoryRequest(spec: Pick<TaskSpec, "resumeAt" | "resumeAtMode" | "restoreFiles" | "rewindFilesTo" | "fileHistory">, deps: Pick<RunnerDeps, "onTrackFailure" | "fileHistoryStore" | "fileHistoryCapture">): FileHistoryRequest;
14
+ /** What {@link resolveFileHistoryRequest} resolved: the validated seats and words, the run's store (after every off
15
+ * switch) and its capture store, and the restore target (`undefined` = no restore asked). */
16
+ export interface FileHistoryRequest {
17
+ onTrackFailure: "refuse" | "proceed-unprotected";
18
+ wiredSeat: FileHistoryStore | "disabled" | undefined;
19
+ runDeclaredOff: boolean;
20
+ captureOff: boolean;
21
+ declaredOff: boolean;
22
+ fileHistoryStore: FileHistoryStore | undefined;
23
+ captureStore: FileHistoryStore | undefined;
24
+ rewindTarget: string | undefined;
25
+ rewindBefore: boolean;
26
+ }
@@ -0,0 +1,90 @@
1
+ export function resolveFileHistoryRequest(spec, deps) {
2
+ const onTrackFailure = deps.onTrackFailure === undefined ? "refuse" : deps.onTrackFailure;
3
+ if (onTrackFailure !== "refuse" && onTrackFailure !== "proceed-unprotected") {
4
+ const e = new Error(`RunnerDeps.onTrackFailure must be "refuse" or "proceed-unprotected"; got ${JSON.stringify(deps.onTrackFailure)} — refusing prepare rather than silently defaulting a first-touch-failure policy.`);
5
+ e.code = "config.invalid_on_track_failure";
6
+ throw e;
7
+ }
8
+ const wiredSeat = resolveWiredFileHistoryStore(deps.fileHistoryStore);
9
+ const runDeclaredOff = resolveRunFileHistoryWord(spec.fileHistory);
10
+ const captureOff = resolveFileHistoryCaptureWord(deps.fileHistoryCapture, wiredSeat);
11
+ const declaredOff = wiredSeat === "disabled" || runDeclaredOff;
12
+ const fileHistoryStore = declaredOff ? undefined : wiredSeat;
13
+ const captureStore = captureOff ? undefined : fileHistoryStore;
14
+ const legacyRewindFiles = spec.rewindFiles;
15
+ if (legacyRewindFiles === true) {
16
+ if (spec.resumeAt !== undefined) {
17
+ const e = new Error(`rewind-files: TaskSpec.rewindFiles is retired (design/381 — rewind now converges the per-edited-file history, not a whole-tree snapshot). This request pairs it with resumeAt, i.e. the RESTORE sense: set restoreFiles: true instead. (The capture sense needs no request anymore — first-touch tracking is on whenever a RunnerDeps.fileHistoryStore is wired and neither RunnerDeps.fileHistoryCapture nor TaskSpec.fileHistory turns it off.)`);
18
+ e.code = "rewind.rewind_files_retired";
19
+ throw e;
20
+ }
21
+ }
22
+ if (spec.restoreFiles === true && spec.resumeAt === undefined) {
23
+ const e = new Error(`rewind-files: restoreFiles: true needs a resumeAt anchor — restoring files means converging them to a specific prior entry's boundary, and this spec names none. Use resumeAt + restoreFiles to move conversation and files together, or rewindFilesTo alone for a code-only restore.`);
24
+ e.code = "rewind.invalid_spec";
25
+ throw e;
26
+ }
27
+ if (spec.resumeAt !== undefined && spec.rewindFilesTo !== undefined) {
28
+ const e = new Error(`rewind-files: resumeAt ("${spec.resumeAt}") and rewindFilesTo ("${spec.rewindFilesTo}") were both set — resumeAt already anchors the file restore when restoreFiles is true, so a separate rewindFilesTo target is a conflicting request. Drop one of the two.`);
29
+ e.code = "rewind.conflicting_targets";
30
+ throw e;
31
+ }
32
+ const rewindTarget = spec.resumeAt !== undefined ? (spec.restoreFiles === true ? spec.resumeAt : undefined) : spec.rewindFilesTo;
33
+ const rewindBefore = rewindTarget !== undefined && spec.resumeAt !== undefined && spec.resumeAtMode === "before";
34
+ if (rewindTarget !== undefined && fileHistoryStore === undefined) {
35
+ if (runDeclaredOff)
36
+ throw captureDisabledRefusal(rewindTarget);
37
+ const e = new Error(wiredSeat === "disabled"
38
+ ? `rewind-files: restoring files to entry "${rewindTarget}" requires a history backend, but this deployment declared file history disabled (RunnerDeps.fileHistoryStore: "disabled"), so there is no history to restore — the files were NOT rewound`
39
+ : `rewind-files: restoring files to entry "${rewindTarget}" requires a history backend, but this deployment wired no RunnerDeps.fileHistoryStore — no file history was ever recorded, so the files were NOT rewound`);
40
+ e.code = "rewind.store_unconfigured";
41
+ throw e;
42
+ }
43
+ return { onTrackFailure, wiredSeat, runDeclaredOff, captureOff, declaredOff, fileHistoryStore, captureStore, rewindTarget, rewindBefore };
44
+ }
45
+ function resolveWiredFileHistoryStore(candidate) {
46
+ if (candidate === undefined || candidate === "disabled")
47
+ return candidate;
48
+ const verbs = ["trackEdit", "annulTrack", "snapshot", "restore", "canRestore", "reap", "adoptScope"];
49
+ const verbOf = (v) => {
50
+ try {
51
+ return candidate[v];
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ };
57
+ const probeable = (typeof candidate === "object" && candidate !== null) || typeof candidate === "function";
58
+ const missing = probeable ? verbs.filter((v) => typeof verbOf(v) !== "function") : [...verbs];
59
+ if (missing.length > 0) {
60
+ const shape = candidate === null ? "null" : probeable ? "a value that is not a FileHistoryStore" : `a ${typeof candidate}`;
61
+ const e = new Error(`RunnerDeps.fileHistoryStore was wired with ${shape} — missing contract verbs: ${missing.join(", ")}. Refusing prepare rather than half-wiring the rewind seat (a present-but-unusable store makes every Edit/Write/NotebookEdit fail its first-touch backup while restore requests report "no store wired"). Write "disabled" (or omit the key) to run without file history.`);
62
+ e.code = "config.invalid_file_history_store";
63
+ throw e;
64
+ }
65
+ return candidate;
66
+ }
67
+ function resolveRunFileHistoryWord(word) {
68
+ if (word === undefined || word === "disabled")
69
+ return word === "disabled";
70
+ const e = new Error(`rewind-files: TaskSpec.fileHistory holds ${shownWord(word)} — the only value is "disabled"; omit the key to leave the deployment's setting in force`);
71
+ e.code = "rewind.invalid_spec";
72
+ throw e;
73
+ }
74
+ function resolveFileHistoryCaptureWord(word, wiredSeat) {
75
+ if (word === undefined || (word === "off" && wiredSeat !== undefined && wiredSeat !== "disabled"))
76
+ return word === "off";
77
+ const e = new Error(word === "off"
78
+ ? `RunnerDeps.fileHistoryCapture: "off" requires a wired RunnerDeps.fileHistoryStore — nothing to switch off; to drop history entirely write fileHistoryStore: "disabled"`
79
+ : `RunnerDeps.fileHistoryCapture holds ${shownWord(word)} — the only value is "off"; omit the key to record whenever a fileHistoryStore is wired`);
80
+ e.code = "config.invalid_file_history_capture";
81
+ throw e;
82
+ }
83
+ function shownWord(word) {
84
+ return typeof word === "string" ? JSON.stringify(word.slice(0, 40)) : word === null || typeof word === "boolean" || typeof word === "number" ? String(word) : `a ${typeof word}`;
85
+ }
86
+ function captureDisabledRefusal(target) {
87
+ const e = new Error(`rewind-files: this run set TaskSpec.fileHistory: "disabled", so it neither records nor restores file history — restoring files to entry "${target}" was refused and the files were NOT rewound`);
88
+ e.code = "rewind.capture_disabled";
89
+ return e;
90
+ }
@@ -29,6 +29,7 @@ import type { AskRuleEvidence, ToolCallRequest } from "../tool-policy.js";
29
29
  import { persistedRuleMandateOf, type RuleClearanceFacts } from "../ask-origin.js";
30
30
  import { type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit, type PersistedRuleUnreadable } from "../hooks.js";
31
31
  import type { PersistedRule, PersistedRuleVerdict, RuleOffer, SegmentCoverage } from "../permission-rule-model.js";
32
+ import { type ToolCallLike } from "../tool-registry.js";
32
33
  import { type OrgRuleResolution } from "../permission-rule-org.js";
33
34
  import { type EffectivePermissionRules, type PermissionRuleStoreProvider } from "../permission-rule-provider.js";
34
35
  /** The one SHELL tool the persisted-rule lane's COMMAND grammar speaks for (the rule-offer face mints for
@@ -246,7 +247,7 @@ export declare function createRuleOffersOf(cfg: {
246
247
  current: string;
247
248
  } | undefined;
248
249
  deniesDirectoryRead: ((directory: string) => boolean) | undefined;
249
- }): (toolName: string, args: unknown, ask: RuleOffersAskFacts | undefined) => {
250
+ }): (call: Pick<ToolCallLike, "toolName" | "face">, args: unknown, ask: RuleOffersAskFacts | undefined) => {
250
251
  ruleOffers?: readonly RuleOffer[];
251
252
  ruleOffersAbsence?: RuleOffersAbsence;
252
253
  execCwd?: string;
@@ -1,12 +1,13 @@
1
1
  import { persistedRuleMandateOf, ruleClearanceOf } from "../ask-origin.js";
2
2
  import {} from "../hooks.js";
3
- import { adjudicatePersistedPathRules, adjudicatePersistedRules, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
3
+ import { adjudicatePersistedPathRules, adjudicatePersistedRules, laneToolNamesOf, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
4
4
  import { isAbsolutePathForm } from "../../tools/fs/safety.js";
5
5
  import { effectivePathTargetOf } from "../effective-path-target.js";
6
- import { declaredPathTargetOf, protectivePathTargetOf } from "../tool-registry.js";
6
+ import { anyNamed, declaredPathTargetOf, firstNamed, protectivePathTargetOf, ruleNamesOf } from "../tool-registry.js";
7
7
  import { orgRuleVerdictFor } from "../permission-rule-org.js";
8
8
  import { ownerStoreOf } from "../permission-rule-provider.js";
9
9
  import { thrownText } from "../thrown-value.js";
10
+ import { strongestAnswer } from "../persisted-rule-arms.js";
10
11
  import { gateWaitFailureOf, withGateTimeout } from "../gate-bounded-await.js";
11
12
  import { ORG_ADJUDICATION_TIMEOUT_MS } from "../permission-rule-org.js";
12
13
  export const PERSISTED_RULE_TOOL = "Bash";
@@ -105,9 +106,9 @@ export function createPermissionRuleLanes(cfg) {
105
106
  }
106
107
  const revisionCell = orgRevisionEvidenceOf(resolution, cfg.onRevisionDefect);
107
108
  const command = req.args?.command;
108
- if (req.toolName !== PERSISTED_RULE_TOOL || typeof command !== "string")
109
+ if (!laneToolNamesOf(req).includes(PERSISTED_RULE_TOOL) || typeof command !== "string")
109
110
  return { status: "available", ...revisionCell };
110
- const verdict = orgRuleVerdictFor(resolution.rules, { tool: req.toolName, command });
111
+ const verdict = orgRuleVerdictFor(resolution.rules, { tool: PERSISTED_RULE_TOOL, command });
111
112
  return verdict === undefined ? { status: "available", ...revisionCell } : { status: "available", verdict, ...revisionCell };
112
113
  },
113
114
  }
@@ -115,8 +116,8 @@ export function createPermissionRuleLanes(cfg) {
115
116
  const personalAdjudicate = async (req) => {
116
117
  if (anonymous && !cfg.localOwnerDeclared)
117
118
  return undefined;
118
- const grammar = ruleToolGrammarOf(req.toolName);
119
- if (grammar === undefined)
119
+ const laneNames = laneToolNamesOf(req);
120
+ if (laneNames.length === 0)
120
121
  return undefined;
121
122
  let view;
122
123
  try {
@@ -128,7 +129,7 @@ export function createPermissionRuleLanes(cfg) {
128
129
  if (view.unreadable !== undefined)
129
130
  skipped(view.unreadable, view.unreadableSources);
130
131
  try {
131
- const answer = judgeAgainst(view.rules, req, grammar);
132
+ const answer = strongestAnswer(laneNames.map((n) => judgeAgainst(view.rules, { ...req, toolName: n }, ruleToolGrammarOf(n))));
132
133
  return view.unreadable !== undefined && (answer === undefined || "segmentCoverage" in answer) ? { unreadable: true } : answer;
133
134
  }
134
135
  catch (err) {
@@ -156,15 +157,17 @@ export function createPermissionRuleLanes(cfg) {
156
157
  return { personal, org };
157
158
  }
158
159
  export function createRuleOffersOf(cfg) {
159
- return (toolName, args, ask) => {
160
+ return (call, args, ask) => {
161
+ const toolName = call.toolName;
160
162
  if (!cfg.laneArmed || toolName !== PERSISTED_RULE_TOOL)
161
163
  return {};
162
164
  if ((cfg.principal === undefined || cfg.principal === "") && !cfg.localOwnerDeclared)
163
165
  return {};
166
+ const tierSeat = firstNamed(cfg.irreversibilityTier, ruleNamesOf(call));
164
167
  const closedDoor = ruleClearanceOf(ask, {
165
- egress: cfg.egressTools.has(toolName),
166
- irreversibility: cfg.irreversibilityTier.get(toolName),
167
- shellGated: cfg.shellGatedTools.has(toolName),
168
+ egress: anyNamed(cfg.egressTools, ruleNamesOf(call)),
169
+ irreversibility: tierSeat?.value,
170
+ shellGated: tierSeat !== undefined && cfg.shellGatedTools.has(tierSeat.name),
168
171
  }).word;
169
172
  if (closedDoor !== undefined)
170
173
  return { ruleOffersAbsence: closedDoor };
@@ -77,7 +77,7 @@ export interface PrepareAskLaneInput {
77
77
  isDelegatedChild?: true;
78
78
  };
79
79
  /** borrowed-readonly — the policy-chain phase's per-tool risk-axes projection (`riskAxes` on the ask). */
80
- riskAxesOf: (toolName: string) => {
80
+ riskAxesOf: (call: Pick<ToolCallRequest, "toolName" | "face">) => {
81
81
  riskAxes?: {
82
82
  irreversible?: boolean;
83
83
  egress?: boolean;
@@ -96,10 +96,10 @@ export function prepareAskLane(input) {
96
96
  const p = approvalPreviewOf(req.toolName, req.args);
97
97
  return { ...(p.preview !== undefined ? { preview: p.preview } : {}), ...(p.withheld !== undefined ? { previewWithheld: p.withheld } : {}) };
98
98
  })(),
99
- ...ruleOffersOf(req.toolName, req.args, decision.action === "ask" ? decision : undefined),
99
+ ...ruleOffersOf(req, req.args, decision.action === "ask" ? decision : undefined),
100
100
  message: decision.message ?? `approval required for "${req.toolName}"`,
101
101
  ...askSourceIdentity(),
102
- ...riskAxesOf(req.toolName),
102
+ ...riskAxesOf(req),
103
103
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
104
104
  ...(decision.action === "ask" ? askMandateMember(decision) : {}),
105
105
  ...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
@@ -12,7 +12,9 @@
12
12
  * before the move (a minted ownedEnv is destroyed there, the session view forgotten). The phase acquires
13
13
  * no resource of its own — the two seats it returns are closures over the store the deployment owns.
14
14
  *
15
- * The module-level helpers below the phase (`restoredAbsPathsOf`, `resolveWiredFileHistoryStore`,
15
+ * The request door (the bad-value doors and every refusal the request decides alone) lives in
16
+ * `./file-history-request.ts` and runs BEFORE the session is acquired; this phase receives its resolution.
17
+ * The module-level helpers below the phase (`restoredAbsPathsOf`,
16
18
  * the refusal's epoch-tail wording helper, `adoptForkedFileHistory` with its per-store carry memo,
17
19
  * `createFileHistoryBoundarySeat`, `resolveFileHistoryCoordinates` / `resolveFileHistoryScope` /
18
20
  * `fileHistoryFilesystemIdentity`, `childScopeRewindRefusal`) moved with it unchanged; the two the test
@@ -22,13 +24,17 @@ import type { ExecutionEnv } from "../../internal/harness.js";
22
24
  import type { StoredSession } from "../../engine/session/session.js";
23
25
  import type { RunnerDeps, TaskResult, TaskSpec, TrackFileEditHook } from "../types.js";
24
26
  import type { FileHistoryBoundarySeat, RunInternals } from "./contracts.js";
27
+ import type { FileHistoryRequest } from "./file-history-request.js";
25
28
  export interface PrepareFileHistoryInput {
26
29
  /** borrowed-readonly — the deployment seats this phase reads: the first-touch failure policy, the history
27
30
  * store and its capture switch (all validated HERE, once), the incident sink, and the retired-epoch diagnostic probe. */
28
- deps: Pick<RunnerDeps, "onTrackFailure" | "fileHistoryStore" | "fileHistoryCapture" | "onError">;
31
+ deps: Pick<RunnerDeps, "onError">;
29
32
  /** borrowed-readonly — the REBOUND spec's rewind request fields and off-switch word (the retired `rewindFiles`
30
33
  * spelling is read off the same object as an unknown key) and the caller's cancel signal for the restore. */
31
- spec: Pick<TaskSpec, "resumeAt" | "resumeAtMode" | "restoreFiles" | "rewindFilesTo" | "acceptPartialRestore" | "signal" | "fileHistory">;
34
+ spec: Pick<TaskSpec, "resumeAt" | "restoreFiles" | "acceptPartialRestore" | "signal">;
35
+ /** borrowed-readonly — the request door's resolution ({@link resolveFileHistoryRequest}), judged before the session was
36
+ * acquired: every refusal the request and the deployment's seats decide on their own has already thrown. */
37
+ request: FileHistoryRequest;
32
38
  /** borrowed-readonly — the trusted lineage a delegating parent handed this run (absent for a root run
33
39
  * and for a host's own fork), read for the recording-scope decision only. */
34
40
  internals: Pick<RunInternals, "fileHistoryLineage"> | undefined;
@@ -2,45 +2,16 @@ import { NodeExecutionEnv } from "../../engine/execution-env/node-execution-env.
2
2
  import { isRemoteExecutionEnv } from "../remote-env.js";
3
3
  import { resolveTrackKey } from "../file-history-store.js";
4
4
  export async function prepareFileHistory(input) {
5
- const { deps, spec, internals, executionEnv, taskRootFinal, handsEnabled, sessionId, session, abortController } = input;
5
+ const { deps, spec, request, internals, executionEnv, taskRootFinal, handsEnabled, sessionId, session, abortController } = input;
6
6
  const rewindNotes = [];
7
- const onTrackFailure = deps.onTrackFailure === undefined ? "refuse" : deps.onTrackFailure;
8
- if (onTrackFailure !== "refuse" && onTrackFailure !== "proceed-unprotected") {
9
- const e = new Error(`RunnerDeps.onTrackFailure must be "refuse" or "proceed-unprotected"; got ${JSON.stringify(deps.onTrackFailure)} — refusing prepare rather than silently defaulting a first-touch-failure policy.`);
10
- e.code = "config.invalid_on_track_failure";
11
- throw e;
12
- }
13
- const wiredSeat = resolveWiredFileHistoryStore(deps.fileHistoryStore);
14
- const runDeclaredOff = resolveRunFileHistoryWord(spec.fileHistory);
15
- const captureOff = resolveFileHistoryCaptureWord(deps.fileHistoryCapture, wiredSeat);
16
- const declaredOff = wiredSeat === "disabled" || runDeclaredOff;
17
- const fileHistoryStore = declaredOff ? undefined : wiredSeat;
18
- const captureStore = captureOff ? undefined : fileHistoryStore;
19
- const legacyRewindFiles = spec.rewindFiles;
20
- if (legacyRewindFiles === true) {
21
- if (spec.resumeAt !== undefined) {
22
- const e = new Error(`rewind-files: TaskSpec.rewindFiles is retired (design/381 — rewind now converges the per-edited-file history, not a whole-tree snapshot). This request pairs it with resumeAt, i.e. the RESTORE sense: set restoreFiles: true instead. (The capture sense needs no request anymore — first-touch tracking is on whenever a RunnerDeps.fileHistoryStore is wired and neither RunnerDeps.fileHistoryCapture nor TaskSpec.fileHistory turns it off.)`);
23
- e.code = "rewind.rewind_files_retired";
24
- throw e;
25
- }
7
+ const { onTrackFailure, captureOff, declaredOff, fileHistoryStore, captureStore, rewindTarget, rewindBefore } = request;
8
+ if (spec.rewindFiles === true) {
26
9
  try {
27
10
  deps.onError?.(new Error(`TaskSpec.rewindFiles (capture sense) is retired and was ignored: per-edited-file first-touch tracking is on whenever RunnerDeps.fileHistoryStore is wired and neither RunnerDeps.fileHistoryCapture nor TaskSpec.fileHistory turns it off, so a per-turn capture request has nothing left to ask for. Drop the field; the restore sense moved to TaskSpec.restoreFiles.`), { phase: "config", sessionId });
28
11
  }
29
12
  catch {
30
13
  }
31
14
  }
32
- if (spec.restoreFiles === true && spec.resumeAt === undefined) {
33
- const e = new Error(`rewind-files: restoreFiles: true needs a resumeAt anchor — restoring files means converging them to a specific prior entry's boundary, and this spec names none. Use resumeAt + restoreFiles to move conversation and files together, or rewindFilesTo alone for a code-only restore.`);
34
- e.code = "rewind.invalid_spec";
35
- throw e;
36
- }
37
- if (spec.resumeAt !== undefined && spec.rewindFilesTo !== undefined) {
38
- const e = new Error(`rewind-files: resumeAt ("${spec.resumeAt}") and rewindFilesTo ("${spec.rewindFilesTo}") were both set — resumeAt already anchors the file restore when restoreFiles is true, so a separate rewindFilesTo target is a conflicting request. Drop one of the two.`);
39
- e.code = "rewind.conflicting_targets";
40
- throw e;
41
- }
42
- const rewindTarget = spec.resumeAt !== undefined ? (spec.restoreFiles === true ? spec.resumeAt : undefined) : spec.rewindFilesTo;
43
- const rewindBefore = rewindTarget !== undefined && spec.resumeAt !== undefined && spec.resumeAtMode === "before";
44
15
  if (spec.resumeAt !== undefined && spec.restoreFiles !== true) {
45
16
  rewindNotes.push({
46
17
  code: "conversation_only",
@@ -64,18 +35,7 @@ export async function prepareFileHistory(input) {
64
35
  await adoptForkedFileHistory(captureStore, session, sessionId, deps.onError);
65
36
  if (rewindTarget !== undefined && historyScope !== sessionId)
66
37
  throw childScopeRewindRefusal(historyScope, rewindTarget);
67
- if (fileHistoryStore === undefined) {
68
- if (rewindTarget !== undefined && runDeclaredOff)
69
- throw captureDisabledRefusal(rewindTarget);
70
- if (rewindTarget !== undefined) {
71
- const e = new Error(wiredSeat === "disabled"
72
- ? `rewind-files: restoring files to entry "${rewindTarget}" requires a history backend, but this deployment declared file history disabled (RunnerDeps.fileHistoryStore: "disabled"), so there is no history to restore — the files were NOT rewound`
73
- : `rewind-files: restoring files to entry "${rewindTarget}" requires a history backend, but this deployment wired no RunnerDeps.fileHistoryStore — no file history was ever recorded, so the files were NOT rewound`);
74
- e.code = "rewind.store_unconfigured";
75
- throw e;
76
- }
77
- }
78
- else if (!handsEnabled) {
38
+ if (fileHistoryStore !== undefined && !handsEnabled) {
79
39
  if (rewindTarget !== undefined) {
80
40
  rewindNotes.push({
81
41
  code: "files_env_unsupported",
@@ -168,52 +128,6 @@ function restoredAbsPathsOf(restored, root) {
168
128
  }
169
129
  return out;
170
130
  }
171
- function resolveWiredFileHistoryStore(candidate) {
172
- if (candidate === undefined || candidate === "disabled")
173
- return candidate;
174
- const verbs = ["trackEdit", "annulTrack", "snapshot", "restore", "canRestore", "reap", "adoptScope"];
175
- const verbOf = (v) => {
176
- try {
177
- return candidate[v];
178
- }
179
- catch {
180
- return undefined;
181
- }
182
- };
183
- const probeable = (typeof candidate === "object" && candidate !== null) || typeof candidate === "function";
184
- const missing = probeable ? verbs.filter((v) => typeof verbOf(v) !== "function") : [...verbs];
185
- if (missing.length > 0) {
186
- const shape = candidate === null ? "null" : probeable ? "a value that is not a FileHistoryStore" : `a ${typeof candidate}`;
187
- const e = new Error(`RunnerDeps.fileHistoryStore was wired with ${shape} — missing contract verbs: ${missing.join(", ")}. Refusing prepare rather than half-wiring the rewind seat (a present-but-unusable store makes every Edit/Write/NotebookEdit fail its first-touch backup while restore requests report "no store wired"). Write "disabled" (or omit the key) to run without file history.`);
188
- e.code = "config.invalid_file_history_store";
189
- throw e;
190
- }
191
- return candidate;
192
- }
193
- function resolveRunFileHistoryWord(word) {
194
- if (word === undefined || word === "disabled")
195
- return word === "disabled";
196
- const e = new Error(`rewind-files: TaskSpec.fileHistory holds ${shownWord(word)} — the only value is "disabled"; omit the key to leave the deployment's setting in force`);
197
- e.code = "rewind.invalid_spec";
198
- throw e;
199
- }
200
- function resolveFileHistoryCaptureWord(word, wiredSeat) {
201
- if (word === undefined || (word === "off" && wiredSeat !== undefined && wiredSeat !== "disabled"))
202
- return word === "off";
203
- const e = new Error(word === "off"
204
- ? `RunnerDeps.fileHistoryCapture: "off" requires a wired RunnerDeps.fileHistoryStore — nothing to switch off; to drop history entirely write fileHistoryStore: "disabled"`
205
- : `RunnerDeps.fileHistoryCapture holds ${shownWord(word)} — the only value is "off"; omit the key to record whenever a fileHistoryStore is wired`);
206
- e.code = "config.invalid_file_history_capture";
207
- throw e;
208
- }
209
- function shownWord(word) {
210
- return typeof word === "string" ? JSON.stringify(word.slice(0, 40)) : word === null || typeof word === "boolean" || typeof word === "number" ? String(word) : `a ${typeof word}`;
211
- }
212
- function captureDisabledRefusal(target) {
213
- const e = new Error(`rewind-files: this run set TaskSpec.fileHistory: "disabled", so it neither records nor restores file history — restoring files to entry "${target}" was refused and the files were NOT rewound`);
214
- e.code = "rewind.capture_disabled";
215
- return e;
216
- }
217
131
  function rewindRefusalTail() {
218
132
  return `Boundaries are keyed by a turn's INITIAL entry and exist only for turns that recorded history (a RunnerDeps.fileHistoryStore wired, capture on, and the run's file history not disabled); a boundary from the retired whole-tree snapshot era is not restorable by this seat, and reaped boundaries have none.`;
219
133
  }
@@ -39,6 +39,7 @@ import type { CwdRef } from "../../tools/fs/fs-shared.js";
39
39
  import type { AskLane, IrreversibilityTier, ParkAsk, Prepared, ReversibilityProbes, StopForDenialLimit } from "./contracts.js";
40
40
  import type { PermissionRuleLanes } from "./permission-rule-lanes.js";
41
41
  import { type ToolRosterDeltaSeat } from "../tool-roster.js";
42
+ import type { ToolCallLike } from "../tool-registry.js";
42
43
  import { type FrameSettlement } from "../frame-settlement.js";
43
44
  /**
44
45
  * The parent-thread human-rejection boundary — judged ONCE here, from the pass's own record (CC parity:
@@ -174,7 +175,7 @@ export interface PrepareGateStationsInput {
174
175
  readonly signal: AbortSignal;
175
176
  };
176
177
  /** borrowed-readonly — the shell-seat provenance set: the shell-running tools whose tier is the engine's shell seat's, not a
177
- * deployment mark (the gate's `shellGated` attribution is the gated tool's membership). */
178
+ * deployment mark (the gate's `shellGated` attribution is the membership of the name the call's tier was read under). */
178
179
  shellGatedTools: ReadonlySet<string>;
179
180
  /** borrowed-readonly — the armed classifier's live seat (read per call: a leg steered into `auto` arms after prepare). */
180
181
  autoModeDecider: {
@@ -211,8 +212,8 @@ export interface PrepareGateStationsInput {
211
212
  questionToolMounted: boolean;
212
213
  /** borrowed-readonly — whether sandbox admission is armed (isolated env + park prerequisites). */
213
214
  sandboxAdmissionArmed: boolean;
214
- /** borrowed-readonly — the per-tool boundary-capability fact the sandbox admission reads. */
215
- sandboxBoundaryCapable: (toolName: string) => boolean;
215
+ /** borrowed-readonly — the per-call boundary-capability fact the sandbox admission reads (the call's wire name and names). */
216
+ sandboxBoundaryCapable: (call: Pick<ToolCallLike, "toolName" | "face">) => boolean;
216
217
  /** borrowed-readonly — the sandbox-admission trace sink (the deferred ancestor disclosure and the gate's own). */
217
218
  emitSandboxAdmitted: (info: {
218
219
  toolCallId: string;
@@ -9,7 +9,7 @@ import { askUnresolvableNotice } from "../ask-unresolvable-notice.js";
9
9
  import { askReachesPerson } from "../wiring-manifest.js";
10
10
  import { classifierUnavailableSeat } from "./classifier-ladder.js";
11
11
  import { PATH_CONFINABLE_WRITE_TOOLS } from "./session-rule-policy.js";
12
- import { toolCallFaceOf } from "../tool-roster.js";
12
+ import { anyNamed, callAnswersTo, callFaceSeat, firstNamed, rosterToolName, ruleNamesOf, toolCallFaceOf } from "../tool-roster.js";
13
13
  import { noteHostRewrite } from "../frame-settlement.js";
14
14
  function screenGateResult(result) {
15
15
  if (result.suspend !== undefined)
@@ -85,11 +85,15 @@ export function prepareGateStations(input) {
85
85
  writeProtectionArmed ||
86
86
  shellReadCheck !== undefined;
87
87
  if (toolCallGateArmedRef.armed) {
88
- harness.on("tool_call", async (e) => {
88
+ harness.on("tool_call", async (called) => {
89
+ const e = { ...called, toolName: rosterToolName(toolRosterDeltas.current, called.toolName) };
90
+ const call = { toolName: e.toolName, ...callFaceSeat(toolCallFaceOf(toolRosterDeltas.current, called.toolName)) };
91
+ const names = ruleNamesOf(call);
92
+ const tierSeat = firstNamed(irreversibilityTier, names);
89
93
  blockedToolCalls.delete(e.toolCallId);
90
94
  inheritedAskGrants.delete(e.toolCallId);
91
95
  {
92
- const complianceDeny = complianceCallDenial(complianceDenies, e.toolName);
96
+ const complianceDeny = names.map((n) => complianceCallDenial(complianceDenies, n)).find((d) => d !== undefined);
93
97
  if (complianceDeny !== undefined) {
94
98
  if (blockedTracked)
95
99
  blockedToolCalls.add(e.toolCallId);
@@ -113,24 +117,24 @@ export function prepareGateStations(input) {
113
117
  hookTimeoutMs,
114
118
  ...(handsCwdRef !== undefined ? { trackedCwd: () => handsCwdRef.current } : {}),
115
119
  ...(hookEnvFace !== undefined ? { hookEnv: hookEnvFace } : {}),
116
- toolFaceOf: (name) => toolCallFaceOf(toolRosterDeltas.current, name),
120
+ toolFaceOf: (name) => toolCallFaceOf(toolRosterDeltas.current, name === e.toolName ? called.toolName : name),
117
121
  adjudicate,
118
122
  resolveAsk: resolveAskBound,
119
123
  suspendAsk,
120
124
  resolveContentAsk,
121
- egress: egressTools.has(e.toolName),
122
- peerMessage: e.toolName === SEND_MESSAGE_TOOL_NAME,
125
+ egress: anyNamed(egressTools, names),
126
+ peerMessage: callAnswersTo(call, SEND_MESSAGE_TOOL_NAME),
123
127
  ...(writeProtectionCheck !== undefined ? { writeProtectionCheck } : {}),
124
128
  ...(shellReadCheck !== undefined ? { shellReadCheck } : {}),
125
- irreversibility: irreversibilityTier.get(e.toolName),
126
- reversibilityProbe: reversibilityProbes.get(e.toolName),
129
+ irreversibility: tierSeat?.value,
130
+ reversibilityProbe: tierSeat !== undefined ? reversibilityProbes.get(tierSeat.name) : undefined,
127
131
  abortSignal: abortController.signal,
128
132
  ...(notifyPermissionDenied ? { permissionDenied: notifyPermissionDenied } : {}),
129
133
  onHookError: notifyHookError,
130
- shellGated: shellGatedTools.has(e.toolName),
134
+ shellGated: tierSeat !== undefined && shellGatedTools.has(tierSeat.name),
131
135
  ...(autoModeDecider.current !== undefined ? { autoMode: { decider: autoModeDecider.current, ...(autoModeDenialTracking.current !== undefined ? { denialTracking: autoModeDenialTracking.current } : {}), unavailable: classifierUnavailableSeat({ station: "leg", ...(autoModeDecider.current.timeoutMs !== undefined ? { budgetMs: autoModeDecider.current.timeoutMs } : {}), approvalSeat: askReachesPerson(askRoute), onNotice: deps.onNotice, sessionId, runId }) } } : {}),
132
136
  permissionMode: () => permissionMode.current,
133
- ...(() => { const s = toolMarkSources.get(e.toolName); return s !== undefined ? { toolMarkSources: s } : {}; })(),
137
+ ...(() => { const s = firstNamed(toolMarkSources, names)?.value; return s !== undefined ? { toolMarkSources: s } : {}; })(),
134
138
  ...(a2aIsolatePeers === true ? { a2aIsolatePeers: true } : {}),
135
139
  onModeReleased,
136
140
  ancestorArbitrationOf: (toolCallId) => ancestorArbitrations.get(toolCallId),
@@ -183,7 +187,7 @@ export function prepareGateStations(input) {
183
187
  ? {
184
188
  sandboxAdmission: {
185
189
  askClassesOf: (toolCallId) => foldAskClasses.get(toolCallId),
186
- boundaryCapable: sandboxBoundaryCapable(e.toolName),
190
+ boundaryCapable: sandboxBoundaryCapable(call),
187
191
  onAdmitted: emitSandboxAdmitted,
188
192
  },
189
193
  }