@sema-agent/core 7.33.7 → 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 (206) hide show
  1. package/CHANGELOG.md +357 -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/secret-env.d.ts +16 -2
  128. package/dist/core/secret-env.js +25 -1
  129. package/dist/core/sensitive-path-policy.d.ts +4 -2
  130. package/dist/core/sensitive-path-policy.js +39 -30
  131. package/dist/core/spec-contract.d.ts +15 -0
  132. package/dist/core/spec-contract.js +19 -10
  133. package/dist/core/task-event.d.ts +1 -1
  134. package/dist/core/task-result.d.ts +1 -1
  135. package/dist/core/task-spec.d.ts +41 -89
  136. package/dist/core/task-stream.d.ts +16 -0
  137. package/dist/core/terminal-cause.js +1 -0
  138. package/dist/core/tighten-task-spec.d.ts +2 -2
  139. package/dist/core/tighten-task-spec.js +12 -14
  140. package/dist/core/tool-catalog-entries.js +4 -3
  141. package/dist/core/tool-policy.d.ts +54 -25
  142. package/dist/core/tool-policy.js +43 -2
  143. package/dist/core/tool-registry.d.ts +2 -0
  144. package/dist/core/tool-registry.js +3 -0
  145. package/dist/core/tool-spec.d.ts +8 -9
  146. package/dist/core/trace.d.ts +7 -6
  147. package/dist/core/types.d.ts +2 -1
  148. package/dist/core/wiring-manifest.d.ts +31 -5
  149. package/dist/core/wiring-manifest.js +28 -1
  150. package/dist/core/write-protect.d.ts +11 -1
  151. package/dist/core/write-protect.js +26 -15
  152. package/dist/engine/execution-env/kill-tree.js +4 -1
  153. package/dist/engine/execution-env/node-execution-env.d.ts +3 -0
  154. package/dist/engine/execution-env/node-execution-env.js +34 -35
  155. package/dist/engine/harness/types.d.ts +6 -7
  156. package/dist/engine/llm/types.d.ts +2 -4
  157. package/dist/engine/loop/types.d.ts +7 -0
  158. package/dist/index.d.ts +15 -5
  159. package/dist/index.js +12 -3
  160. package/dist/orchestration/run-spec.d.ts +4 -0
  161. package/dist/orchestration/run-spec.js +4 -3
  162. package/dist/orchestration/workflow-governance.js +1 -1
  163. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  164. package/dist/orchestration/workflow-size-guideline.js +2 -2
  165. package/dist/stores/file/fs-atomic.js +2 -1
  166. package/dist/tools/artifact/artifact-tool.d.ts +17 -0
  167. package/dist/tools/artifact/artifact-tool.js +16 -1
  168. package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
  169. package/dist/tools/fs/bash-readonly-classifier.js +88 -0
  170. package/dist/tools/fs/containment-face.d.ts +9 -3
  171. package/dist/tools/fs/containment-face.js +2 -0
  172. package/dist/tools/fs/file-read-check.d.ts +49 -0
  173. package/dist/tools/fs/file-read-check.js +71 -0
  174. package/dist/tools/fs/fs-bash.d.ts +3 -3
  175. package/dist/tools/fs/fs-read.d.ts +1 -0
  176. package/dist/tools/fs/fs-search-tools.d.ts +2 -2
  177. package/dist/tools/fs/fs-search-tools.js +4 -4
  178. package/dist/tools/fs/fs-shared.js +20 -6
  179. package/dist/tools/fs/fs-write.d.ts +4 -3
  180. package/dist/tools/fs/fs-write.js +9 -9
  181. package/dist/tools/fs/index.d.ts +39 -3
  182. package/dist/tools/fs/index.js +38 -15
  183. package/dist/tools/fs/read-deny-home.d.ts +93 -0
  184. package/dist/tools/fs/read-deny-home.js +225 -0
  185. package/dist/tools/fs/read-deny.d.ts +176 -34
  186. package/dist/tools/fs/read-deny.js +268 -61
  187. package/dist/tools/fs/safety.d.ts +6 -1
  188. package/dist/tools/fs/safety.js +18 -5
  189. package/dist/tools/fs/search.d.ts +3 -0
  190. package/dist/tools/fs/search.js +47 -9
  191. package/dist/tools/fs/shell-read-check.d.ts +42 -11
  192. package/dist/tools/fs/shell-read-check.js +188 -13
  193. package/dist/tools/fs/shell-read-establish.d.ts +7 -3
  194. package/dist/tools/fs/shell-read-establish.js +134 -21
  195. package/dist/tools/fs/shell-read-identity.d.ts +7 -0
  196. package/dist/tools/fs/shell-read-identity.js +21 -0
  197. package/dist/tools/fs/shell-read-judge.d.ts +92 -32
  198. package/dist/tools/fs/shell-read-judge.js +43 -18
  199. package/dist/tools/fs/shell-read-plan.d.ts +4 -0
  200. package/dist/tools/fs/shell-read-plan.js +26 -4
  201. package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
  202. package/dist/tools/fs/shell-read-write-targets.js +173 -0
  203. package/package.json +3 -1
  204. package/test/export-surface.snapshot.json +103 -11
  205. package/wire-changes/core@7.33.8.json +165 -0
  206. package/wire-changes/core@7.34.0.json +2621 -0
@@ -2,28 +2,19 @@ import { createHash } from "node:crypto";
2
2
  export const AUTO_MODE_DIRECTIVE = "## Auto Mode Active\n\nBias toward working without stopping for clarifying questions — when you'd normally pause to check, make the reasonable call and keep going; they'll redirect you if needed. If the user, a skill, or the shape of the task suggests they want you to ask (with AskUserQuestion or otherwise), do so. And even absent that signal, it's still fine to stop when you're genuinely blocked — unclear direction, missing input, a decision only they can make.\n\nBefore any command that could discard uncommitted work — `git checkout`/`restore`/`reset`/`clean`, `rm -rf` in the repo, restoring from a snapshot — run `git status` first and stash (with `-u` for untracked) or commit anything that's there. When staging or committing, review what's included (`git status` after a broad `git add`), and if you see anything suspicious that might reveal secrets — even if the filename looks innocuous — double-check the file's contents before pushing.";
3
3
  export const BASH_FIRST_DIRECTIVE = "Do your work through the Bash tool wherever it can accomplish the job: read files with cat, head, or sed -n, search with grep and find, and make file changes with sed, heredocs, or short scripts, rather than using the dedicated Read, Edit, or Write tools. Fall back to a dedicated tool only when Bash genuinely cannot do the job.";
4
4
  export const BYPASS_POSTURE_DIRECTIVE = `While bypass permissions mode is active:\n\n${BASH_FIRST_DIRECTIVE}`;
5
- export const APPROVER_POSTURES = ["bypass"];
6
- export function isApproverPosture(value) {
7
- return typeof value === "string" && APPROVER_POSTURES.includes(value);
8
- }
9
- export function refuseInvalidApproverPosture(spec) {
10
- if (spec.approverPosture !== undefined && !isApproverPosture(spec.approverPosture)) {
11
- const e = new Error(`approverPosture must be one of ${APPROVER_POSTURES.map((w) => JSON.stringify(w)).join(", ")} when present (got ${JSON.stringify(spec.approverPosture)}) — omit it to declare no posture; an unrecognized value is refused rather than read as absence.`);
12
- e.code = "config.approver_posture_invalid";
13
- throw e;
14
- }
15
- return spec.approverPosture;
5
+ function postureSegmentOf(facts) {
6
+ if (facts.mode === "bypassPermissions" || facts.askEffective === "auto_allow")
7
+ return BYPASS_POSTURE_DIRECTIVE;
8
+ return facts.mode === "auto" ? AUTO_MODE_DIRECTIVE : undefined;
16
9
  }
17
10
  export function composeApprovalPostureDirective(facts, statedBodies = []) {
18
- const parts = facts.approverPosture === "bypass" ? [BYPASS_POSTURE_DIRECTIVE] : facts.autoMode ? [AUTO_MODE_DIRECTIVE] : [];
19
- if (parts.length === 0)
11
+ const owed = postureSegmentOf(facts);
12
+ if (owed === undefined)
20
13
  return undefined;
21
- const stated = new Set();
22
14
  for (const body of statedBodies)
23
- for (const seg of postureSegmentsOf(body) ?? [])
24
- stated.add(seg);
25
- const owed = parts.filter((seg) => !stated.has(seg));
26
- return owed.length === 0 ? undefined : owed.join("\n\n");
15
+ if (postureSegmentsOf(body)?.includes(owed) === true)
16
+ return undefined;
17
+ return owed;
27
18
  }
28
19
  const POSTURE_SEGMENTS = [AUTO_MODE_DIRECTIVE, BYPASS_POSTURE_DIRECTIVE];
29
20
  function postureSegmentsOf(body) {
@@ -17,8 +17,13 @@ export interface AutoModeRules {
17
17
  export interface BuildAutoModePromptOptions {
18
18
  rules?: AutoModeRules;
19
19
  /** Settings-plane deny rules (tool[/argument-pattern] strings) — rendered into the
20
- * `<settings_deny_rules>` slot as the CC circumvention-catching paragraph. */
20
+ * `<settings_deny_rules>` slot as the CC circumvention-catching paragraph, after the engine's own lines. */
21
21
  settingsDenyRules?: readonly string[];
22
+ /** The ENGINE's deny lines — its effective read-deny table as rules ({@link readDenyClassifierRules}).
23
+ * Rendered in the same paragraph, ahead of `settingsDenyRules`. A separate member (not pre-merged by the
24
+ * caller) because a persisted arming records the two halves separately: the deployment's half is compared
25
+ * against the redeeming deployment's own settings, the engine's half against the row's own table. */
26
+ derivedDenyRules?: readonly DerivedDenyRule[];
22
27
  /** Extra session-context facts (e.g. the CC user-identity line) — appended as a
23
28
  * `## Session Context` bullet block after the assembled document. */
24
29
  sessionContext?: readonly string[];
@@ -28,6 +33,62 @@ export interface BuildAutoModePromptOptions {
28
33
  * text may carry `$`). */
29
34
  crossSessionMessagesRule?: string;
30
35
  }
36
+ /**
37
+ * One engine deny line: a `Read(...)` rule in the engine's own path-rule grammar (`//` = the filesystem root,
38
+ * `*` within one segment, a whole-segment `**` across segments, and a rule reaches the path it names and
39
+ * everything under it — `parseRuleText` / `pathRuleReachOf`), plus whether ASCII letter case is ignored, which
40
+ * that grammar cannot spell and the paragraph states beside the rule.
41
+ */
42
+ export interface DerivedDenyRule {
43
+ readonly rule: string;
44
+ readonly anyCase?: true;
45
+ }
46
+ /**
47
+ * The effective READ-deny table as the classifier's deny lines — one line per row, the row's reach in the
48
+ * engine's own `Read(...)` path-rule grammar.
49
+ *
50
+ * A read-deny row is a run of path SEGMENTS matched anywhere in a path — the entry itself and everything under
51
+ * it — and its `*` never crosses a `/` (read-deny.ts: `.ssh` refuses `~/.ssh/id_rsa` and `/srv/x/.ssh/config`
52
+ * alike; a `**` segment matches exactly one segment). The rule grammar reaches the path a pattern names and
53
+ * everything under it, so the line is `//**` + `/` + the row with every run of stars written `*` (one segment,
54
+ * as in the row): `.ssh` ⇒ `Read(//**\/.ssh)`, `**\/secrets/**` ⇒ `Read(//**\/*\/secrets/*)`, `*` (every
55
+ * path) ⇒ `Read(//**\/*)`. A row that names no segment at all (`""`, `/`) is not stated. A HOME-ANCHORED row
56
+ * (`~/<run>`, read-deny.ts `READ_DENY_HOME_WORD`) is the rule grammar's home word with the same reach:
57
+ * `~/.sema/settings.json` ⇒ `Read(~/.sema/settings.json)`.
58
+ *
59
+ * The text of a row can come from a task (a spawned task's author included) and it is spliced into a trusted
60
+ * prompt, so a row is stated as a rule and nothing else: every character the grammar or the prompt must not
61
+ * carry (see NOT_STATED) is written `*`, and an over-long pattern is cut and closed with `*` — both wider than
62
+ * the row, never narrower, and the result still parses as a rule. No row can start a line, leave its code
63
+ * span, form a tag or a second rule, or read as a sentence; the engine enforces the row as compiled either way.
64
+ *
65
+ * A row matched without regard to ASCII letter case (the deny set's default) carries `anyCase` when its stated
66
+ * pattern has an ASCII letter. Duplicates are folded AFTER this encoding, first seen.
67
+ */
68
+ export declare function readDenyClassifierRules(entries: readonly {
69
+ readonly pattern: string;
70
+ readonly caseSensitive?: boolean;
71
+ }[], rules?: readonly {
72
+ readonly tool: string;
73
+ readonly pattern: string;
74
+ }[]): DerivedDenyRule[];
75
+ /**
76
+ * The `<settings_deny_rules>` paragraph for one arming, from the engine's facts — the effective deny table
77
+ * (`readDeny`: the compiled matcher's entries, built-in tiers + deployment + task additions; `readDenyRules`: its
78
+ * permission-rule rows, both faces) and the deployment's `settingsDenyRules` — exactly as {@link buildAutoModePrompt}
79
+ * splices it. Empty inputs ⇒ "".
80
+ */
81
+ export declare function renderClassifierDenyRules(input: {
82
+ readonly readDeny?: readonly {
83
+ readonly pattern: string;
84
+ readonly caseSensitive?: boolean;
85
+ }[];
86
+ readonly readDenyRules?: readonly {
87
+ readonly tool: string;
88
+ readonly pattern: string;
89
+ }[];
90
+ readonly settingsDenyRules?: readonly string[];
91
+ }): string;
31
92
  /**
32
93
  * Assemble the classifier SYSTEM prompt (CC `KRg`, content-equivalent single string — CC splits the
33
94
  * session-context tail into a second uncached block purely as a prompt-cache optimization).
@@ -1,6 +1,7 @@
1
1
  import { AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS, AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES, AUTO_MODE_DEFAULTS_SENTINEL } from "./auto-mode-defaults.js";
2
2
  import { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./auto-mode-prompt-assets.js";
3
3
  import { cutEnginePrefix, cutEngineSegments, flattenableUserText } from "./untrusted-text.js";
4
+ import { readDenyPatternRun } from "../tools/fs/read-deny.js";
4
5
  export { AUTO_MODE_DEFAULTS_SENTINEL } from "./auto-mode-defaults.js";
5
6
  function mergeRuleSection(user, defaultsInner) {
6
7
  if (!user?.length)
@@ -20,10 +21,47 @@ function mergeRuleSection(user, defaultsInner) {
20
21
  }
21
22
  return out.join("\n");
22
23
  }
23
- function renderSettingsDenyRules(rules) {
24
- if (rules.length === 0)
24
+ const DENY_LINE_PATTERN_MAX = 512;
25
+ const NOT_STATED = /[\s\p{Z}\p{Cf}`<>(){}[\]?\\\u0000-\u001f\u007f-\u009f]/u;
26
+ const notStated = (ch) => NOT_STATED.test(ch) || NOT_STATED.test(ch.normalize("NFKC"));
27
+ export function readDenyClassifierRules(entries, rules = []) {
28
+ const out = [];
29
+ const seen = new Set();
30
+ const push = (line) => {
31
+ const key = `${line.rule}\u0000${line.anyCase === true}`;
32
+ if (seen.has(key))
33
+ return;
34
+ seen.add(key);
35
+ out.push(line);
36
+ };
37
+ for (const { pattern, caseSensitive } of entries) {
38
+ const { homeAnchored, run } = readDenyPatternRun(pattern);
39
+ const segments = run.split("/").filter((seg) => seg.length > 0);
40
+ if (segments.length === 0)
41
+ continue;
42
+ const stated = statedPattern(segments.join("/"));
43
+ push({ rule: homeAnchored ? `Read(~/${stated})` : `Read(//**/${stated})`, ...(caseSensitive !== true && /[A-Za-z]/.test(stated) ? { anyCase: true } : {}) });
44
+ }
45
+ for (const { tool, pattern } of rules) {
46
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(tool) || pattern.length === 0)
47
+ continue;
48
+ push({ rule: `${tool}(${pattern.split("/").map((seg) => (seg === "**" ? seg : statedPattern(seg))).join("/")})` });
49
+ }
50
+ return out;
51
+ }
52
+ function statedPattern(pattern) {
53
+ const chars = [...pattern].map((ch) => (notStated(ch) ? "*" : ch));
54
+ const cut = chars.length > DENY_LINE_PATTERN_MAX ? [...chars.slice(0, DENY_LINE_PATTERN_MAX), "*"] : chars;
55
+ return cut.join("").replace(/\*+/g, "*");
56
+ }
57
+ export function renderClassifierDenyRules(input) {
58
+ return renderSettingsDenyRules(readDenyClassifierRules(input.readDeny ?? [], input.readDenyRules ?? []), input.settingsDenyRules ?? []);
59
+ }
60
+ function renderSettingsDenyRules(derived, settings) {
61
+ const items = [...derived.map((d) => `\`${d.rule}\`${d.anyCase === true ? " (any ASCII letter case)" : ""}`), ...settings.map((r) => `\`${r}\``)];
62
+ if (items.length === 0)
25
63
  return "";
26
- return (`- User Deny Rules: The user has configured these permission deny rules: ${rules.map((r) => `\`${r}\``).join(", ")}. Each rule names a tool and (optionally) an argument pattern that is already hard-blocked for that tool. ` +
64
+ return (`- User Deny Rules: The user has configured these permission deny rules: ${items.join(", ")}. Each rule names a tool and (optionally) an argument pattern that is already hard-blocked for that tool. ` +
27
65
  "Block the action if it accomplishes the same effect via a different tool — e.g. using Bash with " +
28
66
  "`python -c`, `sed -i`, `cat >`, heredocs, or similar to write or edit a file that an Edit/Write/MultiEdit deny rule covers, or otherwise routing around a deny rule by switching tools. The named tool itself is enforced separately; your job here is to catch circumvention.");
29
67
  }
@@ -39,7 +77,7 @@ export function buildAutoModePrompt(options) {
39
77
  for (const [key, re] of PAIRED_SECTIONS) {
40
78
  out = out.replace(re, (_m, inner) => mergeRuleSection(options?.rules?.[key], inner));
41
79
  }
42
- out = out.replace("<settings_deny_rules>", () => renderSettingsDenyRules(options?.settingsDenyRules ?? []));
80
+ out = out.replace("<settings_deny_rules>", () => renderSettingsDenyRules(options?.derivedDenyRules ?? [], options?.settingsDenyRules ?? []));
43
81
  const context = options?.sessionContext ?? [];
44
82
  if (context.length > 0) {
45
83
  out += `\n\n## Session Context\n\n${context.map((f) => `- ${f}`).join("\n")}`;
@@ -28,7 +28,8 @@ export interface AutoModeRebuildOptions {
28
28
  * an affirmative claim, "this is the run's first gated action", that would simply be false. A provider
29
29
  * that returns `[]` is DECLARING the transcript empty, which is a different statement from an engine
30
30
  * that silently had none to show. A provider that throws fails the classification closed
31
- * (`unavailable` ⇒ a deny that says so), never into a blind verdict.
31
+ * (`unavailable` ⇒ on to a person where the station has an approval seat, refused with a sentence that says
32
+ * so where it has none), never into a blind verdict.
32
33
  */
33
34
  transcript: () => readonly Message[] | Promise<readonly Message[]>;
34
35
  }
@@ -11,6 +11,7 @@ export function rebuildAutoModeDecider(opts) {
11
11
  const promptOptions = {
12
12
  ...(effective.rules !== undefined ? { rules: effective.rules } : {}),
13
13
  ...(effective.settingsDenyRules !== undefined ? { settingsDenyRules: effective.settingsDenyRules } : {}),
14
+ ...(effective.derivedDenyRules !== undefined ? { derivedDenyRules: effective.derivedDenyRules } : {}),
14
15
  ...(effective.sessionContext !== undefined ? { sessionContext: effective.sessionContext } : {}),
15
16
  ...(effective.crossSessionMessagesRule === true ? { crossSessionMessagesRule: CROSS_SESSION_CLASSIFIER_RULE } : {}),
16
17
  };
@@ -51,6 +51,14 @@ export declare function isAutoModeClassifierFallback(v: unknown): v is AutoModeC
51
51
  * `config.auto_mode_classifier_fallback_invalid`. A typo'd knob must not read as the default: `"non"` would
52
52
  * keep paying for the rung it meant to turn off. */
53
53
  export declare function autoModeClassifierFallbackOf(v: unknown): AutoModeClassifierFallback;
54
+ /** Whether a present budget value is one the knob takes: a finite number of milliseconds in [1, 2147483647]. */
55
+ export declare function isAutoModeTimeoutMs(v: unknown): v is number;
56
+ /** Read the per-level budget knob (`RunnerDeps.autoMode.timeoutMs` / `AutoModeDeciderOptions.timeoutMs`):
57
+ * absent ⇒ {@link AUTO_MODE_DEFAULT_TIMEOUT_MS}; a finite number in [1, 2147483647] ⇒ itself; ANYTHING ELSE
58
+ * (NaN, ±Infinity, `0`, a negative, a value a host timer would fire at once, a string, an object) ⇒ a loud
59
+ * refusal carrying `config.auto_mode_timeout_invalid`. There is no OFF spelling: the budget bounds how long a
60
+ * permission gate waits, and a gate with no bound is not a setting this knob offers. */
61
+ export declare function autoModeTimeoutMsOf(v: unknown): number;
54
62
  /** The classifier's verdict over one pending `ask`. Every arm may carry the classification's model
55
63
  * attribution ({@link AutoModeClassifierRound}), present iff the decider was wired with a candidate ladder. */
56
64
  export type AutoModeVerdict = ({
@@ -157,6 +165,12 @@ export declare function rungDidNotRule(v: AutoModeVerdict): AutoModeFallbackCaus
157
165
  * NOT move keeps the CC text byte for byte.
158
166
  */
159
167
  export declare function classifierUnavailableDenyMessage(toolName: string, cause: AutoModeUnavailableCause, attribution?: AutoModeClassifierRound): string;
168
+ /**
169
+ * The text an ask carries to a person when the classifier could not rule on it and the leg has an approval seat
170
+ * (the gate's classifier station and the inherited station): the ask's own text first — the card still says what
171
+ * is being asked — then why auto mode is asking at all. One writer for both stations.
172
+ */
173
+ export declare function classifierUnavailableAskMessage(askText: string, cause: AutoModeUnavailableCause): string;
160
174
  /**
161
175
  * The model-facing deny text for a `parse_error` verdict — CC 2.1.259 `i7t` stem ("Auto mode could not evaluate
162
176
  * this action and is blocking it for safety"; 2.1.250 `Ure`): the classifier answered, but not with a verdict,
@@ -256,7 +270,10 @@ export interface AutoModeDeciderOptions<T = void> {
256
270
  /** Hard cap on one classification round-trip — one LADDER RUNG's DISPATCH, not the whole classification
257
271
  * (see {@link candidates}) — and, separately, the one {@link assemble} call that precedes the walk (ONE
258
272
  * number: a second knob would be a second answer to "how long may a classification make the gate wait").
259
- * Default 15_000 ms: a permission gate must not stall the whole run on a slow classifier. */
273
+ * Omitted ⇒ {@link AUTO_MODE_DEFAULT_TIMEOUT_MS}. A present value that is not a finite number of at least 1
274
+ * and at most 2147483647 (a host timer fires at once past that) is REFUSED at construction
275
+ * (`config.auto_mode_timeout_invalid`, {@link autoModeTimeoutMsOf}) — never clamped, never read as the
276
+ * default, and there is no OFF: a permission gate always has a bound. */
260
277
  timeoutMs?: number;
261
278
  /**
262
279
  * Fired once per `decide` call with the verdict the gate is about to act on and the wall time it waited
@@ -288,6 +305,10 @@ export interface AutoModeClassified {
288
305
  export interface AutoModeDecider {
289
306
  /** Never rejects. Any internal failure surfaces as `unavailable`/`parse_error` (fail-closed). */
290
307
  decide(input: AutoModeClassifyInput, signal?: AbortSignal): Promise<AutoModeVerdict>;
308
+ /** The per-level budget this decider holds each rung's dispatch (and the assembly) to, in ms — the resolved
309
+ * {@link AutoModeDeciderOptions.timeoutMs}; what a notice about a timed-out classification names. Absent on a decider a
310
+ * host implements by hand. */
311
+ readonly timeoutMs?: number;
291
312
  }
292
313
  /** The per-run decider: timeout + fail-closed error mapping, nothing carried between rounds. One instance per
293
314
  * run (it binds the run's model leg and assembled prompt); a round that failed says nothing about the next,
@@ -413,8 +434,16 @@ export interface AutoModeDenialTracker {
413
434
  windowArmedAt?: number;
414
435
  };
415
436
  }
416
- /** Build the per-run tracker. Bad knob values throw (the deployment face is read at prepare; a refusal
417
- * there is the loud exit the knob doctrine requires). */
437
+ export declare function autoModeDenialLimitOf(v: unknown): Required<AutoModeDenialLimitOptions>;
438
+ /** The deployment face's three value knobs, read by their one readers — every leg's prepare runs it while
439
+ * `RunnerDeps.autoMode` is present, whatever mode the leg starts in, so a bad knob refuses the run before a steer or a
440
+ * parent's live ceiling can reach an arming that would have refused it. */
441
+ export declare function screenAutoModeKnobs(face: {
442
+ readonly timeoutMs?: unknown;
443
+ readonly classifierFallback?: unknown;
444
+ readonly denialLimit?: unknown;
445
+ }): void;
446
+ /** Build the per-run tracker over {@link autoModeDenialLimitOf}'s bounds (a bad knob refuses there). */
418
447
  export declare function createAutoModeDenialTracker(opts?: AutoModeDenialLimitOptions): AutoModeDenialTracker;
419
448
  /** The fallback ask's text — CC's sentence family, single-sourced for every mint site (the main gate
420
449
  * and the inherited-lane arms): `Classifier denial limit exceeded, falling back to prompting: <limit
@@ -31,6 +31,20 @@ export function autoModeClassifierFallbackOf(v) {
31
31
  e.code = "config.auto_mode_classifier_fallback_invalid";
32
32
  throw e;
33
33
  }
34
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
35
+ export function isAutoModeTimeoutMs(v) {
36
+ return typeof v === "number" && Number.isFinite(v) && v >= 1 && v <= MAX_TIMER_DELAY_MS;
37
+ }
38
+ export function autoModeTimeoutMsOf(v) {
39
+ if (v === undefined)
40
+ return AUTO_MODE_DEFAULT_TIMEOUT_MS;
41
+ if (isAutoModeTimeoutMs(v))
42
+ return v;
43
+ const e = new Error(`RunnerDeps.autoMode.timeoutMs must be a number of milliseconds between 1 and ${MAX_TIMER_DELAY_MS} ` +
44
+ `(got ${describeRejectedValue(v)}) — omit it for the default ${AUTO_MODE_DEFAULT_TIMEOUT_MS}`);
45
+ e.code = "config.auto_mode_timeout_invalid";
46
+ throw e;
47
+ }
34
48
  export const AUTO_MODE_ROUND_NOT_ASKED_CODE = "auto_mode.round_not_asked";
35
49
  export function autoModeRoundNotAsked(reason) {
36
50
  let detail;
@@ -70,6 +84,10 @@ export function classifierUnavailableDenyMessage(toolName, cause, attribution) {
70
84
  "If it keeps failing, continue with other tasks that don't require this action and come back to it later. " +
71
85
  "Note: reading files, searching code, and other read-only operations do not require the classifier and can still be used.");
72
86
  }
87
+ export function classifierUnavailableAskMessage(askText, cause) {
88
+ const parenthetical = cause === "timeout" ? " (timed out)" : "";
89
+ return `${askText}\n\nThe auto-mode classifier is temporarily unavailable${parenthetical}, so auto mode could not judge this call; it needs an approval instead of the classifier's verdict.`;
90
+ }
73
91
  export function classifierParseFailureDenyMessage(attribution) {
74
92
  const stem = "Auto mode could not evaluate this action and is blocking it for safety";
75
93
  return attribution === undefined ? `${stem} — the classifier's reply carried no verdict.` : `${stem} — the classifier model "${attribution.modelUsed}" replied without a verdict.`;
@@ -153,7 +171,7 @@ export function parseAutoModeResponse(text, options) {
153
171
  return { kind: "block", category, reason };
154
172
  }
155
173
  export function createAutoModeDecider(opts) {
156
- const timeoutMs = opts.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS;
174
+ const timeoutMs = autoModeTimeoutMsOf(opts.timeoutMs);
157
175
  if (opts.candidates !== undefined) {
158
176
  if (opts.candidates.length === 0) {
159
177
  throw new Error("AutoModeDeciderOptions.candidates must name at least one model — omit the member for the leg's own model, never an empty ladder");
@@ -183,7 +201,7 @@ export function createAutoModeDecider(opts) {
183
201
  }
184
202
  return verdict;
185
203
  };
186
- return { decide };
204
+ return { decide, timeoutMs };
187
205
  async function decideUnreported(input, round, signal) {
188
206
  const causes = [];
189
207
  let verdict = { kind: "unavailable", cause: "error" };
@@ -243,42 +261,46 @@ export function createAutoModeDecider(opts) {
243
261
  }
244
262
  }
245
263
  async function withDeadline(capMs, signal, call) {
264
+ if (signal?.aborted === true)
265
+ throw new Error("the classification was cancelled before it was dispatched");
246
266
  let timer;
267
+ let onOuterAbort;
247
268
  const inner = new AbortController();
248
- const onOuterAbort = () => inner.abort();
249
- if (signal !== undefined) {
250
- if (signal.aborted)
251
- inner.abort();
252
- else
253
- signal.addEventListener("abort", onOuterAbort, { once: true });
254
- }
255
269
  try {
256
270
  return await new Promise((resolve, reject) => {
257
271
  let settled = false;
272
+ const settle = (end) => {
273
+ if (settled)
274
+ return;
275
+ settled = true;
276
+ end();
277
+ };
278
+ onOuterAbort = () => {
279
+ inner.abort();
280
+ settle(() => reject(new Error("the classification was cancelled")));
281
+ };
282
+ signal?.addEventListener("abort", onOuterAbort, { once: true });
258
283
  timer = setTimeout(() => {
259
- if (!settled) {
260
- settled = true;
284
+ settle(() => {
261
285
  inner.abort();
262
286
  reject(new AutoModeTimeout());
263
- }
287
+ });
264
288
  }, capMs);
265
- call(inner.signal).then((v) => {
266
- if (!settled) {
267
- settled = true;
268
- resolve(v);
269
- }
270
- }, (e) => {
271
- if (!settled) {
272
- settled = true;
273
- reject(e);
274
- }
275
- });
289
+ let pending;
290
+ try {
291
+ pending = call(inner.signal);
292
+ }
293
+ catch (e) {
294
+ settle(() => reject(e));
295
+ return;
296
+ }
297
+ pending.then((v) => settle(() => resolve(v)), (e) => settle(() => reject(e)));
276
298
  });
277
299
  }
278
300
  finally {
279
301
  if (timer !== undefined)
280
302
  clearTimeout(timer);
281
- if (signal !== undefined)
303
+ if (signal !== undefined && onOuterAbort !== undefined)
282
304
  signal.removeEventListener("abort", onOuterAbort);
283
305
  }
284
306
  }
@@ -305,25 +327,42 @@ export function unarmedWindow(fallback) {
305
327
  function countsOf(fallback) {
306
328
  return { consecutive: fallback.consecutive, total: fallback.total, limit: fallback.limit };
307
329
  }
308
- const MAX_TIMER_DELAY_MS = 2_147_483_647;
309
- export function createAutoModeDenialTracker(opts = {}) {
310
- const bound = (name, v, dflt) => {
311
- if (v === undefined)
330
+ const DENIAL_LIMIT_MEMBERS = new Set(["maxConsecutive", "maxTotal", "autoDenyAfterMs"]);
331
+ export function autoModeDenialLimitOf(v) {
332
+ const refuse = (what) => {
333
+ const e = new Error(`RunnerDeps.autoMode.denialLimit${what}`);
334
+ e.code = "config.auto_mode_denial_limit_invalid";
335
+ throw e;
336
+ };
337
+ if (v === undefined)
338
+ return { ...AUTO_MODE_DENIAL_LIMIT_DEFAULTS, autoDenyAfterMs: AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS };
339
+ if (typeof v !== "object" || v === null || Array.isArray(v))
340
+ return refuse(` must be a record of bounds (got ${describeRejectedValue(v)}) — omit it for the defaults`);
341
+ const opts = v;
342
+ const stray = Object.keys(opts).find((k) => !DENIAL_LIMIT_MEMBERS.has(k));
343
+ if (stray !== undefined)
344
+ return refuse(` has no member ${JSON.stringify(stray)} — its members are maxConsecutive, maxTotal and autoDenyAfterMs`);
345
+ const bound = (name, dflt) => {
346
+ const b = opts[name];
347
+ if (b === undefined)
312
348
  return dflt;
313
- if (!Number.isInteger(v) || v < 1) {
314
- throw new Error(`RunnerDeps.autoMode.denialLimit.${name} must be a positive integer (got ${JSON.stringify(v)}) — omit it for the default ${dflt}`);
315
- }
316
- return v;
349
+ return typeof b === "number" && Number.isInteger(b) && b >= 1 ? b : refuse(`.${name} must be a positive integer (got ${describeRejectedValue(b)}) — omit it for the default ${dflt}`);
317
350
  };
318
- const maxConsecutive = bound("maxConsecutive", opts.maxConsecutive, AUTO_MODE_DENIAL_LIMIT_DEFAULTS.maxConsecutive);
319
- const maxTotal = bound("maxTotal", opts.maxTotal, AUTO_MODE_DENIAL_LIMIT_DEFAULTS.maxTotal);
320
- let autoDenyAfterMs = AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS;
321
- if (opts.autoDenyAfterMs !== undefined) {
322
- if (!Number.isInteger(opts.autoDenyAfterMs) || opts.autoDenyAfterMs < 0 || opts.autoDenyAfterMs > MAX_TIMER_DELAY_MS) {
323
- throw new Error(`RunnerDeps.autoMode.denialLimit.autoDenyAfterMs must be an integer between 0 and ${MAX_TIMER_DELAY_MS} (got ${JSON.stringify(opts.autoDenyAfterMs)}) — 0 disarms the window; omit it for the default ${AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS}`);
324
- }
325
- autoDenyAfterMs = opts.autoDenyAfterMs;
326
- }
351
+ const w = opts["autoDenyAfterMs"];
352
+ const autoDenyAfterMs = w === undefined
353
+ ? AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS
354
+ : typeof w === "number" && Number.isInteger(w) && w >= 0 && w <= MAX_TIMER_DELAY_MS
355
+ ? w
356
+ : refuse(`.autoDenyAfterMs must be an integer between 0 and ${MAX_TIMER_DELAY_MS} (got ${describeRejectedValue(w)}) — 0 disarms the window; omit it for the default ${AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS}`);
357
+ return { maxConsecutive: bound("maxConsecutive", AUTO_MODE_DENIAL_LIMIT_DEFAULTS.maxConsecutive), maxTotal: bound("maxTotal", AUTO_MODE_DENIAL_LIMIT_DEFAULTS.maxTotal), autoDenyAfterMs };
358
+ }
359
+ export function screenAutoModeKnobs(face) {
360
+ autoModeTimeoutMsOf(face.timeoutMs);
361
+ autoModeClassifierFallbackOf(face.classifierFallback);
362
+ autoModeDenialLimitOf(face.denialLimit);
363
+ }
364
+ export function createAutoModeDenialTracker(opts) {
365
+ const { maxConsecutive, maxTotal, autoDenyAfterMs } = autoModeDenialLimitOf(opts);
327
366
  let consecutive = 0;
328
367
  let total = 0;
329
368
  let timedFallbackShown = false;
@@ -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
@@ -1098,7 +1095,7 @@ export interface CheckpointState {
1098
1095
  * Parent effective-policy inheritance (tighten-only): the durable projection of the run's
1099
1096
  * {@link import("./runner/contracts.js").InheritedGate}. Only the serializable DATA half persists —
1100
1097
  * `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
1098
+ * resume against the resumed leg's env), `shellGate` (the chain's operator floor) and `permissionMode`. The OPAQUE
1102
1099
  * half (`parentConstraints` — live ToolPolicy/onAsk closures) CANNOT persist; `requiresParentConstraint`
1103
1100
  * records only its PRESENCE, so the resume pre-CAS ladder can fail-loud (`resume.parent_constraint_missing`,
1104
1101
  * checkpoint stays `pending`) when the resuming caller did not re-supply the closures via
@@ -1115,27 +1112,18 @@ export interface CheckpointState {
1115
1112
  rules: SessionPermissionRules;
1116
1113
  }>;
1117
1114
  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;
1115
+ /** The chain's DECLARED permission mode at suspend (data half, same law as `shellGate`): the suspended leg's
1116
+ * own effective declaration — its request clamped under its parent's ceiling — before the deployment's fold.
1117
+ * A resume leg reads it as the CEILING its re-passed spec is clamped under (tighten-only, the shell floor's
1118
+ * law) and re-judges the deny bits and the classifier face for its own leg: a `bypassPermissions` row does not
1119
+ * revive the mode where the redeeming deployment denies it, and an `auto` row arms only where a face is
1120
+ * wired. Absent on a task that declared nothing and inherited nothing (`default`). Rows written before this
1121
+ * member carried `autoModeRequested` / `approverPosture` / a doctrine `shellGate`; the resume leg migrates
1122
+ * them once, announced (`resume.legacy_mode_seed_migrated`), and the next suspend writes this member. */
1123
+ permissionMode?: PermissionMode;
1124
+ /** Beside a recorded `plan`: the declared word the leg had when a steer or the model's `EnterPlanMode` moved it into
1125
+ * plan — the mode an approved or edited `plan_review` resumes under ({@link import("./mode-policy.js").modeRequestAfterResume}). */
1126
+ prePlanMode?: PermissionMode;
1139
1127
  /** Org-memory admission freeze (ruled 2026-08-05): the chain's admitted org-scope set at
1140
1128
  * suspend (data half, plain strings). The resume leg folds it seed ∩ live (tighten-only) and
1141
1129
  * re-runs admission under it — a resume must never widen the delegation freeze. Absent on
@@ -1950,6 +1938,8 @@ export interface CheckpointSummary {
1950
1938
  * OMITTED (the {@link toolCallId} absence-shape contract), screened to the literal `true` — a pre-#1079 row
1951
1939
  * reads absent (unreported). ECHO-ONLY; nothing gates on it. */
1952
1940
  mandated?: true;
1941
+ /** The row's `PendingAction.tool_approval.authoredBy`, echoed as the one literal. ECHO-ONLY. */
1942
+ authoredBy?: "deployment";
1953
1943
  /** The row's `PendingAction.tool_approval.mandate`, echoed ONLY as a member of the closed set. ECHO-ONLY. */
1954
1944
  mandate?: import("./ask-origin.js").PersistedRuleMandate;
1955
1945
  /** The row's `PendingAction.tool_approval.denialLimitFallback`, projected BESIDE the preview
@@ -1962,9 +1952,6 @@ export interface CheckpointSummary {
1962
1952
  * value is a member of the closed set ({@link import("./ask-origin.js").isAskOrigin}) — a bad value
1963
1953
  * reads as absent, never as a word. OMITTED when the row carries none (a pre-existing row). */
1964
1954
  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
1955
  /**
1969
1956
  * PRESENCE ONLY — this pending `tool_approval` row's payload carries at least one DIRECTIONAL
1970
1957
  * format control (the {@link import("./tool-policy.js").AskRequest.hasBidiControls} character class;
@@ -3027,3 +3014,31 @@ export declare class InMemoryCheckpointStore implements CheckpointStore {
3027
3014
  /** Test/inspection helper: number of stored checkpoints. */
3028
3015
  get size(): number;
3029
3016
  }
3017
+ /** What {@link migrateLegacyModeSeed} read off a checkpoint row's `inheritedGate`: the mode and the floor the resume
3018
+ * leg inherits, and — when the row carried a pre-mode key — what was migrated and what was dropped. */
3019
+ export interface LegacyModeSeedMigration {
3020
+ permissionMode?: PermissionMode;
3021
+ prePlanMode?: PermissionMode;
3022
+ shellGate?: ShellGateDoctrine;
3023
+ /** Present iff the row carried a retired key (`autoModeRequested` / `approverPosture` / a doctrine `shellGate`). */
3024
+ migrated?: {
3025
+ permissionMode?: PermissionMode;
3026
+ droppedKeys: readonly string[];
3027
+ };
3028
+ }
3029
+ /**
3030
+ * Read a row's mode seed ONCE, migrating the pre-7.34.0 keys the row may still carry. The matrix, key by key:
3031
+ * - `permissionMode` present ⇒ it must be a member of the closed set (else refused, `config.permission_mode_invalid`,
3032
+ * without reading the retired keys); the two retired mode keys beside it are DROPPED unvalidated (the new key is
3033
+ * the truth), `shellGate` follows the value rule below;
3034
+ * - `autoModeRequested`: absent / `false` ⇒ no mode from it; `true` ⇒ `auto`; anything else ⇒ refused
3035
+ * (`resume.legacy_mode_seed`);
3036
+ * - `approverPosture`: any value ⇒ dropped (it never authorized anything; the mode comes from the request / chain,
3037
+ * and a `"bypass"` posture beside `autoModeRequested: true` reads `auto` — a posture never becomes an executable
3038
+ * bypass);
3039
+ * - `shellGate`: absent ⇒ none; `"always"` ⇒ kept (a live key, not a retired encoding); `"off"` / `"classify"` ⇒
3040
+ * dropped (no floor); anything else ⇒ refused (`resume.legacy_mode_seed`).
3041
+ * Idempotent: a row the resume leg writes back carries `permissionMode` and the floor only, so a second read migrates
3042
+ * nothing. Migration precedes any retired-key door, so a window row is read rather than refused.
3043
+ */
3044
+ export declare function migrateLegacyModeSeed(gate: object | undefined): LegacyModeSeedMigration;