@sema-agent/core 7.33.8 → 7.35.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 (228) hide show
  1. package/CHANGELOG.md +462 -3
  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/errors.d.ts +23 -1
  11. package/dist/brain/errors.js +19 -2
  12. package/dist/brain/open-responses.js +1 -0
  13. package/dist/brain/openai.js +1 -0
  14. package/dist/brain/status-sink.d.ts +7 -1
  15. package/dist/brain/stream-engine.d.ts +3 -0
  16. package/dist/brain/stream-engine.js +2 -1
  17. package/dist/brain/stream-shared.d.ts +3 -1
  18. package/dist/brain/stream-shared.js +16 -4
  19. package/dist/core/a2a.d.ts +3 -1
  20. package/dist/core/a2a.js +1 -1
  21. package/dist/core/ask-origin.d.ts +379 -154
  22. package/dist/core/ask-origin.js +224 -45
  23. package/dist/core/ask-question.js +2 -1
  24. package/dist/core/ask-unresolvable-notice.d.ts +5 -2
  25. package/dist/core/ask-unresolvable-notice.js +4 -3
  26. package/dist/core/auto-mode-arming.d.ts +17 -4
  27. package/dist/core/auto-mode-arming.js +31 -6
  28. package/dist/core/auto-mode-defaults.d.ts +16 -7
  29. package/dist/core/auto-mode-defaults.js +2 -1
  30. package/dist/core/auto-mode-directive.d.ts +18 -30
  31. package/dist/core/auto-mode-directive.js +9 -18
  32. package/dist/core/auto-mode-prompt.d.ts +62 -1
  33. package/dist/core/auto-mode-prompt.js +42 -4
  34. package/dist/core/auto-mode-rebuild.d.ts +2 -1
  35. package/dist/core/auto-mode-rebuild.js +1 -0
  36. package/dist/core/auto-mode.d.ts +32 -3
  37. package/dist/core/auto-mode.js +80 -41
  38. package/dist/core/checkpoint-store.d.ts +54 -35
  39. package/dist/core/checkpoint-store.js +58 -3
  40. package/dist/core/child-clamp-seats.js +1 -0
  41. package/dist/core/classifier-unavailable-arm.d.ts +57 -0
  42. package/dist/core/classifier-unavailable-arm.js +17 -0
  43. package/dist/core/effective-path-target.d.ts +12 -0
  44. package/dist/core/effective-path-target.js +18 -7
  45. package/dist/core/engine-notice.d.ts +53 -2
  46. package/dist/core/fs-write-gate-policy.d.ts +5 -1
  47. package/dist/core/fs-write-gate-policy.js +4 -1
  48. package/dist/core/gate-fold.js +19 -3
  49. package/dist/core/gate-lanes.js +116 -50
  50. package/dist/core/gate-outcome.d.ts +40 -7
  51. package/dist/core/gate-outcome.js +26 -4
  52. package/dist/core/governance-codes.d.ts +18 -2
  53. package/dist/core/governance-codes.js +23 -1
  54. package/dist/core/hooks.d.ts +53 -29
  55. package/dist/core/hooks.js +1 -3
  56. package/dist/core/mcp.js +1 -1
  57. package/dist/core/message-utils.d.ts +2 -1
  58. package/dist/core/message-utils.js +2 -1
  59. package/dist/core/mode-policy.d.ts +185 -0
  60. package/dist/core/mode-policy.js +184 -0
  61. package/dist/core/oracle-isolation.d.ts +3 -3
  62. package/dist/core/permission-rule-model.d.ts +20 -3
  63. package/dist/core/permission-rule-model.js +40 -3
  64. package/dist/core/permission-rule-provider.d.ts +22 -11
  65. package/dist/core/permission-rule-provider.js +17 -7
  66. package/dist/core/permission-rules.d.ts +23 -1
  67. package/dist/core/permission-rules.js +56 -9
  68. package/dist/core/persisted-rule-arms.d.ts +14 -10
  69. package/dist/core/persisted-rule-arms.js +13 -6
  70. package/dist/core/present-plan-tool.d.ts +1 -1
  71. package/dist/core/present-plan-tool.js +3 -3
  72. package/dist/core/read-only-shell.d.ts +19 -19
  73. package/dist/core/read-only-shell.js +38 -12
  74. package/dist/core/retired-keys.d.ts +9 -3
  75. package/dist/core/retired-keys.js +5 -1
  76. package/dist/core/runner/active-skill-scope.js +5 -4
  77. package/dist/core/runner/assemble-result.js +2 -2
  78. package/dist/core/runner/classifier-ladder.d.ts +43 -1
  79. package/dist/core/runner/classifier-ladder.js +29 -0
  80. package/dist/core/runner/contracts.d.ts +62 -38
  81. package/dist/core/runner/denial-limit-arms.d.ts +35 -7
  82. package/dist/core/runner/denial-limit-arms.js +15 -9
  83. package/dist/core/runner/env-capability-census.js +1 -1
  84. package/dist/core/runner/file-history-request.d.ts +26 -0
  85. package/dist/core/runner/file-history-request.js +90 -0
  86. package/dist/core/runner/gate-exit.d.ts +30 -4
  87. package/dist/core/runner/gate-exit.js +28 -7
  88. package/dist/core/runner/permission-rule-lanes.d.ts +11 -6
  89. package/dist/core/runner/permission-rule-lanes.js +55 -48
  90. package/dist/core/runner/persisted-path-rows.d.ts +64 -0
  91. package/dist/core/runner/persisted-path-rows.js +89 -0
  92. package/dist/core/runner/prepare-artifact.d.ts +5 -1
  93. package/dist/core/runner/prepare-artifact.js +9 -3
  94. package/dist/core/runner/prepare-ask-lane.d.ts +6 -4
  95. package/dist/core/runner/prepare-ask-lane.js +3 -3
  96. package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
  97. package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
  98. package/dist/core/runner/prepare-file-history.d.ts +9 -3
  99. package/dist/core/runner/prepare-file-history.js +4 -90
  100. package/dist/core/runner/prepare-gate-stations.d.ts +37 -9
  101. package/dist/core/runner/prepare-gate-stations.js +46 -29
  102. package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
  103. package/dist/core/runner/prepare-hands-readface.js +64 -53
  104. package/dist/core/runner/prepare-inherited-gate.d.ts +30 -23
  105. package/dist/core/runner/prepare-inherited-gate.js +45 -25
  106. package/dist/core/runner/prepare-park-ask.d.ts +6 -4
  107. package/dist/core/runner/prepare-park-ask.js +7 -5
  108. package/dist/core/runner/prepare-policy-chain.d.ts +25 -4
  109. package/dist/core/runner/prepare-policy-chain.js +203 -180
  110. package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
  111. package/dist/core/runner/prepare-protocol-tools.js +13 -3
  112. package/dist/core/runner/prepare-run-refs.d.ts +6 -6
  113. package/dist/core/runner/prepare-run-refs.js +9 -4
  114. package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
  115. package/dist/core/runner/prepare-safety-scan.js +19 -3
  116. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
  117. package/dist/core/runner/prepare-suspend-saga.js +6 -6
  118. package/dist/core/runner/prepare-task.js +56 -24
  119. package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
  120. package/dist/core/runner/prepare-wiring-manifest.js +5 -3
  121. package/dist/core/runner/read-root-grant.d.ts +9 -0
  122. package/dist/core/runner/read-root-grant.js +12 -0
  123. package/dist/core/runner/retired-runner-deps.js +1 -0
  124. package/dist/core/runner/run-brain-sinks.js +3 -1
  125. package/dist/core/runner/run-identity-wiring.js +1 -1
  126. package/dist/core/runner/run-leg.js +29 -18
  127. package/dist/core/runner/run-notification-lane.d.ts +3 -0
  128. package/dist/core/runner/run-notification-lane.js +3 -1
  129. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
  130. package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
  131. package/dist/core/runner/run-turn-boundary.js +1 -1
  132. package/dist/core/runner/runtask.d.ts +1 -0
  133. package/dist/core/runner/runtask.js +8 -11
  134. package/dist/core/runner/session-mode-seats.d.ts +48 -0
  135. package/dist/core/runner/session-mode-seats.js +73 -0
  136. package/dist/core/runner/session-rule-policy.js +3 -2
  137. package/dist/core/runner/stream-mode-verb.d.ts +27 -0
  138. package/dist/core/runner/stream-mode-verb.js +19 -0
  139. package/dist/core/runner/turn-attachments.d.ts +3 -3
  140. package/dist/core/runner/turn-attachments.js +1 -1
  141. package/dist/core/runner/write-protection-station.d.ts +7 -1
  142. package/dist/core/runner/write-protection-station.js +6 -3
  143. package/dist/core/runner-deps.d.ts +31 -34
  144. package/dist/core/runtime-caps.d.ts +12 -3
  145. package/dist/core/safety-axis-vocab.d.ts +2 -2
  146. package/dist/core/safety-axis-vocab.js +1 -1
  147. package/dist/core/secret-env.d.ts +30 -58
  148. package/dist/core/secret-env.js +149 -586
  149. package/dist/core/sensitive-path-policy.d.ts +4 -2
  150. package/dist/core/sensitive-path-policy.js +41 -31
  151. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
  152. package/dist/core/task-event.d.ts +1 -1
  153. package/dist/core/task-result.d.ts +1 -1
  154. package/dist/core/task-spec.d.ts +41 -89
  155. package/dist/core/task-stream.d.ts +16 -0
  156. package/dist/core/terminal-cause.js +1 -0
  157. package/dist/core/tighten-task-spec.d.ts +2 -2
  158. package/dist/core/tighten-task-spec.js +12 -14
  159. package/dist/core/tool-catalog-entries.js +4 -3
  160. package/dist/core/tool-policy.d.ts +84 -30
  161. package/dist/core/tool-policy.js +76 -16
  162. package/dist/core/tool-registry.d.ts +38 -0
  163. package/dist/core/tool-registry.js +25 -1
  164. package/dist/core/tool-roster.d.ts +17 -2
  165. package/dist/core/tool-roster.js +11 -1
  166. package/dist/core/tool-spec.d.ts +8 -9
  167. package/dist/core/trace.d.ts +18 -6
  168. package/dist/core/types.d.ts +2 -1
  169. package/dist/core/wiring-manifest.d.ts +31 -5
  170. package/dist/core/wiring-manifest.js +28 -1
  171. package/dist/core/write-protect.d.ts +11 -1
  172. package/dist/core/write-protect.js +26 -15
  173. package/dist/engine/execution-env/node-execution-env.d.ts +6 -1
  174. package/dist/engine/execution-env/node-execution-env.js +16 -3
  175. package/dist/engine/harness/agent-harness.js +2 -1
  176. package/dist/engine/harness/types.d.ts +6 -7
  177. package/dist/engine/llm/types.d.ts +2 -4
  178. package/dist/engine/loop/types.d.ts +7 -0
  179. package/dist/engine/lsp/node-lsp-manager.js +1 -1
  180. package/dist/index.d.ts +18 -8
  181. package/dist/index.js +15 -6
  182. package/dist/orchestration/workflow-governance.js +1 -1
  183. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  184. package/dist/orchestration/workflow-size-guideline.js +2 -2
  185. package/dist/stores/file/index.d.ts +5 -3
  186. package/dist/stores/file/session-store.d.ts +8 -2
  187. package/dist/stores/file/session-store.js +5 -5
  188. package/dist/tools/artifact/artifact-tool.d.ts +17 -0
  189. package/dist/tools/artifact/artifact-tool.js +16 -1
  190. package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
  191. package/dist/tools/fs/bash-readonly-classifier.js +88 -0
  192. package/dist/tools/fs/containment-face.d.ts +9 -3
  193. package/dist/tools/fs/containment-face.js +2 -0
  194. package/dist/tools/fs/file-read-check.d.ts +50 -0
  195. package/dist/tools/fs/file-read-check.js +79 -0
  196. package/dist/tools/fs/fs-bash.d.ts +3 -3
  197. package/dist/tools/fs/fs-read.d.ts +1 -0
  198. package/dist/tools/fs/fs-search-tools.d.ts +2 -2
  199. package/dist/tools/fs/fs-search-tools.js +4 -4
  200. package/dist/tools/fs/fs-shared.js +20 -6
  201. package/dist/tools/fs/fs-write.d.ts +4 -3
  202. package/dist/tools/fs/fs-write.js +9 -9
  203. package/dist/tools/fs/index.d.ts +39 -3
  204. package/dist/tools/fs/index.js +38 -15
  205. package/dist/tools/fs/read-deny-home.d.ts +93 -0
  206. package/dist/tools/fs/read-deny-home.js +225 -0
  207. package/dist/tools/fs/read-deny.d.ts +176 -34
  208. package/dist/tools/fs/read-deny.js +268 -61
  209. package/dist/tools/fs/safety.d.ts +6 -1
  210. package/dist/tools/fs/safety.js +18 -5
  211. package/dist/tools/fs/search.d.ts +3 -0
  212. package/dist/tools/fs/search.js +47 -9
  213. package/dist/tools/fs/shell-read-check.d.ts +52 -13
  214. package/dist/tools/fs/shell-read-check.js +199 -20
  215. package/dist/tools/fs/shell-read-establish.d.ts +7 -3
  216. package/dist/tools/fs/shell-read-establish.js +134 -21
  217. package/dist/tools/fs/shell-read-identity.d.ts +7 -0
  218. package/dist/tools/fs/shell-read-identity.js +21 -0
  219. package/dist/tools/fs/shell-read-judge.d.ts +92 -32
  220. package/dist/tools/fs/shell-read-judge.js +43 -18
  221. package/dist/tools/fs/shell-read-plan.d.ts +4 -0
  222. package/dist/tools/fs/shell-read-plan.js +26 -4
  223. package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
  224. package/dist/tools/fs/shell-read-write-targets.js +173 -0
  225. package/package.json +1 -1
  226. package/test/export-surface.snapshot.json +117 -11
  227. package/wire-changes/core@7.34.0.json +2621 -0
  228. package/wire-changes/core@7.35.0.json +584 -0
@@ -7,7 +7,7 @@ import type { ReadEntry } from "../tools/fs/safety.js";
7
7
  import type { RepairBundle } from "../agents/repair-loop.js";
8
8
  import type { WorkspaceHandle } from "./remote-env.js";
9
9
  import type { SessionPermissionRules } from "./session-policy-store.js";
10
- import type { ApproverPosture, NestedUsageAccum, ShellGateDoctrine, TaskResult } from "./types.js";
10
+ import type { NestedUsageAccum, PermissionMode, ShellGateDoctrine, TaskResult } from "./types.js";
11
11
  import type { ActiveWorktreeSession } from "../internal/harness-types.js";
12
12
  /**
13
13
  * design/45 — the **durable-checkpoint** primitive: cross-process / resumable `suspend`/`resume`.
@@ -130,11 +130,9 @@ export interface RiskDescriptor {
130
130
  };
131
131
  /** The tool whose call is gated (mirrors the gate's `toolName`). */
132
132
  toolName: string;
133
- /** The run's resolved shellGate doctrine, present ONLY when this ask was minted through the engine's shell
134
- * seat (coarse `always`; a `classify` probe that declined to auto-allow; the read-boundary judge asking under
135
- * `off` — the boundary is judged under every doctrine, so the vocabulary is `TaskSpec.shellGate`'s whole).
136
- * Absent for per-tool safety-axis / plain policy asks. Answers "which doctrine was live when this gate
137
- * fired" from the persisted row alone — three artifact layers could not answer that in the field. */
133
+ /** The operator's shell floor (`"always"`), present ONLY when this ask was minted through the engine's shell
134
+ * seat AND a floor was in force. Absent for a shape-tier ask with no floor, and for per-tool safety-axis /
135
+ * plain policy asks. Answers "was an operator floor live when this gate fired" from the persisted row alone. */
138
136
  shellGateDoctrine?: ShellGateDoctrine;
139
137
  /** A REDACTED, length-capped ONE-LINE summary of the call (the command for a shell gate; a brief key-arg
140
138
  * digest otherwise) for the inbox preview. NEUTRALIZED via
@@ -816,6 +814,9 @@ export type PendingAction = {
816
814
  * when a person's rule also speaks — never a substitute for this one). Display metadata: no gate, CAS or
817
815
  * resume path reads it. */
818
816
  mandated?: true;
817
+ /** The PARK twin of `AskRequest.authoredBy`: the deployment's own governance layer minted this parked question — a
818
+ * person's under every mode, whatever grant or rule the redeeming host holds. Display / seat metadata. */
819
+ authoredBy?: "deployment";
819
820
  /** The PARK twin of `AskRequest.mandate` (same derivation, same precedent): the table word the bit stands on,
820
821
  * present only when it is the whole reason. Written only as a member of the closed set. */
821
822
  mandate?: import("./ask-origin.js").PersistedRuleMandate;
@@ -833,10 +834,6 @@ export type PendingAction = {
833
834
  * Written only when it is a member of the set; a pre-existing row reads absent (unreported).
834
835
  * Display metadata — the resume belts keep reading the gate's own bit. */
835
836
  origin?: import("./ask-origin.js").AskOrigin;
836
- /** #688 C3 (additive, same precedent as `origin`) — the mechanism behind an `origin: "rule_store_unavailable"`
837
- * (`store` / `call`, {@link import("./ask-origin.js").RULE_STORE_UNREADABLE_KINDS}). Written only when it is a
838
- * member of the set; a pre-existing row reads absent. Display metadata for the durable card. */
839
- ruleStoreUnreadable?: import("./ask-origin.js").RuleStoreUnreadable;
840
837
  /**
841
838
  * design/80 D-1 §2 (slice 1a.2): the server-minted **opaque** boundInputHash of {@link args} — a
842
839
  * SHA-256 (hex) via {@link import("./canonical-json.js").boundInputHashOf}, computed ONCE here at
@@ -871,6 +868,10 @@ export type PendingAction = {
871
868
  */
872
869
  family?: import("./tool-face.js").ToolFamily;
873
870
  pathTarget?: import("./tool-face.js").ToolRosterPathTarget;
871
+ /** The names a rule may spell the parked tool by (`ToolCallFace.names`: the wire name, then the aliases that resolve
872
+ * to it) off the same roster row at the same mint — the pre-CAS recheck judges an edit against rules written on
873
+ * either name, as the live gate judged the original. Absent on a row minted before it ⇒ the row's `toolName` alone. */
874
+ names?: readonly string[];
874
875
  /**
875
876
  * #438 (additive; no checkpoint-version bump — the `reminderMark` precedent: an optional field an
876
877
  * older reader ignores. Unlike that precedent seat — which resume DOES read, through its
@@ -1098,7 +1099,7 @@ export interface CheckpointState {
1098
1099
  * Parent effective-policy inheritance (tighten-only): the durable projection of the run's
1099
1100
  * {@link import("./runner/contracts.js").InheritedGate}. Only the serializable DATA half persists —
1100
1101
  * `ancestorRules` (each ancestor's session-rule snapshot: sessionId/principal/rev/rules, re-compiled on
1101
- * resume against the resumed leg's env) and `shellGate` (the chain's max-rank shell doctrine). The OPAQUE
1102
+ * resume against the resumed leg's env), `shellGate` (the chain's operator floor) and `permissionMode`. The OPAQUE
1102
1103
  * half (`parentConstraints` — live ToolPolicy/onAsk closures) CANNOT persist; `requiresParentConstraint`
1103
1104
  * records only its PRESENCE, so the resume pre-CAS ladder can fail-loud (`resume.parent_constraint_missing`,
1104
1105
  * checkpoint stays `pending`) when the resuming caller did not re-supply the closures via
@@ -1115,27 +1116,18 @@ export interface CheckpointState {
1115
1116
  rules: SessionPermissionRules;
1116
1117
  }>;
1117
1118
  shellGate?: ShellGateDoctrine;
1118
- /** The chain's AUTO-MODE INTENT at suspend (data half, same law as `shellGate`): `true` when the
1119
- * suspended leg was an auto-mode task — its own seat, the bit its live chain carried, or the bit
1120
- * an earlier suspend of the same chain recorded (carried forward across a re-suspend). A resume
1121
- * leg reads it as one more INTENT source beside the re-supplied seat and the waking chain, so a
1122
- * redemption in another process (no seat re-passed, no live chain) still arms exactly as the
1123
- * suspend leg did. It is a MEMORY of intent, never an authorization: the resuming deployment's
1124
- * face (`RunnerDeps.autoMode`) and the resuming principal's deny bit (`RuntimeCaps.autoMode`)
1125
- * are judged afresh on every leg — a bit on the row cannot arm where the redeeming deployment
1126
- * would not. A leg never armed here carries the memory as is. Absent on older checkpoints and on non-auto tasks
1127
- * (byte-identical to the pre-bit row); an older worker that ignores it resumes un-armed, the
1128
- * narrower direction. */
1129
- autoModeRequested?: true;
1130
- /** The chain's declared APPROVER POSTURE at suspend (data half, same law as `shellGate`): the word
1131
- * the suspended leg was told to render — its own seat, the one its live chain carried, or the one an
1132
- * earlier suspend of the same chain recorded. A resume leg reads it beside the re-supplied seat and
1133
- * the waking chain, so a redemption in another process states the same posture the suspend leg did.
1134
- * It authorizes nothing (the gate never reads it) and it renders nothing on a resume leg either —
1135
- * the first-turn frame that carries it is already in the restored transcript; what it feeds is the
1136
- * chain the resumed leg hands its OWN children. Absent on older checkpoints and on tasks that
1137
- * declared no posture (byte-identical row). */
1138
- approverPosture?: ApproverPosture;
1119
+ /** The chain's DECLARED permission mode at suspend (data half, same law as `shellGate`): the suspended leg's
1120
+ * own effective declaration — its request clamped under its parent's ceiling — before the deployment's fold.
1121
+ * A resume leg reads it as the CEILING its re-passed spec is clamped under (tighten-only, the shell floor's
1122
+ * law) and re-judges the deny bits and the classifier face for its own leg: a `bypassPermissions` row does not
1123
+ * revive the mode where the redeeming deployment denies it, and an `auto` row arms only where a face is
1124
+ * wired. Absent on a task that declared nothing and inherited nothing (`default`). Rows written before this
1125
+ * member carried `autoModeRequested` / `approverPosture` / a doctrine `shellGate`; the resume leg migrates
1126
+ * them once, announced (`resume.legacy_mode_seed_migrated`), and the next suspend writes this member. */
1127
+ permissionMode?: PermissionMode;
1128
+ /** Beside a recorded `plan`: the declared word the leg had when a steer or the model's `EnterPlanMode` moved it into
1129
+ * plan — the mode an approved or edited `plan_review` resumes under ({@link import("./mode-policy.js").modeRequestAfterResume}). */
1130
+ prePlanMode?: PermissionMode;
1139
1131
  /** Org-memory admission freeze (ruled 2026-08-05): the chain's admitted org-scope set at
1140
1132
  * suspend (data half, plain strings). The resume leg folds it seed ∩ live (tighten-only) and
1141
1133
  * re-runs admission under it — a resume must never widen the delegation freeze. Absent on
@@ -1950,6 +1942,8 @@ export interface CheckpointSummary {
1950
1942
  * OMITTED (the {@link toolCallId} absence-shape contract), screened to the literal `true` — a pre-#1079 row
1951
1943
  * reads absent (unreported). ECHO-ONLY; nothing gates on it. */
1952
1944
  mandated?: true;
1945
+ /** The row's `PendingAction.tool_approval.authoredBy`, echoed as the one literal. ECHO-ONLY. */
1946
+ authoredBy?: "deployment";
1953
1947
  /** The row's `PendingAction.tool_approval.mandate`, echoed ONLY as a member of the closed set. ECHO-ONLY. */
1954
1948
  mandate?: import("./ask-origin.js").PersistedRuleMandate;
1955
1949
  /** The row's `PendingAction.tool_approval.denialLimitFallback`, projected BESIDE the preview
@@ -1962,9 +1956,6 @@ export interface CheckpointSummary {
1962
1956
  * value is a member of the closed set ({@link import("./ask-origin.js").isAskOrigin}) — a bad value
1963
1957
  * reads as absent, never as a word. OMITTED when the row carries none (a pre-existing row). */
1964
1958
  origin?: import("./ask-origin.js").AskOrigin;
1965
- /** The row's `PendingAction.tool_approval.ruleStoreUnreadable` (#688 C3), echoed ONLY when the stored word is a
1966
- * member of the closed set ({@link import("./ask-origin.js").isRuleStoreUnreadable}); OMITTED otherwise. */
1967
- ruleStoreUnreadable?: import("./ask-origin.js").RuleStoreUnreadable;
1968
1959
  /**
1969
1960
  * PRESENCE ONLY — this pending `tool_approval` row's payload carries at least one DIRECTIONAL
1970
1961
  * format control (the {@link import("./tool-policy.js").AskRequest.hasBidiControls} character class;
@@ -3027,3 +3018,31 @@ export declare class InMemoryCheckpointStore implements CheckpointStore {
3027
3018
  /** Test/inspection helper: number of stored checkpoints. */
3028
3019
  get size(): number;
3029
3020
  }
3021
+ /** What {@link migrateLegacyModeSeed} read off a checkpoint row's `inheritedGate`: the mode and the floor the resume
3022
+ * leg inherits, and — when the row carried a pre-mode key — what was migrated and what was dropped. */
3023
+ export interface LegacyModeSeedMigration {
3024
+ permissionMode?: PermissionMode;
3025
+ prePlanMode?: PermissionMode;
3026
+ shellGate?: ShellGateDoctrine;
3027
+ /** Present iff the row carried a retired key (`autoModeRequested` / `approverPosture` / a doctrine `shellGate`). */
3028
+ migrated?: {
3029
+ permissionMode?: PermissionMode;
3030
+ droppedKeys: readonly string[];
3031
+ };
3032
+ }
3033
+ /**
3034
+ * Read a row's mode seed ONCE, migrating the pre-7.34.0 keys the row may still carry. The matrix, key by key:
3035
+ * - `permissionMode` present ⇒ it must be a member of the closed set (else refused, `config.permission_mode_invalid`,
3036
+ * without reading the retired keys); the two retired mode keys beside it are DROPPED unvalidated (the new key is
3037
+ * the truth), `shellGate` follows the value rule below;
3038
+ * - `autoModeRequested`: absent / `false` ⇒ no mode from it; `true` ⇒ `auto`; anything else ⇒ refused
3039
+ * (`resume.legacy_mode_seed`);
3040
+ * - `approverPosture`: any value ⇒ dropped (it never authorized anything; the mode comes from the request / chain,
3041
+ * and a `"bypass"` posture beside `autoModeRequested: true` reads `auto` — a posture never becomes an executable
3042
+ * bypass);
3043
+ * - `shellGate`: absent ⇒ none; `"always"` ⇒ kept (a live key, not a retired encoding); `"off"` / `"classify"` ⇒
3044
+ * dropped (no floor); anything else ⇒ refused (`resume.legacy_mode_seed`).
3045
+ * Idempotent: a row the resume leg writes back carries `permissionMode` and the floor only, so a second read migrates
3046
+ * nothing. Migration precedes any retired-key door, so a window row is read rather than refused.
3047
+ */
3048
+ export declare function migrateLegacyModeSeed(gate: object | undefined): LegacyModeSeedMigration;
@@ -3,11 +3,12 @@ import { uuidv7 } from "../internal/harness.js";
3
3
  import { PROBE_CAUSE_PATH_MAX, PROBE_REASON_MAX, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
4
4
  import { carriesBidiControls } from "./tool-policy.js";
5
5
  import { readDenialLimitFallback } from "./auto-mode.js";
6
- import { isAskOrigin, isPersistedRuleMandate, isRuleStoreUnreadable } from "./ask-origin.js";
6
+ import { isAskOrigin, isPersistedRuleMandate } from "./ask-origin.js";
7
7
  import { checkpointExecutionRecorded, executionRecordDisposition, ExecutionOutcomeConflictError, } from "./checkpoint-execution-record.js";
8
8
  import { renderUntrustedCommandText, stripFormatCharacters } from "./permission-rule-model.js";
9
9
  import { redactSecrets } from "./untrusted-egress.js";
10
10
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
11
+ import { isPermissionMode } from "./ask-origin.js";
11
12
  export function mintCheckpointToken() {
12
13
  return randomBytes(16).toString("hex");
13
14
  }
@@ -298,15 +299,15 @@ export function summarizeCheckpoint(cp) {
298
299
  ...(tool?.previewWithheld === "oversize" || tool?.previewWithheld === "unavailable" ? { previewWithheld: tool.previewWithheld } : {}),
299
300
  ...(tool?.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
300
301
  ...(tool?.mandated === true ? { mandated: true } : {}),
302
+ ...(tool?.authoredBy === "deployment" ? { authoredBy: "deployment" } : {}),
301
303
  ...(() => {
302
304
  const fb = readDenialLimitFallback(tool?.denialLimitFallback);
303
305
  return fb !== undefined ? { denialLimitFallback: fb } : {};
304
306
  })(),
305
307
  ...(() => {
306
308
  const origin = tool?.origin;
307
- const ruleStoreUnreadable = tool?.ruleStoreUnreadable;
308
309
  const mandate = tool?.mandate;
309
- return { ...(isAskOrigin(origin) ? { origin } : {}), ...(isRuleStoreUnreadable(ruleStoreUnreadable) ? { ruleStoreUnreadable } : {}), ...(isPersistedRuleMandate(mandate) ? { mandate } : {}) };
310
+ return { ...(isAskOrigin(origin) ? { origin } : {}), ...(isPersistedRuleMandate(mandate) ? { mandate } : {}) };
310
311
  })(),
311
312
  ...(hasBidi ? { hasBidiControls: true } : {}),
312
313
  };
@@ -732,3 +733,57 @@ export class InMemoryCheckpointStore {
732
733
  return this.cps.size;
733
734
  }
734
735
  }
736
+ export function migrateLegacyModeSeed(gate) {
737
+ if (gate === undefined)
738
+ return {};
739
+ const raw = gate;
740
+ const refusal = (code, message) => {
741
+ const e = new Error(message);
742
+ e.code = code;
743
+ return e;
744
+ };
745
+ const dropped = [];
746
+ let permissionMode;
747
+ const declared = raw["permissionMode"];
748
+ if (declared !== undefined) {
749
+ if (!isPermissionMode(declared))
750
+ throw refusal("config.permission_mode_invalid", `the checkpoint row's inheritedGate.permissionMode is ${JSON.stringify(declared)}, not a permission mode — the row cannot be resumed under a mode this engine does not know`);
751
+ permissionMode = declared;
752
+ if (raw["autoModeRequested"] !== undefined)
753
+ dropped.push("autoModeRequested");
754
+ if (raw["approverPosture"] !== undefined)
755
+ dropped.push("approverPosture");
756
+ }
757
+ else {
758
+ const intent = raw["autoModeRequested"];
759
+ if (intent === true) {
760
+ permissionMode = "auto";
761
+ dropped.push("autoModeRequested");
762
+ }
763
+ else if (intent !== undefined && intent !== false) {
764
+ throw refusal("resume.legacy_mode_seed", `the checkpoint row's inheritedGate.autoModeRequested is ${JSON.stringify(intent)}, which the one-time mode migration cannot read (only true / false / absent are) — the row is refused rather than resumed under a guessed mode`);
765
+ }
766
+ else if (intent === false) {
767
+ dropped.push("autoModeRequested");
768
+ }
769
+ if (raw["approverPosture"] !== undefined)
770
+ dropped.push("approverPosture");
771
+ }
772
+ const prePlan = raw["prePlanMode"];
773
+ if (prePlan !== undefined && (!isPermissionMode(prePlan) || prePlan === "plan"))
774
+ throw refusal("config.permission_mode_invalid", `the checkpoint row's inheritedGate.prePlanMode is ${JSON.stringify(prePlan)}, not a permission mode a plan can return to`);
775
+ const floor = raw["shellGate"];
776
+ let shellGate;
777
+ if (floor === "always")
778
+ shellGate = "always";
779
+ else if (floor === "off" || floor === "classify")
780
+ dropped.push("shellGate");
781
+ else if (floor !== undefined)
782
+ throw refusal("resume.legacy_mode_seed", `the checkpoint row's inheritedGate.shellGate is ${JSON.stringify(floor)}, which is neither the floor word "always" nor a retired doctrine word — the row is refused rather than resumed under a guessed floor`);
783
+ return {
784
+ ...(permissionMode !== undefined ? { permissionMode } : {}),
785
+ ...(prePlan !== undefined ? { prePlanMode: prePlan } : {}),
786
+ ...(shellGate !== undefined ? { shellGate } : {}),
787
+ ...(dropped.length > 0 ? { migrated: { ...(declared === undefined && permissionMode !== undefined ? { permissionMode } : {}), droppedKeys: dropped } } : {}),
788
+ };
789
+ }
@@ -2,6 +2,7 @@ export const CHILD_CLAMP_SEATS = ["handsReadOnly", "interactiveTools", "readFace
2
2
  export const CHILD_CLAMP_SEATS_NOT_CARRIED = Object.freeze({
3
3
  toolPolicy: "not a clamp a child spec copies: a parent's denies reach its children through the ancestor-constraint chain the child's own prepare folds, not by putting the parent's policy object on the child seat",
4
4
  shellGate: "travels on the inherited-gate lane (`InheritedGate.shellGate`, a trusted internals seat), not on the child spec — a spec-level copy would be a second source for the same clamp",
5
+ permissionMode: "travels on the inherited-gate lane (`InheritedGate.permissionMode` / `permissionModeCeiling` — the parent's declared word and its LIVE seat): a child with no word of its own inherits that ceiling on every read, and a word it does declare is clamped under it; a spec-level copy would freeze the parent's word at spawn and be a second source for the same clamp",
5
6
  onAsk: "per-lane by construction: the delegation lane wraps the parent's approver with spawn provenance before it becomes the child's seat, and a lane with no approver must hand down none",
6
7
  hooks: "a deployment seat, not an inherited clamp — `tightenTaskSpec` refuses to merge two hook sets rather than composing them, so there is no tightening value to carry",
7
8
  excludeTools: "roster control rather than containment: the lanes that copy a parent's roster do so beside these seats, and the workflow lane authors its child roster from the governance baseline instead of the host's",
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The ONE exit for an auto-mode classification that could not RULE (`unavailable`: the classifier leg failed, or its
3
+ * per-level budget ran out) — shared by both classifier stations, the leg's gate and an ancestor's frozen classifier
4
+ * in the policy fold, so the rule is written once.
5
+ *
6
+ * ONE fork, on the APPROVAL SEAT (does an ask this station hands on reach a person — a live approver, or a durable
7
+ * park): with one, the ask goes on, its text saying why auto mode is asking, and a person decides; without one, the
8
+ * call is refused with CC's `x1t` sentence and the classification's FORM (`unavailable`) is attested on the deny. The seat is
9
+ * the only input to the fork — not the cause, not the mode. The handed-on ask carries `requiresRealApproval`: no
10
+ * automatic lane may answer it in the classifier's stead — not a blanket `onAsk:"allow"`, not the sandbox admission, not a
11
+ * budget resolver over its park (the park mints the non-budgetable kind). CC refuses either way; asking where a person can be
12
+ * asked is a declared departure (a classifier that is slow or down must not turn an attended session into a string
13
+ * of refusals). Neither arm is a block: the denial limit counts classifier RULINGS, and a person's refusal of the
14
+ * handed-on ask is a person's, not the classifier's.
15
+ *
16
+ * The observer hears every such classification with where the call went, except while the call is being aborted — a halt,
17
+ * or the run's end (the classifier did not fail; the call ended). It is observe-only: a throw never changes the arm.
18
+ */
19
+ import { type AutoModeClassifierRound, type AutoModeUnavailableCause } from "./auto-mode.js";
20
+ import { type PermissionResult, type ToolCallRequest } from "./tool-policy.js";
21
+ /** Where an `unavailable` classification sent the call: `asked` = on to the approval seat, `denied` = refused. */
22
+ export type ClassifierUnavailableDisposition = "asked" | "denied";
23
+ /** What the stations' observer receives — the facts the `classifier.unavailable` notice is composed from. */
24
+ export interface ClassifierUnavailableInfo {
25
+ toolName: string;
26
+ toolCallId: string;
27
+ cause: AutoModeUnavailableCause;
28
+ disposition: ClassifierUnavailableDisposition;
29
+ }
30
+ export type ClassifierUnavailableObserver = (info: ClassifierUnavailableInfo) => void;
31
+ /** The station's two facts for the fork: whether an ask it hands on reaches a person (`true` only — anything else is
32
+ * no seat), and who hears it. */
33
+ export interface ClassifierUnavailableSeat {
34
+ approvalSeat?: boolean;
35
+ onUnavailable?: ClassifierUnavailableObserver;
36
+ }
37
+ /** The arm the fork chose: the ask to hand on, or the refusal. */
38
+ export type ClassifierUnavailableArm<A> = {
39
+ kind: "asked";
40
+ ask: A;
41
+ } | {
42
+ kind: "denied";
43
+ deny: PermissionResult;
44
+ };
45
+ export declare function classifierUnavailableArm<A extends Extract<PermissionResult, {
46
+ action: "ask";
47
+ }>>(args: {
48
+ ask: A;
49
+ req: ToolCallRequest;
50
+ cause: AutoModeUnavailableCause;
51
+ /** The ladder's attribution, for the refusal's sentence (it names both rungs when the ladder moved). */
52
+ attribution?: AutoModeClassifierRound;
53
+ seat: ClassifierUnavailableSeat | undefined;
54
+ /** The call is being aborted (its per-call signal — a halt, or the run's end): the observer is not told (the arm is chosen
55
+ * as ever). */
56
+ aborted: boolean;
57
+ }): ClassifierUnavailableArm<A>;
@@ -0,0 +1,17 @@
1
+ import { classifierUnavailableAskMessage, classifierUnavailableDenyMessage } from "./auto-mode.js";
2
+ import { attestClassifierDenyCause } from "./tool-policy.js";
3
+ export function classifierUnavailableArm(args) {
4
+ const { ask, req, cause } = args;
5
+ const seated = args.seat?.approvalSeat === true;
6
+ if (!args.aborted) {
7
+ try {
8
+ args.seat?.onUnavailable?.({ toolName: req.toolName, toolCallId: req.toolCallId, cause, disposition: seated ? "asked" : "denied" });
9
+ }
10
+ catch {
11
+ }
12
+ }
13
+ if (seated)
14
+ return { kind: "asked", ask: { ...ask, message: classifierUnavailableAskMessage(ask.message ?? `approval required for "${req.toolName}"`, cause), requiresRealApproval: true } };
15
+ const deny = { action: "deny", message: classifierUnavailableDenyMessage(req.toolName, cause, args.attribution), decisionReason: "classifier" };
16
+ return { kind: "denied", deny: attestClassifierDenyCause(deny, "unavailable", req) };
17
+ }
@@ -101,3 +101,15 @@ export type PathTargetReading = {
101
101
  * against names no file, and claiming it names `/<spelling>` was a fabrication no fence should judge.
102
102
  */
103
103
  export declare function effectivePathTargetOf(call: ToolCallLike, pt: ToolCallPathTarget | undefined, bases: ToolPathBases): PathTargetReading;
104
+ /**
105
+ * The SELECTION half of {@link effectivePathTargetOf}, before any identity is applied: the spelling the tool opens and
106
+ * the base it resolves that spelling against — the slot's own value, or an absolute pattern's base directory — or,
107
+ * with `spelled` absent, the base itself (the tool searches all of it). A caller that resolves canonically (the tool,
108
+ * the read face's own station) reads this; the rule lanes read the lexical identity above. Same order as above.
109
+ */
110
+ export declare function effectivePathSpellingOf(call: ToolCallLike, pt: ToolCallPathTarget | undefined, bases: ToolPathBases): {
111
+ readonly spelled?: string;
112
+ readonly base: string | undefined;
113
+ } | {
114
+ readonly refused: Win32NamespaceRefusal;
115
+ } | undefined;
@@ -23,26 +23,37 @@ export function splitAbsoluteGlobPattern(pattern, family) {
23
23
  return { baseDir, relativePattern: spelled.slice(cut + 1) };
24
24
  }
25
25
  export function effectivePathTargetOf(call, pt, bases) {
26
+ const spelling = effectivePathSpellingOf(call, pt, bases);
27
+ if (spelling === undefined)
28
+ return undefined;
29
+ if ("refused" in spelling)
30
+ return { kind: "refused", reason: spelling.refused };
31
+ if (spelling.spelled !== undefined)
32
+ return resolveSpelling(spelling.spelled, spelling.base, bases);
33
+ const declaredBase = spelling.base;
34
+ if (declaredBase === undefined)
35
+ return undefined;
36
+ const family = pathFamilyOf(bases);
37
+ return isAbsoluteForFamily(family, declaredBase) ? readingOf(pathIdentityOf(declaredBase, family, bases.root ?? bases.cwd)) : unreadableBase(declaredBase);
38
+ }
39
+ export function effectivePathSpellingOf(call, pt, bases) {
26
40
  if (pt === undefined)
27
41
  return undefined;
28
42
  const declaredBase = pathTargetBaseOf(pt, bases);
29
43
  const spelled = pathTargetValue(call.args, pt);
30
44
  if (spelled !== undefined)
31
- return resolveSpelling(spelled, declaredBase, bases);
45
+ return { spelled, base: declaredBase };
32
46
  if ((pt.absent ?? "none") === "none")
33
47
  return undefined;
34
48
  if (pt.patternParam !== undefined) {
35
49
  const pattern = pathTargetValue(call.args, { param: pt.patternParam });
36
50
  const split = pattern === undefined ? undefined : splitAbsoluteGlobPattern(pattern, pathFamilyOf(bases));
37
51
  if (split !== undefined && "refused" in split)
38
- return { kind: "refused", reason: split.refused };
52
+ return { refused: split.refused };
39
53
  if (split !== undefined)
40
- return resolveSpelling(split.baseDir, declaredBase, bases);
54
+ return { spelled: split.baseDir, base: declaredBase };
41
55
  }
42
- if (declaredBase === undefined)
43
- return undefined;
44
- const family = pathFamilyOf(bases);
45
- return isAbsoluteForFamily(family, declaredBase) ? readingOf(pathIdentityOf(declaredBase, family, bases.root ?? bases.cwd)) : unreadableBase(declaredBase);
56
+ return { base: declaredBase };
46
57
  }
47
58
  function resolveSpelling(spelled, declaredBase, bases) {
48
59
  const family = pathFamilyOf(bases);
@@ -222,6 +222,18 @@ export interface EngineNotice {
222
222
  * ladder position (0 the seat, 1 the model behind it), since two rungs may share an id. A ruling from
223
223
  * that rung resets its count; an `unavailable` round neither counts nor resets. Once per run per rung,
224
224
  * and by construction before the denial limit's third block hands the user its card.
225
+ * - `"classifier.unavailable"` (`src/core/runner/classifier-ladder.ts`, `classifierUnavailableNotice`; minted by
226
+ * both classifier stations — the leg's gate and an ancestor's frozen classifier in the policy fold) — a
227
+ * classification could NOT RULE (the classifier leg failed, or its per-level budget
228
+ * `RunnerDeps.autoMode.timeoutMs` ran out) and says where the call went: `disposition: "asked"` = on down the
229
+ * ask chain to the leg's approval seat (a live approver or a durable park decides; the handed-on ask carries
230
+ * `requiresRealApproval`, so no automatic lane answers it; a declared departure from CC, which refuses either
231
+ * way), `"denied"` = refused with the classifier's own sentence (no approval seat).
232
+ * `detail: { toolName, toolCallId, cause, disposition, sessionId?, runId? }` — `cause` is the
233
+ * `AutoModeUnavailableCause` word. Once per classification that could not answer (a call judged by an ancestor's
234
+ * classifier and the leg's own can carry two); not minted while the call is being aborted (a halt, or the run's end). Audience
235
+ * `user`: on a lane with no approval face this line is the only place the user hears why the call did not
236
+ * run. It is not a block — the denial limit does not count it.
225
237
  * - `"route.base_url_changed_key_unchanged"` (key↔URL pairing) — `Runner.swapDeps` moved a
226
238
  * same-name entry's `baseUrl` while its Model-visible credential half (auth-bearing headers)
227
239
  * did not change: legal (a provider changing domains), but worth one loud line — if the
@@ -281,6 +293,44 @@ export interface EngineNotice {
281
293
  * secret-shaped keys from a spawned child's environment (a tool's shell, or a language server).
282
294
  * Once per PROCESS, shared by every scrub site ({@link import("./secret-env.js").announceSecretEnvScrub});
283
295
  * `detail: { names }` — every withheld key's name, never a value.
296
+ * - `"config.permission_mode_unavailable"` — the permission mode a task requested is not the mode the leg runs
297
+ * under: the deployment denies it (`by: "operator_deny"` — `RuntimeCaps.bypassPermissions` / `autoMode` is
298
+ * `false`), a requested `auto` has no classifier face (`by: "face_absent"`), or a child's declaration was folded
299
+ * under its parent's ceiling (`by: "inherited_ceiling"`, minted on the PARENT's stream with the child's
300
+ * `sessionId` beside the parent's — a child's own manifest is not shown to the person). The leg runs in
301
+ * `effective` (always `default` for the first two arms), announced once per leg and once per steer that folds;
302
+ * `detail: { requested, effective, by, sessionId, runId }`. Audience `user`.
303
+ * - `"config.permission_mode_invalid"`, `"config.spec_retired_key"`, `"config.shell_gate_value_retired"` and
304
+ * `"resume.legacy_mode_seed"` are ERROR codes, not notices (named here only to keep the mode family's spellings
305
+ * in one place): a `permissionMode` word outside the closed set (on the spec or on a checkpoint row), a spec
306
+ * carrying a retired mode key (`autoModeRequested`, `approverPosture`), a `shellGate` value other than
307
+ * `"always"`, and a checkpoint row whose pre-mode seed the migration cannot read. Each is a door refusal: the
308
+ * code rides the thrown error (the run fails, or the resume is refused) and never crosses
309
+ * {@link deliverEngineNotice}, so all four are deliberately absent from {@link ENGINE_NOTICE_CODES} /
310
+ * {@link NOTICE_AUDIENCE}, like `config.deps_retired_key`. A consumer diffing its own table against the
311
+ * catalog must not add a row for them; it reads them on the error's `code`.
312
+ * - `"permission.ask_released_by_mode"` — the `bypassPermissions` mode released a surviving ask instead of
313
+ * putting it to anyone; once per (mode, askClass) per run, `detail: { mode, askClass, origin?, mandate?, count,
314
+ * firstToolCallId, sessionId, runId }` — `count` is how many releases of that pair the run had made when the
315
+ * notice was minted (the first). The per-call record is `tool_end.gate.disposition.released`. Audience `user`.
316
+ * - `"permission.read_deny_unapplied"` — the mode released a call whose reads the shell read station could not
317
+ * establish (`unestablishable` / `declined`) while a read-deny table is in force: no row of the table was applied
318
+ * to what it reads (C-R124, "released and disclosed"). Every such call, `detail: { mode, toolCallId, tiers, rows,
319
+ * rowCount, sessionId, runId }` (`rows` = the first eight non-built-in rows). Audience `user`.
320
+ * - `"permission.rule_store_unavailable"` — the person's persisted permission rules could not be read (a read
321
+ * failure, a corrupt or unreadable store, a read that did not answer in time), so that read's rows were SKIPPED:
322
+ * none of their allow, ask or deny rows took part in the decisions it served — a deny row among them protected
323
+ * nothing meanwhile — and no card is raised for it. `detail.at` names the read that found it first: `"prepare"`
324
+ * (the path rows the run's file and shell checks take at run start — the next run reads them again) or `"call"`
325
+ * (one call's own rule check — every later call reads again, so the rows apply as soon as a read succeeds). Once
326
+ * per run, whichever read fails first; the trace keeps every read's line (`permission.rule_store_unreadable`).
327
+ * `detail: { at, sessionId, runId }`. Audience `user`.
328
+ * - `"resume.legacy_mode_seed_migrated"` — a checkpoint row written before the one-key mode (7.34.0) carried
329
+ * `autoModeRequested` / `approverPosture` / a doctrine `shellGate`; the resume leg read them once into
330
+ * `permissionMode` (`autoModeRequested: true` ⇒ `auto`; a posture is dropped — it never authorized anything;
331
+ * `shellGate` `"off"` / `"classify"` are dropped, `"always"` kept) and the next suspend writes the new member.
332
+ * `detail: { migrated: { permissionMode?, droppedKeys }, sessionId, runId }`. Audience `operator`. A row the
333
+ * migration cannot read refuses the resume instead (`resume.legacy_mode_seed`, the error-code row above).
284
334
  * - `"config.durable_gate_unavailable"` — a durable-approval MANDATE is in force on a leg with NO
285
335
  * checkpoint store: the per-principal `forceDurableGate` entitlement, or the task's own
286
336
  * `durableApproval` opt-in with no live approver seat (`durableMandateSourceOf`, the predicate the
@@ -686,8 +736,9 @@ export interface EngineNotice {
686
736
  * facility (an in-fold deny, no settlement on the frame). The deny itself is unchanged (the
687
737
  * fail-closed sentence as the tool result); this is its person-facing half, minted at the gate's ONE
688
738
  * deny exit, once per tool call, by `ask-unresolvable-notice.ts`. `detail: { sessionId, toolName,
689
- * toolCallId, cause, parkLaneExisted }` — `parkLaneExisted` says whether a park lane was armed and
690
- * declined/failed (`true`) or none existed (`false`, the usual shape). Audience `"user"`: the refused
739
+ * toolCallId, cause, parkLaneExisted, remedy? }` — `parkLaneExisted` says whether a park lane was armed and
740
+ * declined/failed (`true`) or none existed (`false`, the usual shape); `remedy` (the headless auto-deny only)
741
+ * is the one sentence naming the caller-side change that would have let the call run, as the tool text carries it. Audience `"user"`: the refused
691
742
  * work is the session user's own; the operator hears it through the sink like every code.
692
743
  *
693
744
  * - `"memory.consolidation_recommended"` (design/339 §2.2/§6.2) — the engine-minted per-scope
@@ -1,5 +1,5 @@
1
1
  import type { ExecutionEnv } from "../internal/harness-types.js";
2
- import type { ToolPolicy } from "./tool-policy.js";
2
+ import type { PermissionResult, ToolPolicy } from "./tool-policy.js";
3
3
  /** design/388 B22 — the identity triple the exemption probe hands the store beside the name. */
4
4
  export interface ToolCallIdentity {
5
5
  capabilityId: string;
@@ -31,6 +31,10 @@ export interface FsWriteGatePolicyOptions {
31
31
  * to the ask-resolution chain (live onAsk / durable park / headless deny); `"allow"` is the
32
32
  * acceptEdits-mode shape (domain checks still run so a sensitive-path deny composes unchanged). */
33
33
  defaultWrite: "ask" | "allow";
34
+ /** The ask the default verdict mints in place of the gate's ordinary write question — a permission mode's own
35
+ * sentence over the resolved `path`, with the gate's domain note (a configured domain that was not honored) to
36
+ * append when there is one. Read only where `defaultWrite` is `"ask"`. */
37
+ defaultAsk?: (path: string, note: string | undefined) => PermissionResult;
34
38
  /**
35
39
  * Session-exemption lookup ("allow all for this session" grants recorded by the deployment's approval
36
40
  * surface). Consulted ONCE per covered write, after the domain checks and immediately BEFORE the gate
@@ -109,7 +109,10 @@ export function createFsWriteGatePolicy(opts) {
109
109
  }
110
110
  if (defaultWrite === "allow")
111
111
  return { action: "allow" };
112
- return ask(`approve write to "${path}"?${advisory}${refusedDomain === undefined ? "" : ` — note: the configured write domain ${refusedDomain} was not honored; a domain that holds the root would exempt every write under it`}`);
112
+ const note = refusedDomain === undefined ? undefined : ` — note: the configured write domain ${refusedDomain} was not honored; a domain that holds the root would exempt every write under it`;
113
+ if (opts.defaultAsk !== undefined)
114
+ return opts.defaultAsk(path, note);
115
+ return ask(`approve write to "${path}"?${advisory}${note ?? ""}`);
113
116
  },
114
117
  };
115
118
  }
@@ -50,7 +50,7 @@ export async function runGateFold(pass) {
50
50
  }
51
51
  if ((pass.decision.action === "allow" || pass.decision.action === "ask") && input.shellReadCheck !== undefined) {
52
52
  const station = input.shellReadCheck;
53
- const judged = station.judge(toolName, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, input.trackedCwd?.(), pass.callSignal ?? input.abortSignal, { toolCallId });
53
+ const judged = station.judge(pass.req, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, input.trackedCwd?.(), pass.callSignal ?? input.abortSignal, { toolCallId });
54
54
  if (judged !== null) {
55
55
  let verdict;
56
56
  try {
@@ -61,7 +61,7 @@ export async function runGateFold(pass) {
61
61
  if (kind !== "aborted" && input.abortSignal?.aborted !== true) {
62
62
  traceHookCrash(input, new Error(`shell read station ${kind === "timeout" ? "timed out" : "threw"} for tool "${toolName}" (toolCallId ${toolCallId}) — where this call reads could not be established: ${thrownText(err)}`), notifier);
63
63
  }
64
- verdict = station.unanswered(toolName, kind === "timeout" ? "the read station timed out" : "the read station could not answer");
64
+ verdict = station.unanswered(pass.req, kind === "timeout" ? "the read station timed out" : "the read station could not answer");
65
65
  }
66
66
  applyShellReadVerdict(pass, verdict, toolName, input.shellGated === true);
67
67
  }
@@ -175,6 +175,14 @@ function shellReadAskMessage(toolName, finding) {
175
175
  }
176
176
  function applyShellReadVerdict(pass, verdict, toolName, shellGated) {
177
177
  const account = verdict.sentence !== undefined && verdict.sentence.trim().length > 0 ? inlineUntrusted(verdict.sentence, PROBE_REASON_MAX) : undefined;
178
+ if (verdict.writesPlaced === true)
179
+ pass.shellWritesPlaced = true;
180
+ if (verdict.writeProtected === true)
181
+ pass.writeProtected = true;
182
+ if (verdict.unplacedOptions !== undefined)
183
+ pass.shellUnplacedOptions = verdict.unplacedOptions;
184
+ if (verdict.finding === "unestablishable" || verdict.finding === "declined" || verdict.unestablished === true)
185
+ pass.shellReadUnestablished = true;
178
186
  switch (verdict.disposition) {
179
187
  case "vouch":
180
188
  return;
@@ -207,16 +215,24 @@ function applyShellReadVerdict(pass, verdict, toolName, shellGated) {
207
215
  return;
208
216
  }
209
217
  case "ordinary": {
218
+ verdict.admit?.();
219
+ if (verdict.details !== undefined)
220
+ pass.shellReadDetails = verdict.details;
210
221
  if (pass.decision.action === "allow") {
222
+ const candidate = normalizeReadRootCandidate(verdict.readRootCandidate);
211
223
  pass.decision = {
212
224
  action: "ask",
213
225
  message: shellReadAskMessage(toolName, verdict.finding),
214
226
  decisionReason: "safety",
215
227
  ...(account !== undefined ? { probeReason: account } : {}),
228
+ ...(candidate !== undefined ? { probeReadRootCandidate: candidate } : {}),
216
229
  };
230
+ if (candidate !== undefined)
231
+ pass.shellReadOffer = true;
217
232
  pass.tightenedBy = shellGated ? "shell_gate" : "safety";
218
233
  }
219
- pass.probeBoundaryDeclined = true;
234
+ if (verdict.finding === "declined")
235
+ pass.probeBoundaryDeclined = true;
220
236
  return;
221
237
  }
222
238
  default: {