@sema-agent/core 7.33.7 โ†’ 7.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/CHANGELOG.md +357 -0
  2. package/dist/agents/cross-session-judge.d.ts +16 -7
  3. package/dist/agents/cross-session-judge.js +18 -15
  4. package/dist/agents/peer-session-drain.d.ts +4 -0
  5. package/dist/agents/peer-session-drain.js +5 -2
  6. package/dist/agents/repair-loop.d.ts +1 -1
  7. package/dist/agents/subagent.d.ts +3 -0
  8. package/dist/agents/subagent.js +12 -3
  9. package/dist/brain/anthropic.js +1 -0
  10. package/dist/brain/open-responses.js +1 -0
  11. package/dist/brain/openai.js +1 -0
  12. package/dist/brain/stream-engine.d.ts +3 -0
  13. package/dist/brain/stream-engine.js +2 -1
  14. package/dist/core/a2a.d.ts +3 -1
  15. package/dist/core/a2a.js +1 -1
  16. package/dist/core/ask-origin.d.ts +351 -154
  17. package/dist/core/ask-origin.js +223 -45
  18. package/dist/core/ask-unresolvable-notice.d.ts +5 -2
  19. package/dist/core/ask-unresolvable-notice.js +4 -3
  20. package/dist/core/auto-mode-arming.d.ts +17 -4
  21. package/dist/core/auto-mode-arming.js +31 -6
  22. package/dist/core/auto-mode-defaults.d.ts +16 -7
  23. package/dist/core/auto-mode-defaults.js +2 -1
  24. package/dist/core/auto-mode-directive.d.ts +18 -30
  25. package/dist/core/auto-mode-directive.js +9 -18
  26. package/dist/core/auto-mode-prompt.d.ts +62 -1
  27. package/dist/core/auto-mode-prompt.js +42 -4
  28. package/dist/core/auto-mode-rebuild.d.ts +2 -1
  29. package/dist/core/auto-mode-rebuild.js +1 -0
  30. package/dist/core/auto-mode.d.ts +32 -3
  31. package/dist/core/auto-mode.js +80 -41
  32. package/dist/core/checkpoint-store.d.ts +50 -35
  33. package/dist/core/checkpoint-store.js +58 -3
  34. package/dist/core/child-clamp-seats.js +1 -0
  35. package/dist/core/classifier-unavailable-arm.d.ts +57 -0
  36. package/dist/core/classifier-unavailable-arm.js +17 -0
  37. package/dist/core/effective-path-target.d.ts +12 -0
  38. package/dist/core/effective-path-target.js +18 -7
  39. package/dist/core/engine-notice.d.ts +53 -2
  40. package/dist/core/fs-write-gate-policy.d.ts +5 -1
  41. package/dist/core/fs-write-gate-policy.js +4 -1
  42. package/dist/core/gate-fold.js +17 -1
  43. package/dist/core/gate-lanes.js +111 -46
  44. package/dist/core/gate-outcome.d.ts +40 -7
  45. package/dist/core/gate-outcome.js +26 -4
  46. package/dist/core/governance-codes.d.ts +1 -1
  47. package/dist/core/governance-codes.js +12 -0
  48. package/dist/core/hooks.d.ts +50 -29
  49. package/dist/core/hooks.js +1 -3
  50. package/dist/core/mcp.js +1 -1
  51. package/dist/core/mode-policy.d.ts +182 -0
  52. package/dist/core/mode-policy.js +179 -0
  53. package/dist/core/oracle-isolation.d.ts +3 -3
  54. package/dist/core/permission-rule-model.d.ts +16 -3
  55. package/dist/core/permission-rule-model.js +36 -3
  56. package/dist/core/permission-rule-provider.d.ts +22 -11
  57. package/dist/core/permission-rule-provider.js +17 -7
  58. package/dist/core/permission-rules.d.ts +23 -1
  59. package/dist/core/permission-rules.js +35 -8
  60. package/dist/core/persisted-rule-arms.d.ts +10 -9
  61. package/dist/core/persisted-rule-arms.js +9 -6
  62. package/dist/core/present-plan-tool.d.ts +1 -1
  63. package/dist/core/present-plan-tool.js +3 -3
  64. package/dist/core/read-only-shell.d.ts +19 -19
  65. package/dist/core/read-only-shell.js +38 -12
  66. package/dist/core/retired-keys.d.ts +9 -3
  67. package/dist/core/retired-keys.js +5 -1
  68. package/dist/core/runner/classifier-ladder.d.ts +43 -1
  69. package/dist/core/runner/classifier-ladder.js +29 -0
  70. package/dist/core/runner/contracts.d.ts +37 -35
  71. package/dist/core/runner/denial-limit-arms.d.ts +30 -7
  72. package/dist/core/runner/denial-limit-arms.js +15 -9
  73. package/dist/core/runner/env-capability-census.js +1 -1
  74. package/dist/core/runner/gate-exit.d.ts +30 -4
  75. package/dist/core/runner/gate-exit.js +28 -7
  76. package/dist/core/runner/permission-rule-lanes.d.ts +9 -5
  77. package/dist/core/runner/permission-rule-lanes.js +44 -40
  78. package/dist/core/runner/persisted-path-rows.d.ts +64 -0
  79. package/dist/core/runner/persisted-path-rows.js +89 -0
  80. package/dist/core/runner/prepare-artifact.d.ts +5 -1
  81. package/dist/core/runner/prepare-artifact.js +9 -3
  82. package/dist/core/runner/prepare-ask-lane.d.ts +5 -3
  83. package/dist/core/runner/prepare-ask-lane.js +1 -1
  84. package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
  85. package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
  86. package/dist/core/runner/prepare-gate-stations.d.ts +33 -6
  87. package/dist/core/runner/prepare-gate-stations.js +32 -19
  88. package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
  89. package/dist/core/runner/prepare-hands-readface.js +64 -53
  90. package/dist/core/runner/prepare-inherited-gate.d.ts +22 -22
  91. package/dist/core/runner/prepare-inherited-gate.js +37 -23
  92. package/dist/core/runner/prepare-park-ask.d.ts +2 -2
  93. package/dist/core/runner/prepare-park-ask.js +2 -1
  94. package/dist/core/runner/prepare-policy-chain.d.ts +22 -2
  95. package/dist/core/runner/prepare-policy-chain.js +39 -12
  96. package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
  97. package/dist/core/runner/prepare-protocol-tools.js +13 -3
  98. package/dist/core/runner/prepare-run-refs.d.ts +6 -6
  99. package/dist/core/runner/prepare-run-refs.js +9 -4
  100. package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
  101. package/dist/core/runner/prepare-safety-scan.js +19 -3
  102. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
  103. package/dist/core/runner/prepare-suspend-saga.js +6 -6
  104. package/dist/core/runner/prepare-task.js +51 -22
  105. package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
  106. package/dist/core/runner/prepare-wiring-manifest.js +5 -3
  107. package/dist/core/runner/read-root-grant.d.ts +9 -0
  108. package/dist/core/runner/read-root-grant.js +12 -0
  109. package/dist/core/runner/retired-runner-deps.js +1 -0
  110. package/dist/core/runner/run-identity-wiring.js +1 -1
  111. package/dist/core/runner/run-leg.js +24 -15
  112. package/dist/core/runner/run-notification-lane.js +1 -0
  113. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
  114. package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
  115. package/dist/core/runner/run-turn-boundary.js +1 -1
  116. package/dist/core/runner/runtask.js +3 -0
  117. package/dist/core/runner/stream-mode-verb.d.ts +27 -0
  118. package/dist/core/runner/stream-mode-verb.js +19 -0
  119. package/dist/core/runner/turn-attachments.d.ts +3 -3
  120. package/dist/core/runner/turn-attachments.js +1 -1
  121. package/dist/core/runner/write-protection-station.d.ts +7 -1
  122. package/dist/core/runner/write-protection-station.js +6 -3
  123. package/dist/core/runner-deps.d.ts +31 -34
  124. package/dist/core/runtime-caps.d.ts +12 -3
  125. package/dist/core/safety-axis-vocab.d.ts +2 -2
  126. package/dist/core/safety-axis-vocab.js +1 -1
  127. package/dist/core/secret-env.d.ts +16 -2
  128. package/dist/core/secret-env.js +25 -1
  129. package/dist/core/sensitive-path-policy.d.ts +4 -2
  130. package/dist/core/sensitive-path-policy.js +39 -30
  131. package/dist/core/spec-contract.d.ts +15 -0
  132. package/dist/core/spec-contract.js +19 -10
  133. package/dist/core/task-event.d.ts +1 -1
  134. package/dist/core/task-result.d.ts +1 -1
  135. package/dist/core/task-spec.d.ts +41 -89
  136. package/dist/core/task-stream.d.ts +16 -0
  137. package/dist/core/terminal-cause.js +1 -0
  138. package/dist/core/tighten-task-spec.d.ts +2 -2
  139. package/dist/core/tighten-task-spec.js +12 -14
  140. package/dist/core/tool-catalog-entries.js +4 -3
  141. package/dist/core/tool-policy.d.ts +54 -25
  142. package/dist/core/tool-policy.js +43 -2
  143. package/dist/core/tool-registry.d.ts +2 -0
  144. package/dist/core/tool-registry.js +3 -0
  145. package/dist/core/tool-spec.d.ts +8 -9
  146. package/dist/core/trace.d.ts +7 -6
  147. package/dist/core/types.d.ts +2 -1
  148. package/dist/core/wiring-manifest.d.ts +31 -5
  149. package/dist/core/wiring-manifest.js +28 -1
  150. package/dist/core/write-protect.d.ts +11 -1
  151. package/dist/core/write-protect.js +26 -15
  152. package/dist/engine/execution-env/kill-tree.js +4 -1
  153. package/dist/engine/execution-env/node-execution-env.d.ts +3 -0
  154. package/dist/engine/execution-env/node-execution-env.js +34 -35
  155. package/dist/engine/harness/types.d.ts +6 -7
  156. package/dist/engine/llm/types.d.ts +2 -4
  157. package/dist/engine/loop/types.d.ts +7 -0
  158. package/dist/index.d.ts +15 -5
  159. package/dist/index.js +12 -3
  160. package/dist/orchestration/run-spec.d.ts +4 -0
  161. package/dist/orchestration/run-spec.js +4 -3
  162. package/dist/orchestration/workflow-governance.js +1 -1
  163. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  164. package/dist/orchestration/workflow-size-guideline.js +2 -2
  165. package/dist/stores/file/fs-atomic.js +2 -1
  166. package/dist/tools/artifact/artifact-tool.d.ts +17 -0
  167. package/dist/tools/artifact/artifact-tool.js +16 -1
  168. package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
  169. package/dist/tools/fs/bash-readonly-classifier.js +88 -0
  170. package/dist/tools/fs/containment-face.d.ts +9 -3
  171. package/dist/tools/fs/containment-face.js +2 -0
  172. package/dist/tools/fs/file-read-check.d.ts +49 -0
  173. package/dist/tools/fs/file-read-check.js +71 -0
  174. package/dist/tools/fs/fs-bash.d.ts +3 -3
  175. package/dist/tools/fs/fs-read.d.ts +1 -0
  176. package/dist/tools/fs/fs-search-tools.d.ts +2 -2
  177. package/dist/tools/fs/fs-search-tools.js +4 -4
  178. package/dist/tools/fs/fs-shared.js +20 -6
  179. package/dist/tools/fs/fs-write.d.ts +4 -3
  180. package/dist/tools/fs/fs-write.js +9 -9
  181. package/dist/tools/fs/index.d.ts +39 -3
  182. package/dist/tools/fs/index.js +38 -15
  183. package/dist/tools/fs/read-deny-home.d.ts +93 -0
  184. package/dist/tools/fs/read-deny-home.js +225 -0
  185. package/dist/tools/fs/read-deny.d.ts +176 -34
  186. package/dist/tools/fs/read-deny.js +268 -61
  187. package/dist/tools/fs/safety.d.ts +6 -1
  188. package/dist/tools/fs/safety.js +18 -5
  189. package/dist/tools/fs/search.d.ts +3 -0
  190. package/dist/tools/fs/search.js +47 -9
  191. package/dist/tools/fs/shell-read-check.d.ts +42 -11
  192. package/dist/tools/fs/shell-read-check.js +188 -13
  193. package/dist/tools/fs/shell-read-establish.d.ts +7 -3
  194. package/dist/tools/fs/shell-read-establish.js +134 -21
  195. package/dist/tools/fs/shell-read-identity.d.ts +7 -0
  196. package/dist/tools/fs/shell-read-identity.js +21 -0
  197. package/dist/tools/fs/shell-read-judge.d.ts +92 -32
  198. package/dist/tools/fs/shell-read-judge.js +43 -18
  199. package/dist/tools/fs/shell-read-plan.d.ts +4 -0
  200. package/dist/tools/fs/shell-read-plan.js +26 -4
  201. package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
  202. package/dist/tools/fs/shell-read-write-targets.js +173 -0
  203. package/package.json +3 -1
  204. package/test/export-surface.snapshot.json +103 -11
  205. package/wire-changes/core@7.33.8.json +165 -0
  206. package/wire-changes/core@7.34.0.json +2621 -0
@@ -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
  /**
@@ -1349,8 +1347,8 @@ export interface ToolGateInput {
1349
1347
  onHookError?: (err: unknown) => void;
1350
1348
  /**
1351
1349
  * 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 โ€”
1350
+ * (the read-boundary judge on every shell face, plus the shape tier on every full shell and the
1351
+ * `TaskSpec.shellGate: "always"` floor โ€” design/80 D-E; the seat's `shellGatedTools` membership), not an explicit per-tool mark โ€”
1354
1352
  * attributes a tighten-deny to `source:"shellGate"` instead of `"safety"`.
1355
1353
  */
1356
1354
  shellGated?: boolean;
@@ -1386,7 +1384,8 @@ export interface ToolGateInput {
1386
1384
  * POLICY-class member of the fold: a deny-listed read is REFUSED (`deniedBy: "read_boundary"`, composing
1387
1385
  * under `deny > ask > allow`), an out-of-root or unestablishable read tightens a surviving allow to a
1388
1386
  * 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.
1387
+ * ordinary ask on the full shell face (under `auto` the classifier answers it, shown the read-deny table) and
1388
+ * is refused on the read-only face, and a vouched call is untouched.
1390
1389
  * `null` SYNCHRONOUSLY for a call it does not cover (no suspension point there), a promise for a shell
1391
1390
  * call. Absent โ‡’ no station (no shell mounted) and the decision path is byte-identical.
1392
1391
  */
@@ -1395,10 +1394,9 @@ export interface ToolGateInput {
1395
1394
  * design/143 ๆ‰น2 (A, CC 2.1.207 auto mode): when present, a surviving `ask` is routed to the
1396
1395
  * small-model policy CLASSIFIER before any human/durable resolution:
1397
1396
  * - 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").
1397
+ * - verdict `block` โ†’ deny (`decisionReason:"classifier"`, `source:"classifier"`; CC `automode-blocked`); `parse_error` too;
1398
+ * - `unavailable` โ†’ where `unavailable.approvalSeat` (a person, live or parked) the ask goes on, its text saying why;
1399
+ * else CC's refusal (`cause: "unavailable"`). Never counted by the denial limit (`classifierUnavailableArm`).
1402
1400
  * SCOPE (declared semantic): this covers every surviving ask INCLUDING the deterministic egress/
1403
1401
  * irreversibility safety tightens โ€” auto mode is the deployment's explicit choice to let the
1404
1402
  * classifier be the "explicit ask-resolution" for this session (its rule set covers exactly those
@@ -1425,7 +1423,32 @@ export interface ToolGateInput {
1425
1423
  * `RunnerDeps.autoMode.denialLimit`; a host driving the gate directly may hand its own.
1426
1424
  */
1427
1425
  denialTracking?: AutoModeDenialTracker;
1426
+ unavailable?: import("./classifier-unavailable-arm.js").ClassifierUnavailableSeat;
1428
1427
  };
1428
+ /**
1429
+ * The leg's EFFECTIVE permission mode, read at every arbitration (a steer changes the next call's answer). The
1430
+ * gate's one mode read point sits after the classifier and the sandbox leg and before the park / resolution:
1431
+ * `bypassPermissions` releases every non-immune class (`GateDisposition.released`), `dontAsk` refuses every ask
1432
+ * but a hook's or an operator authority's (`deniedBy: "mode"`), the other modes hand the ask on. ABSENT โ‡’ no
1433
+ * arbitration (a host driving the gate directly keeps the pre-mode chain); the classifier station then reads
1434
+ * `autoMode` presence alone as the arming fact.
1435
+ */
1436
+ permissionMode?: () => import("./ask-origin.js").PermissionMode;
1437
+ /** WHO put the tool's egress / irreversibility marks on the seat ({@link import("./ask-origin.js").ToolMarkSource});
1438
+ * absent or empty with a mark present reads as the deployment's own (fail-closed: an operator's mandate). */
1439
+ toolMarkSources?: ReadonlySet<import("./ask-origin.js").ToolMarkSource>;
1440
+ /** The deployment's A2A peer-isolation switch: a set switch makes the A2A egress default an operator mandate. */
1441
+ a2aIsolatePeers?: boolean;
1442
+ /** 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. */
1443
+ onModeReleased?: (info: {
1444
+ toolName: string;
1445
+ toolCallId: string;
1446
+ release: import("./gate-outcome.js").ModeRelease;
1447
+ readUnestablished?: true;
1448
+ }) => void;
1449
+ /** An ancestor station's arbitration of this call (the inherited lane arbitrates inside the fold and records
1450
+ * here); the exit reads it onto the disposition when the gate's own point did not arbitrate. */
1451
+ ancestorArbitrationOf?: (toolCallId: string) => import("./ask-origin.js").Arbitration | undefined;
1429
1452
  /**
1430
1453
  * The HEADLESS arm of the denial-limit fallback: the fallback ask was resolved by a deny no
1431
1454
  * person made (no approver wired; a blanket `"allow"` refused as no approver; an approver that
@@ -1456,6 +1479,7 @@ export interface ToolGateInput {
1456
1479
  toolCallId: string;
1457
1480
  cause: import("./ask-unresolvable-notice.js").AskUnresolvableCause;
1458
1481
  parkLaneExisted: boolean;
1482
+ remedy?: string;
1459
1483
  }) => void;
1460
1484
  /**
1461
1485
  * design/153 ยง2/ยง7.4 (ไปถ4 ๅคๅฎก, MED): true means this call's ask is MARKED โ€” an inherited ancestor
@@ -1506,7 +1530,7 @@ export interface ToolGateInput {
1506
1530
  * ยท #502: an ask the call's own reversibility probe declared STRUCTURAL (`probeMandated`) joins that
1507
1531
  * family. It is the one PER-CALL member of it, and the reason there has to be one: the built-in
1508
1532
  * 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 โ€”
1533
+ * the deployment declared but which arrives wearing the shape tier's own "maybe" โ€”
1510
1534
  * the very tier the bullet below keeps as this lane's home turf. Without a per-call member, a
1511
1535
  * compound was cleared segment-wise by the conjunction arm and an out-of-root read plus a sender
1512
1536
  * ran on two ordinary standing rules with no question asked.
@@ -1521,12 +1545,11 @@ export interface ToolGateInput {
1521
1545
  */
1522
1546
  persistedRules?: {
1523
1547
  /** 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
1548
+ * the rule set it stands on, a coverage-carrying clean negative, an unreadable report, or
1525
1549
  * `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.
1550
+ * read answers `{ unreadable: true }`, not `undefined`: the DECISION is the same (its rows take no part, as
1551
+ * for a thrown or timed-out lane) but the evidence records `"unavailable"`, not `"no_match"`; the lane owns
1552
+ * announcing the skip. A call unreadable against the rows answers `{ unreadable: true, reason }` (fails closed).
1530
1553
  *
1531
1554
  * For an ALLOW the set is the COVERAGE SET โ€” each member's canonical text plus its add DOTS. A
1532
1555
  * single rule admitting the whole command is a one-element set; a compound admitted by the
@@ -1708,14 +1731,13 @@ export interface AskCarry {
1708
1731
  /** The surviving ask's engine-stamped origin word ({@link import("./ask-origin.js").AskOrigin}), persisted
1709
1732
  * on the row so a durable card is rendered by the same word the synchronous card is. */
1710
1733
  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
1734
  /** The stamped mandate ({@link import("./tool-policy.js").AskRequest.mandated}) โ€” the park site's
1715
1735
  * rule-offer facts read it, so a parked mandated ask offers no rule, like the live card. */
1716
1736
  readonly mandated?: true;
1717
1737
  /** The table word stamped beside it ({@link import("./tool-policy.js").AskRequest.mandate}), for the row's twin. */
1718
1738
  readonly mandate?: import("./ask-origin.js").PersistedRuleMandate;
1739
+ /** The deployment's authorship ({@link import("./tool-policy.js").AskRequest.authoredBy}), for the row's twin. */
1740
+ readonly authoredBy?: "deployment";
1719
1741
  }
1720
1742
  /** What a durable row persists of an {@link AskCarry}: the fallback member COPIED (the row never aliases
1721
1743
  * the gate's object; its window is already `0` โ€” the gate's carry mint is the one writer of that
@@ -1724,7 +1746,6 @@ export interface AskCarry {
1724
1746
  export declare function askCarryRowMembers(carry: AskCarry | undefined): {
1725
1747
  denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1726
1748
  origin?: import("./ask-origin.js").AskOrigin;
1727
- ruleStoreUnreadable?: import("./ask-origin.js").RuleStoreUnreadable;
1728
1749
  };
1729
1750
  /**
1730
1751
  * 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) {
@@ -0,0 +1,182 @@
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
+ /** Bind the deployment's arming facts (once, after the caps resolved); announces a fold-back if one results. */
176
+ bindArming(arming: PermissionModeArming): PermissionModeReceipt;
177
+ /** A steer: the requested word changes; the arming fold is re-judged; a leg that became `auto` arms its classifier.
178
+ * When that arming fails the steer rejects with the failure, stops counting (the requested word is again the newest
179
+ * steer's that still counts, else the spec's โ€” a failed arming never undoes another steer, and never leaves the leg
180
+ * in `auto` on its own word), and the arming may start again on the leg's next move into `auto`. */
181
+ steer(mode: PermissionMode): Promise<PermissionModeReceipt>;
182
+ }
@@ -0,0 +1,179 @@
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
+ bindArming(arming) {
140
+ const before = this.receipt();
141
+ this.arming = arming;
142
+ const receipt = this.receipt();
143
+ if (receipt.foldedBy !== undefined && receipt.foldedBy !== before.foldedBy)
144
+ this.onFold?.(receipt);
145
+ return receipt;
146
+ }
147
+ async steer(mode) {
148
+ const entry = { word: mode };
149
+ const before = this.declared;
150
+ this.steers.push(entry);
151
+ const receipt = this.receipt();
152
+ const arming = this.arming;
153
+ if (receipt.effective === "auto" && arming?.armAuto !== undefined) {
154
+ this.armedForAuto = true;
155
+ try {
156
+ await arming.armAuto();
157
+ }
158
+ catch (e) {
159
+ this.armedForAuto = false;
160
+ const at = this.steers.indexOf(entry);
161
+ if (at >= 0)
162
+ this.steers.splice(at, 1);
163
+ throw e;
164
+ }
165
+ }
166
+ const at = this.steers.indexOf(entry);
167
+ if (at > 0)
168
+ this.steers.splice(0, at);
169
+ if (this.steers.at(-1) === entry) {
170
+ if (mode !== "plan")
171
+ this.prePlan = undefined;
172
+ else if (before !== "plan")
173
+ this.prePlan = before;
174
+ }
175
+ if (receipt.foldedBy !== undefined)
176
+ this.onFold?.(receipt);
177
+ return receipt;
178
+ }
179
+ }
@@ -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.
@@ -558,6 +558,18 @@ export declare function isUsablePathBase(base: string | undefined, family: PathF
558
558
  * no root reached nothing at all).
559
559
  */
560
560
  export declare function pathRuleReachOf(rule: Pick<PersistedRule, "match" | "command" | "behavior">, target: PathIdentity, bases: PathRuleBases): ProgramRunReachOutcome;
561
+ /**
562
+ * Where a path rule's reach STARTS under the call's bases โ€” its HEAD: the location the resolved pattern names before
563
+ * its first segment carrying `*` (the whole path when none does), or a directory rule's directory. Everything the rule
564
+ * reaches lies at or below the head, so a spelling of the head that the environment resolves elsewhere (a link, a
565
+ * firmlink, `/var` โ†’ `/private/var`) is a second spelling of everything the rule reaches โ€” the deny tables judge the
566
+ * rule at both ({@link import("../tools/fs/read-deny.js").withRuleRowsWhereTheyLead}; the reference's "physical twin"
567
+ * of a rule prefix, cli280.js 9766583). The head may be the bare anchor (`/`, a drive or share root โ€” `Read(/**)` at a
568
+ * drive root): an environment can resolve a drive or share root elsewhere. `undefined`: the pattern needs a base the
569
+ * call does not supply (the rule then reads `unreadable`, a reach, everywhere), or a directory rule names the bare
570
+ * anchor (it admits nothing).
571
+ */
572
+ export declare function pathRuleHeadOf(rule: Pick<PersistedRule, "match" | "command" | "behavior">, bases: PathRuleBases): PathIdentity | undefined;
561
573
  /**
562
574
  * Does this rule's command pattern admit `command`?
563
575
  *
@@ -793,7 +805,8 @@ export declare function eligiblePersisted(rule: PersistedRule, call: {
793
805
  * found BEFORE any conjunction: reporting one rule is narrower than reporting a set;
794
806
  * 3. the conjunction arm, compounds only: every segment must be admitted by some eligible rule under
795
807
  * the MATCH reading (the allow side's own under-approximating instrument โ€” never the deny side's
796
- * wider PROGRAM_RUNS reading). Per segment the FIRST table-order admitting rule is selected, and
808
+ * wider PROGRAM_RUNS reading), or be a read-only segment that allows itself ({@link readsOnlyAsSegment});
809
+ * at least one rule must speak. Per segment the FIRST table-order admitting rule is selected, and
797
810
  * the coverage set is those selections in segment order, de-duplicated โ€” the same command against
798
811
  * the same rule table always names the same set (reporting-order determinism, the historical
799
812
  * "order is defined only for reporting" contract extended to sets).
@@ -832,8 +845,8 @@ export interface PersistedRuleVerdict {
832
845
  readonly rules: readonly PersistedRule[];
833
846
  /** The call could not be READ against the person's deny/ask rows (see {@link programRunReachOf}):
834
847
  * `behavior` is `ask`, `rules` names the first tightening row that could not be judged, and this is
835
- * the lexer's reason. The lane projects it as an UNREADABLE answer โ€” the same fail-closed shape an
836
- * unreadable store takes โ€” never as a matched ask rule. */
848
+ * the lexer's reason. The lane projects it as an UNREADABLE answer carrying that reason (it fails closed)
849
+ * โ€” never as a matched ask rule. */
837
850
  readonly unreadable?: string;
838
851
  }
839
852
  /**