@sema-agent/core 7.33.8 → 7.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/CHANGELOG.md +462 -3
  2. package/dist/agents/cross-session-judge.d.ts +16 -7
  3. package/dist/agents/cross-session-judge.js +18 -15
  4. package/dist/agents/peer-session-drain.d.ts +4 -0
  5. package/dist/agents/peer-session-drain.js +5 -2
  6. package/dist/agents/repair-loop.d.ts +1 -1
  7. package/dist/agents/subagent.d.ts +3 -0
  8. package/dist/agents/subagent.js +12 -3
  9. package/dist/brain/anthropic.js +1 -0
  10. package/dist/brain/errors.d.ts +23 -1
  11. package/dist/brain/errors.js +19 -2
  12. package/dist/brain/open-responses.js +1 -0
  13. package/dist/brain/openai.js +1 -0
  14. package/dist/brain/status-sink.d.ts +7 -1
  15. package/dist/brain/stream-engine.d.ts +3 -0
  16. package/dist/brain/stream-engine.js +2 -1
  17. package/dist/brain/stream-shared.d.ts +3 -1
  18. package/dist/brain/stream-shared.js +16 -4
  19. package/dist/core/a2a.d.ts +3 -1
  20. package/dist/core/a2a.js +1 -1
  21. package/dist/core/ask-origin.d.ts +379 -154
  22. package/dist/core/ask-origin.js +224 -45
  23. package/dist/core/ask-question.js +2 -1
  24. package/dist/core/ask-unresolvable-notice.d.ts +5 -2
  25. package/dist/core/ask-unresolvable-notice.js +4 -3
  26. package/dist/core/auto-mode-arming.d.ts +17 -4
  27. package/dist/core/auto-mode-arming.js +31 -6
  28. package/dist/core/auto-mode-defaults.d.ts +16 -7
  29. package/dist/core/auto-mode-defaults.js +2 -1
  30. package/dist/core/auto-mode-directive.d.ts +18 -30
  31. package/dist/core/auto-mode-directive.js +9 -18
  32. package/dist/core/auto-mode-prompt.d.ts +62 -1
  33. package/dist/core/auto-mode-prompt.js +42 -4
  34. package/dist/core/auto-mode-rebuild.d.ts +2 -1
  35. package/dist/core/auto-mode-rebuild.js +1 -0
  36. package/dist/core/auto-mode.d.ts +32 -3
  37. package/dist/core/auto-mode.js +80 -41
  38. package/dist/core/checkpoint-store.d.ts +54 -35
  39. package/dist/core/checkpoint-store.js +58 -3
  40. package/dist/core/child-clamp-seats.js +1 -0
  41. package/dist/core/classifier-unavailable-arm.d.ts +57 -0
  42. package/dist/core/classifier-unavailable-arm.js +17 -0
  43. package/dist/core/effective-path-target.d.ts +12 -0
  44. package/dist/core/effective-path-target.js +18 -7
  45. package/dist/core/engine-notice.d.ts +53 -2
  46. package/dist/core/fs-write-gate-policy.d.ts +5 -1
  47. package/dist/core/fs-write-gate-policy.js +4 -1
  48. package/dist/core/gate-fold.js +19 -3
  49. package/dist/core/gate-lanes.js +116 -50
  50. package/dist/core/gate-outcome.d.ts +40 -7
  51. package/dist/core/gate-outcome.js +26 -4
  52. package/dist/core/governance-codes.d.ts +18 -2
  53. package/dist/core/governance-codes.js +23 -1
  54. package/dist/core/hooks.d.ts +53 -29
  55. package/dist/core/hooks.js +1 -3
  56. package/dist/core/mcp.js +1 -1
  57. package/dist/core/message-utils.d.ts +2 -1
  58. package/dist/core/message-utils.js +2 -1
  59. package/dist/core/mode-policy.d.ts +185 -0
  60. package/dist/core/mode-policy.js +184 -0
  61. package/dist/core/oracle-isolation.d.ts +3 -3
  62. package/dist/core/permission-rule-model.d.ts +20 -3
  63. package/dist/core/permission-rule-model.js +40 -3
  64. package/dist/core/permission-rule-provider.d.ts +22 -11
  65. package/dist/core/permission-rule-provider.js +17 -7
  66. package/dist/core/permission-rules.d.ts +23 -1
  67. package/dist/core/permission-rules.js +56 -9
  68. package/dist/core/persisted-rule-arms.d.ts +14 -10
  69. package/dist/core/persisted-rule-arms.js +13 -6
  70. package/dist/core/present-plan-tool.d.ts +1 -1
  71. package/dist/core/present-plan-tool.js +3 -3
  72. package/dist/core/read-only-shell.d.ts +19 -19
  73. package/dist/core/read-only-shell.js +38 -12
  74. package/dist/core/retired-keys.d.ts +9 -3
  75. package/dist/core/retired-keys.js +5 -1
  76. package/dist/core/runner/active-skill-scope.js +5 -4
  77. package/dist/core/runner/assemble-result.js +2 -2
  78. package/dist/core/runner/classifier-ladder.d.ts +43 -1
  79. package/dist/core/runner/classifier-ladder.js +29 -0
  80. package/dist/core/runner/contracts.d.ts +62 -38
  81. package/dist/core/runner/denial-limit-arms.d.ts +35 -7
  82. package/dist/core/runner/denial-limit-arms.js +15 -9
  83. package/dist/core/runner/env-capability-census.js +1 -1
  84. package/dist/core/runner/file-history-request.d.ts +26 -0
  85. package/dist/core/runner/file-history-request.js +90 -0
  86. package/dist/core/runner/gate-exit.d.ts +30 -4
  87. package/dist/core/runner/gate-exit.js +28 -7
  88. package/dist/core/runner/permission-rule-lanes.d.ts +11 -6
  89. package/dist/core/runner/permission-rule-lanes.js +55 -48
  90. package/dist/core/runner/persisted-path-rows.d.ts +64 -0
  91. package/dist/core/runner/persisted-path-rows.js +89 -0
  92. package/dist/core/runner/prepare-artifact.d.ts +5 -1
  93. package/dist/core/runner/prepare-artifact.js +9 -3
  94. package/dist/core/runner/prepare-ask-lane.d.ts +6 -4
  95. package/dist/core/runner/prepare-ask-lane.js +3 -3
  96. package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
  97. package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
  98. package/dist/core/runner/prepare-file-history.d.ts +9 -3
  99. package/dist/core/runner/prepare-file-history.js +4 -90
  100. package/dist/core/runner/prepare-gate-stations.d.ts +37 -9
  101. package/dist/core/runner/prepare-gate-stations.js +46 -29
  102. package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
  103. package/dist/core/runner/prepare-hands-readface.js +64 -53
  104. package/dist/core/runner/prepare-inherited-gate.d.ts +30 -23
  105. package/dist/core/runner/prepare-inherited-gate.js +45 -25
  106. package/dist/core/runner/prepare-park-ask.d.ts +6 -4
  107. package/dist/core/runner/prepare-park-ask.js +7 -5
  108. package/dist/core/runner/prepare-policy-chain.d.ts +25 -4
  109. package/dist/core/runner/prepare-policy-chain.js +203 -180
  110. package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
  111. package/dist/core/runner/prepare-protocol-tools.js +13 -3
  112. package/dist/core/runner/prepare-run-refs.d.ts +6 -6
  113. package/dist/core/runner/prepare-run-refs.js +9 -4
  114. package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
  115. package/dist/core/runner/prepare-safety-scan.js +19 -3
  116. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
  117. package/dist/core/runner/prepare-suspend-saga.js +6 -6
  118. package/dist/core/runner/prepare-task.js +56 -24
  119. package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
  120. package/dist/core/runner/prepare-wiring-manifest.js +5 -3
  121. package/dist/core/runner/read-root-grant.d.ts +9 -0
  122. package/dist/core/runner/read-root-grant.js +12 -0
  123. package/dist/core/runner/retired-runner-deps.js +1 -0
  124. package/dist/core/runner/run-brain-sinks.js +3 -1
  125. package/dist/core/runner/run-identity-wiring.js +1 -1
  126. package/dist/core/runner/run-leg.js +29 -18
  127. package/dist/core/runner/run-notification-lane.d.ts +3 -0
  128. package/dist/core/runner/run-notification-lane.js +3 -1
  129. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
  130. package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
  131. package/dist/core/runner/run-turn-boundary.js +1 -1
  132. package/dist/core/runner/runtask.d.ts +1 -0
  133. package/dist/core/runner/runtask.js +8 -11
  134. package/dist/core/runner/session-mode-seats.d.ts +48 -0
  135. package/dist/core/runner/session-mode-seats.js +73 -0
  136. package/dist/core/runner/session-rule-policy.js +3 -2
  137. package/dist/core/runner/stream-mode-verb.d.ts +27 -0
  138. package/dist/core/runner/stream-mode-verb.js +19 -0
  139. package/dist/core/runner/turn-attachments.d.ts +3 -3
  140. package/dist/core/runner/turn-attachments.js +1 -1
  141. package/dist/core/runner/write-protection-station.d.ts +7 -1
  142. package/dist/core/runner/write-protection-station.js +6 -3
  143. package/dist/core/runner-deps.d.ts +31 -34
  144. package/dist/core/runtime-caps.d.ts +12 -3
  145. package/dist/core/safety-axis-vocab.d.ts +2 -2
  146. package/dist/core/safety-axis-vocab.js +1 -1
  147. package/dist/core/secret-env.d.ts +30 -58
  148. package/dist/core/secret-env.js +149 -586
  149. package/dist/core/sensitive-path-policy.d.ts +4 -2
  150. package/dist/core/sensitive-path-policy.js +41 -31
  151. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
  152. package/dist/core/task-event.d.ts +1 -1
  153. package/dist/core/task-result.d.ts +1 -1
  154. package/dist/core/task-spec.d.ts +41 -89
  155. package/dist/core/task-stream.d.ts +16 -0
  156. package/dist/core/terminal-cause.js +1 -0
  157. package/dist/core/tighten-task-spec.d.ts +2 -2
  158. package/dist/core/tighten-task-spec.js +12 -14
  159. package/dist/core/tool-catalog-entries.js +4 -3
  160. package/dist/core/tool-policy.d.ts +84 -30
  161. package/dist/core/tool-policy.js +76 -16
  162. package/dist/core/tool-registry.d.ts +38 -0
  163. package/dist/core/tool-registry.js +25 -1
  164. package/dist/core/tool-roster.d.ts +17 -2
  165. package/dist/core/tool-roster.js +11 -1
  166. package/dist/core/tool-spec.d.ts +8 -9
  167. package/dist/core/trace.d.ts +18 -6
  168. package/dist/core/types.d.ts +2 -1
  169. package/dist/core/wiring-manifest.d.ts +31 -5
  170. package/dist/core/wiring-manifest.js +28 -1
  171. package/dist/core/write-protect.d.ts +11 -1
  172. package/dist/core/write-protect.js +26 -15
  173. package/dist/engine/execution-env/node-execution-env.d.ts +6 -1
  174. package/dist/engine/execution-env/node-execution-env.js +16 -3
  175. package/dist/engine/harness/agent-harness.js +2 -1
  176. package/dist/engine/harness/types.d.ts +6 -7
  177. package/dist/engine/llm/types.d.ts +2 -4
  178. package/dist/engine/loop/types.d.ts +7 -0
  179. package/dist/engine/lsp/node-lsp-manager.js +1 -1
  180. package/dist/index.d.ts +18 -8
  181. package/dist/index.js +15 -6
  182. package/dist/orchestration/workflow-governance.js +1 -1
  183. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  184. package/dist/orchestration/workflow-size-guideline.js +2 -2
  185. package/dist/stores/file/index.d.ts +5 -3
  186. package/dist/stores/file/session-store.d.ts +8 -2
  187. package/dist/stores/file/session-store.js +5 -5
  188. package/dist/tools/artifact/artifact-tool.d.ts +17 -0
  189. package/dist/tools/artifact/artifact-tool.js +16 -1
  190. package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
  191. package/dist/tools/fs/bash-readonly-classifier.js +88 -0
  192. package/dist/tools/fs/containment-face.d.ts +9 -3
  193. package/dist/tools/fs/containment-face.js +2 -0
  194. package/dist/tools/fs/file-read-check.d.ts +50 -0
  195. package/dist/tools/fs/file-read-check.js +79 -0
  196. package/dist/tools/fs/fs-bash.d.ts +3 -3
  197. package/dist/tools/fs/fs-read.d.ts +1 -0
  198. package/dist/tools/fs/fs-search-tools.d.ts +2 -2
  199. package/dist/tools/fs/fs-search-tools.js +4 -4
  200. package/dist/tools/fs/fs-shared.js +20 -6
  201. package/dist/tools/fs/fs-write.d.ts +4 -3
  202. package/dist/tools/fs/fs-write.js +9 -9
  203. package/dist/tools/fs/index.d.ts +39 -3
  204. package/dist/tools/fs/index.js +38 -15
  205. package/dist/tools/fs/read-deny-home.d.ts +93 -0
  206. package/dist/tools/fs/read-deny-home.js +225 -0
  207. package/dist/tools/fs/read-deny.d.ts +176 -34
  208. package/dist/tools/fs/read-deny.js +268 -61
  209. package/dist/tools/fs/safety.d.ts +6 -1
  210. package/dist/tools/fs/safety.js +18 -5
  211. package/dist/tools/fs/search.d.ts +3 -0
  212. package/dist/tools/fs/search.js +47 -9
  213. package/dist/tools/fs/shell-read-check.d.ts +52 -13
  214. package/dist/tools/fs/shell-read-check.js +199 -20
  215. package/dist/tools/fs/shell-read-establish.d.ts +7 -3
  216. package/dist/tools/fs/shell-read-establish.js +134 -21
  217. package/dist/tools/fs/shell-read-identity.d.ts +7 -0
  218. package/dist/tools/fs/shell-read-identity.js +21 -0
  219. package/dist/tools/fs/shell-read-judge.d.ts +92 -32
  220. package/dist/tools/fs/shell-read-judge.js +43 -18
  221. package/dist/tools/fs/shell-read-plan.d.ts +4 -0
  222. package/dist/tools/fs/shell-read-plan.js +26 -4
  223. package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
  224. package/dist/tools/fs/shell-read-write-targets.js +173 -0
  225. package/package.json +1 -1
  226. package/test/export-surface.snapshot.json +117 -11
  227. package/wire-changes/core@7.34.0.json +2621 -0
  228. package/wire-changes/core@7.35.0.json +584 -0
@@ -1,4 +1,6 @@
1
- import type { ApproverPosture } from "./task-spec.js";
1
+ import type { AskEffective } from "./wiring-manifest.js";
2
+ /** The permission-mode words the composition reads (the engine's closed set, as the wire spells it). */
3
+ import type { PermissionMode } from "./ask-origin.js";
2
4
  /** The auto-mode directive body (title + the two paragraphs). Rendered when the run's auto-mode
3
5
  * INTENT is resolved true — the seat, the chain it inherited, or the bit a suspend recorded. */
4
6
  export declare const AUTO_MODE_DIRECTIVE: string;
@@ -8,39 +10,25 @@ export declare const BASH_FIRST_DIRECTIVE: string;
8
10
  /** The bypass-posture directive body: the reference implementation's heading over the shell-first
9
11
  * paragraph, the exact pair its bypass arm renders. */
10
12
  export declare const BYPASS_POSTURE_DIRECTIVE: string;
11
- /** The posture words this engine accepts, as literals (the door refuses anything else loudly). The
12
- * TYPE is declared beside the seat it belongs to ({@link ApproverPosture}, task-spec.ts); this array
13
- * is its runtime twin, and the assignment below is the compile-time tie between them. */
14
- export declare const APPROVER_POSTURES: readonly ApproverPosture[];
15
- /** Value-shape check for the word — used by the spec door below and by the chain/checkpoint readers. */
16
- export declare function isApproverPosture(value: unknown): value is ApproverPosture;
17
- /** The SEAT's door screen, declared beside the vocabulary it screens against. The renderer keys on the
18
- * exact word, so any other present value would render zero bytes and be indistinguishable from "no
19
- * posture declared"; it is refused instead. The value that passed is the value RETURNED — the render
20
- * reads that snapshot, never the caller's spec object again. */
21
- export declare function refuseInvalidApproverPosture(spec: {
22
- approverPosture?: ApproverPosture;
23
- }): ApproverPosture | undefined;
24
- /** The resolved posture facts of one leg: both already folded across seat, chain and checkpoint. */
13
+ /** The mode facts of one leg, both the ENGINE's own resolution — never a requested value, never spec shape. */
25
14
  export interface ApprovalPostureFacts {
26
- /** The run's resolved auto-mode intent (seat ∨ inherited chain ∨ checkpoint seed). */
27
- autoMode: boolean;
28
- /** The declared approver posture, absent when the host declared none. */
29
- approverPosture?: ApproverPosture;
15
+ /** The leg's EFFECTIVE permission mode (a requested mode the deployment refused has already folded back). */
16
+ mode: PermissionMode;
17
+ /** The wiring manifest's ask derivation — `auto_allow` means no person answers an ask on this leg. */
18
+ askEffective: AskEffective;
30
19
  }
31
20
  /**
32
- * The ONE composition, and the ONE invariant: **the segment the run's posture calls for is stated
33
- * exactly once in the active model context**. `alreadyStated` is the transcript's ENGINE REGIONS (read
34
- * by metadata — a caller's own bytes are never in one, so a spliced copy cannot suppress the engine's);
35
- * a segment already standing there is not restated, and a segment that has left the context — compacted
36
- * away, or never delivered — is. Returns `undefined` when there is nothing left to say, which is both
37
- * "no posture declared" (zero bytes, byte-identical to the pre-seat shape) and "already said".
21
+ * The ONE composition, and the ONE invariant: **the segment the leg's mode calls for is stated exactly
22
+ * once in the active model context**. `statedBodies` is the transcript's ENGINE REGIONS (read by metadata
23
+ * — a caller's own bytes are never in one, so a spliced copy cannot suppress the engine's); a segment
24
+ * already standing there is not restated, and a segment that has left the context — compacted away, or
25
+ * never delivered — is. Returns `undefined` when there is nothing left to say, which is both "no segment
26
+ * for this mode" (zero bytes) and "already said".
38
27
  *
39
- * Segment-wise rather than body-wise on purpose: a leg whose posture CHANGES — it gains the bypass fact,
40
- * so the segment it owes changes — must state the new segment without restating one already standing,
41
- * and must be able to read a body an earlier leg minted under a composition that joined two. The caller
42
- * wraps the result in the engine's marked reminder shell; this module never wraps, so the mark stays
43
- * owned by its own mint home.
28
+ * Segment-wise rather than body-wise on purpose: a leg whose mode CHANGES — it now runs under bypass, so the
29
+ * segment it owes changes — must state the new segment without restating one already standing, and must be
30
+ * able to read a body an earlier leg minted under a composition that joined two. The caller wraps the result
31
+ * in the engine's marked reminder shell; this module never wraps, so the mark stays owned by its own mint home.
44
32
  */
45
33
  export declare function composeApprovalPostureDirective(facts: ApprovalPostureFacts, statedBodies?: readonly string[]): string | undefined;
46
34
  /** SHA-256 of a directive body — the pins' lock port, so the lock is computed the one way. */
@@ -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;