@sema-agent/core 7.33.8 → 7.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CHANGELOG.md +339 -0
  2. package/dist/agents/cross-session-judge.d.ts +16 -7
  3. package/dist/agents/cross-session-judge.js +18 -15
  4. package/dist/agents/peer-session-drain.d.ts +4 -0
  5. package/dist/agents/peer-session-drain.js +5 -2
  6. package/dist/agents/repair-loop.d.ts +1 -1
  7. package/dist/agents/subagent.d.ts +3 -0
  8. package/dist/agents/subagent.js +12 -3
  9. package/dist/brain/anthropic.js +1 -0
  10. package/dist/brain/open-responses.js +1 -0
  11. package/dist/brain/openai.js +1 -0
  12. package/dist/brain/stream-engine.d.ts +3 -0
  13. package/dist/brain/stream-engine.js +2 -1
  14. package/dist/core/a2a.d.ts +3 -1
  15. package/dist/core/a2a.js +1 -1
  16. package/dist/core/ask-origin.d.ts +351 -154
  17. package/dist/core/ask-origin.js +223 -45
  18. package/dist/core/ask-unresolvable-notice.d.ts +5 -2
  19. package/dist/core/ask-unresolvable-notice.js +4 -3
  20. package/dist/core/auto-mode-arming.d.ts +17 -4
  21. package/dist/core/auto-mode-arming.js +31 -6
  22. package/dist/core/auto-mode-defaults.d.ts +16 -7
  23. package/dist/core/auto-mode-defaults.js +2 -1
  24. package/dist/core/auto-mode-directive.d.ts +18 -30
  25. package/dist/core/auto-mode-directive.js +9 -18
  26. package/dist/core/auto-mode-prompt.d.ts +62 -1
  27. package/dist/core/auto-mode-prompt.js +42 -4
  28. package/dist/core/auto-mode-rebuild.d.ts +2 -1
  29. package/dist/core/auto-mode-rebuild.js +1 -0
  30. package/dist/core/auto-mode.d.ts +32 -3
  31. package/dist/core/auto-mode.js +80 -41
  32. package/dist/core/checkpoint-store.d.ts +50 -35
  33. package/dist/core/checkpoint-store.js +58 -3
  34. package/dist/core/child-clamp-seats.js +1 -0
  35. package/dist/core/classifier-unavailable-arm.d.ts +57 -0
  36. package/dist/core/classifier-unavailable-arm.js +17 -0
  37. package/dist/core/effective-path-target.d.ts +12 -0
  38. package/dist/core/effective-path-target.js +18 -7
  39. package/dist/core/engine-notice.d.ts +53 -2
  40. package/dist/core/fs-write-gate-policy.d.ts +5 -1
  41. package/dist/core/fs-write-gate-policy.js +4 -1
  42. package/dist/core/gate-fold.js +17 -1
  43. package/dist/core/gate-lanes.js +111 -46
  44. package/dist/core/gate-outcome.d.ts +40 -7
  45. package/dist/core/gate-outcome.js +26 -4
  46. package/dist/core/governance-codes.d.ts +1 -1
  47. package/dist/core/governance-codes.js +12 -0
  48. package/dist/core/hooks.d.ts +50 -29
  49. package/dist/core/hooks.js +1 -3
  50. package/dist/core/mcp.js +1 -1
  51. package/dist/core/mode-policy.d.ts +182 -0
  52. package/dist/core/mode-policy.js +179 -0
  53. package/dist/core/oracle-isolation.d.ts +3 -3
  54. package/dist/core/permission-rule-model.d.ts +16 -3
  55. package/dist/core/permission-rule-model.js +36 -3
  56. package/dist/core/permission-rule-provider.d.ts +22 -11
  57. package/dist/core/permission-rule-provider.js +17 -7
  58. package/dist/core/permission-rules.d.ts +23 -1
  59. package/dist/core/permission-rules.js +35 -8
  60. package/dist/core/persisted-rule-arms.d.ts +10 -9
  61. package/dist/core/persisted-rule-arms.js +9 -6
  62. package/dist/core/present-plan-tool.d.ts +1 -1
  63. package/dist/core/present-plan-tool.js +3 -3
  64. package/dist/core/read-only-shell.d.ts +19 -19
  65. package/dist/core/read-only-shell.js +38 -12
  66. package/dist/core/retired-keys.d.ts +9 -3
  67. package/dist/core/retired-keys.js +5 -1
  68. package/dist/core/runner/classifier-ladder.d.ts +43 -1
  69. package/dist/core/runner/classifier-ladder.js +29 -0
  70. package/dist/core/runner/contracts.d.ts +37 -35
  71. package/dist/core/runner/denial-limit-arms.d.ts +30 -7
  72. package/dist/core/runner/denial-limit-arms.js +15 -9
  73. package/dist/core/runner/env-capability-census.js +1 -1
  74. package/dist/core/runner/gate-exit.d.ts +30 -4
  75. package/dist/core/runner/gate-exit.js +28 -7
  76. package/dist/core/runner/permission-rule-lanes.d.ts +9 -5
  77. package/dist/core/runner/permission-rule-lanes.js +44 -40
  78. package/dist/core/runner/persisted-path-rows.d.ts +64 -0
  79. package/dist/core/runner/persisted-path-rows.js +89 -0
  80. package/dist/core/runner/prepare-artifact.d.ts +5 -1
  81. package/dist/core/runner/prepare-artifact.js +9 -3
  82. package/dist/core/runner/prepare-ask-lane.d.ts +5 -3
  83. package/dist/core/runner/prepare-ask-lane.js +1 -1
  84. package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
  85. package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
  86. package/dist/core/runner/prepare-gate-stations.d.ts +33 -6
  87. package/dist/core/runner/prepare-gate-stations.js +32 -19
  88. package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
  89. package/dist/core/runner/prepare-hands-readface.js +64 -53
  90. package/dist/core/runner/prepare-inherited-gate.d.ts +22 -22
  91. package/dist/core/runner/prepare-inherited-gate.js +37 -23
  92. package/dist/core/runner/prepare-park-ask.d.ts +2 -2
  93. package/dist/core/runner/prepare-park-ask.js +2 -1
  94. package/dist/core/runner/prepare-policy-chain.d.ts +22 -2
  95. package/dist/core/runner/prepare-policy-chain.js +39 -12
  96. package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
  97. package/dist/core/runner/prepare-protocol-tools.js +13 -3
  98. package/dist/core/runner/prepare-run-refs.d.ts +6 -6
  99. package/dist/core/runner/prepare-run-refs.js +9 -4
  100. package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
  101. package/dist/core/runner/prepare-safety-scan.js +19 -3
  102. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
  103. package/dist/core/runner/prepare-suspend-saga.js +6 -6
  104. package/dist/core/runner/prepare-task.js +51 -22
  105. package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
  106. package/dist/core/runner/prepare-wiring-manifest.js +5 -3
  107. package/dist/core/runner/read-root-grant.d.ts +9 -0
  108. package/dist/core/runner/read-root-grant.js +12 -0
  109. package/dist/core/runner/retired-runner-deps.js +1 -0
  110. package/dist/core/runner/run-identity-wiring.js +1 -1
  111. package/dist/core/runner/run-leg.js +24 -15
  112. package/dist/core/runner/run-notification-lane.js +1 -0
  113. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
  114. package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
  115. package/dist/core/runner/run-turn-boundary.js +1 -1
  116. package/dist/core/runner/runtask.js +3 -0
  117. package/dist/core/runner/stream-mode-verb.d.ts +27 -0
  118. package/dist/core/runner/stream-mode-verb.js +19 -0
  119. package/dist/core/runner/turn-attachments.d.ts +3 -3
  120. package/dist/core/runner/turn-attachments.js +1 -1
  121. package/dist/core/runner/write-protection-station.d.ts +7 -1
  122. package/dist/core/runner/write-protection-station.js +6 -3
  123. package/dist/core/runner-deps.d.ts +31 -34
  124. package/dist/core/runtime-caps.d.ts +12 -3
  125. package/dist/core/safety-axis-vocab.d.ts +2 -2
  126. package/dist/core/safety-axis-vocab.js +1 -1
  127. package/dist/core/sensitive-path-policy.d.ts +4 -2
  128. package/dist/core/sensitive-path-policy.js +39 -30
  129. package/dist/core/task-event.d.ts +1 -1
  130. package/dist/core/task-result.d.ts +1 -1
  131. package/dist/core/task-spec.d.ts +41 -89
  132. package/dist/core/task-stream.d.ts +16 -0
  133. package/dist/core/terminal-cause.js +1 -0
  134. package/dist/core/tighten-task-spec.d.ts +2 -2
  135. package/dist/core/tighten-task-spec.js +12 -14
  136. package/dist/core/tool-catalog-entries.js +4 -3
  137. package/dist/core/tool-policy.d.ts +54 -25
  138. package/dist/core/tool-policy.js +43 -2
  139. package/dist/core/tool-registry.d.ts +2 -0
  140. package/dist/core/tool-registry.js +3 -0
  141. package/dist/core/tool-spec.d.ts +8 -9
  142. package/dist/core/trace.d.ts +7 -6
  143. package/dist/core/types.d.ts +2 -1
  144. package/dist/core/wiring-manifest.d.ts +31 -5
  145. package/dist/core/wiring-manifest.js +28 -1
  146. package/dist/core/write-protect.d.ts +11 -1
  147. package/dist/core/write-protect.js +26 -15
  148. package/dist/engine/execution-env/node-execution-env.d.ts +3 -0
  149. package/dist/engine/execution-env/node-execution-env.js +11 -1
  150. package/dist/engine/harness/types.d.ts +6 -7
  151. package/dist/engine/llm/types.d.ts +2 -4
  152. package/dist/engine/loop/types.d.ts +7 -0
  153. package/dist/index.d.ts +15 -5
  154. package/dist/index.js +12 -3
  155. package/dist/orchestration/workflow-governance.js +1 -1
  156. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  157. package/dist/orchestration/workflow-size-guideline.js +2 -2
  158. package/dist/tools/artifact/artifact-tool.d.ts +17 -0
  159. package/dist/tools/artifact/artifact-tool.js +16 -1
  160. package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
  161. package/dist/tools/fs/bash-readonly-classifier.js +88 -0
  162. package/dist/tools/fs/containment-face.d.ts +9 -3
  163. package/dist/tools/fs/containment-face.js +2 -0
  164. package/dist/tools/fs/file-read-check.d.ts +49 -0
  165. package/dist/tools/fs/file-read-check.js +71 -0
  166. package/dist/tools/fs/fs-bash.d.ts +3 -3
  167. package/dist/tools/fs/fs-read.d.ts +1 -0
  168. package/dist/tools/fs/fs-search-tools.d.ts +2 -2
  169. package/dist/tools/fs/fs-search-tools.js +4 -4
  170. package/dist/tools/fs/fs-shared.js +20 -6
  171. package/dist/tools/fs/fs-write.d.ts +4 -3
  172. package/dist/tools/fs/fs-write.js +9 -9
  173. package/dist/tools/fs/index.d.ts +39 -3
  174. package/dist/tools/fs/index.js +38 -15
  175. package/dist/tools/fs/read-deny-home.d.ts +93 -0
  176. package/dist/tools/fs/read-deny-home.js +225 -0
  177. package/dist/tools/fs/read-deny.d.ts +176 -34
  178. package/dist/tools/fs/read-deny.js +268 -61
  179. package/dist/tools/fs/safety.d.ts +6 -1
  180. package/dist/tools/fs/safety.js +18 -5
  181. package/dist/tools/fs/search.d.ts +3 -0
  182. package/dist/tools/fs/search.js +47 -9
  183. package/dist/tools/fs/shell-read-check.d.ts +42 -11
  184. package/dist/tools/fs/shell-read-check.js +188 -13
  185. package/dist/tools/fs/shell-read-establish.d.ts +7 -3
  186. package/dist/tools/fs/shell-read-establish.js +134 -21
  187. package/dist/tools/fs/shell-read-identity.d.ts +7 -0
  188. package/dist/tools/fs/shell-read-identity.js +21 -0
  189. package/dist/tools/fs/shell-read-judge.d.ts +92 -32
  190. package/dist/tools/fs/shell-read-judge.js +43 -18
  191. package/dist/tools/fs/shell-read-plan.d.ts +4 -0
  192. package/dist/tools/fs/shell-read-plan.js +26 -4
  193. package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
  194. package/dist/tools/fs/shell-read-write-targets.js +173 -0
  195. package/package.json +1 -1
  196. package/test/export-surface.snapshot.json +103 -11
  197. package/wire-changes/core@7.34.0.json +2621 -0
@@ -5,7 +5,10 @@ import { isAbsolutePathForm } from "../../tools/fs/safety.js";
5
5
  import { effectivePathTargetOf } from "../effective-path-target.js";
6
6
  import { declaredPathTargetOf, protectivePathTargetOf } from "../tool-registry.js";
7
7
  import { orgRuleVerdictFor } from "../permission-rule-org.js";
8
+ import { ownerStoreOf } from "../permission-rule-provider.js";
8
9
  import { thrownText } from "../thrown-value.js";
10
+ import { gateWaitFailureOf, withGateTimeout } from "../gate-bounded-await.js";
11
+ import { ORG_ADJUDICATION_TIMEOUT_MS } from "../permission-rule-org.js";
9
12
  export const PERSISTED_RULE_TOOL = "Bash";
10
13
  const HANDOFF_BOUND = 256;
11
14
  export function inheritedAskRuleEvidence(provider) {
@@ -59,10 +62,7 @@ export function createPermissionRuleLanes(cfg) {
59
62
  if (provider === undefined)
60
63
  return { personal: undefined, org: undefined };
61
64
  const anonymous = cfg.principal === undefined || cfg.principal === "";
62
- const store = anonymous && cfg.localOwnerDeclared
63
- ?
64
- provider.forLocalOwner()
65
- : provider.forPrincipal(anonymous ? undefined : cfg.principal);
65
+ const store = ownerStoreOf(provider, cfg.principal, cfg.localOwnerDeclared);
66
66
  const handed = new Map();
67
67
  const read = () => store.effective({ sessionId: cfg.sessionId }).then((view) => {
68
68
  for (const line of view.disclosures)
@@ -112,43 +112,47 @@ export function createPermissionRuleLanes(cfg) {
112
112
  },
113
113
  }
114
114
  : undefined;
115
- const personal = provider.partitions.durable
116
- ? {
117
- adjudicate: async (req) => {
118
- if (anonymous && !cfg.localOwnerDeclared)
119
- return undefined;
120
- const grammar = ruleToolGrammarOf(req.toolName);
121
- if (grammar === undefined)
122
- return undefined;
123
- let view;
124
- try {
125
- view = await claim(req.toolCallId);
126
- }
127
- catch (err) {
128
- cfg.onDisclosure(thrownText(err));
129
- return { unreadable: true };
130
- }
131
- if (view.unreadable !== undefined) {
132
- cfg.onDisclosure(view.unreadable);
133
- return { unreadable: true };
134
- }
135
- const table = view.rules;
136
- const liveCwd = cfg.liveCwd();
137
- if (grammar === "path") {
138
- return pathRuleLaneAnswer(table, req, { root: cfg.root, sessionId: cfg.sessionId, liveCwd, home: cfg.home });
139
- }
140
- const command = req.args?.command;
141
- if (typeof command !== "string")
142
- return undefined;
143
- const execCwd = liveCwd !== undefined ? { execCwd: liveCwd } : {};
144
- const verdict = adjudicatePersistedRules(table, { tool: req.toolName, command, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
145
- if (verdict !== undefined)
146
- return persistedRuleAnswerOf(verdict);
147
- const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
148
- return coverage !== undefined ? { segmentCoverage: coverage } : undefined;
149
- },
115
+ const personalAdjudicate = async (req) => {
116
+ if (anonymous && !cfg.localOwnerDeclared)
117
+ return undefined;
118
+ const grammar = ruleToolGrammarOf(req.toolName);
119
+ if (grammar === undefined)
120
+ return undefined;
121
+ let view;
122
+ try {
123
+ view = await withGateTimeout(claim(req.toolCallId), ORG_ADJUDICATION_TIMEOUT_MS, cfg.signal, "persistedRules");
150
124
  }
151
- : undefined;
125
+ catch (err) {
126
+ return gateWaitFailureOf(err) === "aborted" ? { unreadable: true } : skipped(thrownText(err));
127
+ }
128
+ if (view.unreadable !== undefined)
129
+ skipped(view.unreadable, view.unreadableSources);
130
+ try {
131
+ const answer = judgeAgainst(view.rules, req, grammar);
132
+ return view.unreadable !== undefined && (answer === undefined || "segmentCoverage" in answer) ? { unreadable: true } : answer;
133
+ }
134
+ catch (err) {
135
+ const why = `judging this call against the rows threw: ${thrownText(err)}`;
136
+ cfg.onDisclosure(`the persisted rule lane could not judge a ${req.toolName} call against the rows it read — ${why}`);
137
+ return { unreadable: true, reason: why };
138
+ }
139
+ };
140
+ const judgeAgainst = (table, req, grammar) => {
141
+ const liveCwd = cfg.liveCwd();
142
+ if (grammar === "path")
143
+ return pathRuleLaneAnswer(table, req, { root: cfg.root, sessionId: cfg.sessionId, liveCwd, home: cfg.home });
144
+ const command = req.args?.command;
145
+ if (typeof command !== "string")
146
+ return undefined;
147
+ const execCwd = liveCwd !== undefined ? { execCwd: liveCwd } : {};
148
+ const verdict = adjudicatePersistedRules(table, { tool: req.toolName, command, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
149
+ if (verdict !== undefined)
150
+ return persistedRuleAnswerOf(verdict);
151
+ const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
152
+ return coverage !== undefined ? { segmentCoverage: coverage } : undefined;
153
+ };
154
+ const skipped = (why, sources) => (cfg.onDisclosure(why), cfg.onStoreUnreadable("call", sources), { unreadable: true });
155
+ const personal = provider.partitions.durable ? { adjudicate: personalAdjudicate } : undefined;
152
156
  return { personal, org };
153
157
  }
154
158
  export function createRuleOffersOf(cfg) {
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The person's persisted PATH rows as a leg's read face takes them at prepare (the gate's rule lanes read the same store
3
+ * per call).
4
+ */
5
+ import { type EffectivePermissionRules, type PermissionRuleStoreProvider } from "../permission-rule-provider.js";
6
+ import type { PathRuleDenyRow } from "../../tools/fs/read-deny.js";
7
+ import type { RunnerDeps } from "../runner-deps.js";
8
+ /** The person's persisted PATH rows a leg's read face takes at prepare ({@link persistedPathRowsOf}). */
9
+ export interface PersistedPathRows {
10
+ /** Every live `Read(//dir/**)` allow row's directory (lexical, absolute): a standing yes to reads under it. */
11
+ readonly readDirectories: readonly string[];
12
+ /** Every live face-keyed path DENY row (`Read(...)`, `Edit(...)`), as a deny-table row resolved at the task's bases. */
13
+ readonly denyRows: readonly PathRuleDenyRow[];
14
+ }
15
+ /**
16
+ * The person's persisted PATH rows, read ONCE at prepare from the same store query the gate's lanes read per call: a
17
+ * `Read(//dir/**)` allow row is a READ DIRECTORY of the run (the Bash read station, Read / Grep / Glob — one set), and a
18
+ * `Read(...)` / `Edit(...)` deny row joins the run's read / write deny tables (every read face, the shell's file commands
19
+ * and traversals). One read, two directions, so a person's allow row never reaches further than their deny rows: a
20
+ * directory it admits still refuses the paths a deny row names. A row the store gains mid-run applies from the next
21
+ * prepare; the in-flight grant (`readRootGrant`) is the mid-run door. The store not armed, or the anonymous floor ⇒ no
22
+ * rows. A read that failed (or did not answer in time) ⇒ no rows, and a partition the read could not read ⇒ none of ITS
23
+ * rows (the partition that was read contributes its own): what could not be read is skipped for this run's tables — its
24
+ * directories widen nothing and its deny rows judge nothing here — disclosed on the trace and announced to the person
25
+ * (`onUnread`); the next prepare reads again, as it would for a row the store gained mid-run. A deny row whose
26
+ * pattern needs a base the task cannot supply (`~/` with no declared home) is left to the per-call lane and disclosed.
27
+ */
28
+ export declare function persistedPathRowsOf(cfg: {
29
+ provider: PermissionRuleStoreProvider | undefined;
30
+ localOwnerDeclared: boolean;
31
+ principal: string | undefined;
32
+ sessionId: string;
33
+ root: string;
34
+ home: string | undefined;
35
+ onDisclosure: (message: string) => void;
36
+ /** The person is told the store (or the named sources of it) was skipped ({@link ruleStoreUnreadableNotice}). */
37
+ onUnread: (at: "prepare", sources?: EffectivePermissionRules["unreadableSources"]) => void;
38
+ /** The run's cut: a read that has not answered by then (or by the gate's await bound) reads as unreadable. */
39
+ signal?: AbortSignal;
40
+ }): Promise<PersistedPathRows>;
41
+ /**
42
+ * The run's ONE `permission.rule_store_unavailable` notice (audience user): the person's rules — the whole store, or the
43
+ * sources of it whose partition could not be read — were skipped; minted by whichever read found it first (the prepare
44
+ * read of the path rows, or a call's own read on the rule lane), once per run however many reads fail after it (the
45
+ * prepare sentence says a later call may be judged without them too). The trace keeps every read's line.
46
+ */
47
+ export declare function ruleStoreUnreadableNotice(onNotice: RunnerDeps["onNotice"], ids: {
48
+ readonly sessionId: string;
49
+ readonly runId: string;
50
+ }): (at: "prepare" | "call", sources?: EffectivePermissionRules["unreadableSources"]) => void;
51
+ /** {@link persistedPathRowsOf} for one task's prepare: only where a read face mounts (hands), disclosed on the run's trace,
52
+ * cut by the run's own signals. `undefined` = nothing to read, returned synchronously: a run with no person's store (or
53
+ * no hands) gains no suspension point on its prepare. */
54
+ export declare function persistedPathRowsOfTask(t: {
55
+ deps: Pick<RunnerDeps, "permissionRuleStore" | "localOwnerRules" | "tracer">;
56
+ principal: string | undefined;
57
+ sessionId: string;
58
+ root: string;
59
+ home: string | undefined;
60
+ taskId: string;
61
+ handsEnabled: boolean;
62
+ signals: readonly AbortSignal[];
63
+ onUnread: (at: "prepare", sources?: EffectivePermissionRules["unreadableSources"]) => void;
64
+ }): Promise<PersistedPathRows> | undefined;
@@ -0,0 +1,89 @@
1
+ import { ownerStoreOf } from "../permission-rule-provider.js";
2
+ import { READ_RULE_TOOL, eligiblePersisted, isUsablePathBase, ruleBasesNeeded } from "../permission-rule-model.js";
3
+ import { isAbsolutePathForm, pathFamilyOf } from "../../tools/fs/safety.js";
4
+ import { PATH_DENY_RULE_FACES, pathRuleDenyRowOf } from "../permission-rules.js";
5
+ import { inlineUntrusted } from "../untrusted-text.js";
6
+ import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, gateWaitFailureOf, withGateTimeout } from "../gate-bounded-await.js";
7
+ import { thrownText } from "../thrown-value.js";
8
+ import { emitTrace } from "../trace.js";
9
+ import { deliverEngineNotice } from "../engine-notice.js";
10
+ export async function persistedPathRowsOf(cfg) {
11
+ const none = { readDirectories: [], denyRows: [] };
12
+ const provider = cfg.provider;
13
+ if (provider === undefined || !readsPersistedPathRows(provider, cfg.principal, cfg.localOwnerDeclared))
14
+ return none;
15
+ let view;
16
+ try {
17
+ view = await withGateTimeout(ownerStoreOf(provider, cfg.principal, cfg.localOwnerDeclared).effective({ sessionId: cfg.sessionId }), DEFAULT_GATE_AWAIT_TIMEOUT_MS, cfg.signal, "persistedPathRows");
18
+ }
19
+ catch (err) {
20
+ return gateWaitFailureOf(err) === "aborted" ? none : skipped(cfg, thrownText(err));
21
+ }
22
+ if (view.unreadable !== undefined)
23
+ skipped(cfg, view.unreadable, view.unreadableSources);
24
+ const call = { tool: READ_RULE_TOOL, cwd: cfg.root, sessionId: cfg.sessionId };
25
+ const bases = { root: cfg.root, ...(cfg.home !== undefined && isAbsolutePathForm(cfg.home) ? { home: cfg.home } : {}) };
26
+ const family = pathFamilyOf(bases);
27
+ const readDirectories = [];
28
+ const denyRows = [];
29
+ for (const rule of view.rules) {
30
+ if (rule.status !== "live")
31
+ continue;
32
+ if (rule.behavior === "allow" && rule.tool === READ_RULE_TOOL && rule.match === "subpath" && eligiblePersisted(rule, call, "allow")) {
33
+ if (!readDirectories.includes(rule.command))
34
+ readDirectories.push(rule.command);
35
+ continue;
36
+ }
37
+ const faces = PATH_DENY_RULE_FACES[rule.tool];
38
+ if (rule.behavior !== "deny" || faces === undefined || (rule.match !== "path" && rule.match !== "subpath") || !eligiblePersisted(rule, { ...call, tool: rule.tool }, "deny"))
39
+ continue;
40
+ const missing = ruleBasesNeeded(rule).filter((b) => !isUsablePathBase(b === "cwd" ? bases.root : bases[b], family));
41
+ if (missing.length > 0) {
42
+ cfg.onDisclosure(`the persisted deny rule ${inlineUntrusted(rule.rule, 200)} needs a ${missing.join(" / ")} base this task does not declare — the shell's file commands and the search tools do not judge it on this leg`);
43
+ continue;
44
+ }
45
+ denyRows.push(pathRuleDenyRowOf({ parsed: rule, ruleText: rule.rule, faces }, bases));
46
+ }
47
+ return { readDirectories, denyRows };
48
+ }
49
+ function skipped(cfg, why, sources) {
50
+ cfg.onDisclosure(`the persisted path rules could not be read at prepare, so this run's read directories and read / write deny tables take none of ${sources !== undefined ? `the ${sourcesWord(sources)} rows` : "their rows"} (the next prepare reads again): ${why}`);
51
+ cfg.onUnread("prepare", sources);
52
+ return { readDirectories: [], denyRows: [] };
53
+ }
54
+ function sourcesWord(sources) {
55
+ return sources.length <= 1 ? sources.join("") : `${sources.slice(0, -1).join(", ")} and ${sources[sources.length - 1]}`;
56
+ }
57
+ const SKIPPED_STORE_SENTENCE = {
58
+ prepare: (which) => `${which} could not be read when this run started, so this run's file and shell checks skip their path rows (Read / Edit deny rows and Read directory rows) — a deny row among them protects nothing on this run. Each call's own rule check reads the rules again (a call whose own read fails too is judged without them), and the next run reads the path rows again.`,
59
+ call: (which) => `${which} could not be read for a call on this run, so that call's own rule check skipped them: none of their allow, ask or deny rows took part in it, and a deny row among them protected nothing there (path rows the run took when it started still apply to its file and shell checks). Every later call reads them again, and they apply again as soon as a read succeeds.`,
60
+ };
61
+ export function ruleStoreUnreadableNotice(onNotice, ids) {
62
+ let sent = false;
63
+ return (at, sources) => {
64
+ if (sent)
65
+ return;
66
+ sent = true;
67
+ const which = sources !== undefined && sources.length > 0 ? `your persisted permission rules from the ${sourcesWord(sources)} sources` : "your persisted permission rules";
68
+ deliverEngineNotice(onNotice, { code: "permission.rule_store_unavailable", message: SKIPPED_STORE_SENTENCE[at](which), detail: { at, sessionId: ids.sessionId, runId: ids.runId } });
69
+ };
70
+ }
71
+ function readsPersistedPathRows(provider, principal, localOwnerDeclared) {
72
+ return provider.partitions.durable && ((principal !== undefined && principal !== "") || localOwnerDeclared);
73
+ }
74
+ export function persistedPathRowsOfTask(t) {
75
+ const provider = t.deps.permissionRuleStore;
76
+ if (!t.handsEnabled || provider === undefined || !readsPersistedPathRows(provider, t.principal, t.deps.localOwnerRules === true))
77
+ return undefined;
78
+ return persistedPathRowsOf({
79
+ provider,
80
+ localOwnerDeclared: t.deps.localOwnerRules === true,
81
+ principal: t.principal,
82
+ sessionId: t.sessionId,
83
+ root: t.root,
84
+ home: t.home,
85
+ onDisclosure: (message) => emitTrace(t.deps.tracer, () => ({ kind: "permission.rule_store_unreadable", version: 1, taskId: t.taskId, message, ts: Date.now() })),
86
+ onUnread: t.onUnread,
87
+ signal: AbortSignal.any([...t.signals]),
88
+ });
89
+ }
@@ -12,6 +12,7 @@
12
12
  * publishes the declared store into the run's {@link ArtifactStoreRef} for that second point.
13
13
  * Synchronous: the stretch has no await (the file read is a closure the tool calls later).
14
14
  */
15
+ import type { PermissionModeController } from "../mode-policy.js";
15
16
  import type { ExecutionEnv } from "../../internal/harness.js";
16
17
  import type { OnAsk } from "../tool-policy.js";
17
18
  import { type ArtifactPlane } from "../artifact-host.js";
@@ -22,7 +23,7 @@ import { type ArtifactStoreRef } from "./read-root-grant.js";
22
23
  import type { Prepared } from "./contracts.js";
23
24
  export interface PrepareArtifactInput {
24
25
  /** borrowed-readonly — the deployment's hosting seat and the notice sink the refusal arm announces on. */
25
- deps: Pick<RunnerDeps, "artifactHost" | "onNotice">;
26
+ deps: Pick<RunnerDeps, "artifactHost" | "onNotice" | "tracer">;
26
27
  /** borrowed-readonly — the engine-minted run id: the host's `onMount` key and every call context's `runId`. */
27
28
  runId: string;
28
29
  /** borrowed-readonly — the host task id, the call context's audit attribution. */
@@ -34,6 +35,9 @@ export interface PrepareArtifactInput {
34
35
  /** borrowed-readonly — the run's effective approver (`spec.onAsk ?? deps.onAsk`, the seat the tool gate resolves at):
35
36
  * the tool's shared-read consent gate rides it as a mount dep (a core mount's execute ctx is minimal). */
36
37
  onAsk: OnAsk | undefined;
38
+ /** borrowed-readonly — the leg's live permission-mode seat; the tool's shared-read consent gate arbitrates at it before
39
+ * the approver is consulted (a released read is delivered, undisclosed to nobody: the gate records it). */
40
+ permissionMode: PermissionModeController;
37
41
  /** borrowed-readonly — the run's resolved env: the byte source of a published local file. */
38
42
  executionEnv: ExecutionEnv;
39
43
  /** borrowed-readonly — the canonical task root the fs fence contains reads to; undefined ⇒ no local-file publish. */
@@ -6,11 +6,12 @@ import { defineTool } from "../tools.js";
6
6
  import { inlineUntrusted } from "../untrusted-text.js";
7
7
  import { physicalPathOfExisting } from "../physical-path.js";
8
8
  import { resolveKey, violationDetails, violationText } from "../../tools/fs/safety.js";
9
- import { rootAdmittingArtifactStore } from "./read-root-grant.js";
9
+ import { rootAdmittingArtifactStore, withdrawStandingGrantsAdmitting } from "./read-root-grant.js";
10
+ import { emitTrace } from "../trace.js";
10
11
  import { createArtifactTool } from "../../tools/artifact/artifact-tool.js";
11
12
  import { thrownText } from "../thrown-value.js";
12
13
  export function prepareArtifact(input) {
13
- const { deps, runId, taskId, sessionId, agentName, onAsk, executionEnv, rootCanonical, additionalRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, roster, firstPartyOffload, envHandToolNames, artifactStoreRef } = input;
14
+ const { deps, runId, taskId, sessionId, agentName, onAsk, permissionMode, executionEnv, rootCanonical, additionalRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, roster, firstPartyOffload, envHandToolNames, artifactStoreRef } = input;
14
15
  const host = deps.artifactHost;
15
16
  if (host === undefined)
16
17
  return { artifactPlane: undefined };
@@ -29,6 +30,11 @@ export function prepareArtifact(input) {
29
30
  const storePhysical = storageDir !== undefined && rootCanonical !== undefined ? physicalPathOfExisting(storageDir) : undefined;
30
31
  if (artifactStoreRef !== undefined)
31
32
  artifactStoreRef.current = storePhysical;
33
+ if (artifactStoreRef !== undefined && storePhysical !== undefined) {
34
+ for (const entry of withdrawStandingGrantsAdmitting(storePhysical, artifactStoreRef)) {
35
+ emitTrace(deps.tracer, () => ({ kind: "config.additional_directory_skipped", version: 1, taskId, entry, field: "persistedReadRule", reason: `admits_artifact_store: the local artifact store ${storageDir} lies inside it or it inside the store — the Artifact tool's ledger and stored files stay outside every root the file tools reach`, ts: Date.now() }));
36
+ }
37
+ }
32
38
  if (storePhysical !== undefined && rootCanonical !== undefined) {
33
39
  const overlapping = rootAdmittingArtifactStore(storePhysical, [rootCanonical, ...additionalRootsCanonical()]);
34
40
  if (overlapping !== undefined)
@@ -60,7 +66,7 @@ export function prepareArtifact(input) {
60
66
  return { ok: false, reason: "too_large", size: r.value.byteLength, limit: maxBytes, judgedOn: "bytes" };
61
67
  return { ok: true, key: rk.key, bytes: r.value };
62
68
  };
63
- const artifactSpec = createArtifactTool({ host, capabilities: verdict.capabilities, runId, taskId, sessionId, ...(agentName !== undefined ? { agentName } : {}), readLocalFile, ...(onAsk !== undefined ? { onAsk } : {}) });
69
+ const artifactSpec = createArtifactTool({ host, capabilities: verdict.capabilities, runId, taskId, sessionId, ...(agentName !== undefined ? { agentName } : {}), readLocalFile, ...(onAsk !== undefined ? { onAsk } : {}), permissionMode: () => permissionMode.current, ...(deps.onNotice !== undefined ? { onNotice: deps.onNotice } : {}) });
64
70
  roster.mount(firstPartyOffload(defineTool(artifactSpec)), { source: "builtin", mountedBy: "optional" });
65
71
  envHandToolNames.add(ARTIFACT_TOOL_NAME);
66
72
  return { artifactPlane: verdict.capabilities.plane };
@@ -83,9 +83,11 @@ export interface PrepareAskLaneInput {
83
83
  egress?: boolean;
84
84
  };
85
85
  };
86
- /** borrowed-mutable — the auto-mode denial tracker, or undefined. Writer here: the carry mint arms the auto-deny
87
- * window on it iff the seat about to be called is live (the tracker's one writer). */
88
- autoModeDenialTracking: AutoModeDenialTracker | undefined;
86
+ /** borrowed-mutable — the auto-mode denial tracker's live seat (undefined until an arming). Writer here: the carry mint
87
+ * arms the auto-deny window on it iff the seat about to be called is live (the tracker's one writer). */
88
+ autoModeDenialTracking: {
89
+ readonly current: AutoModeDenialTracker | undefined;
90
+ };
89
91
  /** borrowed-readonly — the REBOUND spec; only `taskId` is read (the late-settlement observer's tag). */
90
92
  spec: Pick<TaskSpec, "taskId">;
91
93
  /** borrowed-readonly — the deployment seats this lane reports on, as a Pick over the SAME `deps` object (receiver
@@ -103,7 +103,7 @@ export function prepareAskLane(input) {
103
103
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
104
104
  ...(decision.action === "ask" ? askMandateMember(decision) : {}),
105
105
  ...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
106
- ...(decision.action === "ask" ? gateAskCarry(decision, isLiveApproverSeat(onAsk), autoModeDenialTracking) : {}),
106
+ ...(decision.action === "ask" ? gateAskCarry(decision, isLiveApproverSeat(onAsk), autoModeDenialTracking.current) : {}),
107
107
  ...(decision.action === "ask" && decision.probeReason !== undefined ? { probeReason: decision.probeReason } : {}),
108
108
  ...(decision.action === "ask" && decision.probeCause !== undefined ? { probeCause: decision.probeCause } : {}),
109
109
  ...(decision.action === "ask" && decision.probeReadRootCandidate !== undefined ? { readRootCandidate: decision.probeReadRootCandidate } : {}),
@@ -24,6 +24,7 @@ import { forkGovernanceDenial } from "../../agents/subagent.js";
24
24
  import type { PromptEpochArtifact } from "../../prompt-assembly/artifact.js";
25
25
  import { type WorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
26
26
  import { type AutoModeArmingRecipe } from "../auto-mode-arming.js";
27
+ import type { PermissionModeController } from "../mode-policy.js";
27
28
  import { type AutoModeDecider, type AutoModeDenialTracker } from "../auto-mode.js";
28
29
  import { type CheckpointSeat } from "../checkpoint-seat.js";
29
30
  import { type ComplianceCapability } from "../compliance.js";
@@ -109,10 +110,23 @@ export interface PrepareCapsAndWorkflowInput {
109
110
  requestReview: (opts?: {
110
111
  reason?: string;
111
112
  }) => void;
112
- /** borrowed-readonly — the plan-mode primitive the EnterPlanMode tool calls. */
113
- enterPlanMode: () => void;
114
- /** borrowed-readonly — the auto-mode INTENT (own seat ∨ chain ∨ checkpoint), the arming's first arm. */
115
- autoModeIntent: boolean;
113
+ /** borrowed-readonly — the plan-mode primitive the EnterPlanMode tool calls: a steer of the leg's mode seat into `plan`. */
114
+ enterPlanMode: () => Promise<void>;
115
+ /** borrowed-readonly — the leg's live permission-mode seat: the arming's first arm is `declared === "auto"`, and the
116
+ * arming facts (the deny bits, the face, the re-entrant arm) are bound onto it here. */
117
+ permissionMode: PermissionModeController;
118
+ /** borrowed-readonly — LATE-BOUND: the effective read-deny table the hands phase compiles (the rows the classifier's deny
119
+ * paragraph renders); read at ARMING, which the driver runs after that phase — never at this phase's own call. */
120
+ readDenyEntries: () => {
121
+ readonly entries: ReadonlyArray<{
122
+ readonly pattern: string;
123
+ readonly caseSensitive?: boolean;
124
+ }>;
125
+ readonly ruleRows: ReadonlyArray<{
126
+ readonly tool: string;
127
+ readonly pattern: string;
128
+ }>;
129
+ };
116
130
  /** borrowed-readonly — the peer lane's mount verdict (the classifier's cross-session rule fills only while mounted). */
117
131
  peerLaneActive: boolean;
118
132
  /** borrowed-readonly — whether the BUILT-IN SendMessage is on this face (the lane rule's second term). */
@@ -153,15 +167,26 @@ export interface PrepareCapsAndWorkflowResult {
153
167
  agentForkDenial: ReturnType<typeof forkGovernanceDenial>;
154
168
  /** owned — the observers grant (`allowObservers === true`, explicit opt-in). */
155
169
  observersActive: boolean;
156
- /** owned — the arming outcome as a closed reason (the manifest's auto-mode face). */
157
- /** The leg's auto-mode arming fact (`armed` ⇔ `reason === "armed"`; `deniedSource` only beside a STATED `denied`). */
158
- autoModeArmReason: AutoModeArmFact;
159
- /** owned — the armed classifier, or undefined when any arming arm failed. */
160
- autoModeDecider: AutoModeDecider | undefined;
161
- /** owned — the denial tracker built beside the decider (same face, same arming). */
162
- autoModeDenialTracking: AutoModeDenialTracker | undefined;
163
- /** owned — the serializable arming recipe (persistArming opt-in only). */
164
- autoModeArming: AutoModeArmingRecipe | undefined;
170
+ /** owned — the arming outcome as a closed reason (the manifest's auto-mode face), read LIVE: a leg steered into `auto`
171
+ * after prepare arms then, and the fact follows. */
172
+ autoModeArmReason: () => AutoModeArmFact;
173
+ /** owned — the armed classifier's live seat (`current` undefined until an arming succeeds — at prepare for an `auto`
174
+ * leg, or at a steer into `auto`); every consumer reads `current` at its own call. */
175
+ autoModeDeciderRef: {
176
+ readonly current: AutoModeDecider | undefined;
177
+ };
178
+ /** owned — the denial tracker built beside the decider (same face, same arming), same live seat. */
179
+ autoModeDenialTrackingRef: {
180
+ readonly current: AutoModeDenialTracker | undefined;
181
+ };
182
+ /** owned — the serializable arming recipe (persistArming opt-in only), same live seat. */
183
+ autoModeArmingRef: {
184
+ readonly current: AutoModeArmingRecipe | undefined;
185
+ };
186
+ /** owned — arm the classifier for THIS leg (idempotent; a no-op unless the effective mode is `auto` and the face and the
187
+ * deny bit admit it). The driver calls it once after the hands phase (the deny table must exist first); the mode seat
188
+ * calls it again on a steer into `auto`. */
189
+ armAutoMode: () => Promise<void>;
165
190
  /** owned — self-orchestration is ACTIVE for this leg (deployment ready ∧ not entitlement-denied); drives the prompt injection. */
166
191
  selfOrchestrationActive: boolean;
167
192
  /** owned — the Workflow tool is on the roster (the delegation surface's second door). */
@@ -192,6 +217,7 @@ declare const RUNTIME_CAP_KEYS: {
192
217
  readonly forceDurableGate: true;
193
218
  readonly autoMode: true;
194
219
  readonly autoModeDenySource: true;
220
+ readonly bypassPermissions: true;
195
221
  readonly allowMemoryOptOut: true;
196
222
  };
197
223
  export type RuntimeCapKeysCoverEveryCap = AssertAllKeysHandled<Exclude<keyof RuntimeCaps, keyof typeof RUNTIME_CAP_KEYS>>;
@@ -7,8 +7,9 @@ import { createRunWorkflowTool } from "../../orchestration/run-workflow-tool.js"
7
7
  import { isSelfOrchestrationActive } from "../../orchestration/workflow-script-runner.js";
8
8
  import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
9
9
  import { autoModeArmingRecipeOf } from "../auto-mode-arming.js";
10
- import { buildAutoModePrompt, renderAutoModeAction, renderAutoModeWindow } from "../auto-mode-prompt.js";
11
- import { autoModeClassifierFallbackOf, createAutoModeDecider, createAutoModeDenialTracker } from "../auto-mode.js";
10
+ import { AUTO_MODE_CLASSIFIER_MAX_RETRIES } from "../auto-mode-defaults.js";
11
+ import { buildAutoModePrompt, readDenyClassifierRules, renderAutoModeAction, renderAutoModeWindow } from "../auto-mode-prompt.js";
12
+ import { autoModeClassifierFallbackOf, createAutoModeDecider, createAutoModeDenialTracker, screenAutoModeKnobs } from "../auto-mode.js";
12
13
  import { checkpointStoreOfSeat } from "../checkpoint-seat.js";
13
14
  import { COMPLIANCE_CAPABILITIES, WEB_FETCH_TOOL_NAME, resolveComplianceDenies } from "../compliance.js";
14
15
  import { createPresentPlanTool, createEnterPlanModeTool } from "../present-plan-tool.js";
@@ -16,7 +17,7 @@ import { brainToRuntime } from "../runtime.js";
16
17
  import { defaultTaskRegistry } from "../task-registry.js";
17
18
  import { defineTool, isDefineToolProduct, rebindDefineToolCtx } from "../tools.js";
18
19
  const MOUNT_REMEDY = "author it as a plain-object ToolSpec (own `name` and `execute`), hand the defineTool product itself, or wrap a product through the supported wrapper form (a fresh object re-stamped with `stampDefineToolBrand` whose rebind re-wraps the rebound product)";
19
- import { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "../runtime-caps.js";
20
+ import { isAutoModeDenySource } from "../runtime-caps.js";
20
21
  import { derivedRouteFallsBack } from "./derived-route-fallback.js";
21
22
  import { CLASSIFIER_ATTEMPT_SUFFIX, CLASSIFIER_THINK_SEAT, announceClassifierRoleDefaulted, classifierLadderRungs, classifierReplyAsksAgain, classifierReplyFailure, classifierReplyVerdictText, classifierRoundObserver, classifierRungSeat, classifierSeatModel } from "./classifier-ladder.js";
22
23
  import { REPORT_FINDINGS_TOOL_NAME, createReportBlockedTool, createReportFindingsTool } from "./synthetic-tools.js";
@@ -38,6 +39,7 @@ const RUNTIME_CAP_KEYS = {
38
39
  forceDurableGate: true,
39
40
  autoMode: true,
40
41
  autoModeDenySource: true,
42
+ bypassPermissions: true,
41
43
  allowMemoryOptOut: true,
42
44
  };
43
45
  function snapshotRuntimeCaps(record) {
@@ -54,7 +56,7 @@ function screenAutoModeCap(caps, onError, sessionId) {
54
56
  return { caps, faulted: false };
55
57
  if (typeof caps !== "object" || caps === null || Array.isArray(caps)) {
56
58
  onError?.(new Error(`runtimeCapsResolver returned a non-record value (${JSON.stringify(caps)}) — read as a resolver fault: every per-principal capability is DENIED for this run (auto mode is not armed; asks flow the original chain). Return a RuntimeCaps object or undefined.`), { phase: "config", sessionId });
57
- return { caps: { allowWorkflows: false, allowFork: false, autoMode: false }, faulted: true };
59
+ return { caps: { allowWorkflows: false, allowFork: false, autoMode: false, bypassPermissions: false }, faulted: true };
58
60
  }
59
61
  let record;
60
62
  try {
@@ -62,22 +64,34 @@ function screenAutoModeCap(caps, onError, sessionId) {
62
64
  }
63
65
  catch (err) {
64
66
  onError?.(new Error(`runtimeCapsResolver's record threw while being read (${thrownText(err)}) — read as a resolver fault: every per-principal capability is DENIED for this run (auto mode is not armed; asks flow the original chain).`), { phase: "config", sessionId });
65
- return { caps: { allowWorkflows: false, allowFork: false, autoMode: false }, faulted: true };
67
+ return { caps: { allowWorkflows: false, allowFork: false, autoMode: false, bypassPermissions: false }, faulted: true };
66
68
  }
69
+ let screened = record;
70
+ let faulted = false;
71
+ const fault = (message) => {
72
+ onError?.(new Error(`runtimeCapsResolver returned ${message}`), { phase: "config", sessionId });
73
+ faulted = true;
74
+ };
67
75
  const { autoMode, autoModeDenySource } = record;
68
76
  if (autoModeDenySource !== undefined && (!isAutoModeDenySource(autoModeDenySource) || autoMode !== false)) {
69
- onError?.(new Error(`runtimeCapsResolver returned autoModeDenySource=${JSON.stringify(autoModeDenySource)} beside autoMode=${JSON.stringify(autoMode)} — read as a resolver fault: auto mode is DENIED for this run (the classifier is not armed; asks flow the original chain). Name a source only beside autoMode:false, and only one of ${AUTO_MODE_DENY_SOURCES.join("|")}.`), { phase: "config", sessionId });
70
- const { autoModeDenySource: _refused, ...rest } = record;
71
- return { caps: { ...rest, autoMode: false }, faulted: true };
77
+ fault(`autoModeDenySource=${JSON.stringify(autoModeDenySource)} beside autoMode=${JSON.stringify(autoMode)} — read as a resolver fault: auto mode is DENIED for this run (the classifier is not armed; asks flow to the approver / park)`);
78
+ const { autoModeDenySource: _refused, ...rest } = screened;
79
+ screened = { ...rest, autoMode: false };
72
80
  }
73
- if (record.autoMode === undefined || typeof record.autoMode === "boolean")
74
- return { caps: record, faulted: false };
75
- onError?.(new Error(`runtimeCapsResolver returned a non-boolean autoMode (${JSON.stringify(record.autoMode)}) — read as a resolver fault: auto mode is DENIED for this run (the classifier is not armed; asks flow the original chain). Return true, false, or omit the key.`), { phase: "config", sessionId });
76
- return { caps: { ...record, autoMode: false }, faulted: true };
81
+ else if (autoMode !== undefined && typeof autoMode !== "boolean") {
82
+ fault(`a non-boolean autoMode (${JSON.stringify(autoMode)}) — read as a resolver fault: auto mode is DENIED for this run (the classifier is not armed; asks flow the same way they do with no auto-mode face)`);
83
+ screened = { ...screened, autoMode: false };
84
+ }
85
+ if (record.bypassPermissions !== undefined && typeof record.bypassPermissions !== "boolean") {
86
+ fault(`a non-boolean bypassPermissions (${JSON.stringify(record.bypassPermissions)}) — read as a resolver fault: bypassPermissions is DENIED for this run (a requested bypass folds back to default)`);
87
+ screened = { ...screened, bypassPermissions: false };
88
+ }
89
+ return { caps: screened, faulted };
77
90
  }
78
- function mintPersistedArming(am, laneRule, classifierSystemPrompt, onError, sessionId) {
91
+ function mintPersistedArming(am, laneRule, classifierSystemPrompt, derivedDenyRules, onError, sessionId) {
79
92
  const arming = autoModeArmingRecipeOf(laneRule ? { ...am, crossSessionMessagesRule: true } : am, {
80
93
  promptDigest: `apv1:${createHash("sha256").update(classifierSystemPrompt).digest("hex")}`,
94
+ derivedDenyRules,
81
95
  });
82
96
  if (arming === undefined) {
83
97
  onError?.(new Error("RunnerDeps.autoMode.persistArming is set but the auto-mode face did not canonicalize into an arming recipe " +
@@ -100,7 +114,7 @@ function autoModeArmReasonOf(intent, facePresent, caps, capsFaulted) {
100
114
  return { armed: true, reason: "armed" };
101
115
  }
102
116
  export async function prepareCapsAndWorkflow(input) {
103
- const { spec, deps, parentSpecSeat, checkpointSeat, internals, session, sessionId, runId, hostTaskId, taskScope, taskRootFinal, model, thinking, lockedPreflight, toolEffects, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, harnessRef, frozenOnAsk, carrierReadFace, carrierWriteFace, forwardEvent, inheritedGateForChildren, enrichSpecToolCtx, maybeOffload, requestReview, enterPlanMode, autoModeIntent, peerLaneActive, peerSendMessageBuiltIn, runnerSelf } = input;
117
+ const { spec, deps, parentSpecSeat, checkpointSeat, internals, session, sessionId, runId, hostTaskId, taskScope, taskRootFinal, model, thinking, lockedPreflight, toolEffects, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, harnessRef, frozenOnAsk, carrierReadFace, carrierWriteFace, forwardEvent, inheritedGateForChildren, enrichSpecToolCtx, maybeOffload, requestReview, enterPlanMode, permissionMode, readDenyEntries, peerLaneActive, peerSendMessageBuiltIn, runnerSelf } = input;
104
118
  const roster = new RosterBuilder(toolEffects);
105
119
  for (const t of spec.tools ?? []) {
106
120
  const mounted = mountCallerTool(t);
@@ -151,7 +165,7 @@ export async function prepareCapsAndWorkflow(input) {
151
165
  }
152
166
  catch (err) {
153
167
  deps.onError?.(asThrownError(err), { phase: "config", sessionId });
154
- runtimeCaps = { allowWorkflows: false, allowFork: false, autoMode: false };
168
+ runtimeCaps = { allowWorkflows: false, allowFork: false, autoMode: false, bypassPermissions: false };
155
169
  runtimeCapsFaulted = true;
156
170
  }
157
171
  const screened = screenAutoModeCap(runtimeCaps, deps.onError, sessionId);
@@ -193,11 +207,17 @@ export async function prepareCapsAndWorkflow(input) {
193
207
  }
194
208
  const agentForkDenial = forkGovernanceDenial(spec.enableFork, runtimeCaps?.allowFork);
195
209
  const observersActive = runtimeCaps?.allowObservers === true;
196
- let autoModeDecider;
197
- let autoModeDenialTracking;
198
- let autoModeArming;
199
- const autoModeArmReason = autoModeArmReasonOf(autoModeIntent, deps.autoMode !== undefined, runtimeCaps, runtimeCapsFaulted);
200
- if (autoModeIntent && deps.autoMode !== undefined && runtimeCaps?.autoMode !== false) {
210
+ const autoModeDeciderRef = { current: undefined };
211
+ const autoModeDenialTrackingRef = { current: undefined };
212
+ const autoModeArmingRef = { current: undefined };
213
+ const autoModeArmReason = () => autoModeArmReasonOf(permissionMode.declared === "auto", deps.autoMode !== undefined, runtimeCaps, runtimeCapsFaulted);
214
+ if (deps.autoMode !== undefined)
215
+ screenAutoModeKnobs(deps.autoMode);
216
+ const armAutoMode = async () => {
217
+ if (autoModeDeciderRef.current !== undefined)
218
+ return;
219
+ if (permissionMode.declared !== "auto" || deps.autoMode === undefined || runtimeCaps?.autoMode === false)
220
+ return;
201
221
  const am = deps.autoMode;
202
222
  const classifierSeat = classifierSeatModel({ deps, main: model });
203
223
  let classifierModel = classifierSeat.model;
@@ -206,11 +226,14 @@ export async function prepareCapsAndWorkflow(input) {
206
226
  }
207
227
  announceClassifierRoleDefaulted({ onNotice: deps.onNotice, seat: { ...classifierSeat, model: classifierModel }, ...(sessionId !== undefined ? { sessionId } : {}), ...(runId !== undefined ? { runId } : {}) });
208
228
  const classifierLaneRule = peerLaneActive && peerSendMessageBuiltIn;
209
- const classifierSystemPrompt = buildAutoModePrompt(classifierLaneRule ? { ...am, crossSessionMessagesRule: CROSS_SESSION_CLASSIFIER_RULE } : am);
229
+ const table = readDenyEntries();
230
+ const derivedDenyRules = readDenyClassifierRules(table.entries, table.ruleRows);
231
+ let classifierSystemPromptMemo;
232
+ const classifierSystemPrompt = () => (classifierSystemPromptMemo ??= buildAutoModePrompt({ ...am, derivedDenyRules, ...(classifierLaneRule ? { crossSessionMessagesRule: CROSS_SESSION_CLASSIFIER_RULE } : {}) }));
210
233
  const classifierRuntime = brainToRuntime(deps.brain);
211
234
  const classifierRungs = classifierLadderRungs(am.classifierFallback, classifierModel, model);
212
- autoModeDenialTracking = createAutoModeDenialTracker(am.denialLimit);
213
- autoModeDecider = createAutoModeDecider({
235
+ const denialTracking = createAutoModeDenialTracker(am.denialLimit);
236
+ const decider = createAutoModeDecider({
214
237
  ...(am.timeoutMs !== undefined ? { timeoutMs: am.timeoutMs } : {}),
215
238
  candidates: classifierRungs.map((m) => m.id),
216
239
  onClassified: classifierRoundObserver({ tracer: deps.tracer, onNotice: deps.onNotice, taskId: hostTaskId, seatModelId: classifierModel.id, ...(sessionId !== undefined ? { sessionId } : {}), ...(runId !== undefined ? { runId } : {}) }),
@@ -229,8 +252,9 @@ export async function prepareCapsAndWorkflow(input) {
229
252
  }
230
253
  const classifierAuth = await spec.getApiKeyAndHeaders?.(rungModel);
231
254
  const request = async (seat, suffix) => {
232
- const response = await classifierRuntime.completeSimple(rungModel, { systemPrompt: classifierSystemPrompt, messages: [{ role: "user", content: userPrompt + suffix, timestamp: Date.now() }] }, {
255
+ const response = await classifierRuntime.completeSimple(rungModel, { systemPrompt: classifierSystemPrompt(), messages: [{ role: "user", content: userPrompt + suffix, timestamp: Date.now() }] }, {
233
256
  signal,
257
+ maxRetries: AUTO_MODE_CLASSIFIER_MAX_RETRIES,
234
258
  ...seat,
235
259
  ...(classifierAuth?.apiKey !== undefined ? { apiKey: classifierAuth.apiKey } : {}),
236
260
  ...(classifierAuth?.headers !== undefined ? { headers: classifierAuth.headers } : {}),
@@ -246,9 +270,14 @@ export async function prepareCapsAndWorkflow(input) {
246
270
  return classifierReplyVerdictText(response);
247
271
  },
248
272
  });
249
- if (am.persistArming === true)
250
- autoModeArming = mintPersistedArming(am, classifierLaneRule, classifierSystemPrompt, deps.onError, sessionId);
251
- }
273
+ const arming = am.persistArming === true ? mintPersistedArming(am, classifierLaneRule, classifierSystemPrompt(), derivedDenyRules, deps.onError, sessionId) : undefined;
274
+ if (autoModeDeciderRef.current !== undefined)
275
+ return;
276
+ autoModeDeciderRef.current = decider;
277
+ autoModeDenialTrackingRef.current = denialTracking;
278
+ autoModeArmingRef.current = arming;
279
+ };
280
+ permissionMode.bindArming({ caps: runtimeCaps, autoFacePresent: deps.autoMode !== undefined, armAuto: armAutoMode, onArmFault: (e) => deps.onError?.(asThrownError(e), { phase: "config", sessionId }) });
252
281
  const deploymentWorkflowReady = runnerSelf !== undefined && isSelfOrchestrationActive(spec, deps);
253
282
  const selfOrchestrationActive = deploymentWorkflowReady && runtimeCaps?.allowWorkflows !== false;
254
283
  if (deploymentWorkflowReady && runtimeCaps?.allowWorkflows === false) {
@@ -313,7 +342,7 @@ export async function prepareCapsAndWorkflow(input) {
313
342
  const co = memoryEngineSession?.captureOptOut;
314
343
  return assembleParentCaptureState(co?.optedOut() ?? false, co?.indeterminate() ?? false, ctl, [...(internals?.memoryCaptureAncestors ?? []), { sessionId, ...(ctl !== undefined ? { controlDir: ctl } : {}) }]);
315
344
  },
316
- autoModeReview: () => (autoModeDecider !== undefined ? { decider: autoModeDecider } : undefined),
345
+ autoModeReview: () => (autoModeDeciderRef.current !== undefined ? { decider: autoModeDeciderRef.current } : undefined),
317
346
  workflowDepth: internals?.workflowDepth,
318
347
  parentCwd: taskRootFinal,
319
348
  parentThinking: () => childThinkingSeat(undefined, deploymentSubagentThinking(runnerSelf.agentCatalog?.roles), harnessRef.current?.getThinkingLevel() ?? thinking).thinking,
@@ -332,5 +361,5 @@ export async function prepareCapsAndWorkflow(input) {
332
361
  inheritedGateForChildren,
333
362
  }), { source: "builtin", mountedBy: "workflow" });
334
363
  }
335
- return { roster, blockedRef, runtimeCaps, runtimeCapsFaulted, complianceDenies, complianceDegraded, agentForkDenial, observersActive, autoModeArmReason, autoModeDecider, autoModeDenialTracking, autoModeArming, selfOrchestrationActive, workflowToolsActive, workflowSizeGuideline };
364
+ return { roster, blockedRef, runtimeCaps, runtimeCapsFaulted, complianceDenies, complianceDegraded, agentForkDenial, observersActive, autoModeArmReason, autoModeDeciderRef, autoModeDenialTrackingRef, autoModeArmingRef, armAutoMode, selfOrchestrationActive, workflowToolsActive, workflowSizeGuideline };
336
365
  }