@sema-agent/core 7.53.0 → 7.54.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 (244) hide show
  1. package/CHANGELOG.md +277 -1
  2. package/LICENSE +3 -1
  3. package/dist/agents/peer-session-drain.d.ts +0 -1
  4. package/dist/agents/peer-session-drain.js +0 -2
  5. package/dist/agents/send-message-tool.js +3 -4
  6. package/dist/agents/subagent.d.ts +3 -5
  7. package/dist/agents/subagent.js +18 -11
  8. package/dist/agents/tool-filter.d.ts +7 -4
  9. package/dist/agents/tool-filter.js +7 -3
  10. package/dist/core/a2a.js +4 -2
  11. package/dist/core/allow-entry.d.ts +1 -0
  12. package/dist/core/allow-entry.js +1 -0
  13. package/dist/core/approval-object.d.ts +87 -0
  14. package/dist/core/approval-object.js +85 -0
  15. package/dist/core/arg-summary.d.ts +3 -2
  16. package/dist/core/arg-summary.js +17 -11
  17. package/dist/core/ask-origin.d.ts +43 -27
  18. package/dist/core/ask-origin.js +20 -11
  19. package/dist/core/ask-question.d.ts +0 -1
  20. package/dist/core/ask-question.js +4 -67
  21. package/dist/core/ask-unresolvable-notice.d.ts +3 -2
  22. package/dist/core/auto-compaction.d.ts +49 -50
  23. package/dist/core/auto-compaction.js +41 -51
  24. package/dist/core/auto-mode-prompt.d.ts +4 -2
  25. package/dist/core/auto-mode-prompt.js +4 -3
  26. package/dist/core/canonical-json.d.ts +69 -0
  27. package/dist/core/canonical-json.js +130 -8
  28. package/dist/core/checkpoint-requirements.d.ts +13 -7
  29. package/dist/core/checkpoint-requirements.js +4 -1
  30. package/dist/core/checkpoint-store.d.ts +33 -18
  31. package/dist/core/checkpoint-store.js +5 -4
  32. package/dist/core/child-clamp-seats.d.ts +45 -6
  33. package/dist/core/child-clamp-seats.js +56 -4
  34. package/dist/core/consolidate-scope.d.ts +1 -1
  35. package/dist/core/cross-boundary-taint.d.ts +213 -0
  36. package/dist/core/cross-boundary-taint.js +196 -0
  37. package/dist/core/decision-receipt.d.ts +44 -1
  38. package/dist/core/decision-receipt.js +16 -0
  39. package/dist/core/effective-path-target.d.ts +3 -0
  40. package/dist/core/effective-path-target.js +4 -0
  41. package/dist/core/engine-notice.d.ts +34 -2
  42. package/dist/core/engine-notice.js +17 -1
  43. package/dist/core/engine-storage-backend.d.ts +73 -0
  44. package/dist/core/engine-storage-backend.js +1 -0
  45. package/dist/core/file-history-store.d.ts +6 -2
  46. package/dist/core/file-history-store.js +1 -0
  47. package/dist/core/gate-fold.js +10 -2
  48. package/dist/core/gate-lanes.js +30 -21
  49. package/dist/core/gate-outcome.d.ts +7 -3
  50. package/dist/core/governance-codes.d.ts +7 -2
  51. package/dist/core/governance-codes.js +6 -0
  52. package/dist/core/hooks.d.ts +14 -14
  53. package/dist/core/hooks.js +1 -2
  54. package/dist/core/locked-config.d.ts +28 -1
  55. package/dist/core/locked-config.js +30 -3
  56. package/dist/core/lsp.d.ts +1 -1
  57. package/dist/core/mcp-server-spec.d.ts +66 -9
  58. package/dist/core/mcp-server-spec.js +52 -19
  59. package/dist/core/mcp.js +5 -4
  60. package/dist/core/memory-engine/engine.js +2 -1
  61. package/dist/core/memory-engine/export-bundle.d.ts +1 -1
  62. package/dist/core/memory-engine/export-bundle.js +1 -1
  63. package/dist/core/memory-engine/file-backend.d.ts +2 -6
  64. package/dist/core/memory-engine/file-backend.js +1 -12
  65. package/dist/core/memory.d.ts +3 -3
  66. package/dist/core/mode-policy.d.ts +13 -8
  67. package/dist/core/mode-policy.js +3 -2
  68. package/dist/core/mods/kernel/receive.d.ts +2 -1
  69. package/dist/core/mods/kernel/receive.js +7 -2
  70. package/dist/core/parent-spec-seat.d.ts +10 -13
  71. package/dist/core/parent-spec-seat.js +14 -6
  72. package/dist/core/pause-registry.d.ts +35 -1
  73. package/dist/core/pause-registry.js +10 -0
  74. package/dist/core/permission-rule-consent.d.ts +18 -5
  75. package/dist/core/permission-rule-consent.js +13 -8
  76. package/dist/core/permission-rule-model.d.ts +29 -6
  77. package/dist/core/permission-rule-model.js +6 -1
  78. package/dist/core/permission-rule-org.d.ts +37 -0
  79. package/dist/core/permission-rule-org.js +34 -42
  80. package/dist/core/permission-rule-session.js +2 -2
  81. package/dist/core/permission-rule-store.d.ts +8 -0
  82. package/dist/core/permission-rule-store.js +1 -0
  83. package/dist/core/permission-rule-sync.js +3 -2
  84. package/dist/core/read-only-shell.d.ts +1 -1
  85. package/dist/core/read-only-shell.js +30 -9
  86. package/dist/core/remote-env.d.ts +41 -1
  87. package/dist/core/remote-env.js +18 -0
  88. package/dist/core/resume-startup-cause.d.ts +18 -0
  89. package/dist/core/resume-startup-cause.js +12 -0
  90. package/dist/core/runner/approval-feedback.d.ts +46 -0
  91. package/dist/core/runner/approval-feedback.js +41 -0
  92. package/dist/core/runner/compaction-call-options.d.ts +3 -3
  93. package/dist/core/runner/compaction-call-options.js +2 -2
  94. package/dist/core/runner/contracts.d.ts +33 -41
  95. package/dist/core/runner/cross-boundary-lane.d.ts +87 -0
  96. package/dist/core/runner/cross-boundary-lane.js +190 -0
  97. package/dist/core/runner/decide-continuation.d.ts +4 -4
  98. package/dist/core/runner/decide-continuation.js +3 -1
  99. package/dist/core/runner/gate-exit.d.ts +32 -12
  100. package/dist/core/runner/gate-exit.js +75 -33
  101. package/dist/core/runner/inherited-ask-grants.js +2 -2
  102. package/dist/core/runner/person-path-rows.d.ts +2 -2
  103. package/dist/core/runner/prepare-acquire-reconcile.d.ts +3 -4
  104. package/dist/core/runner/prepare-acquire-reconcile.js +3 -3
  105. package/dist/core/runner/prepare-ask-lane.js +3 -3
  106. package/dist/core/runner/prepare-caps-and-workflow.d.ts +6 -5
  107. package/dist/core/runner/prepare-caps-and-workflow.js +3 -3
  108. package/dist/core/runner/prepare-config-doors.d.ts +12 -15
  109. package/dist/core/runner/prepare-config-doors.js +8 -3
  110. package/dist/core/runner/prepare-context-lane.js +6 -3
  111. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
  112. package/dist/core/runner/prepare-delegation-surface.js +2 -3
  113. package/dist/core/runner/prepare-execution-env.d.ts +1 -2
  114. package/dist/core/runner/prepare-execution-env.js +2 -1
  115. package/dist/core/runner/prepare-gate-stations.d.ts +2 -0
  116. package/dist/core/runner/prepare-gate-stations.js +4 -3
  117. package/dist/core/runner/prepare-hands-readface.d.ts +2 -2
  118. package/dist/core/runner/prepare-hands-readface.js +1 -1
  119. package/dist/core/runner/prepare-inherited-gate.d.ts +2 -0
  120. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  121. package/dist/core/runner/prepare-park-ask.d.ts +1 -1
  122. package/dist/core/runner/prepare-park-ask.js +9 -3
  123. package/dist/core/runner/prepare-policy-chain.js +2 -7
  124. package/dist/core/runner/prepare-safety-scan.d.ts +7 -2
  125. package/dist/core/runner/prepare-safety-scan.js +34 -27
  126. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -2
  127. package/dist/core/runner/prepare-suspend-saga.js +6 -2
  128. package/dist/core/runner/prepare-task.js +13 -10
  129. package/dist/core/runner/prepare-turn-wiring.d.ts +10 -5
  130. package/dist/core/runner/prepare-turn-wiring.js +14 -49
  131. package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
  132. package/dist/core/runner/prepare-wiring-manifest.js +2 -2
  133. package/dist/core/runner/prepare-workspace-restore.js +2 -2
  134. package/dist/core/runner/resume-admission.js +9 -9
  135. package/dist/core/runner/resume-apply.js +4 -4
  136. package/dist/core/runner/resume-policy-outcome.d.ts +2 -2
  137. package/dist/core/runner/resume-policy-outcome.js +18 -15
  138. package/dist/core/runner/resume-preflight.js +2 -2
  139. package/dist/core/runner/resume-review-outcome.js +3 -3
  140. package/dist/core/runner/run-leg.js +57 -33
  141. package/dist/core/runner/run-settle-and-teardown.js +2 -0
  142. package/dist/core/runner/run-terminal-adoption.js +15 -4
  143. package/dist/core/runner/session-file-state-replay.d.ts +5 -2
  144. package/dist/core/runner/session-file-state-replay.js +3 -2
  145. package/dist/core/runner/stream-steer-verb.js +2 -2
  146. package/dist/core/runner/tool-face-overlay.d.ts +24 -6
  147. package/dist/core/runner/tool-face-overlay.js +24 -15
  148. package/dist/core/runner-deps.d.ts +9 -9
  149. package/dist/core/session-store.d.ts +35 -16
  150. package/dist/core/session-store.js +20 -12
  151. package/dist/core/{shell-lexer.d.ts → shell-run-candidates.d.ts} +36 -12
  152. package/dist/core/{shell-lexer.js → shell-run-candidates.js} +145 -225
  153. package/dist/core/shell-wrapper-table.d.ts +5 -3
  154. package/dist/core/shell-wrapper-table.js +1 -1
  155. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
  156. package/dist/core/store-contracts/session-store-contract.d.ts +49 -0
  157. package/dist/core/store-contracts/session-store-contract.js +639 -0
  158. package/dist/core/store-contracts/session-store-incarnation-contract.d.ts +12 -0
  159. package/dist/core/store-contracts/session-store-incarnation-contract.js +5 -2
  160. package/dist/core/task-event.d.ts +6 -4
  161. package/dist/core/task-registry-agent.js +1 -0
  162. package/dist/core/task-registry-shared.d.ts +5 -0
  163. package/dist/core/task-registry.d.ts +7 -0
  164. package/dist/core/task-registry.js +17 -0
  165. package/dist/core/task-result.d.ts +1 -1
  166. package/dist/core/task-spec.d.ts +6 -6
  167. package/dist/core/tighten-task-spec.d.ts +6 -4
  168. package/dist/core/tighten-task-spec.js +2 -2
  169. package/dist/core/tool-catalog-entries.d.ts +1 -1
  170. package/dist/core/tool-catalog-entries.js +52 -52
  171. package/dist/core/tool-face-reader.d.ts +49 -0
  172. package/dist/core/tool-face-reader.js +94 -0
  173. package/dist/core/tool-face.d.ts +17 -8
  174. package/dist/core/tool-face.js +1 -0
  175. package/dist/core/tool-policy.d.ts +64 -23
  176. package/dist/core/tool-policy.js +62 -56
  177. package/dist/core/tool-roster.d.ts +46 -28
  178. package/dist/core/tool-roster.js +91 -14
  179. package/dist/core/tool-spec-members.js +2 -2
  180. package/dist/core/tool-spec.d.ts +20 -4
  181. package/dist/core/tools.js +2 -0
  182. package/dist/core/trace.d.ts +24 -2
  183. package/dist/core/types.d.ts +2 -2
  184. package/dist/core/types.js +1 -1
  185. package/dist/core/untrusted-text.d.ts +7 -0
  186. package/dist/core/untrusted-text.js +3 -8
  187. package/dist/core/user-attachment.d.ts +43 -0
  188. package/dist/core/user-attachment.js +92 -15
  189. package/dist/engine/compaction/compaction.d.ts +2 -2
  190. package/dist/engine/compaction/utils.d.ts +2 -2
  191. package/dist/engine/harness/types.d.ts +7 -4
  192. package/dist/engine/llm/types.d.ts +7 -2
  193. package/dist/engine/llm/types.js +2 -1
  194. package/dist/engine/session/repo-utils.d.ts +5 -0
  195. package/dist/engine/session/repo-utils.js +9 -0
  196. package/dist/engine/session/storage-base.js +2 -2
  197. package/dist/index.d.ts +23 -9
  198. package/dist/index.js +19 -8
  199. package/dist/internal/llm.d.ts +1 -1
  200. package/dist/internal/llm.js +1 -1
  201. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  202. package/dist/orchestration/run-workflow-tool.js +1 -0
  203. package/dist/orchestration/workflow-governance.d.ts +1 -1
  204. package/dist/orchestration/workflow.d.ts +3 -0
  205. package/dist/orchestration/workflow.js +2 -1
  206. package/dist/prompt-assembly/catalog/schema.d.ts +3 -0
  207. package/dist/prompt-assembly/catalog/schema.js +3 -0
  208. package/dist/prompt-assembly/overlay.js +2 -2
  209. package/dist/prompt-assembly/select.js +2 -2
  210. package/dist/prompts/default.d.ts +1 -1
  211. package/dist/shell/lex.d.ts +87 -8
  212. package/dist/shell/lex.js +283 -49
  213. package/dist/stores/file/index.d.ts +8 -6
  214. package/dist/tools/artifact/artifact-tool.js +10 -0
  215. package/dist/tools/fs/bash-program-position.d.ts +3 -15
  216. package/dist/tools/fs/bash-program-position.js +3 -10
  217. package/dist/tools/fs/bash-readonly-classifier.js +2 -0
  218. package/dist/tools/fs/fs-bash.d.ts +3 -3
  219. package/dist/tools/fs/fs-bash.js +72 -101
  220. package/dist/tools/fs/fs-read.d.ts +12 -0
  221. package/dist/tools/fs/fs-read.js +10 -9
  222. package/dist/tools/fs/fs-shared.d.ts +27 -31
  223. package/dist/tools/fs/fs-shared.js +12 -15
  224. package/dist/tools/fs/fs-write.js +1 -1
  225. package/dist/tools/fs/index.js +2 -2
  226. package/dist/tools/fs/read-deny-home.d.ts +1 -1
  227. package/dist/tools/fs/read-deny-home.js +5 -4
  228. package/dist/tools/fs/read-deny.d.ts +16 -3
  229. package/dist/tools/fs/read-deny.js +12 -9
  230. package/dist/tools/fs/safety.d.ts +7 -6
  231. package/dist/tools/fs/shell-read-bindings.d.ts +5 -3
  232. package/dist/tools/fs/shell-read-bindings.js +15 -16
  233. package/dist/tools/fs/shell-read-device.js +1 -1
  234. package/dist/tools/fs/shell-read-plan.js +36 -47
  235. package/dist/tools/fs/shell-read-word.d.ts +1 -6
  236. package/dist/tools/fs/shell-read-word.js +2 -20
  237. package/dist/tools/fs/shell-reentry-text.js +1 -1
  238. package/dist/tools/web.d.ts +2 -2
  239. package/package.json +4 -1
  240. package/test/export-surface.snapshot.json +5654 -5520
  241. package/wire-changes/core@7.53.0.json +30 -8
  242. package/wire-changes/core@7.54.0.json +2870 -0
  243. package/dist/core/shell-scan.d.ts +0 -60
  244. package/dist/core/shell-scan.js +0 -183
@@ -1,3 +1,12 @@
1
+ import { applyClampSeats, clampFieldSeatsOf } from "./child-clamp-seats.js";
2
+ import { assertNoRetiredKeys } from "./retired-keys.js";
3
+ const RETIRED_PARENT_SPEC_SEAT_KEYS = Object.freeze({
4
+ memoryOff: "the request-level memory-off fact is the clamp table's `memory` row now — carry `memory: { enabled: false }`",
5
+ });
6
+ export function assertParentSpecSeatCurrent(seat) {
7
+ if (seat !== undefined)
8
+ assertNoRetiredKeys(seat, RETIRED_PARENT_SPEC_SEAT_KEYS, "ParentSpecSeat");
9
+ }
1
10
  export function parentSpecSeatOf(spec, frozenOnQuestion) {
2
11
  return Object.freeze({
3
12
  ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
@@ -15,14 +24,15 @@ export function parentSpecSeatOf(spec, frozenOnQuestion) {
15
24
  ...(spec.fileHistory !== undefined ? { fileHistory: spec.fileHistory } : {}),
16
25
  ...(spec.promptForm !== undefined ? { promptForm: spec.promptForm } : {}),
17
26
  ...(spec.promptBinding !== undefined ? { promptBinding: spec.promptBinding } : {}),
18
- ...(spec.memory?.enabled === false ? { memoryOff: true } : {}),
27
+ ...clampFieldSeatsOf(spec),
19
28
  });
20
29
  }
21
30
  export function applyParentSpecSeat(child, seat) {
22
31
  if (seat === undefined)
23
32
  return child;
33
+ assertParentSpecSeatCurrent(seat);
24
34
  const fill = (key, value) => child[key] === undefined && value !== undefined ? { [key]: value } : {};
25
- return {
35
+ const landed = {
26
36
  ...child,
27
37
  ...fill("principal", seat.principal),
28
38
  ...fill("clientContext", seat.clientContext),
@@ -39,15 +49,13 @@ export function applyParentSpecSeat(child, seat) {
39
49
  ...fill("promptBinding", seat.promptBinding),
40
50
  ...(seat.oneShot === true ? { oneShot: true } : {}),
41
51
  ...(seat.memoryPersistenceCapable === false ? { memoryPersistenceCapable: false } : fill("memoryPersistenceCapable", seat.memoryPersistenceCapable)),
42
- ...(seat.memoryOff === true ? { memory: memoryTurnedOff(child.memory) } : {}),
43
52
  };
44
- }
45
- export function memoryTurnedOff(memory) {
46
- return { ...(memory ?? {}), enabled: false };
53
+ return applyClampSeats(landed, clampFieldSeatsOf(seat));
47
54
  }
48
55
  const SEAT_FACTS_ON_CTX = ["principal", "oneShot", "onQuestion", "memoryPersistenceCapable"];
49
56
  export function parentSpecSeatOfCtx(ctx) {
50
57
  const seat = ctx.parentSpecSeat;
58
+ assertParentSpecSeatCurrent(seat);
51
59
  const facts = SEAT_FACTS_ON_CTX.filter((k) => ctx[k] !== undefined);
52
60
  if (seat === undefined) {
53
61
  if (facts.length === 0)
@@ -3,10 +3,12 @@
3
3
  * hanging / what brings it back), the three word types and the resume match DERIVED from it, and the
4
4
  * registry-bound `gate` × `pendingAction` pair with its checked constructor. A vocabulary module on the
5
5
  * floor: it types against `checkpoint-store.ts` (type-only, no runtime edge) and `checkpoint-store.ts`
6
- * reaches down for the pair type.
6
+ * reaches down for the pair type and the card reading; its one runtime edge is the card vocabulary
7
+ * (`tool-face.ts`, itself a leaf).
7
8
  */
8
9
  import type { AssertAllKeysHandled } from "./ask-origin.js";
9
10
  import type { Checkpoint, CheckpointGate, CheckpointRow, PendingAction, ResumeOutcome } from "./checkpoint-store.js";
11
+ import { type ToolApprovalCard } from "./tool-face.js";
10
12
  /**
11
13
  * THE registry of pauses — one row per way a run can stop and wait, and the ONE place the three faces
12
14
  * of that pause are declared together: WHY it stopped (`CheckpointGate.kind`, the row key), WHAT it left
@@ -93,6 +95,38 @@ export declare function isQuestionGate(gate: {
93
95
  readonly kind: GateKind;
94
96
  readonly approvalCard?: unknown;
95
97
  }): boolean;
98
+ /** What a parked row's gate says about its approval card — the four answers {@link parkedApprovalCardOf} gives. */
99
+ export type ParkedApprovalCardReading =
100
+ /** An approval gate carrying a word of the closed card set (`TOOL_APPROVAL_CARDS`). */
101
+ {
102
+ readonly kind: "card";
103
+ readonly card: ToolApprovalCard;
104
+ }
105
+ /** An approval gate carrying no card. */
106
+ | {
107
+ readonly kind: "no_card";
108
+ }
109
+ /** An approval gate carrying a value outside the closed set (a word a later engine minted, a hand-edited row,
110
+ * a non-string). `word` is the value as read, unscreened: a caller that renders it bounds and redacts it. */
111
+ | {
112
+ readonly kind: "unknown_word";
113
+ readonly word: unknown;
114
+ }
115
+ /** Not an approval gate: a registered gate whose `pending` column is not `tool_approval`, or a gate kind the
116
+ * registry does not hold. No card is read on it. */
117
+ | {
118
+ readonly kind: "not_approval";
119
+ };
120
+ /**
121
+ * The one reading of a parked row's approval card. A gate is an approval gate when its kind is a registry row whose
122
+ * `pending` column is `tool_approval`; only there is the card read, and only a member of the closed set is a card.
123
+ * Each member of `gate` is read once (the value screened is the value answered). Pure: the answer says nothing about
124
+ * how loud an `unknown_word` should be — that is the caller's to decide.
125
+ */
126
+ export declare function parkedApprovalCardOf(gate: {
127
+ readonly kind: unknown;
128
+ readonly approvalCard?: unknown;
129
+ }): ParkedApprovalCardReading;
96
130
  /** The one resume-match predicate: does this outcome gate word answer a checkpoint of this kind? `wake`
97
131
  * answers any (it passed its own, stricter, non-gate validation upstream); every other word must be the
98
132
  * row's `resume` column. Replaces a six-arm hand-written disjunction. */
@@ -1,3 +1,4 @@
1
+ import { isToolApprovalCard } from "./tool-face.js";
1
2
  export const PAUSE_REGISTRY = {
2
3
  human: { pending: "tool_approval", resume: "policy_ask", facet: "A", taskStatus: "suspended", terminal: false },
3
4
  irreversible_ask: { pending: "tool_approval", resume: "policy_ask", facet: "A", taskStatus: "suspended", terminal: false },
@@ -15,6 +16,15 @@ export function facetOf(gate) {
15
16
  export function isQuestionGate(gate) {
16
17
  return PAUSE_REGISTRY[gate.kind].pending === "tool_approval" && gate.approvalCard === "question";
17
18
  }
19
+ export function parkedApprovalCardOf(gate) {
20
+ const kind = gate.kind;
21
+ if (!isGateKind(kind) || PAUSE_REGISTRY[kind].pending !== "tool_approval")
22
+ return { kind: "not_approval" };
23
+ const word = gate.approvalCard;
24
+ if (word === undefined)
25
+ return { kind: "no_card" };
26
+ return isToolApprovalCard(word) ? { kind: "card", card: word } : { kind: "unknown_word", word };
27
+ }
18
28
  export function resumeGateMatches(gateKind, outcomeGate) {
19
29
  return outcomeGate === "wake" || PAUSE_REGISTRY[gateKind].resume === outcomeGate;
20
30
  }
@@ -38,8 +38,15 @@ export interface RuleCandidate {
38
38
  rule: string;
39
39
  scope: RuleScope;
40
40
  }
41
+ /** The approval-record kinds, spelled once: {@link RuleApprovalKind} is derived from this tuple. */
42
+ export declare const RULE_APPROVAL_KINDS: readonly ["card", "import", "starter"];
41
43
  /** What the approval record is for — it decides the provenance the minted adds carry. */
42
- export type RuleApprovalKind = "card" | "import" | "starter";
44
+ export type RuleApprovalKind = (typeof RULE_APPROVAL_KINDS)[number];
45
+ /** The approval-record states, spelled once: {@link RuleApprovalState} is derived from this tuple. */
46
+ export declare const RULE_APPROVAL_STATES: readonly ["pending", "approved", "redeemed"];
47
+ /** Where one approval record stands: `pending` as minted, `approved` once a person confirms, `redeemed` once a
48
+ * redemption has recorded its dot (recorded before the rule is written, so a crash leaves a replayable intent). */
49
+ export type RuleApprovalState = (typeof RULE_APPROVAL_STATES)[number];
43
50
  /**
44
51
  * design/375 §4.1 — one "don't ask again" OPTION as the record stores it: references into the
45
52
  * record's `candidates` by index, never rule text of its own. The display triple (rule, match,
@@ -78,6 +85,10 @@ export type RuleOffer2 = {
78
85
  readonly uncoveredSegments?: number;
79
86
  readonly uncoveredDetail?: readonly UncoveredSegmentDetail[];
80
87
  };
88
+ /** The CURRENT record-form version stamp ({@link RuleApprovalRecord.schema}), spelled once: every mint writes it and
89
+ * every readback compares against it by equality (an older or absent stamp is a stale row, never a floor). A
90
+ * durable backend's stale gate reads this value rather than its own number. */
91
+ export declare const RULE_APPROVAL_RECORD_SCHEMA = 4;
81
92
  /** A durable record of one pending/settled human decision about minting rules.
82
93
  *
83
94
  * Identity (design/182 §4.5, additive): a v1 record carries `principal` and nothing else — it stays
@@ -101,9 +112,9 @@ export interface RuleApprovalRecord {
101
112
  * bargain, unchanged). `4`: every candidate carries its `behavior` (the settings import now carries
102
113
  * the deny/ask lists too) — a `schema: 3` row is stale by the same equality gate.
103
114
  */
104
- schema: 4;
115
+ schema: typeof RULE_APPROVAL_RECORD_SCHEMA;
105
116
  kind: RuleApprovalKind;
106
- state: "pending" | "approved" | "redeemed";
117
+ state: RuleApprovalState;
107
118
  candidates: RuleCandidate[];
108
119
  /**
109
120
  * The card's OPTION structure over `candidates` (design/375 §4.1). The flat candidate list stays
@@ -504,8 +515,10 @@ export declare function redeemRuleTicket(opts: {
504
515
  owner?: RuleOwner;
505
516
  deps: RuleConsentDeps;
506
517
  }): Promise<RedeemResult>;
518
+ /** The settings layer names, spelled once: {@link ImportedSettingsLayer} is derived from this tuple. */
519
+ export declare const IMPORTED_SETTINGS_LAYERS: readonly ["userSettings", "projectSettings", "localSettings"];
507
520
  /** The three user-editable settings layers this version reads. */
508
- export type ImportedSettingsLayer = "userSettings" | "projectSettings" | "localSettings";
521
+ export type ImportedSettingsLayer = (typeof IMPORTED_SETTINGS_LAYERS)[number];
509
522
  /** One layer to read. `root` is the canonical directory a project-layer rule is scoped to — taken from
510
523
  * where the FILE actually lives, since the local layer is not always at a repository root. */
511
524
  export interface CcImportLayer {
@@ -528,7 +541,7 @@ export interface ImportPreview {
528
541
  reason: string;
529
542
  }>;
530
543
  /**
531
- * ALWAYS EMPTY in this version — a compatibility seat, kept on the wire shape so an existing reader
544
+ * ALWAYS EMPTY in this version — a compatibility seat (RL-1336-import-translated), kept on the wire shape so an existing reader
532
545
  * keeps finding the key (removing a wire field would break readers for no gain).
533
546
  *
534
547
  * The seat once carried the entries whose MATCH FORM was rewritten on the way in (the space-star
@@ -1,14 +1,18 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { escapeForDisclosure, hasUnrenderableCharacters, parseRuleText, ruleAdmitsCommand, ruleBreadthWarningsOf, ruleToolGrammarOf, segmentCoverageOf, suggestRuleForName, suggestRulesForCommand, RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, isRuleBehavior, } from "./permission-rule-model.js";
2
+ import { escapeForDisclosure, hasUnrenderableCharacters, parseRuleText, ruleAdmitsCommand, ruleBreadthWarningsOf, ruleToolGrammarOf, segmentCoverageOf, suggestRuleForName, suggestRulesForCommand, EDITED_RULE_BREADTH_WARNING_CODES, UNCOVERED_SEGMENT_REASONS, RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, isRuleBehavior, } from "./permission-rule-model.js";
3
3
  import { effectiveOrThrow } from "./permission-rule-provider.js";
4
4
  import { errText, isValidConsentScope, landRuleAdd, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
5
+ const isUncoveredSegmentReason = (v) => UNCOVERED_SEGMENT_REASONS.some((word) => v === word);
6
+ export const RULE_APPROVAL_KINDS = Object.freeze(["card", "import", "starter"]);
7
+ export const RULE_APPROVAL_STATES = Object.freeze(["pending", "approved", "redeemed"]);
8
+ export const RULE_APPROVAL_RECORD_SCHEMA = 4;
5
9
  export class InMemoryRuleApprovalRecordStore {
6
10
  rows = new Map();
7
11
  async get(id) {
8
12
  const r = this.rows.get(id);
9
13
  if (r === undefined)
10
14
  return undefined;
11
- if (r.schema !== 4) {
15
+ if (r.schema !== RULE_APPROVAL_RECORD_SCHEMA) {
12
16
  return {
13
17
  staleSchema: true,
14
18
  id: r.id,
@@ -156,7 +160,7 @@ function approvalRecordDamageOf(rec) {
156
160
  if (typeof row !== "object" ||
157
161
  row === null ||
158
162
  typeof row.segment !== "string" ||
159
- (row.reason !== "redirection" && row.reason !== "no_rule_form" && row.reason !== "cap_overflow")) {
163
+ !UNCOVERED_SEGMENT_REASONS.some((word) => row.reason === word)) {
160
164
  return `card batch offer ${at} reason row ${ri} is not a segment/reason row from the closed reason set`;
161
165
  }
162
166
  }
@@ -195,7 +199,7 @@ function approvalRecordDamageOf(rec) {
195
199
  return "the edited seat is not an object";
196
200
  const w = rec.edited.warnings;
197
201
  if (w !== undefined) {
198
- if (!Array.isArray(w) || w.length !== 1 || (w[0] !== "broad_prefix" && w[0] !== "compound_prefix")) {
202
+ if (!Array.isArray(w) || w.length !== 1 || !EDITED_RULE_BREADTH_WARNING_CODES.some((code) => w[0] === code)) {
199
203
  return "the edited seat's warnings are not a single-code array from the closed breadth-code set";
200
204
  }
201
205
  }
@@ -238,7 +242,7 @@ export function ruleOffersOfRecord(rec) {
238
242
  return true;
239
243
  const segment = row.segment;
240
244
  const reason = row.reason;
241
- if (typeof segment !== "string" || (reason !== "redirection" && reason !== "no_rule_form" && reason !== "cap_overflow"))
245
+ if (typeof segment !== "string" || !isUncoveredSegmentReason(reason))
242
246
  return true;
243
247
  detail.push({ segment, reason });
244
248
  }
@@ -348,7 +352,7 @@ export async function prepareCardApproval(opts) {
348
352
  const record = {
349
353
  id: mintId(opts.deps, "rar"),
350
354
  ...recordIdentityOf(owner),
351
- schema: 4,
355
+ schema: RULE_APPROVAL_RECORD_SCHEMA,
352
356
  kind: "card",
353
357
  state: "pending",
354
358
  rev: 0,
@@ -667,6 +671,7 @@ async function applyRedemption(args) {
667
671
  function originOfRecordKind(kind) {
668
672
  return kind === "import" ? "imported-cc" : kind === "starter" ? "starter" : "user";
669
673
  }
674
+ export const IMPORTED_SETTINGS_LAYERS = Object.freeze(["userSettings", "projectSettings", "localSettings"]);
670
675
  const IMPORT_UNCOVERED_LAYERS = { flagSettings: "not-imported-v1", policySettings: "not-imported-v1" };
671
676
  export async function prepareCcImport(opts) {
672
677
  const owner = resolveCallerOwner(opts.principal, opts.owner, "prepareCcImport");
@@ -760,7 +765,7 @@ export async function prepareCcImport(opts) {
760
765
  const record = {
761
766
  id: mintId(opts.deps, "rar"),
762
767
  ...recordIdentityOf(owner),
763
- schema: 4,
768
+ schema: RULE_APPROVAL_RECORD_SCHEMA,
764
769
  kind: "import",
765
770
  state: "pending",
766
771
  rev: 0,
@@ -794,7 +799,7 @@ export async function prepareStarterBatch(opts) {
794
799
  const record = {
795
800
  id: mintId(opts.deps, "rar"),
796
801
  ...recordIdentityOf(owner),
797
- schema: 4,
802
+ schema: RULE_APPROVAL_RECORD_SCHEMA,
798
803
  kind: "starter",
799
804
  state: "pending",
800
805
  rev: 0,
@@ -15,7 +15,7 @@
15
15
  * The two DIRECTIONS read a command differently, on purpose: an allow must cover the WHOLE call
16
16
  * ({@link ruleAdmitsCommand} — one recorded yes clears exactly what it spells), while a deny/ask
17
17
  * speaks about any PROGRAM RUN the call may perform ({@link programRunReachOf} — whole command or
18
- * any segment of a chain, read with a real shell lexer, `shell-lexer.ts`, and answering `unreadable`
18
+ * any segment of a chain, read off the one shell lexer by `shell-run-candidates.ts`, and answering `unreadable`
19
19
  * for what it cannot read rather than silence). Precedence across behaviors is the upstream one,
20
20
  * deny > ask > allow ({@link RULE_BEHAVIOR_PRECEDENCE}, the one table every adjudication reads).
21
21
  *
@@ -235,6 +235,10 @@ export declare function nameRuleSubjectOf(toolName: string, args: unknown): stri
235
235
  * name, as a policy's rule does. Every lane that judges a call by a rule's word reads it here: the policy compiler, the
236
236
  * person's stored rows, the org partition, the allow lists. Empty ⇒ no grammar speaks for the call. */
237
237
  export declare function laneToolNamesOf(req: Pick<import("./tool-policy.js").ToolCallRequest, "toolName" | "face">): readonly string[];
238
+ /** The match words a persisted rule is stored with, spelled once: {@link PersistedRuleMatch} is derived from this
239
+ * tuple (each form is documented there). A store backend that checks a stored row's `match` reads this value
240
+ * rather than keeping its own list. */
241
+ export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildcard", "subpath", "path", "name", "name_prefix", "host", "subdomain"];
238
242
  /**
239
243
  * Match forms, one family per grammar (`prefix` and `wildcard` are one predicate):
240
244
  * · `exact` — admits the one string the rule spells.
@@ -261,7 +265,7 @@ export declare function laneToolNamesOf(req: Pick<import("./tool-policy.js").Too
261
265
  * · `host` / `subdomain` — the DOMAIN grammar ({@link parseDomainRuleContent}): one exact host, or every
262
266
  * host below one domain.
263
267
  */
264
- export type PersistedRuleMatch = "exact" | "prefix" | "wildcard" | "subpath" | "path" | "name" | "name_prefix" | "host" | "subdomain";
268
+ export type PersistedRuleMatch = (typeof PERSISTED_RULE_MATCHES)[number];
265
269
  /**
266
270
  * Where a rule applies — design/382 §4.1, the THREE consent dimensions.
267
271
  *
@@ -306,9 +310,12 @@ export interface RuleDot {
306
310
  actor: string;
307
311
  counter: number;
308
312
  }
313
+ /** The add provenance words, spelled once: {@link RuleAddOrigin} is derived from this tuple, and the sync wire's and
314
+ * the session overlay's add checks read it. */
315
+ export declare const RULE_ADD_ORIGINS: readonly ["user", "imported-cc", "starter"];
309
316
  /** Where one add came from. Stored PER ADD — never folded across dots (a folded provenance cannot
310
317
  * follow a dot that is later deleted on its own). */
311
- export type RuleAddOrigin = "user" | "imported-cc" | "starter";
318
+ export type RuleAddOrigin = (typeof RULE_ADD_ORIGINS)[number];
312
319
  /** One add of one logical rule, carrying its own dot and its own provenance. */
313
320
  export interface RuleAdd {
314
321
  dot: RuleDot;
@@ -462,7 +469,7 @@ export declare const BARE_INTERPRETER_NAMES: ReadonlySet<string>;
462
469
  *
463
470
  * Refusing the keyword puts the whole command outside the ALLOW lane, which is the honest answer: no rule
464
471
  * can be minted for it and no allow matches it, so it asks. The DENY/ASK direction reads these shapes
465
- * with its own lexer (`shell-lexer.ts`: a prefixing keyword is stripped so `do curl evil.example` runs
472
+ * with its run-candidate view (`shell-run-candidates.ts`: a prefixing keyword is stripped so `do curl evil.example` runs
466
473
  * `curl`; a `for`/`case` header, whose commands need a syntax tree, is `unreadable` and asks).
467
474
  *
468
475
  * `!` is here for the same reason with a sharper edge: it is a keyword that PREFIXES a real command, so
@@ -780,11 +787,14 @@ export declare function programRunReachOf(rule: Pick<PersistedRule, "match" | "c
780
787
  /** The boolean face of {@link programRunReachOf}: `reached` only. The shadow view and the pins that
781
788
  * assert a reach read this; a lane that must fail closed on `unreadable` reads the outcome. */
782
789
  export declare function ruleReachesProgramRun(rule: Pick<PersistedRule, "match" | "command">, command: string): boolean;
790
+ /** The breadth-warning codes, spelled once: {@link EditedRuleBreadthWarning}'s `code` is derived from this tuple,
791
+ * and a consent record's `edited.warnings` stores exactly these words (its readback refuses any other). */
792
+ export declare const EDITED_RULE_BREADTH_WARNING_CODES: readonly ["broad_prefix", "compound_prefix"];
783
793
  /** design/382 §3.3-5 — one BREADTH note about a rule a person is about to persist: which wide shape
784
794
  * it has, said in words a surface can show beside the input line. A warning is never a refusal —
785
795
  * the closed refusal set is untouched — and never adjudication input. */
786
796
  export interface EditedRuleBreadthWarning {
787
- readonly code: "broad_prefix" | "compound_prefix";
797
+ readonly code: (typeof EDITED_RULE_BREADTH_WARNING_CODES)[number];
788
798
  /** The baseline sentence (a surface may reword it, never invert it — the same contract as the
789
799
  * §3.5 reason baselines). */
790
800
  readonly message: string;
@@ -1187,6 +1197,11 @@ export interface SegmentRuleSuggestion {
1187
1197
  * show a reader something other than what they authorize. */
1188
1198
  readonly segment: string;
1189
1199
  }
1200
+ /** The batch member discriminants as a value: the `kind` words of {@link RuleOfferBatchMember}, both directions
1201
+ * fenced at compile time ({@link RuleOfferBatchMemberKindsCoverEveryArm} / {@link RuleOfferBatchMemberArmsCoverEveryKind}).
1202
+ * A consumer applying the degrade arm below (a member whose `kind` is outside this set drops the whole batch offer)
1203
+ * reads this value rather than keeping its own list. */
1204
+ export declare const RULE_OFFER_BATCH_MEMBER_KINDS: readonly ["command", "directoryRead"];
1190
1205
  /**
1191
1206
  * design/382 §2.3 (B3, BREAKING) — one member of a batch offer, a DISCRIMINATED union keyed by
1192
1207
  * `kind`:
@@ -1215,6 +1230,10 @@ export type RuleOfferBatchMember = (SegmentRuleSuggestion & {
1215
1230
  /** The (folded) cd segment this authorization came from — rendering context, never adjudication input. */
1216
1231
  readonly segment: string;
1217
1232
  };
1233
+ /** Compile fence: an arm whose `kind` is not in {@link RULE_OFFER_BATCH_MEMBER_KINDS} turns the argument into that word. */
1234
+ export type RuleOfferBatchMemberKindsCoverEveryArm = AssertAllKeysHandled<Exclude<RuleOfferBatchMember["kind"], (typeof RULE_OFFER_BATCH_MEMBER_KINDS)[number]>>;
1235
+ /** Compile fence, the other direction: a word in {@link RULE_OFFER_BATCH_MEMBER_KINDS} that no arm carries. */
1236
+ export type RuleOfferBatchMemberArmsCoverEveryKind = AssertAllKeysHandled<Exclude<(typeof RULE_OFFER_BATCH_MEMBER_KINDS)[number], RuleOfferBatchMember["kind"]>>;
1218
1237
  /**
1219
1238
  * design/375 §3.1 — ONE "don't ask again" option on an approval card. A CLOSED discriminated union
1220
1239
  * (`kind`); consumers parse it per element, and an element whose `kind` they do not know is dropped
@@ -1278,6 +1297,10 @@ export type RuleOffer = {
1278
1297
  */
1279
1298
  readonly uncoveredDetail?: readonly UncoveredSegmentDetail[];
1280
1299
  };
1300
+ /** The uncovered-segment reason words, spelled once: {@link UncoveredSegmentDetail}'s `reason` is derived from this
1301
+ * tuple, {@link UNCOVERED_SEGMENT_REASON_BASELINE} is keyed by it, and a consent record's reason rows are read
1302
+ * against it. */
1303
+ export declare const UNCOVERED_SEGMENT_REASONS: readonly ["redirection", "no_rule_form", "cap_overflow"];
1281
1304
  /** design/382 §3.5 — the closed reason set for one uncovered segment of a batch offer.
1282
1305
  *
1283
1306
  * · `"redirection"` — the segment carries a shell redirection: refused by discipline (#490 修①,
@@ -1300,7 +1323,7 @@ export interface UncoveredSegmentDetail {
1300
1323
  /** The folded, trimmed segment text — raw post-rewrite command bytes, same contract as
1301
1324
  * {@link SegmentRuleSuggestion.segment}. */
1302
1325
  readonly segment: string;
1303
- readonly reason: "redirection" | "no_rule_form" | "cap_overflow";
1326
+ readonly reason: (typeof UNCOVERED_SEGMENT_REASONS)[number];
1304
1327
  }
1305
1328
  /**
1306
1329
  * design/382 §3.5-2 — the BASELINE "why + outlet" sentence per uncovered-segment reason: the
@@ -8,7 +8,7 @@ import { isShellFaceToolName, pathTargetOf, protectivePathTargetOf } from "./too
8
8
  import { heldValues } from "./tool-name-readings.js";
9
9
  import { carriesDotSegment, isAbsoluteForFamily, isIdentityDir, isPathIdentity, isShellRootedSpellingUnmapped, isUncIdentity, pathFamilyOf, pathIdentityOf } from "../tools/fs/safety.js";
10
10
  import { bindPathRules, judgeRuleRows, rowsWhereTheyLead } from "./path-rule-binding.js";
11
- import { readShellCommand } from "./shell-lexer.js";
11
+ import { readShellCommand } from "./shell-run-candidates.js";
12
12
  import { lineDescriptorsOfText, readOnlyLineObjection, readOnlyShellVerdict } from "./read-only-shell.js";
13
13
  import { ARTIFACT_ACTIONS } from "./artifact-host.js";
14
14
  import { ARTIFACT_TOOL_NAME } from "./tool-catalog-entries.js";
@@ -111,9 +111,11 @@ function ruleWordOf(name) {
111
111
  return COMMAND_RULE_TOOL;
112
112
  return ruleToolGrammarOf(name) !== undefined ? name : undefined;
113
113
  }
114
+ export const PERSISTED_RULE_MATCHES = Object.freeze(["exact", "prefix", "wildcard", "subpath", "path", "name", "name_prefix", "host", "subdomain"]);
114
115
  function isCommandMatch(match) {
115
116
  return match === "exact" || match === "prefix" || match === "wildcard";
116
117
  }
118
+ export const RULE_ADD_ORIGINS = Object.freeze(["user", "imported-cc", "starter"]);
117
119
  export const MAX_RULE_TEXT_CHARS = 512;
118
120
  export const NOTICE_RULE_TEXT_MAX = 2 * MAX_RULE_TEXT_CHARS;
119
121
  export function noticeRuleText(text) {
@@ -811,6 +813,7 @@ function admitsUnder(rule, command, reading) {
811
813
  return false;
812
814
  return folded === rule.command || folded.startsWith(rule.command + " ");
813
815
  }
816
+ export const EDITED_RULE_BREADTH_WARNING_CODES = Object.freeze(["broad_prefix", "compound_prefix"]);
814
817
  export function ruleBreadthWarningsOf(rule) {
815
818
  if (rule.match !== "prefix" && rule.match !== "wildcard")
816
819
  return [];
@@ -1159,6 +1162,8 @@ export function segmentCoverageOf(command, rules, call) {
1159
1162
  };
1160
1163
  });
1161
1164
  }
1165
+ export const RULE_OFFER_BATCH_MEMBER_KINDS = Object.freeze(["command", "directoryRead"]);
1166
+ export const UNCOVERED_SEGMENT_REASONS = Object.freeze(["redirection", "no_rule_form", "cap_overflow"]);
1162
1167
  export const UNCOVERED_SEGMENT_REASON_BASELINE = {
1163
1168
  redirection: "This part carries a redirection, so it is approved per use — no rule can cover this spelling. If you trust its redirection-free form, a rule can be written for that form (the card's edit row, or settings).",
1164
1169
  no_rule_form: "No rule form exists for this part; it can be approved per use.",
@@ -276,6 +276,27 @@ export declare function orgRuleListOf(rules: unknown): {
276
276
  } | {
277
277
  readonly refused: string;
278
278
  };
279
+ /** A snapshot the partition holds whose rules the list judgment refused: the envelope it carried and the words it was refused in,
280
+ * judged ONCE, on the list as it stood at the receipt. It has no `rules` at all (so no reading can take it for a table of zero
281
+ * rows) and keeps none of the caller's value (a list past the cap is a live array, and the partition awaits its persistence
282
+ * between the receipt and the validator, so a source that shrank the list it handed over in between would otherwise be admitted,
283
+ * uncopied and mutable, on a judgment it no longer meets). It is never installed or served. */
284
+ export interface RefusedOrgSnapshot extends Omit<OrgRuleSnapshot, "rules"> {
285
+ readonly refused: string;
286
+ }
287
+ /** What the partition holds of one snapshot, fetched or read back: one that can be installed, or the refusal of one that cannot.
288
+ * Read it by its shape — `rules` is on the first arm only, and {@link validateOrgSnapshot} is the one way from a held snapshot to an
289
+ * installable one. */
290
+ export type HeldOrgSnapshot = OrgRuleSnapshot | RefusedOrgSnapshot;
291
+ /** A snapshot as the partition holds it — fetched from the source or read back from its persistence alike: its envelope's members
292
+ * ({@link ORG_SNAPSHOT_ENVELOPE_MEMBERS} through {@link scalarMembersOf}; anything else it carries is never read or kept), its
293
+ * rows as the one list reader reads them (each row its policy) — so a snapshot read back reads, and compares, exactly as the one
294
+ * fetched, and what is persisted is never larger than these. Rules the list judgment refuses (not a list, a Proxy, past the cap)
295
+ * are held as that refusal alone ({@link RefusedOrgSnapshot}; the list judgment runs no code of the value's); such a snapshot — and
296
+ * one that is not an object, whose envelope has no members — is never installed or served. A list that cannot be received, and a
297
+ * Proxy snapshot (its traps would answer for the envelope), throw: an envelope that cannot be copied. Exported for the pins; the
298
+ * package root does not carry it. */
299
+ export declare function orgSnapshotCopyOf(v: unknown): HeldOrgSnapshot;
279
300
  /**
280
301
  * The ONE reading of an org snapshot's rows: each row read once ({@link readOrgRow}), and the one judgment of a call
281
302
  * against them. Every reader of org rows goes through it — the partition's degraded line, the lane's adjudication,
@@ -319,6 +340,22 @@ export declare function orgUnreadableClause(reason: string): string;
319
340
  * exactly the ones that do not render.
320
341
  */
321
342
  export declare function describeUnreadableOrgRows(unreadable: readonly UnreadableOrgRow[], revision?: unknown): string | undefined;
343
+ /**
344
+ * Envelope validation of one held snapshot — what the staleness bound and the anti-rollback fence need to judge it. Answers
345
+ * the snapshot as an installable one, or the words it is refused in: a finite revision, a finite observation time not in the
346
+ * future beyond the skew allowance, and rules that are a LIST ({@link listRefusalOf}, judged at the receipt — a held snapshot
347
+ * whose list was refused there is answered in those words). The rows inside are not judged here: a row this layer cannot
348
+ * read is read as such by {@link readOrgRules}, and never takes the rest of the snapshot down with it.
349
+ *
350
+ * Run on BOTH the fetch path (before install) and the serving path (before a persisted snapshot adjudicates), so the
351
+ * two cannot diverge on what an installable snapshot is — see backlog #177. The only way from a {@link HeldOrgSnapshot} to an
352
+ * installable {@link OrgRuleSnapshot}: install and serve read the `admitted` arm.
353
+ */
354
+ export declare function validateOrgSnapshot(s: HeldOrgSnapshot, nowMs: number): {
355
+ readonly admitted: OrgRuleSnapshot;
356
+ } | {
357
+ readonly refused: string;
358
+ };
322
359
  /**
323
360
  * design/182 §7.2 — which org rule speaks for this call, if any: {@link readOrgRules}'s judgment, over a snapshot read for
324
361
  * this call alone. Matching uses the SAME parser and the SAME tightening reach the personal deny/ask rows use
@@ -81,7 +81,7 @@ export function createOrgRulePartition(cfg) {
81
81
  throw new Error(`the org snapshot source did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms`);
82
82
  if ("e" in settled)
83
83
  throw settled.e;
84
- fetched = orgSnapshotCopyOf(settled.v);
84
+ fetched = settled.v === undefined ? undefined : orgSnapshotCopyOf(settled.v);
85
85
  }
86
86
  catch (err) {
87
87
  fetchFailed = true;
@@ -102,29 +102,30 @@ export function createOrgRulePartition(cfg) {
102
102
  const persisted = await guardedRead("persistence");
103
103
  if (fetched === undefined)
104
104
  return persisted;
105
- const invalid = validateOrgSnapshot(fetched, nowMs);
106
- if (invalid !== undefined) {
107
- disclosures.push(`org snapshot refused: ${invalid}; the previously installed snapshot (if any) remains in force`);
105
+ const judged = validateOrgSnapshot(fetched, nowMs);
106
+ if ("refused" in judged) {
107
+ disclosures.push(`org snapshot refused: ${judged.refused}; the previously installed snapshot (if any) remains in force`);
108
108
  return persisted;
109
109
  }
110
+ const incoming = judged.admitted;
110
111
  const persistedFloor = persisted === undefined ? undefined : antiRollbackFloor(persisted);
111
112
  if (persisted !== undefined && !persistedMarkIsCoherent(persisted)) {
112
113
  disclosures.push(`the durable org state carries a high-water mark (${escapeForDisclosure(persisted.revisionHighWater)}) that is not a finite number at or above its own snapshot revision (${escapeForDisclosure(persisted.snapshot?.revision)}) — the anti-rollback fence is being held at ${escapeForDisclosure(persistedFloor)} instead, and the durable state should be repaired`);
113
114
  }
114
- if (persistedFloor !== undefined && fetched.revision < persistedFloor) {
115
- disclosures.push(`org snapshot revision ${fetched.revision} is below the installed high-water mark ${escapeForDisclosure(persistedFloor)} — refusing the rollback; the current deny set is unchanged`);
115
+ if (persistedFloor !== undefined && incoming.revision < persistedFloor) {
116
+ disclosures.push(`org snapshot revision ${incoming.revision} is below the installed high-water mark ${escapeForDisclosure(persistedFloor)} — refusing the rollback; the current deny set is unchanged`);
116
117
  return persisted;
117
118
  }
118
- if (persisted !== undefined && fetched.revision === persisted.snapshot.revision && sameOrgPolicyContent(fetched, persisted.snapshot) && fetched.fetchedAtMs <= persisted.snapshot.fetchedAtMs) {
119
+ if (persisted !== undefined && incoming.revision === persisted.snapshot.revision && sameOrgPolicyAsHeld(incoming, persisted.snapshot) && incoming.fetchedAtMs <= persisted.snapshot.fetchedAtMs) {
119
120
  return persisted;
120
121
  }
121
- if (persisted !== undefined && fetched.revision === persisted.snapshot.revision && !sameOrgPolicyContent(fetched, persisted.snapshot)) {
122
- disclosures.push(`org snapshot revision ${fetched.revision} equals the installed revision but carries DIFFERENT policy content — refusing the swap; the current deny set is unchanged`);
122
+ if (persisted !== undefined && incoming.revision === persisted.snapshot.revision && !sameOrgPolicyAsHeld(incoming, persisted.snapshot)) {
123
+ disclosures.push(`org snapshot revision ${incoming.revision} equals the installed revision but carries DIFFERENT policy content — refusing the swap; the current deny set is unchanged`);
123
124
  return persisted;
124
125
  }
125
126
  const state = {
126
- revisionHighWater: Math.max(persistedFloor !== undefined && Number.isFinite(persistedFloor) ? persistedFloor : 0, fetched.revision),
127
- snapshot: fetched,
127
+ revisionHighWater: Math.max(persistedFloor !== undefined && Number.isFinite(persistedFloor) ? persistedFloor : 0, incoming.revision),
128
+ snapshot: incoming,
128
129
  installedAtMs: nowMs,
129
130
  };
130
131
  try {
@@ -148,24 +149,25 @@ export function createOrgRulePartition(cfg) {
148
149
  disclosures.push(`the installed org snapshot claims a FUTURE observation time (${escapeForDisclosure(installed.snapshot.fetchedAtMs)} vs now ${decisionMs}) — org adjudication is UNAVAILABLE; the consuming gate must tighten every terminal allow to a real-approval ask`);
149
150
  return { source: "org", status: "unavailable", rules: [], disclosures };
150
151
  }
151
- const servingRefusal = validateOrgSnapshot(installed.snapshot, decisionMs);
152
- if (servingRefusal !== undefined) {
153
- disclosures.push(`the installed org snapshot (revision ${escapeForDisclosure(installed.snapshot.revision)}) does not meet this version's validator: ${servingRefusal} — org adjudication is UNAVAILABLE; the consuming gate must tighten every terminal allow to a real-approval ask`);
152
+ const serving = validateOrgSnapshot(installed.snapshot, decisionMs);
153
+ if ("refused" in serving) {
154
+ disclosures.push(`the installed org snapshot (revision ${escapeForDisclosure(installed.snapshot.revision)}) does not meet this version's validator: ${serving.refused} — org adjudication is UNAVAILABLE; the consuming gate must tighten every terminal allow to a real-approval ask`);
154
155
  return { source: "org", status: "unavailable", rules: [], disclosures };
155
156
  }
156
- if (withinBound(installed.snapshot)) {
157
+ const served = serving.admitted;
158
+ if (withinBound(served)) {
157
159
  const freshlyInstalled = fetched !== undefined && installed.snapshot === fetched;
158
160
  if (!freshlyInstalled && !fetchFailed && fetched === undefined) {
159
161
  disclosures.push("org snapshot source returned no snapshot — continuing on the last-known-good");
160
162
  }
161
- const degraded = describeUnreadableOrgRows(readListedOrgRules(installed.snapshot.rules).unreadable, installed.snapshot.revision);
163
+ const degraded = describeUnreadableOrgRows(readListedOrgRules(served.rules).unreadable, served.revision);
162
164
  if (degraded !== undefined)
163
165
  disclosures.push(degraded);
164
166
  return {
165
167
  source: "org",
166
168
  status: freshlyInstalled ? "fresh" : "last-known-good",
167
- rules: structuredClone(installed.snapshot.rules),
168
- revision: installed.snapshot.revision,
169
+ rules: structuredClone(served.rules),
170
+ revision: served.revision,
169
171
  disclosures,
170
172
  };
171
173
  }
@@ -203,6 +205,9 @@ export function sameOrgPolicyContent(a, b) {
203
205
  const policy = (r) => canonicalize(orgRowPolicyOf(r));
204
206
  return a.rules.every((r, i) => policy(b.rules[i]) === policy(r));
205
207
  }
208
+ function sameOrgPolicyAsHeld(a, b) {
209
+ return !("refused" in b) && sameOrgPolicyContent(a, b);
210
+ }
206
211
  const DISCLOSED_UNREADABLE_ROWS = 20;
207
212
  function reachOfRowHead(text, behavior) {
208
213
  const head = typeof text === "string" ? ruleSpellingOf(text)?.toolName : undefined;
@@ -221,13 +226,8 @@ function rowReaches(reach, words, face) {
221
226
  return reach.words.some((w) => words.includes(w)) || (face !== undefined && reach.faces.includes(face));
222
227
  }
223
228
  function listRefusalOf(rules) {
224
- if (typeof rules === "object" && rules !== null) {
225
- if (types.isProxy(rules))
226
- return "rules is a Proxy, not an array";
227
- const held = HELD_LIST_REFUSALS.get(rules);
228
- if (held !== undefined)
229
- return held;
230
- }
229
+ if (typeof rules === "object" && rules !== null && types.isProxy(rules))
230
+ return "rules is a Proxy, not an array";
231
231
  if (!Array.isArray(rules))
232
232
  return "rules is not an array";
233
233
  const cap = DEFAULT_CAPS.maxRules;
@@ -262,22 +262,14 @@ export function orgRuleListOf(rules) {
262
262
  const copy = received.value;
263
263
  return { rows: Array.from({ length: copy.length }, (_, i) => orgRowPolicyOf(Object.hasOwn(copy, i) ? copy[i] : undefined)) };
264
264
  }
265
- const HELD_LIST_REFUSALS = new WeakMap();
266
- function heldRefusalOf(words) {
267
- const held = Object.freeze({});
268
- HELD_LIST_REFUSALS.set(held, words);
269
- return held;
270
- }
271
- function orgSnapshotCopyOf(v) {
272
- if (v === undefined)
273
- return undefined;
265
+ export function orgSnapshotCopyOf(v) {
274
266
  if (typeof v === "object" && v !== null && types.isProxy(v))
275
267
  throw new Error("the snapshot is a Proxy, not data");
276
268
  const envelope = scalarMembersOf(v, ORG_SNAPSHOT_ENVELOPE_MEMBERS);
277
269
  const rules = typeof v === "object" && v !== null ? v.rules : undefined;
278
- const refusal = listRefusalOf(rules);
279
- if (refusal !== undefined)
280
- return { ...envelope, rules: heldRefusalOf(refusal) };
270
+ const refused = listRefusalOf(rules);
271
+ if (refused !== undefined)
272
+ return { ...envelope, refused };
281
273
  const list = orgRuleListOf(rules);
282
274
  if ("refused" in list)
283
275
  throw new Error(`the snapshot's ${list.refused}`);
@@ -379,15 +371,15 @@ export function describeUnreadableOrgRows(unreadable, revision) {
379
371
  return (`the org snapshot${revision !== undefined ? ` (revision ${escapeForDisclosure(revision)})` : ""} is served DEGRADED: ${unreadable.length} row(s) this layer cannot read — ${named}${more > 0 ? ` and ${more} more` : ""}; ` +
380
372
  `each stands at its strictest reading over the calls it may reach (a deny row, or one whose behavior cannot be read, refuses them; an ask row asks a person), and every readable row stands as published — republish the named rows in the shell tool's command grammar`);
381
373
  }
382
- function validateOrgSnapshot(s, nowMs) {
374
+ export function validateOrgSnapshot(s, nowMs) {
383
375
  if (typeof s.revision !== "number" || !Number.isFinite(s.revision))
384
- return `revision is not a finite number (${escapeForDisclosure(s.revision)})`;
376
+ return { refused: `revision is not a finite number (${escapeForDisclosure(s.revision)})` };
385
377
  if (typeof s.fetchedAtMs !== "number" || !Number.isFinite(s.fetchedAtMs))
386
- return `fetchedAtMs is not a finite number (${escapeForDisclosure(s.fetchedAtMs)})`;
378
+ return { refused: `fetchedAtMs is not a finite number (${escapeForDisclosure(s.fetchedAtMs)})` };
387
379
  if (s.fetchedAtMs > nowMs + ORG_FETCHED_AT_SKEW_ALLOWANCE_MS) {
388
- return `fetchedAtMs is ${s.fetchedAtMs - nowMs}ms in the future (allowance ${ORG_FETCHED_AT_SKEW_ALLOWANCE_MS}ms) — a future-dated observation would satisfy the staleness bound indefinitely`;
380
+ return { refused: `fetchedAtMs is ${s.fetchedAtMs - nowMs}ms in the future (allowance ${ORG_FETCHED_AT_SKEW_ALLOWANCE_MS}ms) — a future-dated observation would satisfy the staleness bound indefinitely` };
389
381
  }
390
- return listRefusalOf(s.rules);
382
+ return "refused" in s ? { refused: s.refused } : { admitted: s };
391
383
  }
392
384
  export function orgRuleVerdictFor(rules, call) {
393
385
  return readListedOrgRules(rules).verdictFor(call);