@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
@@ -1071,22 +1071,20 @@ export interface PersistedRuleHit {
1071
1071
  readonly rules: readonly PersistedRuleHitRule[];
1072
1072
  }
1073
1073
  /**
1074
- * design/252 G-2 โ€” the answer a lane gives when it could not READ its store.
1074
+ * design/252 G-2 โ€” the answer a lane gives when it could not READ its store (no `reason`; the CALL form is `reason`'s).
1075
1075
  *
1076
- * Its DECISION meaning is identical to "no rule" โ€” a loosening seam that cannot read its source fails
1077
- * toward asking, and this shape changes no verdict anywhere. What it changes is the RECORD: without it,
1078
- * an unreadable rule store and a store that genuinely holds no matching rule are the same answer
1079
- * (`undefined`), and the ask's evidence has to report one of them as the other. The absence vocabulary
1080
- * distinguishes "we looked and found nothing" (`"no_match"`) from "we could not look" (`"unavailable"`),
1081
- * and this is how a lane says which one it means.
1076
+ * The store form's DECISION meaning is identical to "no rule": the store's rows โ€” allow, ask and deny alike โ€” take
1077
+ * no part in this call's decision (skipped, the way a settings file with errors is skipped whole; the lane announces
1078
+ * it, and the rows apply again once a read succeeds). What it changes is the RECORD: without it, an unreadable store
1079
+ * and a store that holds no matching rule are the same answer (`undefined`), and the ask's evidence would report one
1080
+ * as the other โ€” `"no_match"` ("we looked and found nothing") vs `"unavailable"` ("we could not look").
1082
1081
  */
1083
1082
  export interface PersistedRuleUnreadable {
1084
1083
  readonly unreadable: true;
1085
- /** WHY the lane could not read: absent for a store read failure; for a call the lane could not READ
1086
- * against the person's standing deny/ask rows (an expansion where a rule reads a word, an unterminated
1087
- * quote, a syntax error โ€” `programRunReachOf`'s `unreadable` word), the lexer's reason. The DECISION
1088
- * is the same fail-closed one either way (the question "does one of my rules forbid this?" is
1089
- * unanswered); the reason is what the ask's message says instead of "the store could not be read". */
1084
+ /** WHY the lane could not read: absent for a store read failure (its rows take no part); for a CALL the lane could
1085
+ * not read against the person's standing deny/ask rows (an expansion where a rule reads a word, an unterminated quote,
1086
+ * a syntax error โ€” `programRunReachOf`'s `unreadable` word), the lexer's reason โ€” the call then fails closed: an allow
1087
+ * tightens to a real-approval ask of origin `rule_store_unavailable`, the reason in its message. */
1090
1088
  readonly reason?: string;
1091
1089
  }
1092
1090
  /**
@@ -1113,6 +1111,9 @@ export interface ToolGateInput {
1113
1111
  * isolation scope (onHookError itself, the deny observer, the durable-park observer). The runner
1114
1112
  * wires this to an `observer.notify_failed` trace event; absent = counted only (unchanged). */
1115
1113
  onNotifyError?: (failure: import("./safe-notify.js").SafeNotifyFailure) => void;
1114
+ /** The call. `toolName` is the name every policy, hook and name-keyed table of this gate reads โ€” the wire name of the
1115
+ * tool the call dispatches to: the Runner's station resolves a call made under an alias through the roster
1116
+ * (`rosterToolName`) before this gate runs; a host driving the gate resolves it the same way. */
1116
1117
  event: {
1117
1118
  toolCallId: string;
1118
1119
  toolName: string;
@@ -1349,8 +1350,8 @@ export interface ToolGateInput {
1349
1350
  onHookError?: (err: unknown) => void;
1350
1351
  /**
1351
1352
  * design/134 R5: true when this tool's `irreversibility` tier was installed by the ENGINE'S SHELL SEAT
1352
- * (the read-boundary judge under every doctrine, plus the `TaskSpec.shellGate` doctrine's residual-risk
1353
- * classifier โ€” design/80 D-E; the seat's `shellGatedTools` membership), not an explicit per-tool mark โ€”
1353
+ * (the read-boundary judge on every shell face, plus the shape tier on every full shell and the
1354
+ * `TaskSpec.shellGate: "always"` floor โ€” design/80 D-E; the seat's `shellGatedTools` membership), not an explicit per-tool mark โ€”
1354
1355
  * attributes a tighten-deny to `source:"shellGate"` instead of `"safety"`.
1355
1356
  */
1356
1357
  shellGated?: boolean;
@@ -1386,7 +1387,8 @@ export interface ToolGateInput {
1386
1387
  * POLICY-class member of the fold: a deny-listed read is REFUSED (`deniedBy: "read_boundary"`, composing
1387
1388
  * under `deny > ask > allow`), an out-of-root or unestablishable read tightens a surviving allow to a
1388
1389
  * MANDATED ask (or marks an existing ask mandated), a read the station declined to judge tightens to an
1389
- * ordinary ask on the classify seat and a mandated one on the off seat, and a vouched call is untouched.
1390
+ * ordinary ask on the full shell face (under `auto` the classifier answers it, shown the read-deny table) and
1391
+ * is refused on the read-only face, and a vouched call is untouched.
1390
1392
  * `null` SYNCHRONOUSLY for a call it does not cover (no suspension point there), a promise for a shell
1391
1393
  * call. Absent โ‡’ no station (no shell mounted) and the decision path is byte-identical.
1392
1394
  */
@@ -1395,10 +1397,9 @@ export interface ToolGateInput {
1395
1397
  * design/143 ๆ‰น2 (A, CC 2.1.207 auto mode): when present, a surviving `ask` is routed to the
1396
1398
  * small-model policy CLASSIFIER before any human/durable resolution:
1397
1399
  * - verdict `allow` โ†’ the ask resolves to allow (`decisionReason:"classifier"`) โ€” no suspend, no onAsk;
1398
- * - verdict `block` โ†’ deny (`decisionReason:"classifier"`, `source:"classifier"`; CC `automode-blocked`);
1399
- * - `unavailable`/`parse_error` โ†’ FAIL-CLOSED: the ask continues down the ORIGINAL chain
1400
- * (durable suspend / onAsk / headless auto-deny) exactly as if auto mode were absent
1401
- * (CC `automode-unavailable`/`automode-parsing-error` โ€” "NOT a policy decision").
1400
+ * - verdict `block` โ†’ deny (`decisionReason:"classifier"`, `source:"classifier"`; CC `automode-blocked`); `parse_error` too;
1401
+ * - `unavailable` โ†’ where `unavailable.approvalSeat` (a person, live or parked) the ask goes on, its text saying why;
1402
+ * else CC's refusal (`cause: "unavailable"`). Never counted by the denial limit (`classifierUnavailableArm`).
1402
1403
  * SCOPE (declared semantic): this covers every surviving ask INCLUDING the deterministic egress/
1403
1404
  * irreversibility safety tightens โ€” auto mode is the deployment's explicit choice to let the
1404
1405
  * classifier be the "explicit ask-resolution" for this session (its rule set covers exactly those
@@ -1425,7 +1426,32 @@ export interface ToolGateInput {
1425
1426
  * `RunnerDeps.autoMode.denialLimit`; a host driving the gate directly may hand its own.
1426
1427
  */
1427
1428
  denialTracking?: AutoModeDenialTracker;
1429
+ unavailable?: import("./classifier-unavailable-arm.js").ClassifierUnavailableSeat;
1428
1430
  };
1431
+ /**
1432
+ * The leg's EFFECTIVE permission mode, read at every arbitration (a steer changes the next call's answer). The
1433
+ * gate's one mode read point sits after the classifier and the sandbox leg and before the park / resolution:
1434
+ * `bypassPermissions` releases every non-immune class (`GateDisposition.released`), `dontAsk` refuses every ask
1435
+ * but a hook's or an operator authority's (`deniedBy: "mode"`), the other modes hand the ask on. ABSENT โ‡’ no
1436
+ * arbitration (a host driving the gate directly keeps the pre-mode chain); the classifier station then reads
1437
+ * `autoMode` presence alone as the arming fact.
1438
+ */
1439
+ permissionMode?: () => import("./ask-origin.js").PermissionMode;
1440
+ /** WHO put the tool's egress / irreversibility marks on the seat ({@link import("./ask-origin.js").ToolMarkSource});
1441
+ * absent or empty with a mark present reads as the deployment's own (fail-closed: an operator's mandate). */
1442
+ toolMarkSources?: ReadonlySet<import("./ask-origin.js").ToolMarkSource>;
1443
+ /** The deployment's A2A peer-isolation switch: a set switch makes the A2A egress default an operator mandate. */
1444
+ a2aIsolatePeers?: boolean;
1445
+ /** Observe a mode RELEASE (fired before the released allow leaves the arbitration point; `readUnestablished`: the read station could not establish what the call reads). Observe-only. */
1446
+ onModeReleased?: (info: {
1447
+ toolName: string;
1448
+ toolCallId: string;
1449
+ release: import("./gate-outcome.js").ModeRelease;
1450
+ readUnestablished?: true;
1451
+ }) => void;
1452
+ /** An ancestor station's arbitration of this call (the inherited lane arbitrates inside the fold and records
1453
+ * here); the exit reads it onto the disposition when the gate's own point did not arbitrate. */
1454
+ ancestorArbitrationOf?: (toolCallId: string) => import("./ask-origin.js").Arbitration | undefined;
1429
1455
  /**
1430
1456
  * The HEADLESS arm of the denial-limit fallback: the fallback ask was resolved by a deny no
1431
1457
  * person made (no approver wired; a blanket `"allow"` refused as no approver; an approver that
@@ -1456,6 +1482,7 @@ export interface ToolGateInput {
1456
1482
  toolCallId: string;
1457
1483
  cause: import("./ask-unresolvable-notice.js").AskUnresolvableCause;
1458
1484
  parkLaneExisted: boolean;
1485
+ remedy?: string;
1459
1486
  }) => void;
1460
1487
  /**
1461
1488
  * design/153 ยง2/ยง7.4 (ไปถ4 ๅคๅฎก, MED): true means this call's ask is MARKED โ€” an inherited ancestor
@@ -1506,7 +1533,7 @@ export interface ToolGateInput {
1506
1533
  * ยท #502: an ask the call's own reversibility probe declared STRUCTURAL (`probeMandated`) joins that
1507
1534
  * family. It is the one PER-CALL member of it, and the reason there has to be one: the built-in
1508
1535
  * shell probe raises it for a read outside the session's allowed directories, which is a boundary
1509
- * the deployment declared but which arrives wearing the classify doctrine's own "maybe" tier โ€”
1536
+ * the deployment declared but which arrives wearing the shape tier's own "maybe" โ€”
1510
1537
  * the very tier the bullet below keeps as this lane's home turf. Without a per-call member, a
1511
1538
  * compound was cleared segment-wise by the conjunction arm and an out-of-root read plus a sender
1512
1539
  * ran on two ordinary standing rules with no question asked.
@@ -1521,12 +1548,11 @@ export interface ToolGateInput {
1521
1548
  */
1522
1549
  persistedRules?: {
1523
1550
  /** The lane's verdict for this call โ€” a {@link PersistedRuleHit} naming the behavior that spoke and
1524
- * the rule set it stands on, a coverage-carrying clean negative, an unreadable-store report, or
1551
+ * the rule set it stands on, a coverage-carrying clean negative, an unreadable report, or
1525
1552
  * `undefined` (the store was READ and no rule speaks). Must not throw โ€” and a store that cannot be
1526
- * read answers `{ unreadable: true }`, NEVER `undefined`: the gate reads the unreadable report as
1527
- * fail-closed (an allow tightens to a real-approval ask of origin `rule_store_unavailable`), while
1528
- * `undefined` is a successful no-match that leaves an allow standing. An adapter that answers
1529
- * `undefined` on a read failure silently disarms every deny/ask row it holds.
1553
+ * read answers `{ unreadable: true }`, not `undefined`: the DECISION is the same (its rows take no part, as
1554
+ * for a thrown or timed-out lane) but the evidence records `"unavailable"`, not `"no_match"`; the lane owns
1555
+ * announcing the skip. A call unreadable against the rows answers `{ unreadable: true, reason }` (fails closed).
1530
1556
  *
1531
1557
  * For an ALLOW the set is the COVERAGE SET โ€” each member's canonical text plus its add DOTS. A
1532
1558
  * single rule admitting the whole command is a one-element set; a compound admitted by the
@@ -1708,14 +1734,13 @@ export interface AskCarry {
1708
1734
  /** The surviving ask's engine-stamped origin word ({@link import("./ask-origin.js").AskOrigin}), persisted
1709
1735
  * on the row so a durable card is rendered by the same word the synchronous card is. */
1710
1736
  readonly origin?: import("./ask-origin.js").AskOrigin;
1711
- /** #688 C3: the mechanism behind an `origin: "rule_store_unavailable"` (`store` / `call`), persisted on the row
1712
- * so a durable card says the same thing the synchronous card does. */
1713
- readonly ruleStoreUnreadable?: import("./ask-origin.js").RuleStoreUnreadable;
1714
1737
  /** The stamped mandate ({@link import("./tool-policy.js").AskRequest.mandated}) โ€” the park site's
1715
1738
  * rule-offer facts read it, so a parked mandated ask offers no rule, like the live card. */
1716
1739
  readonly mandated?: true;
1717
1740
  /** The table word stamped beside it ({@link import("./tool-policy.js").AskRequest.mandate}), for the row's twin. */
1718
1741
  readonly mandate?: import("./ask-origin.js").PersistedRuleMandate;
1742
+ /** The deployment's authorship ({@link import("./tool-policy.js").AskRequest.authoredBy}), for the row's twin. */
1743
+ readonly authoredBy?: "deployment";
1719
1744
  }
1720
1745
  /** What a durable row persists of an {@link AskCarry}: the fallback member COPIED (the row never aliases
1721
1746
  * the gate's object; its window is already `0` โ€” the gate's carry mint is the one writer of that
@@ -1724,7 +1749,6 @@ export interface AskCarry {
1724
1749
  export declare function askCarryRowMembers(carry: AskCarry | undefined): {
1725
1750
  denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1726
1751
  origin?: import("./ask-origin.js").AskOrigin;
1727
- ruleStoreUnreadable?: import("./ask-origin.js").RuleStoreUnreadable;
1728
1752
  };
1729
1753
  /**
1730
1754
  * The design/37 **two-phase tool gate** โ€” the single chokepoint that makes the load-bearing invariant
@@ -7,7 +7,7 @@ import { runGateLanes } from "./gate-lanes.js";
7
7
  export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./gate-lanes.js";
8
8
  export { cloneObserverInput } from "./runner/gate-exit.js";
9
9
  import { brandPolicyAskClass } from "./ask-class.js";
10
- import { isAskOrigin, isRuleStoreUnreadable } from "./ask-origin.js";
10
+ import { isAskOrigin } from "./ask-origin.js";
11
11
  import { inlineUntrusted } from "./untrusted-text.js";
12
12
  export { formatHookFeedback } from "./reminder-mint.js";
13
13
  import { createSafeNotifier } from "./safe-notify.js";
@@ -238,11 +238,9 @@ export function askCarryRowMembers(carry) {
238
238
  if (carry === undefined)
239
239
  return {};
240
240
  const origin = carry.origin;
241
- const ruleStoreUnreadable = carry.ruleStoreUnreadable;
242
241
  return {
243
242
  ...(carry.denialLimitFallback !== undefined ? { denialLimitFallback: { ...carry.denialLimitFallback } } : {}),
244
243
  ...(isAskOrigin(origin) ? { origin } : {}),
245
- ...(isRuleStoreUnreadable(ruleStoreUnreadable) ? { ruleStoreUnreadable } : {}),
246
244
  };
247
245
  }
248
246
  export async function runToolGate(input) {
package/dist/core/mcp.js CHANGED
@@ -1671,7 +1671,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1671
1671
  parameters: (effectiveInputSchema ?? { type: "object" }),
1672
1672
  ...(mcpMaxResultSizeChars !== undefined ? { mcpMaxResultSizeChars } : {}),
1673
1673
  ...(mcpAlwaysLoad ? { mcpAlwaysLoad: true } : {}),
1674
- mcpRemoteName: remoteName,
1674
+ mcpRemoteName: remoteName, ...(t.annotations?.readOnlyHint === true ? { mcpReadOnlyHint: true } : {}),
1675
1675
  execute: async (_toolCallId, params, signal) => {
1676
1676
  const what = `The call to tool ${inlineUntrusted(remoteName)}`;
1677
1677
  if (isServerRevoked?.(spec.name) === true) {
@@ -10,7 +10,8 @@ export declare function messageRole(m: AgentMessage): string | undefined;
10
10
  * same terms rather than on four. The READ twin of `noteResponseModel` (`brain/stream-shared.ts`), which
11
11
  * states the same law on the write side; this side re-tests rather than trusting, because a HOST's own
12
12
  * Brain mints these messages too. `undefined` โ‡’ the provider named the stamped id, named nothing, or
13
- * named something unreadable โ€” ONE reading, never three.
13
+ * named something unreadable โ€” ONE reading, never three. Readable = {@link isProviderEchoShape}, the write
14
+ * twin's own predicate.
14
15
  */
15
16
  export declare function providerReportedModel(m: {
16
17
  readonly model?: unknown;
@@ -1,9 +1,10 @@
1
+ import { isProviderEchoShape } from "../brain/errors.js";
1
2
  export function messageRole(m) {
2
3
  return m.role;
3
4
  }
4
5
  export function providerReportedModel(m) {
5
6
  const reported = m.responseModel;
6
- if (typeof reported !== "string" || reported === "")
7
+ if (typeof reported !== "string" || !isProviderEchoShape(reported))
7
8
  return undefined;
8
9
  return reported === m.model ? undefined : reported;
9
10
  }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * The permission MODE's two derivations, beside the arbitration that reads it (`ask-origin.ts`):
3
+ *
4
+ * - {@link modePolicyOf} โ€” what a mode INSTALLS on the leg: the plan pair and the mode's own write gate (the
5
+ * reference implementation's per-mode defaults: `plan` keeps every tool mounted and ASKS at the call for a write
6
+ * and for a tool it did not define that is not read-only, `acceptEdits` writes inside the root without asking,
7
+ * `default` / `auto` / `dontAsk` ask on every covered write โ€” auto's classifier and dontAsk's refusal then act on
8
+ * that ask โ€” and `bypassPermissions` installs no gate). One writer of the mode (the caller, the steer verb, the
9
+ * model's `EnterPlanMode`), one interpreter (this engine): a library embedder's `plan` is not a word a serving
10
+ * layer has to translate first.
11
+ * - {@link PermissionModeController} โ€” the one RESOLUTION of a leg's effective mode, read live by every mode
12
+ * consumer (the gate's arbitration, the mode gate policy, the manifest, the children's ceiling): the
13
+ * requested word, clamped under the parent's live ceiling, folded back to `default` where the deployment
14
+ * denies it or the arming face is absent. A steer rewrites the requested word; nothing caches the answer.
15
+ */
16
+ import type { AgentTool, ExecutionEnv } from "../internal/harness-types.js";
17
+ import type { ContainmentFace } from "../tools/fs/containment-face.js";
18
+ import type { ToolCallRequest, ToolPolicy } from "./tool-policy.js";
19
+ import { type ToolRoster } from "./tool-roster.js";
20
+ import { type PermissionMode } from "./ask-origin.js";
21
+ import type { RuntimeCaps } from "./runtime-caps.js";
22
+ /** WHY an effective mode is not the requested one: the deployment's deny bit (`RuntimeCaps.bypassPermissions` /
23
+ * `RuntimeCaps.autoMode`), a requested `auto` with no classifier face wired, or a child's declaration folded under
24
+ * its parent's ceiling. */
25
+ export declare const PERMISSION_MODE_FOLD_REASONS: readonly ["operator_deny", "face_absent", "inherited_ceiling"];
26
+ export type PermissionModeFoldReason = (typeof PERMISSION_MODE_FOLD_REASONS)[number];
27
+ /** The mode's seats on a leg. `writeGate`: `none` โ€” no mode write gate; `ask` โ€” every covered write asks;
28
+ * `accept_root` โ€” writes inside the leg's write roots (the task root and every additional working directory) are allowed,
29
+ * writes outside them ask; `plan` โ€” every write {@link planRefusesWrite} names is the mode's OWN question (the plan
30
+ * sentence, `decisionReason: "mode"`: a person may say yes, no rule and no automatic lane answers it). `fileCommands`: the
31
+ * shell commands whose ordinary question the mode answers itself ({@link modeAcceptsFileCommand}) โ€” the reference
32
+ * implementation's `acceptEdits` set ("auto-approve file edits and common file commands"); empty for every other mode.
33
+ * `writeFace`: the write face the mode states where no seat declared one (the mode row of {@link import("../tools/fs/containment-face.js").resolveContainmentFace}) โ€”
34
+ * `bypassPermissions` writes outside the write roots (`open`); every other mode states nothing and the default fence stands. */
35
+ export interface ModePolicy {
36
+ readonly enablePlanMode: boolean;
37
+ readonly writeGate: "none" | "ask" | "accept_root" | "plan";
38
+ readonly fileCommands: readonly string[];
39
+ readonly writeFace: ContainmentFace | undefined;
40
+ }
41
+ /** The `acceptEdits` file commands, verbatim from the reference implementation (2.1.280): a closed set. */
42
+ export declare const ACCEPT_EDITS_FILE_COMMANDS: readonly ["mkdir", "touch", "rm", "rmdir", "mv", "cp", "sed"];
43
+ /** The mode โ†’ seats table, total over {@link PermissionMode}. */
44
+ export declare function modePolicyOf(mode: PermissionMode): ModePolicy;
45
+ /**
46
+ * Is this call a shell command the mode's {@link ModePolicy.fileCommands} cover โ€” every segment a SIMPLE command
47
+ * whose command word is, as written, one of them? The reference implementation's reading (it compares the first
48
+ * word of each command against its list): no redirection, no pipe or background, no substitution, no leading
49
+ * assignment, no wrapper in front, a literal unquoted command word โ€” `/bin/rm` and `"rm"` are not `rm`. The answer
50
+ * is only ever a license for the mode to answer the call's ORDINARY question: the gate's arbitration reads it beside
51
+ * the class set and the read station's `writesPlaced` fact (every write target established inside the write roots, no
52
+ * removal of a root, every program in the text), so a deny rule, an ask rule, the delete guard, a read-boundary finding
53
+ * or a write the station could not place still stands. A `run_in_background` call is not accepted (as a `&` is not).
54
+ */
55
+ export declare function modeAcceptsFileCommand(mode: PermissionMode, toolName: string, args: unknown): boolean;
56
+ /** What the mode gate policy resolves paths against. `acceptDirs` = the leg's WRITE ROOTS (the task root plus every additional
57
+ * working directory), the set `acceptEdits` allows writes inside โ€” absent โ‡’ the root alone; `exemptDirs` = the task's scratchpad,
58
+ * never asked about; `roster` = the leg's live tool roster (who defined the called tool and its effect) and `readOnlyDeclared` =
59
+ * whether the mounted tool of that name declares itself read-only, both read per call by {@link planRefusesWrite}. */
60
+ export interface ModeGateSeats {
61
+ env: ExecutionEnv;
62
+ rootPath: string;
63
+ acceptDirs?: readonly string[];
64
+ exemptDirs?: readonly string[];
65
+ roster?: {
66
+ readonly current: ToolRoster | undefined;
67
+ };
68
+ readOnlyDeclared?: (toolName: string) => boolean;
69
+ }
70
+ /** Does the LAST mounted instance of this name (the one a call dispatches to) declare itself read-only โ€” an MCP server's
71
+ * `readOnlyHint` ({@link AgentTool.mcpReadOnlyHint})? The mode gate's `readOnlyDeclared` seat over the leg's live tools. */
72
+ export declare function mountedToolDeclaresReadOnly(tools: readonly AgentTool[], toolName: string): boolean;
73
+ /**
74
+ * Is this call a WRITE the `plan` mode asks about? Two faces, one question โ€” does the call write by its declaration:
75
+ * ยท a covered path write (the catalog's protective floor: a file-write tool, or a same-name tool wearing its slot), and
76
+ * ยท a call to a tool the engine does not define (a caller's, an MCP server's, an A2A peer's) that is not read-only โ€” the
77
+ * reference implementation's plan arm for a tool it did not write (`!isReadOnly` โ‡’ the mode). Read-only is the tool's
78
+ * effect on the leg's roster being `read`, or โ€” only where that effect is the fail-closed default (nobody declared one)
79
+ * โ€” the mounted tool's own read-only declaration (`readOnlyDeclared`: an MCP server's `readOnlyHint`, which the
80
+ * reference reads for this question). A declared effect (the tool's own, or the deployment's `toolAxes`) always
81
+ * outranks the server's claim; the effect is the repeat-safety axis a server's self-claim never lowers.
82
+ * The engine's own tools answer through their own seats: a child a delegation spawns inherits `plan` as its ceiling, and
83
+ * the shell station and the tool marks raise their own questions, which reach a person under `plan` as under `default`.
84
+ * A name the roster does not carry dispatches nowhere and is not judged here.
85
+ */
86
+ export declare function planRefusesWrite(req: ToolCallRequest, roster: ToolRoster | undefined, readOnlyDeclared?: (toolName: string) => boolean): boolean;
87
+ /**
88
+ * The mode's WRITE GATE as one live policy layer: every call reads the mode ref and dispatches to the gate the
89
+ * table names for it, so a steer changes the next call's gate without re-composing the chain. The three gates are
90
+ * built once (their root / exemption screens run at construction). The plan gate is the ask gate with the plan
91
+ * sentence in place of the ordinary write question: the same structural exemptions (the scratchpad, the advertised
92
+ * writable roots โ€” the reference exempts its scratchpad and agent-memory files under plan too) admit a write, everything
93
+ * else the mode names asks.
94
+ */
95
+ export declare function createModeGatePolicy(modeRef: {
96
+ readonly current: PermissionMode;
97
+ }, seats: ModeGateSeats): ToolPolicy;
98
+ /**
99
+ * The mode a resumed leg REQUESTS: the re-passed spec's word, else the word the checkpoint row recorded โ€” except where the
100
+ * resume approves or edits a `plan_review` over a row recorded in `plan`: the plan exits to the word the leg had before it
101
+ * (`prePlanMode`, the reference implementation's ExitPlanMode: `prePlanMode ?? "default"`; absent โ‡’ the leg's own default),
102
+ * unless the resume names another mode โ€” the approver's choice. A re-passed `plan` does not keep an approved plan open.
103
+ */
104
+ export declare function modeRequestAfterResume(seat: PermissionMode | undefined, recorded: {
105
+ readonly permissionMode?: PermissionMode;
106
+ readonly prePlanMode?: PermissionMode;
107
+ }, outcome: {
108
+ readonly gate: string;
109
+ readonly decision?: string;
110
+ } | undefined): PermissionMode | undefined;
111
+ /** The three-member answer every mode question gets โ€” the manifest member, the steer verb's return value and the
112
+ * fold notice's detail share it. `requested` is absent when the caller declared nothing (a root inherits `default`,
113
+ * a child inherits its parent). */
114
+ export interface PermissionModeReceipt {
115
+ readonly requested?: PermissionMode;
116
+ readonly effective: PermissionMode;
117
+ readonly foldedBy?: PermissionModeFoldReason;
118
+ }
119
+ /** The arming facts the controller folds a declared mode against, bound once the deployment's caps resolved. */
120
+ export interface PermissionModeArming {
121
+ readonly caps: Pick<RuntimeCaps, "bypassPermissions" | "autoMode"> | undefined;
122
+ readonly autoFacePresent: boolean;
123
+ /** Arm the classifier for a leg whose effective mode became `auto` after prepare (a steer, a parent's live ceiling);
124
+ * idempotent. A failed arming leaves no classifier: a steer is rolled back and rejects with the failure; an arming a
125
+ * read started (no caller awaits it) reports it through {@link onArmFault}. */
126
+ readonly armAuto?: () => Promise<void> | void;
127
+ /** Where an arming a read of {@link PermissionModeController.current} started reports its failure. Observe-only: a throw
128
+ * or a rejection from it is contained. */
129
+ readonly onArmFault?: (error: unknown) => void;
130
+ }
131
+ /** A live ceiling: the parent leg's own controller (`current` tracks the parent's steers), or a recorded word. */
132
+ export type PermissionModeCeiling = {
133
+ readonly current: PermissionMode;
134
+ } | PermissionMode;
135
+ /**
136
+ * The one resolution of a leg's effective mode. `current` is derived on every read from three inputs โ€”
137
+ * the requested word (the spec's, or the newest steer's whose arming has not failed), the ceiling (a parent's live controller, or the
138
+ * word a checkpoint recorded) and the arming facts โ€” so a parent's steer reaches a running child on the
139
+ * child's next read with no forwarding, and nothing holds a stale answer.
140
+ */
141
+ export declare class PermissionModeController {
142
+ private readonly requestedBySpec;
143
+ private readonly ceiling;
144
+ /** The steers that still count, oldest first: each is pending (its arming running) or took effect. A steer whose
145
+ * arming fails leaves the list; one that takes effect drops the older ones (a newer steer that took effect outranks
146
+ * them whatever they do next). */
147
+ private readonly steers;
148
+ private arming;
149
+ private readonly onFold;
150
+ private prePlan;
151
+ constructor(requestedBySpec: PermissionMode | undefined, ceiling: PermissionModeCeiling | undefined, opts?: {
152
+ onFold?: (receipt: PermissionModeReceipt) => void;
153
+ prePlanMode?: PermissionMode;
154
+ });
155
+ /** The declared word the leg had when a steer (or the model's `EnterPlanMode`) last moved it into `plan`, while it is
156
+ * there; a leg that started in plan has none, and a steer out of plan clears it. A checkpoint row records it. */
157
+ get prePlanMode(): PermissionMode | undefined;
158
+ /** The word the caller asked for right now: the newest steer that still counts, else the spec's. */
159
+ get requested(): PermissionMode | undefined;
160
+ private ceilingNow;
161
+ /** The declared mode after the ceiling clamp โ€” what a checkpoint records and what the leg's children inherit. */
162
+ get declared(): PermissionMode;
163
+ private declaration;
164
+ private armedForAuto;
165
+ /** Contains the fault observer: its synchronous throw and its rejection both stop here. */
166
+ private readonly faultSeat;
167
+ /** The effective mode: declared, then folded against the arming facts. A read that finds the leg in `auto` with its
168
+ * classifier not yet armed (a parent's steer reached this child through the live ceiling) starts the arming โ€” idempotent,
169
+ * not awaited: the call that observed the transition may still meet the person, the next one meets the classifier. A
170
+ * failure goes to {@link PermissionModeArming.onArmFault}; the leg then has no classifier, so its asks go on to the
171
+ * approval seat as under `default`. */
172
+ get current(): PermissionMode;
173
+ /** The three-member answer as it stands right now. */
174
+ receipt(): PermissionModeReceipt;
175
+ /** The ceiling's source moved to another leg (the parent session's next leg); `before` is this leg's receipt under the
176
+ * previous one. A ceiling fold the move produced โ€” or changed โ€” is announced, as the construction-time fold is. */
177
+ ceilingRebound(before: PermissionModeReceipt): void;
178
+ /** Bind the deployment's arming facts (once, after the caps resolved); announces a fold-back if one results. */
179
+ bindArming(arming: PermissionModeArming): PermissionModeReceipt;
180
+ /** A steer: the requested word changes; the arming fold is re-judged; a leg that became `auto` arms its classifier.
181
+ * When that arming fails the steer rejects with the failure, stops counting (the requested word is again the newest
182
+ * steer's that still counts, else the spec's โ€” a failed arming never undoes another steer, and never leaves the leg
183
+ * in `auto` on its own word), and the arming may start again on the leg's next move into `auto`. */
184
+ steer(mode: PermissionMode): Promise<PermissionModeReceipt>;
185
+ }
@@ -0,0 +1,184 @@
1
+ import { createFsWriteGatePolicy } from "./fs-write-gate-policy.js";
2
+ import { isProtectedWrite } from "./tool-registry.js";
3
+ import { rosterEntryNamed } from "./tool-roster.js";
4
+ import { clampPermissionMode } from "./ask-origin.js";
5
+ import { argvOf, lexShellText } from "../shell/lex.js";
6
+ import { COMMAND_RULE_TOOL } from "./permission-rule-model.js";
7
+ import { createSafeNotifier } from "./safe-notify.js";
8
+ export const PERMISSION_MODE_FOLD_REASONS = ["operator_deny", "face_absent", "inherited_ceiling"];
9
+ export const ACCEPT_EDITS_FILE_COMMANDS = ["mkdir", "touch", "rm", "rmdir", "mv", "cp", "sed"];
10
+ const MODE_POLICY_TABLE = {
11
+ plan: { enablePlanMode: true, writeGate: "plan", fileCommands: [], writeFace: undefined },
12
+ acceptEdits: { enablePlanMode: false, writeGate: "accept_root", fileCommands: ACCEPT_EDITS_FILE_COMMANDS, writeFace: undefined },
13
+ default: { enablePlanMode: false, writeGate: "ask", fileCommands: [], writeFace: undefined },
14
+ auto: { enablePlanMode: false, writeGate: "ask", fileCommands: [], writeFace: undefined },
15
+ dontAsk: { enablePlanMode: false, writeGate: "ask", fileCommands: [], writeFace: undefined },
16
+ bypassPermissions: { enablePlanMode: false, writeGate: "none", fileCommands: [], writeFace: "open" },
17
+ };
18
+ export function modePolicyOf(mode) {
19
+ return MODE_POLICY_TABLE[mode];
20
+ }
21
+ export function modeAcceptsFileCommand(mode, toolName, args) {
22
+ const commands = modePolicyOf(mode).fileCommands;
23
+ if (commands.length === 0 || toolName !== COMMAND_RULE_TOOL)
24
+ return false;
25
+ const command = args?.command;
26
+ if (typeof command !== "string" || args.run_in_background === true)
27
+ return false;
28
+ const lexed = lexShellText(command);
29
+ if ("reject" in lexed || lexed.segments.length === 0 || lexed.substitutions.length > 0)
30
+ return false;
31
+ return lexed.segments.every((segment) => {
32
+ if (segment.connector === "|" || segment.connector === "&" || segment.tokens.some((t) => t.kind !== "word"))
33
+ return false;
34
+ const head = argvOf(segment)[0];
35
+ return head !== undefined && head.assignment === undefined && head.expansions.length === 0 && head.raw === head.folded && commands.includes(head.raw);
36
+ });
37
+ }
38
+ export function mountedToolDeclaresReadOnly(tools, toolName) {
39
+ return tools.findLast((t) => t.name === toolName)?.mcpReadOnlyHint === true;
40
+ }
41
+ const ENGINE_ROSTER_SOURCES = new Set(["builtin", "synthetic"]);
42
+ export function planRefusesWrite(req, roster, readOnlyDeclared) {
43
+ if (isProtectedWrite(req))
44
+ return true;
45
+ const row = roster === undefined ? undefined : rosterEntryNamed(roster, req.toolName);
46
+ return row !== undefined && !ENGINE_ROSTER_SOURCES.has(row.source) && row.effect !== "read" && !(row.effectProvenance === "default" && readOnlyDeclared?.(row.name) === true);
47
+ }
48
+ const planAsk = (message) => ({ action: "ask", message, decisionReason: "mode" });
49
+ export function createModeGatePolicy(modeRef, seats) {
50
+ const exemptDirs = seats.exemptDirs !== undefined && seats.exemptDirs.length > 0 ? { exemptDirs: seats.exemptDirs } : {};
51
+ const askGate = createFsWriteGatePolicy({ env: seats.env, rootPath: seats.rootPath, ...exemptDirs, defaultWrite: "ask" });
52
+ const acceptRootGate = createFsWriteGatePolicy({ env: seats.env, rootPath: seats.rootPath, acceptDirs: seats.acceptDirs !== undefined && seats.acceptDirs.length > 0 ? [...seats.acceptDirs] : [seats.rootPath], ...exemptDirs, defaultWrite: "ask" });
53
+ const planGate = createFsWriteGatePolicy({ env: seats.env, rootPath: seats.rootPath, ...exemptDirs, defaultWrite: "ask", defaultAsk: (path, note) => planAsk(`Cannot write to ${path} while in plan mode.${note ?? ""}`) });
54
+ return {
55
+ async check(req, signal) {
56
+ const gate = modePolicyOf(modeRef.current).writeGate;
57
+ if (gate === "none")
58
+ return { action: "allow" };
59
+ if (gate === "plan") {
60
+ if (!planRefusesWrite(req, seats.roster?.current, seats.readOnlyDeclared))
61
+ return { action: "allow" };
62
+ if (!isProtectedWrite(req))
63
+ return planAsk(`Cannot call ${req.toolName} while in plan mode.`);
64
+ return planGate.check(req, signal);
65
+ }
66
+ return (gate === "ask" ? askGate : acceptRootGate).check(req, signal);
67
+ },
68
+ };
69
+ }
70
+ export function modeRequestAfterResume(seat, recorded, outcome) {
71
+ const exitsPlan = outcome?.gate === "plan_review" && outcome.decision !== "reject" && recorded.permissionMode === "plan";
72
+ if (!exitsPlan)
73
+ return seat ?? recorded.permissionMode;
74
+ return seat !== undefined && seat !== "plan" ? seat : recorded.prePlanMode;
75
+ }
76
+ export class PermissionModeController {
77
+ requestedBySpec;
78
+ ceiling;
79
+ steers = [];
80
+ arming;
81
+ onFold;
82
+ prePlan;
83
+ constructor(requestedBySpec, ceiling, opts) {
84
+ this.requestedBySpec = requestedBySpec;
85
+ this.ceiling = ceiling;
86
+ this.onFold = opts?.onFold;
87
+ if (requestedBySpec === "plan")
88
+ this.prePlan = opts?.prePlanMode;
89
+ }
90
+ get prePlanMode() {
91
+ return this.declared === "plan" ? this.prePlan : undefined;
92
+ }
93
+ get requested() {
94
+ return this.steers.at(-1)?.word ?? this.requestedBySpec;
95
+ }
96
+ ceilingNow() {
97
+ return this.ceiling === undefined ? undefined : typeof this.ceiling === "string" ? this.ceiling : this.ceiling.current;
98
+ }
99
+ get declared() {
100
+ return this.declaration().effective;
101
+ }
102
+ declaration() {
103
+ const ceiling = this.ceilingNow();
104
+ if (ceiling === undefined)
105
+ return { effective: this.requested ?? "default", clamped: false };
106
+ return clampPermissionMode(this.requested, ceiling);
107
+ }
108
+ armedForAuto = false;
109
+ faultSeat = createSafeNotifier();
110
+ get current() {
111
+ const effective = this.receipt().effective;
112
+ const arming = this.arming;
113
+ if (effective === "auto" && !this.armedForAuto && arming?.armAuto !== undefined) {
114
+ this.armedForAuto = true;
115
+ void new Promise((resolve) => resolve(arming.armAuto?.())).catch((e) => this.faultSeat.notifyAsync(() => arming.onArmFault?.(e), "permissionMode.onArmFault"));
116
+ }
117
+ return effective;
118
+ }
119
+ receipt() {
120
+ const { effective: declared, clamped } = this.declaration();
121
+ const requested = this.requested;
122
+ const base = requested !== undefined ? { requested } : {};
123
+ const a = this.arming;
124
+ const armingFold = a === undefined
125
+ ? undefined
126
+ : declared === "bypassPermissions" && a.caps?.bypassPermissions === false
127
+ ? "operator_deny"
128
+ : declared === "auto" && !a.autoFacePresent
129
+ ? "face_absent"
130
+ : declared === "auto" && a.caps?.autoMode === false
131
+ ? "operator_deny"
132
+ : undefined;
133
+ if (armingFold !== undefined)
134
+ return { ...base, effective: "default", foldedBy: armingFold };
135
+ if (clamped)
136
+ return { ...base, effective: declared, foldedBy: "inherited_ceiling" };
137
+ return { ...base, effective: declared };
138
+ }
139
+ ceilingRebound(before) {
140
+ const after = this.receipt();
141
+ if (after.foldedBy === "inherited_ceiling" && (before.foldedBy !== "inherited_ceiling" || before.effective !== after.effective))
142
+ this.onFold?.(after);
143
+ }
144
+ bindArming(arming) {
145
+ const before = this.receipt();
146
+ this.arming = arming;
147
+ const receipt = this.receipt();
148
+ if (receipt.foldedBy !== undefined && receipt.foldedBy !== before.foldedBy)
149
+ this.onFold?.(receipt);
150
+ return receipt;
151
+ }
152
+ async steer(mode) {
153
+ const entry = { word: mode };
154
+ const before = this.declared;
155
+ this.steers.push(entry);
156
+ const receipt = this.receipt();
157
+ const arming = this.arming;
158
+ if (receipt.effective === "auto" && arming?.armAuto !== undefined) {
159
+ this.armedForAuto = true;
160
+ try {
161
+ await arming.armAuto();
162
+ }
163
+ catch (e) {
164
+ this.armedForAuto = false;
165
+ const at = this.steers.indexOf(entry);
166
+ if (at >= 0)
167
+ this.steers.splice(at, 1);
168
+ throw e;
169
+ }
170
+ }
171
+ const at = this.steers.indexOf(entry);
172
+ if (at > 0)
173
+ this.steers.splice(0, at);
174
+ if (this.steers.at(-1) === entry) {
175
+ if (mode !== "plan")
176
+ this.prePlan = undefined;
177
+ else if (before !== "plan")
178
+ this.prePlan = before;
179
+ }
180
+ if (receipt.foldedBy !== undefined)
181
+ this.onFold?.(receipt);
182
+ return receipt;
183
+ }
184
+ }
@@ -10,7 +10,7 @@
10
10
  * checks), and the {@link OracleIsolationVerdict}.isolationClass tiering that CAPS a non-isolated grader at
11
11
  * `candidate_only` (never auto-accept).
12
12
  * - SERVICE: the actually-isolated grader env (E2B microVM with `capabilities.isolation === true`), minted by
13
- * the trusted control plane through {@link import("./types.js").RunnerDeps.graderEnvFactory}.
13
+ * the trusted control plane and handed to the repair loop as its grader env (the engine has no seat for it).
14
14
  *
15
15
  * ๐Ÿ”ด **Honest residual (design/77 ยง1 Residual + ยง9):** the bash write-probe is point-in-time and
16
16
  * necessary-not-sufficient โ€” core, running *inside the worker*, cannot prove a mount is truly immutable (it
@@ -37,8 +37,8 @@ export interface OracleIsolationVerdict {
37
37
  reason?: string;
38
38
  }
39
39
  /**
40
- * The isolated grader environment, provisioned by the trusted control plane. `provenance` is settable ONLY at
41
- * the {@link import("./types.js").RunnerDeps.graderEnvFactory} call site โ€” it is NEVER reconstructed from
40
+ * The isolated grader environment, provisioned by the trusted control plane. `provenance` is settable ONLY by
41
+ * the trusted control plane that mints the grader env โ€” it is NEVER reconstructed from
42
42
  * worker-visible state (a worker that could mint a `{provenance:'control_plane'}` object would defeat the whole
43
43
  * gate). `immutableOraclePaths` are the paths the oracle/spec is supposed to live behind (e.g. a hidden held-out
44
44
  * test dir) that the worker must not be able to corrupt; the bash write-probe (check C) targets them.