@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
@@ -4,6 +4,7 @@
4
4
  * beside it in `task-limits.ts`; the terminal record it produces in `task-result.ts`. Layer 0
5
5
  * vocabulary; `types.ts` re-exports the three, so no consumer's import changes.
6
6
  */
7
+ import type { PermissionMode } from "./ask-origin.js";
7
8
  import type { TSchema } from "typebox";
8
9
  import type { ThinkingLevel } from "../internal/harness.js";
9
10
  import type { ActorAssertion, Model, ResilienceOptions } from "../internal/llm.js";
@@ -31,15 +32,12 @@ export type ImageInput = {
31
32
  url: string;
32
33
  };
33
34
  /** A single self-contained task with its full configuration. */
34
- /** The shell-gate doctrine vocabulary — {@link TaskSpec.shellGate}'s value set, and the word the engine persists on a
35
- * shell-gated ask (`RiskDescriptor.shellGateDoctrine`) and folds across a delegation chain (max-rank: `off` <
36
- * `classify` < `always`). One closed set; every seat that names a doctrine spells it through this alias. */
37
- export type ShellGateDoctrine = "off" | "classify" | "always";
38
- /** The approval POSTURE a host declares for a task — {@link TaskSpec.approverPosture}'s value set: a
39
- * statement about who answers this run's approval cards, never an instruction to the gate. One word
40
- * today (`"bypass"` — the host answers every card itself, automatically). The engine only RENDERS it,
41
- * so adding a word here changes what the model is told and nothing else. */
42
- export type ApproverPosture = "bypass";
35
+ /** The shell-gate FLOOR vocabulary — {@link TaskSpec.shellGate}'s value set, the word the engine persists on a
36
+ * shell-gated ask (`RiskDescriptor.shellGateDoctrine`) and carries down a delegation chain. One value: `"always"`
37
+ * (every shell command is confirmed, under every permission mode — an operator's mandate). The shape tier itself
38
+ * is installed on every full shell regardless; what a permission mode does with the resulting ask is the gate's
39
+ * arbitration, never a doctrine word. */
40
+ export type ShellGateDoctrine = "always";
43
41
  export interface TaskSpec {
44
42
  taskId?: string;
45
43
  /** The instruction / latest user message for this turn. */
@@ -791,8 +789,8 @@ export interface TaskSpec {
791
789
  /**
792
790
  * Interactive read-boundary whitelist (ruled 2026-08-05, both-arms ruling, arm A): extra directories
793
791
  * the READ faces may access — and nothing else. Joins the containment allowlist of the read-only
794
- * tools (read_file / grep / glob / repo_map / `bash_readonly`) and the `shellGate:"classify"`
795
- * read-boundary (so a provably read-only command like `head <dir>/settings.json` auto-allows instead
792
+ * tools (read_file / grep / glob / repo_map / `bash_readonly`) and the shell read station's
793
+ * boundary (so a provably read-only command like `head <dir>/settings.json` auto-allows instead
796
794
  * of asking on every call), but is NEVER admitted by the write faces (edit_file / write_file /
797
795
  * notebook_edit refuse these paths exactly as before). Use `additionalDirectories` when the task
798
796
  * should also write there. Same canonicalization and fail-closed skip rules as
@@ -805,7 +803,7 @@ export interface TaskSpec {
805
803
  * design/199 件B — TASK-layer ADDITIONS to the built-in sensitive-path READ deny set
806
804
  * ({@link import("../tools/fs/read-deny.js").READ_FACE_DEFAULT_DENY_ENTRIES}). Judged by the
807
805
  * structured read faces (Read/Grep/Glob/RepoMap, their traversals, the compaction attachment reader) and
808
- * by the shell READ STATION under every doctrine — the full shell (`Bash`, `Monitor`, whatever
806
+ * by the shell READ STATION under every mode — the full shell (`Bash`, `Monitor`, whatever
809
807
  * {@link shellGate} says) and the `bash_readonly` shell mounted by {@link handsReadOnly} alike: a
810
808
  * deny-listed shell read is REFUSED at the gate (`deniedBy: "read_boundary"`), never asked, with the same
811
809
  * sentence the structured faces refuse with — one deny judge for every read face — in BOTH containment
@@ -814,8 +812,8 @@ export interface TaskSpec {
814
812
  * {@link RunnerDeps.readDenyPatterns}; `[]` ≡ absent (union identity); NOTHING at the task layer
815
813
  * can remove a built-in — the one removal channel is the DEPLOYMENT's built-in configuration
816
814
  * ({@link RunnerDeps.readDenyBuiltinTiers} / {@link RunnerDeps.readDenyBuiltinExclude}, #245
817
- * revision of D-4), which no TaskSpec key reaches — and that half is OFF by default, so on an
818
- * unconfigured deployment the entries here are the WHOLE judged set. Bad entry shapes refuse loudly at prepare
815
+ * revision of D-4), which no TaskSpec key reaches (an unconfigured deployment judges the default tiers,
816
+ * {@link import("../tools/fs/read-deny.js").READ_DENY_DEFAULT_TIERS}). Bad entry shapes refuse loudly at prepare
819
817
  * (#123). The write faces are untouched.
820
818
  */
821
819
  readDenyPatterns?: readonly import("../tools/fs/read-deny.js").ReadDenyEntry[];
@@ -890,87 +888,41 @@ export interface TaskSpec {
890
888
  */
891
889
  enableFork?: boolean;
892
890
  /**
893
- * design/80 D-2 (part-1): gate the injected `bash` hand tool, since a full shell can run egress (`curl`,
894
- * `git push`) and irreversible (`rm`) commands that the per-tool egress/irreversibility marks otherwise miss.
895
- * The §4-OQ4 doctrine: shell-in-presence ⇒ always-gate UNLESS a real parsed classifier is wired.
896
- * - `"off"` (default): `bash` is unmarked — the current behavior (subject only to the deployment's policy).
897
- * - `"always"`: every `bash` command tightens to an `irreversible_ask` durable suspend (fail-closed; the
898
- * correct default for an unattended/supervisor deployment with no classifier).
899
- * - `"classify"`: mark `bash` `irreversibility:"maybe"` with {@link import("../tools/fs/index.js").bashReversibilityProbe}
900
- * — the SHAPE classifier alone: a provably-benign single allowlisted read-only command (no operators)
901
- * auto-allows; everything else (writes, egress, pipes, unknown commands) tightens to ask. WHERE a command
902
- * reads is not this doctrine's question under any value: the gate's shell read station judges every
903
- * shell call's read candidates against the deny set and the containment roots (a deny-listed read is
904
- * refused, an out-of-root read is a mandated ask) whether `shellGate` is `"off"`, `"classify"` or
905
- * `"always"`. An egress command (e.g. `curl`) suspends under
906
- * the IRREVERSIBLE axis (the shell mark is irreversibility, not egress) — the kind is non-budgetable either
907
- * way, so the budget resolver still never auto-approves it; the axis label is informational.
908
- * NOT unconditional: a POSITIVE per-tool mark on the shell tool keeps its seat — an explicit
909
- * `irreversibility:"always"` is never downgraded (the tier combine is tighten-only) and an explicit
910
- * `"maybe"` keeps its own probe (a probe-less explicit `"maybe"` stays fail-closed ask; the doctrine's
911
- * generic probe installs only on doctrine-owned seats). An explicit `"never"` is NOT a mark — the
912
- * doctrine governs that seat as if unmarked.
913
- * No effect under `handsReadOnly` (that mounts `bash_readonly`, already allowlisted) or with no execution env.
914
- * **Re-supply on resume:** like `toolPolicy`/`tools`/`durableApproval`, `shellGate` is part of the resume
915
- * task config — a resume that omits it leaves the resumed run's SUBSEQUENT `bash` calls ungated (the approved
916
- * pending call itself runs once, having been adjudicated). Re-pass the same value the original task used.
891
+ * The OPERATOR's shell floor: `"always"` ⇒ every `bash` / `Monitor` command is a confirmed ask (an
892
+ * `operator_forced` ask — kept under `bypassPermissions`, not settled by `dontAsk`; a standing allow rule never
893
+ * clears it). Absent ⇒ no floor: the shape tier the engine installs on every full shell (a provably read-only
894
+ * single command runs, everything else asks) is arbitrated by the task's {@link permissionMode}. The retired
895
+ * values `"off"` / `"classify"` are refused at the door (`config.shell_gate_value_retired`): the tier is no longer a
896
+ * doctrine a caller chooses, and the read boundary is judged on every shell call under every mode. A positive
897
+ * per-tool mark keeps its seat (an explicit `irreversibility:"always"` is never downgraded). No effect under
898
+ * `handsReadOnly` or with no execution env. Re-supplied on resume like `toolPolicy` (a checkpoint carries the
899
+ * chain's floor). Delegation: the floor travels the inherited-gate lane; a child cannot lift it.
917
900
  */
918
901
  shellGate?: ShellGateDoctrine;
919
902
  /**
920
- * The caller's AUTO-MODE INTENT for this task — the "user turned auto on" half of the classifier
921
- * arming (CC 2.1.250 polarity: auto mode is something the USER enables at the permission-mode
922
- * seat, and something an ORGANIZATION may only DENY; it is never something an organization grants
923
- * on a user's behalf). The engine arms its per-run classifier only when ALL THREE hold:
924
- * - this seat is `true` (intent),
925
- * - {@link RunnerDeps.autoMode} is present (the deployment is classifier-capable — the trust gate;
926
- * classifier RULES still enter ONLY through that face, never through this seat), and
927
- * - {@link RuntimeCaps.autoMode} is not `false` (the per-principal DENY bit; ABSENT is NOT a denial).
903
+ * The task's PERMISSION MODE — the ONE key that says how the engine answers a surviving ask (six words,
904
+ * {@link import("./ask-origin.js").PERMISSION_MODES}; spelled as the wire spells it). ROOT task absent ⇒
905
+ * `"default"`; an engine-spawned CHILD absent ⇒ its parent's effective mode (a child's own declaration is
906
+ * adopted only where the reference implementation's rank table admits it under the parent's — a folded
907
+ * declaration is disclosed on `wiring_manifest.permissionMode.foldedBy: "inherited_ceiling"` and announced,
908
+ * never silent). A value outside the set is refused at the door (`config.permission_mode_invalid`).
928
909
  *
929
- * **Ownership** — same split as {@link shellGate}: the service layer TRANSLATES the effective
930
- * permission mode into this seat (at the same point it translates the mode into `shellGate`);
931
- * core only CONSUMES it. Absent ⇒ not an auto-mode task (byte-identical to the pre-seat shape:
932
- * asks flow the original chain). Only the literal `true` is a value here: any other present value
933
- * (`false`, `"true"`, `1`) is refused at the door (`config.auto_mode_requested_invalid`) rather than
934
- * read as either polarity — a marshalled string must never silently arm, or silently disarm, a
935
- * classifier.
910
+ * What the mode DOES is one table and one predicate ({@link import("./ask-origin.js").MODE_ARBITRATION},
911
+ * {@link import("./ask-origin.js").arbitrateByMode}) read after every rule, tighten and classifier station:
912
+ * `bypassPermissions` releases every ask but the immune classes (an explicit ask rule, a hook's ask, the
913
+ * question tool, an unverifiable recursive delete, an org rule, an operator's forced confirmation, unreadable
914
+ * governance, the classifier's denial-limit fallback) and discloses each release; `dontAsk` refuses every ask
915
+ * but a hook's or an operator authority's; `auto` arms the classifier (`RunnerDeps.autoMode` present and
916
+ * `RuntimeCaps.autoMode` not `false`, else folded to `default` and announced); `plan` reads only and mounts the
917
+ * plan pair; `acceptEdits` writes inside the root without asking; `default` asks. The mode also installs its own
918
+ * seats ({@link import("./mode-policy.js").modePolicyOf}).
936
919
  *
937
- * **Inheritance**: the intent is session-wide, like a permission mode — an engine-spawned child of
938
- * an auto-mode task inherits it through the trusted constraint chain
939
- * (`InheritedGate.autoModeRequested`), never through a model-authored argument; the child's own
940
- * deny bit and deployment face are still evaluated for the child. **On resume:** the intent the
941
- * suspend leg resolved (this seat, or the bit its chain carried) is recorded on the checkpoint's
942
- * data half (`CheckpointState.inheritedGate.autoModeRequested`) and read back as one more intent
943
- * source, so a redemption in another process arms as the suspend leg did without re-passing the
944
- * seat; re-passing it is still honoured (the sources fold by OR). Intent only — the resuming
945
- * deployment's face and the resuming principal's deny bit are judged afresh on every leg.
946
- */
947
- autoModeRequested?: true;
948
- /**
949
- * The host's declared APPROVER POSTURE for this task — a typed fact seat, the same ownership split
950
- * as {@link shellGate} and {@link autoModeRequested}: the service layer declares it (it is the layer
951
- * that knows who sits at the cards), core only CONSUMES it. `"bypass"` says the host answers every
952
- * approval card itself, automatically.
953
- *
954
- * **The gate does not read it.** Cards are raised, routed and recorded byte-identically with and
955
- * without this seat — no rule is relaxed, no ask is skipped, no decision is pre-empted. The seat's
956
- * one effect is that the ENGINE tells the model which posture the run is under, in its own marked
957
- * `<system-reminder>` on the first turn. That is the whole point of the seat: a posture is a fact of
958
- * the run, and a host that instead splices the sentence into the objective hands the model
959
- * reminder-shaped text with no engine mark, which the mark declaration tells it to treat as data and
960
- * flag. One fact, one seat, one mint.
961
- *
962
- * Absent ⇒ no posture declared, zero bytes rendered (byte-identical to the pre-seat shape). Only the
963
- * literal `"bypass"` is a value: any other present value is refused at the door
964
- * (`config.approver_posture_invalid`) rather than folded back to absence — a marshalled typo must not
965
- * silently un-declare a posture the host meant to declare.
966
- *
967
- * **Inheritance / resume:** session-wide, exactly like {@link autoModeRequested} — an engine-spawned
968
- * child inherits it through the trusted constraint chain (`InheritedGate.approverPosture`), and the
969
- * resolved value is recorded on the checkpoint's data half so a redemption in another process still
970
- * knows the posture without the seat being re-passed. Re-passing it is honoured; the sources fold
971
- * seat-first.
920
+ * `bypassPermissions` is a REQUEST: it takes effect only where the deployment does not deny it
921
+ * ({@link RuntimeCaps.bypassPermissions} not `false`); a denied request runs in `default`, announced
922
+ * (`config.permission_mode_unavailable`). A running leg changes mode through `TaskStream.setPermissionMode`.
923
+ * Persists on the checkpoint's data half (`CheckpointState.inheritedGate.permissionMode`), re-judged on every leg.
972
924
  */
973
- approverPosture?: ApproverPosture;
925
+ permissionMode?: PermissionMode;
974
926
  /** Task-scoped MCP servers, materialized into tools then disposed. */
975
927
  mcp?: McpServerSpec[];
976
928
  /** Task-scoped A2A peers (remote agents), whose advertised skills mount as `a2a__<peer>__<skill>`
@@ -275,6 +275,22 @@ export interface TaskStream extends AsyncIterable<TaskEvent> {
275
275
  }): Promise<{
276
276
  outcome: "created" | "existed";
277
277
  }>;
278
+ /**
279
+ * Change the running leg's PERMISSION MODE (`TaskSpec.permissionMode`'s live half — a shell's mode toggle, a serving
280
+ * layer's `permission_mode` control frame). The requested word is rewritten on the leg's mode seat; the deployment's
281
+ * fold is re-judged (`bypassPermissions` where `RuntimeCaps.bypassPermissions` is `false`, or `auto` with no
282
+ * classifier face / a `false` deny bit, runs `default` and is announced `config.permission_mode_unavailable`); a leg
283
+ * that became `auto` arms its classifier now — an arming that fails rolls the steer back (the word it replaced stands)
284
+ * and the verb rejects with the arming's code; the NEXT tool call reads the new mode at the gate's arbitration point
285
+ * (a tightening and a loosening both take effect one call later, on one seat), and a child leg derives its own
286
+ * effective mode from this seat on its next read. Resolves the same three-member receipt `wiring_manifest.permissionMode`
287
+ * carries — `{ requested, effective, foldedBy? }` — on EVERY call, moved or not, so a caller renders what the engine
288
+ * says and never what it sent. Typed refusals: `steering.invalid_content` (a word outside the closed set) and the
289
+ * steer-family `steering.not_running` once the task has finished. AUTHORITY (steer-lane law): holding this stream IS
290
+ * the capability handle; a service exposing the verb owns the principal check at its own face — and a deployment that
291
+ * must never let a caller reach `bypassPermissions` says so with the deny bit, which this verb honours.
292
+ */
293
+ setPermissionMode(mode: import("./ask-origin.js").PermissionMode): Promise<import("./mode-policy.js").PermissionModeReceipt>;
278
294
  /**
279
295
  * **RE-DIAL one MCP server** on the running task, the action `RefreshMcpTools` deliberately
280
296
  * is not (its contract is "never dials or re-dials"). Core holds no MCP connection across task
@@ -4,6 +4,7 @@ export const TERMINAL_CAUSE_IS_REPLAYABLE = {
4
4
  blocked: false,
5
5
  paused: false,
6
6
  };
7
+ Object.freeze(TERMINAL_CAUSE_IS_REPLAYABLE);
7
8
  export function isTerminalCauseKind(kind) {
8
9
  return typeof kind === "string" && Object.hasOwn(TERMINAL_CAUSE_IS_REPLAYABLE, kind);
9
10
  }
@@ -13,8 +13,8 @@ export declare class TaskSpecTightenError extends Error {
13
13
  * would drop the base policy entirely.
14
14
  * - **`handsReadOnly`** — `true` is stricter. `base:true` + `override:false` ⇒ **throw** (loosening). Otherwise
15
15
  * the stricter (`true` if either is true) wins.
16
- * - **`shellGate`** — `off < classify < always`. `override` may only equal-or-raise the rank; lowering it ⇒
17
- * **throw**. The stricter of the two wins.
16
+ * - **`shellGate`** — one value (`"always"`, the operator floor). Either side set ⇒ the floor stands; it cannot
17
+ * be loosened because absence is the only other state and an override cannot unset a base value.
18
18
  * - **`onAsk`** — no safe automatic merge (the approver and the policy must be coordinated). Both set to
19
19
  * DIFFERENT values ⇒ **throw**; equal ⇒ keep; one set ⇒ that one.
20
20
  * - **`hooks`** — no standard composition (stacking hook results is caller-specific). Both set ⇒ **throw**;
@@ -1,9 +1,5 @@
1
1
  import { combinePolicies } from "./tool-policy.js";
2
- const SHELL_GATE_RANK = {
3
- off: 0,
4
- classify: 1,
5
- always: 2,
6
- };
2
+ import { clampPermissionMode } from "./ask-origin.js";
7
3
  export class TaskSpecTightenError extends Error {
8
4
  constructor(message) {
9
5
  super(message);
@@ -51,16 +47,18 @@ export function tightenTaskSpec(base, overrides) {
51
47
  if (base.handsReadOnly !== undefined || overrides.handsReadOnly !== undefined) {
52
48
  merged.handsReadOnly = base.handsReadOnly === true || overrides.handsReadOnly === true;
53
49
  }
54
- if (base.shellGate !== undefined || overrides.shellGate !== undefined) {
55
- const baseGate = base.shellGate ?? "off";
56
- const overrideGate = overrides.shellGate;
57
- if (overrideGate !== undefined && SHELL_GATE_RANK[overrideGate] < SHELL_GATE_RANK[baseGate]) {
58
- throw new TaskSpecTightenError(`tightenTaskSpec: cannot loosen shellGate from "${baseGate}" to "${overrideGate}" (override may only tighten)`);
50
+ if (base.permissionMode !== undefined && overrides.permissionMode !== undefined) {
51
+ const { effective, clamped } = clampPermissionMode(overrides.permissionMode, base.permissionMode);
52
+ if (clamped) {
53
+ throw new TaskSpecTightenError(`tightenTaskSpec: permissionMode override "${overrides.permissionMode}" would loosen the base "${base.permissionMode}" (the override may only tighten; a bypass override needs a bypass base, acceptEdits and auto never adopt each other)`);
59
54
  }
60
- merged.shellGate =
61
- overrideGate !== undefined && SHELL_GATE_RANK[overrideGate] > SHELL_GATE_RANK[baseGate]
62
- ? overrideGate
63
- : (base.shellGate ?? overrideGate);
55
+ merged.permissionMode = effective;
56
+ }
57
+ else if (base.permissionMode !== undefined || overrides.permissionMode !== undefined) {
58
+ merged.permissionMode = overrides.permissionMode ?? base.permissionMode;
59
+ }
60
+ if (base.shellGate !== undefined || overrides.shellGate !== undefined) {
61
+ merged.shellGate = base.shellGate ?? overrides.shellGate;
64
62
  }
65
63
  if (base.onAsk !== undefined && overrides.onAsk !== undefined && base.onAsk !== overrides.onAsk) {
66
64
  throw new TaskSpecTightenError("tightenTaskSpec: conflicting onAsk on base and overrides — onAsk cannot be merged automatically " +
@@ -56,7 +56,7 @@ export const TOOL_CATALOG_ENTRIES = [
56
56
  },
57
57
  {
58
58
  id: "bash", name: "Bash", source: "builtin", definedIn: "src/tools/fs/fs-bash.ts", mountedBy: ["hands"],
59
- cards: ["bash", "readonly_out_of_root", "bash_invalid_timeout"],
59
+ cards: ["bash", "readonly_out_of_root", "read_unestablished", "bash_invalid_timeout"],
60
60
  face: {
61
61
  family: "shell", effect: "write", contentOrigin: "execution", contract: c(FULL_SHELL_CONTRACT_ID),
62
62
  ruleFace: SHELL_RULE_FACE,
@@ -66,7 +66,7 @@ export const TOOL_CATALOG_ENTRIES = [
66
66
  },
67
67
  {
68
68
  id: "bash-readonly", name: "Bash", source: "builtin", definedIn: "src/tools/fs/fs-bash.ts", mountedBy: ["hands"],
69
- cards: ["bash", "readonly_out_of_root", "bash_invalid_timeout"],
69
+ cards: ["bash", "readonly_out_of_root", "read_unestablished", "bash_invalid_timeout"],
70
70
  face: {
71
71
  family: "shell", effect: "read", contract: c(READONLY_SHELL_CONTRACT_ID),
72
72
  ruleFace: SHELL_RULE_FACE,
@@ -108,7 +108,7 @@ export const TOOL_CATALOG_ENTRIES = [
108
108
  { id: "task-stop-hands", name: "TaskStop", source: "builtin", definedIn: "src/tools/fs/fs-bash.ts", mountedBy: ["hands"], cards: [], face: { family: "delegate", effect: "write", contract: TASK_STOP_CONTRACT } },
109
109
  { id: "task-output", name: "TaskOutput", source: "builtin", definedIn: "src/core/task-registry.ts", mountedBy: ["delegation"], cards: ["task-output"], face: { family: "delegate", effect: "read", contract: TASK_OUTPUT_CONTRACT } },
110
110
  { id: "task-stop", name: "TaskStop", source: "builtin", definedIn: "src/core/task-registry.ts", mountedBy: ["delegation"], cards: ["task-stop"], face: { family: "delegate", effect: "write", contract: TASK_STOP_CONTRACT } },
111
- { id: "monitor", name: "Monitor", source: "builtin", definedIn: "src/tools/monitor.ts", mountedBy: ["backgroundTasks"], cards: ["monitor-start", "readonly_out_of_root"], face: { family: "other", effect: "write", contract: c("core.monitor@1"), offloadThresholdChars: 10_000 } },
111
+ { id: "monitor", name: "Monitor", source: "builtin", definedIn: "src/tools/monitor.ts", mountedBy: ["backgroundTasks"], cards: ["monitor-start", "readonly_out_of_root", "read_unestablished"], face: { family: "other", effect: "write", contract: c("core.monitor@1"), offloadThresholdChars: 10_000 } },
112
112
  { id: "send-message", name: "SendMessage", source: "builtin", definedIn: "src/agents/send-message-tool.ts", mountedBy: ["delegation", "hostInternals", "peerLane"], cards: ["send-message"], face: { family: "protocol", effect: "write", contract: c("core.send_message@1") } },
113
113
  { id: "agent-transcript", name: "AgentTranscript", source: "builtin", definedIn: "src/agents/agent-transcript-tool.ts", mountedBy: ["delegation"], cards: ["agent-transcript"], face: { family: "delegate", effect: "read", contract: c("core.agent_transcript@1") } },
114
114
  { id: "list-agents", name: "ListAgents", source: "builtin", definedIn: "src/agents/list-agents-tool.ts", mountedBy: ["delegation", "peerLane"], cards: ["list-agents"], face: { family: "protocol", effect: "read", aliases: ["ListPeers"], contract: c("core.list_agents@1") } },
@@ -156,6 +156,7 @@ export const ENGINE_CARDS = Object.freeze({
156
156
  path_not_in_root: "src/tools/fs/safety.ts",
157
157
  read_path_denied: "src/tools/fs/safety.ts",
158
158
  readonly_out_of_root: "src/tools/fs/shell-read-check.ts",
159
+ read_unestablished: "src/tools/fs/shell-read-check.ts",
159
160
  });
160
161
  const byId = new Map();
161
162
  for (const e of TOOL_CATALOG_ENTRIES) {
@@ -1,4 +1,4 @@
1
- import type { AskOrigin } from "./ask-origin.js";
1
+ import { type AskOrigin } from "./ask-origin.js";
2
2
  import { type ReadRootGrantScreen } from "./read-root-grant-screen.js";
3
3
  import { type Settlement } from "./gate-outcome.js";
4
4
  /** A tool call presented to a policy before it executes. */
@@ -325,9 +325,8 @@ export type PermissionResult = {
325
325
  /** This ask is MANDATED — no persisted allow rule and no STANDING ANSWER (a release remembered from an
326
326
  * earlier call, at any scope, a session included) may retire it; only an answer produced for THIS call
327
327
  * clears it — a person, a hook, a deployment's blanket `onAsk: "allow"` seat, or the live deciders the
328
- * mandate tables admit ({@link import("./ask-origin.js").classifierMayAnswerMandate} /
329
- * `sandboxMayAdmitMandate`); {@link requiresRealApproval} is the stronger demand that also refuses the
330
- * blanket seat. ENGINE-STAMPED at the gate's single origin stamp from the ONE derivation
328
+ * ask's class row admits ({@link import("./ask-origin.js").MODE_ARBITRATION}); {@link requiresRealApproval}
329
+ * is the stronger demand that also refuses the blanket seat. ENGINE-STAMPED at the gate's single origin stamp from the ONE derivation
331
330
  * the approval card's rule-offer word also reads ({@link import("./ask-origin.js").ruleClearanceOf},
332
331
  * which lists the doors). TIGHTENING-ONLY: a layer below the mint may not clear one a layer above
333
332
  * set, and a policy self-declaring it only opts its own ask OUT of automatic answering. Carried
@@ -376,19 +375,34 @@ export type PermissionResult = {
376
375
  * fallback / the reserved question tool / a marked call / plain policy). ENGINE-STAMPED inside the
377
376
  * gate at the same single stamp point as `ruleEvidence`, derived from what the gate's own layers
378
377
  * did ({@link import("./ask-origin.js").askOriginOf}) — a policy that self-declares it is overwritten
379
- * there, so the member is never a channel a policy can loosen through. Consumers: the gate's
380
- * auto-mode classifier step admits an ask by ONE table lookup on this word
381
- * ({@link import("./ask-origin.js").classifierMayAnswer}) instead of a chain of per-kind
382
- * exclusions; an approval surface may render the card by it. Optional on the TYPE only because a
383
- * decision is caller-composable; every ask the gate hands to its resolver carries it. */
378
+ * there, so the member is never a channel a policy can loosen through. A DISCLOSURE word: the lanes
379
+ * read the ask's class set ({@link import("./ask-origin.js").classifyAsk}), an approval surface may
380
+ * render the card by it. Optional on the TYPE only because a decision is caller-composable; every ask
381
+ * the gate hands to its resolver carries it. */
384
382
  origin?: import("./ask-origin.js").AskOrigin;
385
- /** #688 C3 (additive): WHICH mechanism stands behind an `origin: "rule_store_unavailable"` — one word from
386
- * {@link import("./ask-origin.js").RULE_STORE_UNREADABLE_KINDS} (`store`: the wired store could not be read;
387
- * `call`: the call could not be read against the person's rows — the lexer's sentence is in `message`).
388
- * ENGINE-STAMPED at the same single stamp point as `origin`, from the gate's own lane read; a policy's
389
- * self-declared value is overwritten or removed there. Present ⇔ the origin is `rule_store_unavailable`.
390
- * Carried onto the approval request and the durable row beside the origin word. */
391
- ruleStoreUnreadable?: import("./ask-origin.js").RuleStoreUnreadable;
383
+ /** WHICH integrity guard minted this ask — the unverifiable recursive delete or the session-transcript
384
+ * write. ENGINE-STAMPED by the two guard policies; a fold-internal fact the class derivation reads
385
+ * ({@link import("./ask-origin.js").classifyAsk}) so the two guards, which mint the same
386
+ * `requiresRealApproval` ask, are told apart by a fact and not by their prose. Never copied onto the
387
+ * approval request or the park row. */
388
+ guard?: "unverifiable_delete" | "transcript_write" | "artifact_shared_read";
389
+ /** The call could not be READ against a rule that may reach it — a content-form deny row, a content-form
390
+ * ask row, or a persisted-rule row (the lexer's word). ENGINE-STAMPED at the three unreadable mint sites;
391
+ * the class derivation reads it as `rule_unreadable` (a mode may release it, a person may clear it, no
392
+ * automatic lane may). It replaces the former practice of stamping `matchedAskRule` on an unreadable
393
+ * read: a rule that COULD NOT be matched is not a rule that matched. Fold-internal; not on the request. */
394
+ ruleUnreadable?: "content_deny" | "content_ask" | "persisted_call";
395
+ /** The ask was authored by the DEPLOYMENT's own governance (an operator's must-ask list, a managed command
396
+ * policy's default ask) — a fact a host's policy states on its own ask so the class derivation reads it
397
+ * as `operator_forced`: a permission mode releases nothing an operator mandated, and `dontAsk` does not
398
+ * settle it. Public: a host composing such a policy sets it; {@link combinePolicies} carries it onto the
399
+ * surviving ask (tighten-only). Not on the approval request — the card shows `origin` / `mandate`. */
400
+ authoredBy?: "deployment";
401
+ /** The classes of the OTHER asks a policy fold collapsed into this one
402
+ * ({@link import("./ask-origin.js").decisionAskClassesOf} per contributing ask, unioned), so a member the
403
+ * fold does not copy — a guard mark, an unreadable-rule fact, a bare real-approval bit — is not lost behind
404
+ * the ask that won the slot. ENGINE-WRITTEN by {@link combinePolicies}; fold-internal, never on the request. */
405
+ foldedAskClasses?: readonly import("./ask-origin.js").AskClass[];
392
406
  } | {
393
407
  action: "deny";
394
408
  updatedInput?: unknown;
@@ -495,7 +509,23 @@ export interface ToolPolicy {
495
509
  * describes the policy must read that bit, not the field's presence.
496
510
  */
497
511
  readonly projection?: ToolPolicyProjection;
512
+ /** The policy's path DENY rules that speak for a face of the engine's deny table (a `Read(<pattern>)` / `Edit(<pattern>)`
513
+ * deny compiled by `createPermissionRulePolicy`), as rows the prepared leg joins to its read and write tables — so the
514
+ * shell's reads and writes and the traversals meet the rule, not only the tool it names. Carried through
515
+ * {@link combinePolicies}; read with {@link toolPolicyPathDenyRules}. Deny-only: a row can only refuse more. */
516
+ readonly pathDenyRules?: readonly import("../tools/fs/read-deny.js").PathRuleDenyRow[];
517
+ /** Does this policy guard a write to this ABSOLUTE target — would it refuse the write if a write tool made it? The
518
+ * policy's answer for a writer it cannot see as a call (the shell's file commands), read by the shell station's
519
+ * `writesPlaced` fact alone: a guarded target is not placed, so no permission mode answers the command and its ask
520
+ * reaches a person — it is not refused. A PURE judgment: no side effect, no audit record, no hook, no approver; `true`
521
+ * on a target it cannot judge. Carried through {@link combinePolicies} (any member's `true` is the answer). */
522
+ readonly writeTargetGuard?: (target: string, signal?: AbortSignal) => Promise<boolean>;
498
523
  }
524
+ /** Every path deny row the given policies carry (combined policies included), in order — the deny tables' rule rows. */
525
+ export declare function toolPolicyPathDenyRules(policies: readonly (ToolPolicy | undefined)[]): readonly import("../tools/fs/read-deny.js").PathRuleDenyRow[];
526
+ /** The one write-target guard the given policies carry together (combined policies included): `true` when any member
527
+ * guards the target, a member that throws counting as guarding it. `undefined` when none carries one. */
528
+ export declare function toolPolicyWriteTargetGuard(policies: readonly (ToolPolicy | undefined)[]): ToolPolicy["writeTargetGuard"];
499
529
  /**
500
530
  * One deterministic DENY component of a {@link ToolPolicyProjection} (F-012 L1). All fields are plain
501
531
  * serializable data — a component round-trips JSON/`structuredClone` unchanged, which is what lets a
@@ -910,10 +940,10 @@ export declare const COARSE_SHELL_TOOLS: string[];
910
940
  * you MUST compose explicitly: `combinePolicies(deploymentBaseline, createCoarseCommandNamePolicy(…))`
911
941
  * (deny-wins — both policies' denies still fire). {@link tightenTaskSpec} does this for you.
912
942
  *
913
- * Under `TaskSpec.shellGate: "classify"` a plain `ask` from this policy stays a plain ask: the shell seat's
914
- * `"maybe"` tier is the doctrine's probe instrument, not a risk label, so the park is the budgetable kind
943
+ * On the full shell face a plain `ask` from this policy stays a plain ask: the shell seat's
944
+ * `"maybe"` tier is the shape probe's instrument, not a risk label, so the park is the budgetable kind
915
945
  * (`human` under `durableApproval`) unless the probe mandated the call (an out-of-root read) or could not
916
- * answer. A command the doctrine's OWN tighten refuses (`rm -rf …` with no policy asking first) still parks
946
+ * answer. A command the shape tier's OWN tighten refuses (`rm -rf …` with no policy asking first) still parks
917
947
  * non-budgetable — the safety axis is judged per call by `safetyAxisOf`.
918
948
  */
919
949
  export declare function createCoarseCommandNamePolicy(opts: {
@@ -1299,6 +1329,11 @@ export interface AskRequest {
1299
1329
  * gate, never caller/worker-settable; tightening-only.
1300
1330
  */
1301
1331
  readonly mandated?: true;
1332
+ /** The ask was minted by the deployment's own governance layer (the {@link PermissionResult} ask arm's member of the same
1333
+ * name, carried through every request mint and onto the park row): a person's question under every mode — no persisted
1334
+ * rule, standing answer or mode release retires it. A host approval seat that settles asks from a session grant reads
1335
+ * it to keep its hands off. Filled by the gate, never caller/worker-settable. */
1336
+ readonly authoredBy?: "deployment";
1302
1337
  /** WHICH word of the closed {@link import("./ask-origin.js").PERSISTED_RULE_MANDATES} set {@link mandated} stands
1303
1338
  * on — present ⇔ that table word is the bit's WHOLE reason (`probe_mandate` = a read outside the session's
1304
1339
  * directories). ABSENT beside `mandated: true` = another door holds too (governance, a hook, an ancestor's
@@ -1317,12 +1352,6 @@ export interface AskRequest {
1317
1352
  * that forwards a decision's member here does not compile, which is the point: the window is the
1318
1353
  * route's fact, and forwarding it silently is how a configured window becomes a wait with none. */
1319
1354
  readonly denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1320
- /** #688 C3 (additive) — the wire twin of the {@link PermissionResult} ask-arm member of the same name: which
1321
- * mechanism stands behind an `origin: "rule_store_unavailable"` (`store` / `call`,
1322
- * {@link import("./ask-origin.js").RULE_STORE_UNREADABLE_KINDS}). Present ⇔ that origin; a consumer that
1323
- * renders "the rule store is unreachable" vs "this command could not be read against your rules" branches
1324
- * here, never on the message text. Filled by the gate's own carry station from the engine-stamped decision. */
1325
- readonly ruleStoreUnreadable?: import("./ask-origin.js").RuleStoreUnreadable;
1326
1355
  /** (additive) WHICH AUTHORITY raised this ask — the wire twin of the {@link PermissionResult} ask-arm
1327
1356
  * member of the same name, one word from the closed {@link import("./ask-origin.js").AskOrigin} set.
1328
1357
  * The gate's own mint station copies the engine-stamped word; the three inherited-lane stations
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { homedir } from "node:os";
3
3
  import { brandPolicyAskClass } from "./ask-class.js";
4
+ import { decisionAskClassesOf, headlessDenialMessage } from "./ask-origin.js";
4
5
  import { ownReadRootCandidate, readRootGrantSeat } from "./read-root-grant-screen.js";
5
6
  import { decidersNote, settledByPerson } from "./gate-outcome.js";
6
7
  import { sanitizeAutoModeArmingRecipe } from "./auto-mode-arming.js";
@@ -121,6 +122,26 @@ export const ASK_EVIDENCE_ABSENCE_VALUES = ["not_wired", "not_adjudicated", "una
121
122
  export function decisionText(d) {
122
123
  return d.message;
123
124
  }
125
+ export function toolPolicyPathDenyRules(policies) {
126
+ return policies.flatMap((p) => (Array.isArray(p?.pathDenyRules) ? p.pathDenyRules : []));
127
+ }
128
+ export function toolPolicyWriteTargetGuard(policies) {
129
+ const guards = policies.flatMap((p) => (typeof p?.writeTargetGuard === "function" ? [p.writeTargetGuard] : []));
130
+ if (guards.length === 0)
131
+ return undefined;
132
+ return async (target, signal) => {
133
+ for (const guard of guards) {
134
+ try {
135
+ if ((await guard(target, signal)) !== false)
136
+ return true;
137
+ }
138
+ catch {
139
+ return true;
140
+ }
141
+ }
142
+ return false;
143
+ };
144
+ }
124
145
  export function namespacedCoveringEntries(entries) {
125
146
  return (entries ?? []).filter(isNamespacedCoveringRuleName);
126
147
  }
@@ -452,6 +473,8 @@ export function createApprovalPolicy(opts) {
452
473
  }
453
474
  export function combinePolicies(...policies) {
454
475
  const nameSets = policies.flatMap(toolPolicyNameSets);
476
+ const pathDenyRules = toolPolicyPathDenyRules(policies);
477
+ const writeTargetGuard = toolPolicyWriteTargetGuard(policies);
455
478
  const childProjections = policies.map((p) => p.projection);
456
479
  const projectedComponents = [];
457
480
  let combinedRemainder = false;
@@ -472,6 +495,8 @@ export function combinePolicies(...policies) {
472
495
  return {
473
496
  ...(combinedProjection !== undefined ? { projection: combinedProjection } : {}),
474
497
  ...(nameSets.length > 0 ? { nameSets } : {}),
498
+ ...(pathDenyRules.length > 0 ? { pathDenyRules } : {}),
499
+ ...(writeTargetGuard !== undefined ? { writeTargetGuard } : {}),
475
500
  async check(req, signal) {
476
501
  let asked;
477
502
  let current = req;
@@ -482,6 +507,8 @@ export function combinePolicies(...policies) {
482
507
  let probeMandateSeen = false;
483
508
  let mandateSeen = false;
484
509
  let fallbackSeen;
510
+ let authoredBySeen = false;
511
+ const contributors = [];
485
512
  for (const p of policies) {
486
513
  const d = refuseOutOfContractDecision(await p.check(current, signal));
487
514
  if (d.action === "deny") {
@@ -516,6 +543,10 @@ export function combinePolicies(...policies) {
516
543
  mandateSeen = true;
517
544
  if (d.action === "ask" && d.denialLimitFallback !== undefined && fallbackSeen === undefined)
518
545
  fallbackSeen = d.denialLimitFallback;
546
+ if (d.action === "ask" && d.authoredBy === "deployment")
547
+ authoredBySeen = true;
548
+ if (d.action === "ask")
549
+ contributors.push(d);
519
550
  }
520
551
  if (asked) {
521
552
  const merged = rewrite?.updatedInput;
@@ -523,7 +554,14 @@ export function combinePolicies(...policies) {
523
554
  const withMark = probeMandateSeen && withRuleAsk.probeMandated !== true ? { ...withRuleAsk, probeMandated: true } : withRuleAsk;
524
555
  const withMandate = mandateSeen && withMark.mandated !== true ? { ...withMark, mandated: true } : withMark;
525
556
  const withFallback = fallbackSeen !== undefined && withMandate.denialLimitFallback === undefined ? { ...withMandate, denialLimitFallback: fallbackSeen, requiresRealApproval: true } : withMandate;
526
- return merged !== undefined ? { ...withFallback, updatedInput: merged } : withFallback;
557
+ const withAuthor = authoredBySeen && withFallback.authoredBy === undefined ? { ...withFallback, authoredBy: "deployment" } : withFallback;
558
+ const folded = new Set(withAuthor.foldedAskClasses ?? []);
559
+ for (const c of contributors)
560
+ if (c !== asked)
561
+ for (const k of decisionAskClassesOf(c))
562
+ folded.add(k);
563
+ const withClasses = folded.size > 0 ? { ...withAuthor, foldedAskClasses: [...folded] } : withAuthor;
564
+ return merged !== undefined ? { ...withClasses, updatedInput: merged } : withClasses;
527
565
  }
528
566
  const allowed = rewrite ?? ALLOW;
529
567
  if (!anyRecord)
@@ -829,6 +867,7 @@ export function createUnverifiableDeletePolicy(opts) {
829
867
  action: "ask",
830
868
  decisionReason: "safety",
831
869
  requiresRealApproval: true,
870
+ guard: "unverifiable_delete",
832
871
  message: `Unverifiable recursive delete (fail-closed unless cleared): ${finding}. ` +
833
872
  `Re-run the delete with the resolved literal path written into the command itself ` +
834
873
  `(or assign the variable in the same command, e.g. \`DIR=/exact/path; rm -rf "$DIR"\`) so the target can be verified.`,
@@ -905,6 +944,7 @@ export function createTranscriptIntegrityPolicy(opts) {
905
944
  action: "ask",
906
945
  decisionReason: "safety",
907
946
  requiresRealApproval: true,
947
+ guard: "transcript_write",
908
948
  message: `Session-transcript write (fail-closed unless cleared): ${what}. Session transcripts (the .jsonl files ` +
909
949
  `under the agent data dir's sessions/ directory) are harness-written session state, not agent working ` +
910
950
  `files — modifying or deleting them tampers with the run's own audit trail. Reading them (ls/cat/grep ` +
@@ -956,6 +996,7 @@ export function isLiveApproverSeat(onAsk) {
956
996
  return typeof onAsk === "function";
957
997
  }
958
998
  export const DURABLE_MANDATE_SOURCES = ["force_durable_gate", "durable_approval"];
999
+ Object.freeze(DURABLE_MANDATE_SOURCES);
959
1000
  export function durableMandateSourceOf(args) {
960
1001
  if (args.forceDurableGate)
961
1002
  return "force_durable_gate";
@@ -1250,7 +1291,7 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
1250
1291
  if (onAsk === undefined || onAsk === "deny") {
1251
1292
  return {
1252
1293
  action: "deny",
1253
- message: `approval required for "${req.toolName}" but no approver is wired (headless auto-deny): ${req.message}`,
1294
+ message: headlessDenialMessage(req.toolName, req.message),
1254
1295
  decisionReason: "mode",
1255
1296
  settlement: settledByNobody("no_approver"),
1256
1297
  };
@@ -28,6 +28,8 @@ export declare function pathConfinableWriteToolNames(): ReadonlySet<string>;
28
28
  export declare function skillScopeWriteToolNames(): ReadonlySet<string>;
29
29
  /** A-8 — every definition that names a path target at all (read or write): the file-tool glob lane. */
30
30
  export declare function pathTargetingToolNames(): ReadonlySet<string>;
31
+ /** The tools whose declared path target is a READ (a file read, a search root): the file read station's membership. */
32
+ export declare function readTargetToolNames(): ReadonlySet<string>;
31
33
  /** A-10 — the micro-compaction whitelist (`compactable: true`). */
32
34
  export declare function compactableToolNames(): ReadonlySet<string>;
33
35
  /** A-5 — the aggregate-budget exemptions (`budgetExempt: true`). */
@@ -31,6 +31,9 @@ export function skillScopeWriteToolNames() {
31
31
  export function pathTargetingToolNames() {
32
32
  return names((e) => e.face.pathTarget !== undefined);
33
33
  }
34
+ export function readTargetToolNames() {
35
+ return names((e) => e.face.pathTarget?.access === "read");
36
+ }
34
37
  export function compactableToolNames() {
35
38
  return names((e) => e.face.compactable === true);
36
39
  }