@sema-agent/core 7.33.8 → 7.34.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 (197) hide show
  1. package/CHANGELOG.md +339 -0
  2. package/dist/agents/cross-session-judge.d.ts +16 -7
  3. package/dist/agents/cross-session-judge.js +18 -15
  4. package/dist/agents/peer-session-drain.d.ts +4 -0
  5. package/dist/agents/peer-session-drain.js +5 -2
  6. package/dist/agents/repair-loop.d.ts +1 -1
  7. package/dist/agents/subagent.d.ts +3 -0
  8. package/dist/agents/subagent.js +12 -3
  9. package/dist/brain/anthropic.js +1 -0
  10. package/dist/brain/open-responses.js +1 -0
  11. package/dist/brain/openai.js +1 -0
  12. package/dist/brain/stream-engine.d.ts +3 -0
  13. package/dist/brain/stream-engine.js +2 -1
  14. package/dist/core/a2a.d.ts +3 -1
  15. package/dist/core/a2a.js +1 -1
  16. package/dist/core/ask-origin.d.ts +351 -154
  17. package/dist/core/ask-origin.js +223 -45
  18. package/dist/core/ask-unresolvable-notice.d.ts +5 -2
  19. package/dist/core/ask-unresolvable-notice.js +4 -3
  20. package/dist/core/auto-mode-arming.d.ts +17 -4
  21. package/dist/core/auto-mode-arming.js +31 -6
  22. package/dist/core/auto-mode-defaults.d.ts +16 -7
  23. package/dist/core/auto-mode-defaults.js +2 -1
  24. package/dist/core/auto-mode-directive.d.ts +18 -30
  25. package/dist/core/auto-mode-directive.js +9 -18
  26. package/dist/core/auto-mode-prompt.d.ts +62 -1
  27. package/dist/core/auto-mode-prompt.js +42 -4
  28. package/dist/core/auto-mode-rebuild.d.ts +2 -1
  29. package/dist/core/auto-mode-rebuild.js +1 -0
  30. package/dist/core/auto-mode.d.ts +32 -3
  31. package/dist/core/auto-mode.js +80 -41
  32. package/dist/core/checkpoint-store.d.ts +50 -35
  33. package/dist/core/checkpoint-store.js +58 -3
  34. package/dist/core/child-clamp-seats.js +1 -0
  35. package/dist/core/classifier-unavailable-arm.d.ts +57 -0
  36. package/dist/core/classifier-unavailable-arm.js +17 -0
  37. package/dist/core/effective-path-target.d.ts +12 -0
  38. package/dist/core/effective-path-target.js +18 -7
  39. package/dist/core/engine-notice.d.ts +53 -2
  40. package/dist/core/fs-write-gate-policy.d.ts +5 -1
  41. package/dist/core/fs-write-gate-policy.js +4 -1
  42. package/dist/core/gate-fold.js +17 -1
  43. package/dist/core/gate-lanes.js +111 -46
  44. package/dist/core/gate-outcome.d.ts +40 -7
  45. package/dist/core/gate-outcome.js +26 -4
  46. package/dist/core/governance-codes.d.ts +1 -1
  47. package/dist/core/governance-codes.js +12 -0
  48. package/dist/core/hooks.d.ts +50 -29
  49. package/dist/core/hooks.js +1 -3
  50. package/dist/core/mcp.js +1 -1
  51. package/dist/core/mode-policy.d.ts +182 -0
  52. package/dist/core/mode-policy.js +179 -0
  53. package/dist/core/oracle-isolation.d.ts +3 -3
  54. package/dist/core/permission-rule-model.d.ts +16 -3
  55. package/dist/core/permission-rule-model.js +36 -3
  56. package/dist/core/permission-rule-provider.d.ts +22 -11
  57. package/dist/core/permission-rule-provider.js +17 -7
  58. package/dist/core/permission-rules.d.ts +23 -1
  59. package/dist/core/permission-rules.js +35 -8
  60. package/dist/core/persisted-rule-arms.d.ts +10 -9
  61. package/dist/core/persisted-rule-arms.js +9 -6
  62. package/dist/core/present-plan-tool.d.ts +1 -1
  63. package/dist/core/present-plan-tool.js +3 -3
  64. package/dist/core/read-only-shell.d.ts +19 -19
  65. package/dist/core/read-only-shell.js +38 -12
  66. package/dist/core/retired-keys.d.ts +9 -3
  67. package/dist/core/retired-keys.js +5 -1
  68. package/dist/core/runner/classifier-ladder.d.ts +43 -1
  69. package/dist/core/runner/classifier-ladder.js +29 -0
  70. package/dist/core/runner/contracts.d.ts +37 -35
  71. package/dist/core/runner/denial-limit-arms.d.ts +30 -7
  72. package/dist/core/runner/denial-limit-arms.js +15 -9
  73. package/dist/core/runner/env-capability-census.js +1 -1
  74. package/dist/core/runner/gate-exit.d.ts +30 -4
  75. package/dist/core/runner/gate-exit.js +28 -7
  76. package/dist/core/runner/permission-rule-lanes.d.ts +9 -5
  77. package/dist/core/runner/permission-rule-lanes.js +44 -40
  78. package/dist/core/runner/persisted-path-rows.d.ts +64 -0
  79. package/dist/core/runner/persisted-path-rows.js +89 -0
  80. package/dist/core/runner/prepare-artifact.d.ts +5 -1
  81. package/dist/core/runner/prepare-artifact.js +9 -3
  82. package/dist/core/runner/prepare-ask-lane.d.ts +5 -3
  83. package/dist/core/runner/prepare-ask-lane.js +1 -1
  84. package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
  85. package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
  86. package/dist/core/runner/prepare-gate-stations.d.ts +33 -6
  87. package/dist/core/runner/prepare-gate-stations.js +32 -19
  88. package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
  89. package/dist/core/runner/prepare-hands-readface.js +64 -53
  90. package/dist/core/runner/prepare-inherited-gate.d.ts +22 -22
  91. package/dist/core/runner/prepare-inherited-gate.js +37 -23
  92. package/dist/core/runner/prepare-park-ask.d.ts +2 -2
  93. package/dist/core/runner/prepare-park-ask.js +2 -1
  94. package/dist/core/runner/prepare-policy-chain.d.ts +22 -2
  95. package/dist/core/runner/prepare-policy-chain.js +39 -12
  96. package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
  97. package/dist/core/runner/prepare-protocol-tools.js +13 -3
  98. package/dist/core/runner/prepare-run-refs.d.ts +6 -6
  99. package/dist/core/runner/prepare-run-refs.js +9 -4
  100. package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
  101. package/dist/core/runner/prepare-safety-scan.js +19 -3
  102. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
  103. package/dist/core/runner/prepare-suspend-saga.js +6 -6
  104. package/dist/core/runner/prepare-task.js +51 -22
  105. package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
  106. package/dist/core/runner/prepare-wiring-manifest.js +5 -3
  107. package/dist/core/runner/read-root-grant.d.ts +9 -0
  108. package/dist/core/runner/read-root-grant.js +12 -0
  109. package/dist/core/runner/retired-runner-deps.js +1 -0
  110. package/dist/core/runner/run-identity-wiring.js +1 -1
  111. package/dist/core/runner/run-leg.js +24 -15
  112. package/dist/core/runner/run-notification-lane.js +1 -0
  113. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
  114. package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
  115. package/dist/core/runner/run-turn-boundary.js +1 -1
  116. package/dist/core/runner/runtask.js +3 -0
  117. package/dist/core/runner/stream-mode-verb.d.ts +27 -0
  118. package/dist/core/runner/stream-mode-verb.js +19 -0
  119. package/dist/core/runner/turn-attachments.d.ts +3 -3
  120. package/dist/core/runner/turn-attachments.js +1 -1
  121. package/dist/core/runner/write-protection-station.d.ts +7 -1
  122. package/dist/core/runner/write-protection-station.js +6 -3
  123. package/dist/core/runner-deps.d.ts +31 -34
  124. package/dist/core/runtime-caps.d.ts +12 -3
  125. package/dist/core/safety-axis-vocab.d.ts +2 -2
  126. package/dist/core/safety-axis-vocab.js +1 -1
  127. package/dist/core/sensitive-path-policy.d.ts +4 -2
  128. package/dist/core/sensitive-path-policy.js +39 -30
  129. package/dist/core/task-event.d.ts +1 -1
  130. package/dist/core/task-result.d.ts +1 -1
  131. package/dist/core/task-spec.d.ts +41 -89
  132. package/dist/core/task-stream.d.ts +16 -0
  133. package/dist/core/terminal-cause.js +1 -0
  134. package/dist/core/tighten-task-spec.d.ts +2 -2
  135. package/dist/core/tighten-task-spec.js +12 -14
  136. package/dist/core/tool-catalog-entries.js +4 -3
  137. package/dist/core/tool-policy.d.ts +54 -25
  138. package/dist/core/tool-policy.js +43 -2
  139. package/dist/core/tool-registry.d.ts +2 -0
  140. package/dist/core/tool-registry.js +3 -0
  141. package/dist/core/tool-spec.d.ts +8 -9
  142. package/dist/core/trace.d.ts +7 -6
  143. package/dist/core/types.d.ts +2 -1
  144. package/dist/core/wiring-manifest.d.ts +31 -5
  145. package/dist/core/wiring-manifest.js +28 -1
  146. package/dist/core/write-protect.d.ts +11 -1
  147. package/dist/core/write-protect.js +26 -15
  148. package/dist/engine/execution-env/node-execution-env.d.ts +3 -0
  149. package/dist/engine/execution-env/node-execution-env.js +11 -1
  150. package/dist/engine/harness/types.d.ts +6 -7
  151. package/dist/engine/llm/types.d.ts +2 -4
  152. package/dist/engine/loop/types.d.ts +7 -0
  153. package/dist/index.d.ts +15 -5
  154. package/dist/index.js +12 -3
  155. package/dist/orchestration/workflow-governance.js +1 -1
  156. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  157. package/dist/orchestration/workflow-size-guideline.js +2 -2
  158. package/dist/tools/artifact/artifact-tool.d.ts +17 -0
  159. package/dist/tools/artifact/artifact-tool.js +16 -1
  160. package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
  161. package/dist/tools/fs/bash-readonly-classifier.js +88 -0
  162. package/dist/tools/fs/containment-face.d.ts +9 -3
  163. package/dist/tools/fs/containment-face.js +2 -0
  164. package/dist/tools/fs/file-read-check.d.ts +49 -0
  165. package/dist/tools/fs/file-read-check.js +71 -0
  166. package/dist/tools/fs/fs-bash.d.ts +3 -3
  167. package/dist/tools/fs/fs-read.d.ts +1 -0
  168. package/dist/tools/fs/fs-search-tools.d.ts +2 -2
  169. package/dist/tools/fs/fs-search-tools.js +4 -4
  170. package/dist/tools/fs/fs-shared.js +20 -6
  171. package/dist/tools/fs/fs-write.d.ts +4 -3
  172. package/dist/tools/fs/fs-write.js +9 -9
  173. package/dist/tools/fs/index.d.ts +39 -3
  174. package/dist/tools/fs/index.js +38 -15
  175. package/dist/tools/fs/read-deny-home.d.ts +93 -0
  176. package/dist/tools/fs/read-deny-home.js +225 -0
  177. package/dist/tools/fs/read-deny.d.ts +176 -34
  178. package/dist/tools/fs/read-deny.js +268 -61
  179. package/dist/tools/fs/safety.d.ts +6 -1
  180. package/dist/tools/fs/safety.js +18 -5
  181. package/dist/tools/fs/search.d.ts +3 -0
  182. package/dist/tools/fs/search.js +47 -9
  183. package/dist/tools/fs/shell-read-check.d.ts +42 -11
  184. package/dist/tools/fs/shell-read-check.js +188 -13
  185. package/dist/tools/fs/shell-read-establish.d.ts +7 -3
  186. package/dist/tools/fs/shell-read-establish.js +134 -21
  187. package/dist/tools/fs/shell-read-identity.d.ts +7 -0
  188. package/dist/tools/fs/shell-read-identity.js +21 -0
  189. package/dist/tools/fs/shell-read-judge.d.ts +92 -32
  190. package/dist/tools/fs/shell-read-judge.js +43 -18
  191. package/dist/tools/fs/shell-read-plan.d.ts +4 -0
  192. package/dist/tools/fs/shell-read-plan.js +26 -4
  193. package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
  194. package/dist/tools/fs/shell-read-write-targets.js +173 -0
  195. package/package.json +1 -1
  196. package/test/export-surface.snapshot.json +103 -11
  197. package/wire-changes/core@7.34.0.json +2621 -0
@@ -24,12 +24,15 @@
24
24
  */
25
25
  import type { AgentHarness, AgentTool } from "../../internal/harness.js";
26
26
  import type { AskClass } from "../ask-class.js";
27
+ import type { Arbitration, ToolMarkSource } from "../ask-origin.js";
28
+ import type { PermissionModeController } from "../mode-policy.js";
27
29
  import type { AutoModeDecider, AutoModeDenialTracker } from "../auto-mode.js";
28
30
  import { type ComplianceCapability } from "../compliance.js";
29
31
  import { type HookEnvCapabilities, type HookInvocationIdentity, type Hooks } from "../hooks.js";
30
32
  import type { ToolPolicy } from "../tool-policy.js";
31
33
  import { type GateOutcome } from "../gate-outcome.js";
32
34
  import type { RunnerDeps, TaskSpec, ToolEffect } from "../types.js";
35
+ import { type WiringManifest } from "../wiring-manifest.js";
33
36
  import type { WriteProtectionCheck } from "../write-protect.js";
34
37
  import type { ShellReadCheck } from "../../tools/fs/shell-read-check.js";
35
38
  import type { CwdRef } from "../../tools/fs/fs-shared.js";
@@ -134,8 +137,6 @@ export interface PrepareGateStationsInput {
134
137
  hookIdentity: HookInvocationIdentity;
135
138
  /** borrowed-readonly — the leg's reminder provenance mark (every model-facing deny reason and hook context carries it). */
136
139
  reminderMark: string;
137
- /** borrowed-readonly — the plan-mode latch (`Prepared.planModeRef`); `active` is read per call. Not written here. */
138
- planModeRef: Prepared["planModeRef"];
139
140
  /** borrowed-readonly — the per-task identity (the trace rows' taskId). */
140
141
  hostTaskId: string;
141
142
  /** borrowed-readonly — the acquired session id (the settlement defects' error tag). */
@@ -147,6 +148,12 @@ export interface PrepareGateStationsInput {
147
148
  /** borrowed-readonly — the hand's live tracked cwd, or undefined; `current` is read per screening / per post seat. Not
148
149
  * written here. */
149
150
  handsCwdRef: CwdRef | undefined;
151
+ /** borrowed-readonly — the leg's read-deny table in force, read at a release: the active built-in tiers and the rows
152
+ * beside them (the deployment's and the task's patterns, the read-face rule rows). Empty ⇒ no table. */
153
+ readDenyInForce: () => {
154
+ readonly tiers: readonly string[];
155
+ readonly rows: readonly string[];
156
+ };
150
157
  /** borrowed-readonly — the read-only env capability face every hook context carries, or undefined. */
151
158
  hookEnvFace: HookEnvCapabilities | undefined;
152
159
  /** borrowed-readonly — the leg's write-protection gate input, compiled ONCE by the wiring-manifest phase
@@ -169,10 +176,30 @@ export interface PrepareGateStationsInput {
169
176
  /** borrowed-readonly — the shell-seat provenance set: the shell-running tools whose tier is the engine's shell seat's, not a
170
177
  * deployment mark (the gate's `shellGated` attribution is the gated tool's membership). */
171
178
  shellGatedTools: ReadonlySet<string>;
172
- /** borrowed-readonly — the auto-mode decider, or undefined (the classifier lane the gate routes a surviving ask through). */
173
- autoModeDecider: AutoModeDecider | undefined;
174
- /** borrowed-readonly — the auto-mode denial tracker, or undefined (handed to the gate beside the decider). */
175
- autoModeDenialTracking: AutoModeDenialTracker | undefined;
179
+ /** borrowed-readonly — the armed classifier's live seat (read per call: a leg steered into `auto` arms after prepare). */
180
+ autoModeDecider: {
181
+ readonly current: AutoModeDecider | undefined;
182
+ };
183
+ /** borrowed-readonly — the denial tracker's live seat beside the decider's. */
184
+ autoModeDenialTracking: {
185
+ readonly current: AutoModeDenialTracker | undefined;
186
+ };
187
+ /** borrowed-readonly — the leg's ONE ask derivation (`wiring_manifest.ask`: form + effective), read for the classifier's seat. */
188
+ askRoute: WiringManifest["ask"];
189
+ /** borrowed-readonly — the leg's live permission-mode seat: the gate's arbitration point reads `current` per call. */
190
+ permissionMode: PermissionModeController;
191
+ /** borrowed-readonly — WHO marked each tool's seat (the safety scan's map, the protocol fold's rows); read per call into
192
+ * the gate's class derivation. */
193
+ toolMarkSources: ReadonlyMap<string, ReadonlySet<ToolMarkSource>>;
194
+ /** borrowed-readonly — the deployment's A2A isolation switch (`RunnerDeps.a2a.isolatePeers`), when set. */
195
+ a2aIsolatePeers?: true;
196
+ /** borrowed-readonly — the run's invocation id (the released-ask account's detail). */
197
+ runId: string;
198
+ /** borrowed-mutable — the inherited stations' mode arbitrations, keyed by tool call (the policy-chain phase writes; the
199
+ * gate's exit reads and this station deletes at the call's end, the fold-record lifecycle). */
200
+ ancestorArbitrations: Map<string, Exclude<Arbitration, {
201
+ kind: "ask";
202
+ }>>;
176
203
  /** borrowed-readonly — the headless denial-limit stop seat (wired unconditionally). */
177
204
  stopForDenialLimit: StopForDenialLimit;
178
205
  /** borrowed-readonly — the persisted allow-rule lane, or undefined (absent entirely with no store). */
@@ -1,12 +1,13 @@
1
1
  import { SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
2
2
  import { complianceCallDenial } from "../compliance.js";
3
3
  import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, runHookSeat, runToolGate } from "../hooks.js";
4
- import { PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
5
4
  import { screenGateOutcome } from "../gate-outcome.js";
6
5
  import { directDeny } from "./gate-exit.js";
7
6
  import { emitTrace } from "../trace.js";
8
7
  import { deliverEngineNotice } from "../engine-notice.js";
9
8
  import { askUnresolvableNotice } from "../ask-unresolvable-notice.js";
9
+ import { askReachesPerson } from "../wiring-manifest.js";
10
+ import { classifierUnavailableSeat } from "./classifier-ladder.js";
10
11
  import { PATH_CONFINABLE_WRITE_TOOLS } from "./session-rule-policy.js";
11
12
  import { toolCallFaceOf } from "../tool-roster.js";
12
13
  import { noteHostRewrite } from "../frame-settlement.js";
@@ -42,7 +43,29 @@ export function maybeHumanRejectionHalt(input) {
42
43
  };
43
44
  }
44
45
  export function prepareGateStations(input) {
45
- const { askLane, parkAsk, tools, toolRosterDeltas, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, planModeRef, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, writeProtectionCheck, shellReadCheck, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash, frameSettlement } = input;
46
+ const { askLane, parkAsk, tools, toolRosterDeltas, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, writeProtectionCheck, shellReadCheck, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, askRoute, permissionMode, toolMarkSources, a2aIsolatePeers, ancestorArbitrations, runId, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash, frameSettlement } = input;
47
+ const releasesAnnounced = new Map();
48
+ const onModeReleased = (info) => {
49
+ const table = info.readUnestablished === true ? input.readDenyInForce() : undefined;
50
+ if (table !== undefined && table.tiers.length + table.rows.length > 0) {
51
+ const named = [...(table.tiers.length > 0 ? [`built-in tiers ${table.tiers.join(", ")}`] : []), ...table.rows.slice(0, 8), ...(table.rows.length > 8 ? [`${table.rows.length - 8} more`] : [])].join("; ");
52
+ deliverEngineNotice(deps.onNotice, {
53
+ code: "permission.read_deny_unapplied",
54
+ message: `the ${info.release.mode} permission mode released "${info.toolName}" although a read-deny table is in force (${named}): the read boundary could not establish what this command reads, so no row of the table was applied to it`,
55
+ detail: { mode: info.release.mode, toolCallId: info.toolCallId, tiers: [...table.tiers], rows: table.rows.slice(0, 8), rowCount: table.rows.length, sessionId, runId },
56
+ });
57
+ }
58
+ const key = `${info.release.mode}\u0000${info.release.askClass}`;
59
+ const count = (releasesAnnounced.get(key) ?? 0) + 1;
60
+ releasesAnnounced.set(key, count);
61
+ if (count !== 1)
62
+ return;
63
+ deliverEngineNotice(deps.onNotice, {
64
+ code: "permission.ask_released_by_mode",
65
+ message: `the ${info.release.mode} permission mode released a ${info.release.askClass} ask on "${info.toolName}" without putting it to anyone (further releases of this pair in this run ride each call's tool_end.gate.disposition.released)`,
66
+ detail: { mode: info.release.mode, askClass: info.release.askClass, ...(info.release.origin !== undefined ? { origin: info.release.origin } : {}), ...(info.release.mandate !== undefined ? { mandate: info.release.mandate } : {}), count, firstToolCallId: info.toolCallId, sessionId, runId },
67
+ });
68
+ };
46
69
  if (askLane !== undefined && parkAsk !== undefined) {
47
70
  const { adjudicate, resolveAskBound, notifyPermissionDenied, notifyHookError } = askLane;
48
71
  const { resolveContentAsk, suspendAsk } = parkAsk;
@@ -78,22 +101,6 @@ export function prepareGateStations(input) {
78
101
  return { block: true, reason: formatHookFeedback(complianceDeny, reminderMark), preToolContext: [] };
79
102
  }
80
103
  }
81
- if (planModeRef.active && (toolEffects.get(e.toolName) ?? "write") !== "read") {
82
- if (blockedTracked)
83
- blockedToolCalls.add(e.toolCallId);
84
- const planDenyReason = `Plan mode is active — "${e.toolName}" is a write/mutating tool and is read-only-blocked. ` +
85
- `Research with read-only tools, then call ${PRESENT_PLAN_TOOL_NAME} with your plan to get it approved before acting.`;
86
- const gate = directDeny("plan_mode");
87
- gateOutcomes.set(e.toolCallId, gate);
88
- if (notifyPermissionDenied) {
89
- await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: planDenyReason, gate, identity: hookIdentity });
90
- }
91
- return {
92
- block: true,
93
- reason: formatHookFeedback(planDenyReason, reminderMark),
94
- preToolContext: [],
95
- };
96
- }
97
104
  let result;
98
105
  try {
99
106
  result = await runToolGate({
@@ -121,7 +128,12 @@ export function prepareGateStations(input) {
121
128
  ...(notifyPermissionDenied ? { permissionDenied: notifyPermissionDenied } : {}),
122
129
  onHookError: notifyHookError,
123
130
  shellGated: shellGatedTools.has(e.toolName),
124
- ...(autoModeDecider ? { autoMode: { decider: autoModeDecider, ...(autoModeDenialTracking !== undefined ? { denialTracking: autoModeDenialTracking } : {}) } } : {}),
131
+ ...(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
+ permissionMode: () => permissionMode.current,
133
+ ...(() => { const s = toolMarkSources.get(e.toolName); return s !== undefined ? { toolMarkSources: s } : {}; })(),
134
+ ...(a2aIsolatePeers === true ? { a2aIsolatePeers: true } : {}),
135
+ onModeReleased,
136
+ ancestorArbitrationOf: (toolCallId) => ancestorArbitrations.get(toolCallId),
125
137
  onHeadlessDenialLimit: stopForDenialLimit,
126
138
  onAskUnresolvable: (info) => deliverEngineNotice(deps.onNotice, askUnresolvableNotice({ sessionId, ...info })),
127
139
  ...(permissionRuleLane
@@ -182,6 +194,7 @@ export function prepareGateStations(input) {
182
194
  inheritedUnavailableAsks.delete(e.toolCallId);
183
195
  inheritedAskGrants.delete(e.toolCallId);
184
196
  foldAskClasses.delete(e.toolCallId);
197
+ ancestorArbitrations.delete(e.toolCallId);
185
198
  }
186
199
  const ancestorAdmitted = ancestorSandboxAdmissions.get(e.toolCallId);
187
200
  ancestorSandboxAdmissions.delete(e.toolCallId);
@@ -1,14 +1,16 @@
1
- import { type BeforeWriteHook, type CwdRef, type ReadDenyMatcher, type ContainmentFace } from "../../tools/fs/index.js";
1
+ import { type BeforeWriteHook, type CwdRef, type ReadDenyMatcher, type PathRuleDenyRow, type ContainmentFace } from "../../tools/fs/index.js";
2
2
  import { type ReadFileState } from "../../tools/fs/safety.js";
3
3
  import { type ShellReadAdmission, type ShellReadCheck } from "../../tools/fs/shell-read-check.js";
4
+ import type { ShellReadBoundary } from "../../tools/fs/shell-read-judge.js";
4
5
  import type { RunnerDeps, ShellGateDoctrine, TaskSpec } from "../types.js";
5
6
  import type { Model } from "../../internal/llm.js";
6
7
  import type { AgentHarness, AgentTool, ExecutionEnv, WorkspaceState } from "../../internal/harness.js";
7
8
  import type { AdvertisedWritableDirs } from "./advertised-writable-dirs.js";
8
9
  import { type ArtifactStoreRef, type GrantReadRoot } from "./read-root-grant.js";
9
10
  import type { StoredSession } from "../session.js";
10
- import type { IrreversibilityTier, PrepareResume, ReversibilityProbes, RunInternals, ToolFaceSnapshot, ToolProgressLane } from "./contracts.js";
11
+ import type { IrreversibilityTier, PrepareResume, ReversibilityProbes, RunInternals, ToolFaceSnapshot, ToolProgressLane, WriteFaceSeat } from "./contracts.js";
11
12
  import { RosterBuilder } from "../tool-roster.js";
13
+ import type { PermissionMode } from "../ask-origin.js";
12
14
  /** Test seam (mirrors `__resetMalformedNoticeSeatAnnouncement`): never called by production code.
13
15
  * Deliberately asymmetric — it resets only the console latch: the WeakSet arm needs no seam
14
16
  * because a test resets it by minting a fresh sink function (identity IS the ledger key), while
@@ -51,13 +53,31 @@ export interface PrepareHandsReadFaceInput {
51
53
  /** borrowed-readonly — the registry scope (`internals?.registryScope ?? spec.principal ??
52
54
  * "default"`, computed in the driver); the band's task-registry scope. */
53
55
  taskScope: string;
56
+ /** borrowed-readonly — the deny tables' permission-rule rows: the path deny rules of the leg's caller policy and of every
57
+ * ancestor's (`toolPolicyPathDenyRules`); joined to the read face's matcher and to the shell station's write face. */
58
+ pathDenyRules: readonly PathRuleDenyRow[];
59
+ /** borrowed-readonly — the two judges a shell command's write targets meet for the mode's file-command placement: the
60
+ * leg's write-protection judge (read per judgment — it is compiled at the manifest phase, before any gate is mounted)
61
+ * and the chain's write-target guard (`toolPolicyWriteTargetGuard`). Absent ⇒ neither (a standalone phase drive). */
62
+ writeGuards?: {
63
+ readonly protection: () => ShellReadBoundary["writeProtectionJudge"];
64
+ readonly target: ShellReadBoundary["writeTargetGuard"];
65
+ };
66
+ /** borrowed-readonly — the directories of the person's persisted `Read(//dir/**)` allow rows: read directories of this
67
+ * run, canonicalized and admitted exactly as `additionalReadDirectories` entries are. */
68
+ persistedReadDirectories?: readonly string[];
69
+ /** borrowed-readonly — the leg's mode seat, read LIVE by the write face ({@link writeFaceReader}): its mode's face
70
+ * is the resolver's row 3′ on every write. */
71
+ permissionMode: {
72
+ readonly current: PermissionMode;
73
+ };
54
74
  /** borrowed-readonly — the D-6 spec-time full-shell reachability precomputation; an input to the
55
75
  * face resolver on BOTH arms (the driver's post-exclusion roster assertion pins it to the
56
76
  * mounted truth — that assertion is driver-side, after this phase). */
57
77
  fullShellReachable: boolean;
58
- /** borrowed-readonly — the MAX-RANK fold of spec.shellGate with the inherited chain's doctrine
59
- * (computed in the driver's inherited-gate block); the shellGate fold's doctrine value. */
60
- effectiveShellGate: ShellGateDoctrine;
78
+ /** borrowed-readonly — the operator's shell FLOOR (`"always"`: the spec's, or the chain's), or undefined (no floor:
79
+ * the shape tier installs on its own and the permission mode arbitrates its asks). */
80
+ effectiveShellGate: ShellGateDoctrine | undefined;
61
81
  /** borrowed-readonly — B-1's frozen task-start face snapshot; only `.exclude` is read here (the
62
82
  * Monitor-mount honesty bit for the gh rate-limit hint). */
63
83
  toolFaceSnapshot: ToolFaceSnapshot;
@@ -123,8 +143,8 @@ export interface PrepareHandsReadFaceInput {
123
143
  /** borrowed-mutable — the shellGate fold `.add`s Bash/Monitor here. Same-object contract as
124
144
  * above. */
125
145
  irreversibleTools: Set<string>;
126
- /** borrowed-mutable — the classify doctrine's probe installs (`.set`, only where the doctrine
127
- * owns the seat). Same-object contract as above. */
146
+ /** borrowed-mutable — the shape tier's probe installs (`.set`, only where the shell seat
147
+ * owns the tier). Same-object contract as above. */
128
148
  reversibilityProbes: ReversibilityProbes;
129
149
  /** borrowed-readonly — the run's reminder provenance mark (minted/adopted
130
150
  * by the driver's adoption ladder); threaded into the hands band so the fs-side engine mints
@@ -184,9 +204,12 @@ export interface PrepareHandsReadFaceResult {
184
204
  /** owned — real shell + write hands + background-capable env: gates the driver's TaskOutput/
185
205
  * TaskStop/Monitor/SendMessage mounts (P11) and the Monitor arm of the shellGate fold here. */
186
206
  backgroundTaskToolsActive: boolean;
187
- /** owned — this run's shell READ STATION, the gate input (`ToolGateInput.shellReadCheck`): ONE judgment of
188
- * where a shell command reads, for every shell face this leg mounts, under every doctrine. ⇔ a real shell. */
207
+ /** owned — this run's READ STATIONS as the one gate input (`ToolGateInput.shellReadCheck`): the shell's (ONE judgment of
208
+ * where a shell command reads, for every shell face this leg mounts, under every mode and floor) and the file tools'
209
+ * (an out-of-root Read / Grep / Glob target is a question). ⇔ hands with a real shell, or hands with an approval face. */
189
210
  shellReadCheck: ShellReadCheck | undefined;
211
+ /** owned — the shell read station is mounted (⇔ a real shell): the wiring manifest's cell. */
212
+ shellReadStationArmed: boolean;
190
213
  /** owned — the same station's pre-spawn ADMISSION, published for the P11 face that spawns a shell command
191
214
  * and mounts LATER than the band (`Monitor`): the SAME function the band's shell faces got. ⇔ a real shell. */
192
215
  shellReadAdmission: ShellReadAdmission | undefined;
@@ -194,13 +217,13 @@ export interface PrepareHandsReadFaceResult {
194
217
  * (the hands-less value lives in `handsLessResolvedFace`). The driver assigns this onto its
195
218
  * outer `let` for the call-time closures (ctx enrichment / checkpoint mint / envFacts). */
196
219
  resolvedReadFace: ContainmentFace | undefined;
197
- /** write-back — the run's RESOLVED WRITE face where hands mount; undefined ⇔ no hands (the
198
- * hands-less value lives in `handsLessResolvedWriteFace`). The read seat's twin, resolved by the
199
- * SAME order on its OWN seats — the two never read each other. */
200
- resolvedWriteFace: ContainmentFace | undefined;
201
- /** write-back — the hands-LESS legs' resolved WRITE face (the posture is a property of the RUN,
202
- * not the mount): the delegation carriers and the checkpoint mint need it on every leg. */
203
- handsLessResolvedWriteFace: ContainmentFace | undefined;
220
+ /** write-back — the run's WRITE face in force where hands mount, read per call ({@link writeFaceReader}); undefined ⇔
221
+ * no hands (the hands-less reader is `handsLessResolvedWriteFace`). The read seat's twin, resolved by the SAME order on
222
+ * its OWN seats — the two never read each other. */
223
+ resolvedWriteFace: WriteFaceSeat | undefined;
224
+ /** write-back — the hands-LESS legs' write face in force (the posture is a property of the RUN, not the mount): the
225
+ * delegation carriers and the checkpoint mint read it on every leg. */
226
+ handsLessResolvedWriteFace: WriteFaceSeat | undefined;
204
227
  /** write-back (D-3 hold) — the NORMALIZED deny-set additions in force (deployment ∪ task ∪
205
228
  * resume-seed; zero-builtin compile — the persisted/carrier form, never the judged union's
206
229
  * post-dedup remainder). Driver assigns onto its outer `let`. */
@@ -234,7 +257,7 @@ export interface PrepareHandsReadFaceResult {
234
257
  * task additions). Undefined ⇔ hands-less (its consumers only exist with hands). */
235
258
  readDenyMatcher: ReadDenyMatcher | undefined;
236
259
  /** owned — the shell-seat PROVENANCE set: the shell-running tools whose tier the engine's shell seat (read
237
- * boundary + shellGate doctrine) is the SOLE POSITIVE source of — no explicit deployment mark on that name.
260
+ * boundary + shape tier + shellGate floor) is the SOLE POSITIVE source of — no explicit deployment mark on that name.
238
261
  * The one seat every consumer reads (risk grading, the gate's tighten attribution, the persisted-rule mandate
239
262
  * boundary): `shellGatedTools.has(toolName)`. Minted here, never grown afterwards; empty when the seat fold
240
263
  * did not run (no real writable shell). */
@@ -1,19 +1,22 @@
1
1
  import { emitTrace } from "../trace.js";
2
2
  import { StubExecutionEnv } from "../stub-env.js";
3
3
  import { hasBackgroundShell } from "../background-shell.js";
4
- import { boundaryIsTheSandbox } from "../remote-env.js";
5
4
  import { defaultTaskRegistry } from "../task-registry.js";
6
- import { bashReversibilityProbe, activeReadDenyBuiltinTiers, compileReadDeny, readDenySeat, createHandsToolkit, deploymentFaceClampNotice, pdfModelCapabilitiesOf, inheritSessionReadFileState, resolveContainmentFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, } from "../../tools/fs/index.js";
5
+ import { bashReversibilityProbe, activeReadDenyBuiltinTiers, readDenyBuiltinConfigOf, compileReadDeny, compileWriteDeny, withRuleRowsWhereTheyLead, readDenySeat, createHandsToolkit, deploymentFaceClampNotice, pdfModelCapabilitiesOf, inheritSessionReadFileState, backgroundOutputReadExemption, resolveContainmentFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, } from "../../tools/fs/index.js";
7
6
  import { resolveKey, sha256 } from "../../tools/fs/safety.js";
7
+ import { placeReadDenyAtHome, whereItsAnsweredPrefixLeads } from "../../tools/fs/read-deny-home.js";
8
8
  import { createShellOverflowSpoolFence } from "../../tools/fs/fs-shared.js";
9
9
  import { createShellReadStation } from "../../tools/fs/shell-read-check.js";
10
+ import { composeReadChecks, createFileReadStation } from "../../tools/fs/file-read-check.js";
11
+ import { readTargetToolNames } from "../tool-registry.js";
10
12
  import { normalizeFileText } from "../../tools/fs/encoding.js";
11
13
  import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
12
14
  import { deliverEngineNotice } from "../types.js";
13
- import { readRootGrantOver } from "./read-root-grant.js";
15
+ import { readRootGrantOver, rootAdmittingArtifactStore } from "./read-root-grant.js";
14
16
  import { sessionSeatAnchorHolds } from "./session-read-file-states.js";
15
17
  import { rebaseWorkspacePath } from "./workspace-path.js";
16
18
  import { RosterBuilder } from "../tool-roster.js";
19
+ import { modePolicyOf } from "../mode-policy.js";
17
20
  const faceClampAnnouncedSinks = { read: new WeakSet(), write: new WeakSet() };
18
21
  const faceClampConsoleAnnounced = { read: false, write: false };
19
22
  function announceDeploymentFaceClamp(kind, sink) {
@@ -52,6 +55,15 @@ function assertStringArraySeat(value, seat, entryLaw) {
52
55
  }
53
56
  }
54
57
  }
58
+ function writeFaceReader(input, onDeploymentClamp) {
59
+ const { spec, deps, resume, fullShellReachable, permissionMode } = input;
60
+ const seats = { kind: "write", readOnlyMount: spec.handsReadOnly === true, orgGoverned: deps.permissionRuleStore?.partitions.org === true, fullShellReachable };
61
+ resolveContainmentFace({ ...seats, specFace: spec.writeFace, depsFace: deps.writeFace, ...(onDeploymentClamp !== undefined ? { onDeploymentClamp } : {}) });
62
+ const recorded = resume === undefined ? "live" : resume.seed.writeFace?.face;
63
+ const folded = (word) => (recorded === "roots" ? "roots" : word === "open" && recorded !== "open" && recorded !== "live" ? undefined : word);
64
+ const faceUnder = (modeFace) => resolveContainmentFace({ ...seats, specFace: folded(spec.writeFace), depsFace: folded(deps.writeFace), ...(modeFace !== undefined ? { modeFace } : {}) });
65
+ return { inForce: () => faceUnder(modePolicyOf(permissionMode.current).writeFace), fenced: faceUnder("open") === "roots" };
66
+ }
55
67
  export function resolveHandsLessReadFace(input) {
56
68
  const { resume, fullShellReachable, spec, deps } = input;
57
69
  assertStringArraySeat(deps.readDenyPatterns, "RunnerDeps.readDenyPatterns", "compiler-owned");
@@ -71,21 +83,9 @@ export function resolveHandsLessReadFace(input) {
71
83
  });
72
84
  const seed = resume?.seed.readFace;
73
85
  handsLessResolvedFace = resume !== undefined && (seed === undefined || seed.face === "roots") ? "roots" : liveFace;
74
- const liveWriteFace = resolveContainmentFace({
75
- kind: "write",
76
- specFace: spec.writeFace,
77
- depsFace: deps.writeFace,
78
- readOnlyMount: spec.handsReadOnly === true,
79
- orgGoverned: deps.permissionRuleStore?.partitions.org === true,
80
- fullShellReachable,
81
- });
82
- const seedWrite = resume?.seed.writeFace;
83
- handsLessResolvedWriteFace = resume !== undefined && (seedWrite === undefined || seedWrite.face === "roots") ? "roots" : liveWriteFace;
86
+ handsLessResolvedWriteFace = writeFaceReader(input);
84
87
  const denyAdditions = [...(deps.readDenyPatterns ?? []), ...(spec.readDenyPatterns ?? []), ...(seed?.denyEntries ?? [])];
85
- readDenyBuiltinTiersActive = activeReadDenyBuiltinTiers({
86
- ...(deps.readDenyBuiltinTiers !== undefined ? { tiers: deps.readDenyBuiltinTiers } : {}),
87
- ...(deps.readDenyBuiltinExclude !== undefined ? { exclude: deps.readDenyBuiltinExclude } : {}),
88
- });
88
+ readDenyBuiltinTiersActive = activeReadDenyBuiltinTiers(readDenyBuiltinConfigOf({ tiers: deps.readDenyBuiltinTiers, exclude: deps.readDenyBuiltinExclude }));
89
89
  if (denyAdditions.length > 0) {
90
90
  readDenyAdditionsNormalized = compileReadDeny(denyAdditions, "readDenyPatterns", { tiers: [] }).entries;
91
91
  }
@@ -99,6 +99,7 @@ export function resolveHandsLessReadFace(input) {
99
99
  rebaseWsPath: (p) => p,
100
100
  backgroundTaskToolsActive: false,
101
101
  shellReadCheck: undefined,
102
+ shellReadStationArmed: false,
102
103
  shellReadAdmission: undefined,
103
104
  resolvedReadFace: undefined,
104
105
  resolvedWriteFace: undefined,
@@ -133,6 +134,8 @@ export async function prepareHandsMount(input) {
133
134
  let rebaseWsPath = (p) => p;
134
135
  let backgroundTaskToolsActive = false;
135
136
  let shellReadStation;
137
+ let shellReadTools = new Set();
138
+ let fileReadStation;
136
139
  let resolvedReadFace;
137
140
  let resolvedWriteFace;
138
141
  let readDenyAdditionsNormalized = [];
@@ -176,6 +179,10 @@ export async function prepareHandsMount(input) {
176
179
  };
177
180
  await canonicalizeExtraDirs(spec.additionalDirectories, "additionalDirectories", additionalRootsCanonical);
178
181
  await canonicalizeExtraDirs(spec.additionalReadDirectories, "additionalReadDirectories", additionalReadRootsCanonical);
182
+ const standingCanonical = [];
183
+ await canonicalizeExtraDirs(input.persistedReadDirectories, "persistedReadRule", standingCanonical);
184
+ artifactStoreRef.standing = { keys: standingCanonical.filter((k) => !additionalReadRootsCanonical.includes(k) && !additionalRootsCanonical.includes(k)), readRoots: additionalReadRootsCanonical };
185
+ additionalReadRootsCanonical.push(...standingCanonical);
179
186
  if (advertisedWritableDirs !== undefined) {
180
187
  if (spec.envFacts?.scratchpadDir)
181
188
  await advertisedWritableDirs.admit({ path: spec.envFacts.scratchpadDir, advertiser: "scratchpad", materializeMarker: ".sema-scratchpad" });
@@ -212,11 +219,10 @@ export async function prepareHandsMount(input) {
212
219
  }
213
220
  const seedReadFaceSection = resume?.seed.readFace;
214
221
  const readDenyAdditions = [...(deps.readDenyPatterns ?? []), ...(spec.readDenyPatterns ?? []), ...(seedReadFaceSection?.denyEntries ?? [])];
215
- const readDenyBuiltinCfg = {
216
- ...(deps.readDenyBuiltinTiers !== undefined ? { tiers: [...deps.readDenyBuiltinTiers] } : {}),
217
- ...(deps.readDenyBuiltinExclude !== undefined ? { exclude: [...deps.readDenyBuiltinExclude] } : {}),
218
- };
219
- readDenyMatcher = compileReadDeny(readDenyAdditions, "readDenyPatterns", readDenyBuiltinCfg);
222
+ const readDenyBuiltinCfg = readDenyBuiltinConfigOf({ tiers: deps.readDenyBuiltinTiers, exclude: deps.readDenyBuiltinExclude });
223
+ const pathDenyRules = await withRuleRowsWhereTheyLead(input.pathDenyRules, (head) => whereItsAnsweredPrefixLeads(executionEnv, head));
224
+ const readDenyPlacement = await placeReadDenyAtHome(executionEnv, (homes, linked) => compileReadDeny(readDenyAdditions, "readDenyPatterns", readDenyBuiltinCfg, pathDenyRules, homes, linked));
225
+ readDenyMatcher = readDenyPlacement.matcher;
220
226
  readDenyBuiltinTiersActive = activeReadDenyBuiltinTiers(readDenyBuiltinCfg);
221
227
  readDenyAdditionsNormalized = compileReadDeny(readDenyAdditions, "readDenyPatterns", { tiers: [] }).entries;
222
228
  let liveReadFace = resolveContainmentFace({
@@ -231,19 +237,7 @@ export async function prepareHandsMount(input) {
231
237
  if (resume !== undefined && (seedReadFaceSection === undefined || seedReadFaceSection.face === "roots"))
232
238
  liveReadFace = "roots";
233
239
  resolvedReadFace = liveReadFace;
234
- let liveWriteFace = resolveContainmentFace({
235
- kind: "write",
236
- specFace: spec.writeFace,
237
- depsFace: deps.writeFace,
238
- readOnlyMount: handsReadOnly,
239
- orgGoverned: deps.permissionRuleStore?.partitions.org === true,
240
- fullShellReachable,
241
- onDeploymentClamp: () => announceDeploymentFaceClamp("write", deps.onNotice),
242
- });
243
- const seedWriteFaceSection = resume?.seed.writeFace;
244
- if (resume !== undefined && (seedWriteFaceSection === undefined || seedWriteFaceSection.face === "roots"))
245
- liveWriteFace = "roots";
246
- resolvedWriteFace = liveWriteFace;
240
+ resolvedWriteFace = writeFaceReader(input, () => announceDeploymentFaceClamp("write", deps.onNotice));
247
241
  const sessionSeats = input.internals?.sessionReadStates;
248
242
  let seatLeafId;
249
243
  try {
@@ -297,31 +291,55 @@ export async function prepareHandsMount(input) {
297
291
  }
298
292
  : undefined;
299
293
  const denySeat = readDenySeat(readDenyMatcher);
294
+ const writeDeny = compileWriteDeny(pathDenyRules);
295
+ const writeDenySeat = writeDeny.ruleRows.length > 0 ? { writeDenyMatch: (p) => writeDeny.matchPath(p)?.pattern ?? null } : {};
300
296
  const shellReadBoundary = () => ({
301
297
  roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
302
298
  ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
303
299
  ...(executionEnv.homeDir !== undefined ? { homeDir: executionEnv.homeDir } : {}),
304
300
  ...denySeat,
301
+ ...writeDenySeat,
302
+ writeRoots: [rootCanonical, ...additionalRootsCanonical],
303
+ ...((judge) => (judge !== undefined ? { writeProtectionJudge: judge } : {}))(input.writeGuards?.protection()),
304
+ ...(input.writeGuards?.target !== undefined ? { writeTargetGuard: input.writeGuards.target } : {}),
305
305
  ...(resolvedReadFace !== undefined ? { face: resolvedReadFace } : {}),
306
306
  });
307
307
  if (handsIncludeShell) {
308
308
  const spoolFence = handsReadOnly ? createShellOverflowSpoolFence(executionEnv) : undefined;
309
- const readonlyBoundary = () => spoolFence !== undefined ? { roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical], cwd: rootCanonical, ...denySeat, alsoInside: spoolFence } : undefined;
309
+ const readonlyBoundary = () => spoolFence !== undefined ? { roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical], cwd: rootCanonical, ...denySeat, ...writeDenySeat, alsoInside: spoolFence } : undefined;
310
+ shellReadTools = new Set(handsReadOnly ? ["Bash"] : backgroundTaskToolsActive ? ["Bash", "Monitor"] : ["Bash"]);
310
311
  shellReadStation = createShellReadStation({
311
312
  env: executionEnv,
312
- tools: new Set(handsReadOnly ? ["Bash"] : backgroundTaskToolsActive ? ["Bash", "Monitor"] : ["Bash"]),
313
- seatOf: () => (handsReadOnly ? "readonly" : effectiveShellGate === "classify" ? "bash_classify" : "bash_off"),
313
+ tools: shellReadTools,
314
+ seatOf: () => (handsReadOnly ? "readonly" : "full_shell"),
314
315
  boundaryOf: () => readonlyBoundary() ?? shellReadBoundary(), rootsCanonical: true,
315
316
  });
316
317
  }
318
+ if (!handsReadOnly) {
319
+ const bgOutputExempt = backgroundOutputReadExemption(defaultTaskRegistry, { taskOwner: hostTaskId, taskScope, ...(sessionId !== undefined ? { sessionId } : {}) }, executionEnv);
320
+ fileReadStation = createFileReadStation({
321
+ env: executionEnv,
322
+ rootCanonical,
323
+ roots: () => [...additionalRootsCanonical, ...additionalReadRootsCanonical],
324
+ ...(readDenyMatcher !== undefined ? { readDeny: readDenyMatcher } : {}),
325
+ ...(liveReadFace !== undefined ? { readFace: liveReadFace } : {}),
326
+ ...("homeDir" in executionEnv && executionEnv.homeDir !== undefined ? { homeDir: executionEnv.homeDir } : {}),
327
+ tools: readTargetToolNames(),
328
+ excluded: (key) => artifactStoreRef.current !== undefined && rootAdmittingArtifactStore(artifactStoreRef.current, [key]) !== undefined,
329
+ exempt: (key) => bgOutputExempt?.(key) === true,
330
+ });
331
+ }
317
332
  const band = createHandsToolkit(executionEnv, readFileState, rootCanonical, {
318
333
  ...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}),
319
334
  additionalReadRoots: additionalReadRootsCanonical,
320
335
  ...(readDenyAdditions.length > 0 ? { readDenyPatterns: readDenyAdditions } : {}),
336
+ ...(pathDenyRules.length > 0 ? { pathDenyRules } : {}),
337
+ readDenyHomes: readDenyPlacement.homes,
338
+ readDenyHomeLinks: readDenyPlacement.linked,
321
339
  ...(readDenyBuiltinCfg.tiers !== undefined ? { readDenyBuiltinTiers: readDenyBuiltinCfg.tiers } : {}),
322
340
  ...(readDenyBuiltinCfg.exclude !== undefined ? { readDenyBuiltinExclude: readDenyBuiltinCfg.exclude } : {}),
323
341
  readFace: liveReadFace,
324
- writeFace: liveWriteFace,
342
+ writeFaceLive: resolvedWriteFace.inForce,
325
343
  reminderMark: input.reminderMark,
326
344
  reminderDisclosureCounts: input.reminderDisclosureCounts,
327
345
  ...(input.trackFileEdit !== undefined ? { trackFileEdit: input.trackFileEdit } : {}),
@@ -329,6 +347,7 @@ export async function prepareHandsMount(input) {
329
347
  includeShell: handsIncludeShell,
330
348
  readOnly: handsReadOnly,
331
349
  ...(shellReadStation !== undefined ? { shellReadAdmission: shellReadStation.admit } : {}),
350
+ ...(fileReadStation !== undefined ? { readAdmission: fileReadStation.admitted } : {}),
332
351
  ...(handsCwdRef ? { cwdRef: handsCwdRef } : {}),
333
352
  taskRegistry: defaultTaskRegistry,
334
353
  taskOwner: hostTaskId,
@@ -368,27 +387,18 @@ export async function prepareHandsMount(input) {
368
387
  }
369
388
  }
370
389
  roster.mountAll(band.map((t) => (envHandToolNames.add(t.name), firstPartyOffload(t))), { source: "builtin", mountedBy: "hands" });
371
- const shellGate = effectiveShellGate;
390
+ const shellFloor = effectiveShellGate;
372
391
  if (!(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
373
- if (shellGate === "off") {
374
- deps.onError?.(new Error(`shell gate doctrine is "off" while a real writable shell (Bash) is mounted — the read boundary is ` +
375
- `still judged on every command (a deny-listed read is refused; an out-of-root read asks, and the ask is mandated${boundaryIsTheSandbox(executionEnv) ? "; under an isolated sandbox the paths are judged as written" : "; the paths are judged as the filesystem resolves them, symlinks and patterns included"}), but no ` +
376
- `residual shell-risk classifier is installed: writes, egress and unknown commands are adjudicated by ` +
377
- `the ordinary policy/hook chain only. Set spec.shellGate to "classify" or "always" if this deployment ` +
378
- `expects doctrine-gated shell behavior.`), { phase: "config", sessionId, classification: "shell-gate-off" });
379
- }
380
392
  const installShellSeat = (name) => {
381
393
  const tierBefore = irreversibilityTier.get(name);
382
394
  const seatOwned = !egressTools.has(name) && tierBefore !== "always" && tierBefore !== "maybe";
383
- if (seatOwned)
384
- shellGatedTools.add(name);
385
- if (shellGate === "off" && tierBefore !== "always")
386
- return;
387
- const tier = shellGate === "always" || tierBefore === "always" ? "always" : "maybe";
395
+ const tier = shellFloor === "always" || tierBefore === "always" ? "always" : "maybe";
388
396
  irreversibilityTier.set(name, tier);
389
397
  irreversibleTools.add(name);
390
398
  if (seatOwned && tier === "maybe")
391
399
  reversibilityProbes.set(name, bashReversibilityProbe(undefined, shellReadBoundary));
400
+ if (seatOwned)
401
+ shellGatedTools.add(name);
392
402
  };
393
403
  installShellSeat("Bash");
394
404
  if (backgroundTaskToolsActive)
@@ -403,7 +413,8 @@ export async function prepareHandsMount(input) {
403
413
  wsSnapshot,
404
414
  rebaseWsPath,
405
415
  backgroundTaskToolsActive,
406
- shellReadCheck: shellReadStation?.check,
416
+ shellReadCheck: composeReadChecks([...(shellReadStation !== undefined ? [{ check: shellReadStation.check, tools: shellReadTools }] : []), ...(fileReadStation !== undefined ? [fileReadStation] : [])]),
417
+ shellReadStationArmed: shellReadStation !== undefined,
407
418
  shellReadAdmission: shellReadStation?.admit,
408
419
  resolvedReadFace,
409
420
  resolvedWriteFace,