@sema-agent/core 7.7.0 → 7.9.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 (186) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/agents/agent-transcript-tool.js +2 -2
  3. package/dist/agents/list-agents-tool.js +2 -3
  4. package/dist/agents/observer.js +2 -2
  5. package/dist/agents/send-message-tool.js +2 -1
  6. package/dist/agents/subagent-steps.js +5 -4
  7. package/dist/agents/subagent.d.ts +1 -1
  8. package/dist/agents/subagent.js +2 -3
  9. package/dist/core/ask-origin.d.ts +12 -1
  10. package/dist/core/ask-origin.js +5 -1
  11. package/dist/core/ask-question.js +2 -2
  12. package/dist/core/checkpoint-store.d.ts +64 -24
  13. package/dist/core/context-edit.d.ts +0 -22
  14. package/dist/core/context-edit.js +2 -11
  15. package/dist/core/engine-notice.d.ts +6 -0
  16. package/dist/core/fs-write-gate-policy.d.ts +7 -1
  17. package/dist/core/fs-write-gate-policy.js +7 -7
  18. package/dist/core/gate-fold.js +2 -2
  19. package/dist/core/gate-lanes.js +53 -26
  20. package/dist/core/gate-outcome.d.ts +8 -4
  21. package/dist/core/gate-outcome.js +2 -1
  22. package/dist/core/governance-codes.d.ts +1 -1
  23. package/dist/core/governance-codes.js +6 -0
  24. package/dist/core/hooks.d.ts +64 -33
  25. package/dist/core/hooks.js +1 -1
  26. package/dist/core/lsp.js +2 -2
  27. package/dist/core/mcp-server-spec.d.ts +17 -0
  28. package/dist/core/mcp.js +3 -1
  29. package/dist/core/memory-engine/tools.js +4 -12
  30. package/dist/core/permission-rule-consent.d.ts +29 -36
  31. package/dist/core/permission-rule-consent.js +110 -60
  32. package/dist/core/permission-rule-model.d.ts +254 -55
  33. package/dist/core/permission-rule-model.js +323 -43
  34. package/dist/core/permission-rule-org.d.ts +9 -8
  35. package/dist/core/permission-rule-org.js +6 -15
  36. package/dist/core/permission-rule-provider.d.ts +10 -6
  37. package/dist/core/permission-rule-provider.js +12 -8
  38. package/dist/core/permission-rule-session.d.ts +7 -6
  39. package/dist/core/permission-rule-session.js +29 -10
  40. package/dist/core/permission-rule-store.d.ts +40 -18
  41. package/dist/core/permission-rule-store.js +68 -42
  42. package/dist/core/permission-rule-sync.d.ts +9 -3
  43. package/dist/core/permission-rule-sync.js +29 -25
  44. package/dist/core/permission-rule-syntax.d.ts +30 -0
  45. package/dist/core/permission-rule-syntax.js +44 -0
  46. package/dist/core/permission-rules.d.ts +85 -33
  47. package/dist/core/permission-rules.js +104 -88
  48. package/dist/core/persisted-rule-arms.d.ts +56 -0
  49. package/dist/core/persisted-rule-arms.js +48 -0
  50. package/dist/core/present-plan-tool.js +3 -4
  51. package/dist/core/runner/active-skill-scope.js +7 -4
  52. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  53. package/dist/core/runner/contracts.d.ts +46 -3
  54. package/dist/core/runner/permission-rule-lanes.d.ts +33 -26
  55. package/dist/core/runner/permission-rule-lanes.js +27 -21
  56. package/dist/core/runner/prepare-ask-lane.d.ts +1 -1
  57. package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -4
  58. package/dist/core/runner/prepare-caps-and-workflow.js +16 -11
  59. package/dist/core/runner/prepare-defer-classify.d.ts +5 -26
  60. package/dist/core/runner/prepare-defer-classify.js +10 -10
  61. package/dist/core/runner/prepare-delegation-surface.d.ts +5 -4
  62. package/dist/core/runner/prepare-delegation-surface.js +15 -17
  63. package/dist/core/runner/prepare-gate-stations.d.ts +4 -1
  64. package/dist/core/runner/prepare-gate-stations.js +4 -2
  65. package/dist/core/runner/prepare-hands-readface.d.ts +3 -2
  66. package/dist/core/runner/prepare-hands-readface.js +3 -2
  67. package/dist/core/runner/prepare-inherited-gate.d.ts +1 -1
  68. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  69. package/dist/core/runner/prepare-lsp.d.ts +4 -3
  70. package/dist/core/runner/prepare-lsp.js +3 -2
  71. package/dist/core/runner/prepare-memory-engine-session.d.ts +4 -4
  72. package/dist/core/runner/prepare-memory-engine-session.js +4 -2
  73. package/dist/core/runner/prepare-offload-wrappers.js +7 -5
  74. package/dist/core/runner/prepare-park-ask.d.ts +4 -0
  75. package/dist/core/runner/prepare-park-ask.js +4 -1
  76. package/dist/core/runner/prepare-project-context.d.ts +5 -23
  77. package/dist/core/runner/prepare-project-context.js +7 -5
  78. package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
  79. package/dist/core/runner/prepare-protocol-tools.d.ts +5 -1
  80. package/dist/core/runner/prepare-protocol-tools.js +38 -21
  81. package/dist/core/runner/prepare-question-face.d.ts +4 -3
  82. package/dist/core/runner/prepare-question-face.js +5 -3
  83. package/dist/core/runner/prepare-run-refs.d.ts +16 -0
  84. package/dist/core/runner/prepare-run-refs.js +6 -0
  85. package/dist/core/runner/prepare-safety-scan.js +48 -2
  86. package/dist/core/runner/prepare-task.js +37 -19
  87. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -2
  88. package/dist/core/runner/prepare-tool-disclosure-mount.js +3 -1
  89. package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
  90. package/dist/core/runner/prepare-wiring-manifest.js +3 -8
  91. package/dist/core/runner/resume-admission.d.ts +53 -0
  92. package/dist/core/runner/resume-admission.js +83 -0
  93. package/dist/core/runner/resume-apply.d.ts +50 -0
  94. package/dist/core/runner/resume-apply.js +184 -0
  95. package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
  96. package/dist/core/runner/resume-checkpoint-screen.js +108 -0
  97. package/dist/core/runner/resume-claim.d.ts +32 -0
  98. package/dist/core/runner/resume-claim.js +27 -0
  99. package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
  100. package/dist/core/runner/resume-internals-and-config.js +50 -0
  101. package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
  102. package/dist/core/runner/resume-policy-outcome.js +127 -0
  103. package/dist/core/runner/resume-preflight.d.ts +40 -0
  104. package/dist/core/runner/resume-preflight.js +122 -0
  105. package/dist/core/runner/resume-review-outcome.d.ts +30 -0
  106. package/dist/core/runner/resume-review-outcome.js +88 -0
  107. package/dist/core/runner/run-harness-handlers.d.ts +46 -0
  108. package/dist/core/runner/run-harness-handlers.js +345 -0
  109. package/dist/core/runner/run-turn-boundary.d.ts +69 -0
  110. package/dist/core/runner/run-turn-boundary.js +694 -0
  111. package/dist/core/runner/runtask.d.ts +20 -0
  112. package/dist/core/runner/runtask.js +129 -1774
  113. package/dist/core/runner/session-rule-policy.js +9 -4
  114. package/dist/core/runner/synthetic-tools.js +5 -8
  115. package/dist/core/runner/tool-disclosure.d.ts +4 -0
  116. package/dist/core/runner/tool-disclosure.js +9 -5
  117. package/dist/core/runner/tool-end-body.d.ts +12 -5
  118. package/dist/core/runner/tool-end-body.js +8 -7
  119. package/dist/core/runner/tool-face-overlay.d.ts +40 -0
  120. package/dist/core/runner/tool-face-overlay.js +125 -0
  121. package/dist/core/runner/tool-output-projection.d.ts +14 -1
  122. package/dist/core/runner/tool-output-projection.js +24 -18
  123. package/dist/core/runner/turn-attachments.d.ts +2 -2
  124. package/dist/core/sensitive-path-policy.d.ts +3 -0
  125. package/dist/core/sensitive-path-policy.js +6 -5
  126. package/dist/core/shared-memory/tools.js +3 -6
  127. package/dist/core/store-contracts/checkpoint-store-contract.js +23 -0
  128. package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
  129. package/dist/core/task-event.d.ts +10 -0
  130. package/dist/core/task-registry.js +4 -5
  131. package/dist/core/task-spec.d.ts +8 -0
  132. package/dist/core/tool-catalog-entries.d.ts +48 -0
  133. package/dist/core/tool-catalog-entries.js +174 -0
  134. package/dist/core/tool-conformance.d.ts +22 -0
  135. package/dist/core/tool-conformance.js +37 -0
  136. package/dist/core/tool-face.d.ts +171 -0
  137. package/dist/core/tool-face.js +25 -0
  138. package/dist/core/tool-policy.d.ts +24 -0
  139. package/dist/core/tool-policy.js +4 -2
  140. package/dist/core/tool-registry.d.ts +114 -0
  141. package/dist/core/tool-registry.js +114 -0
  142. package/dist/core/tool-result-budget.d.ts +0 -3
  143. package/dist/core/tool-result-budget.js +4 -3
  144. package/dist/core/tool-result-store.d.ts +0 -18
  145. package/dist/core/tool-result-store.js +2 -18
  146. package/dist/core/tool-roster.d.ts +459 -0
  147. package/dist/core/tool-roster.js +507 -0
  148. package/dist/core/tool-spec.d.ts +54 -3
  149. package/dist/core/tools.js +7 -0
  150. package/dist/core/wiring-manifest.d.ts +13 -0
  151. package/dist/core/wiring-manifest.js +2 -1
  152. package/dist/core/write-protect.d.ts +2 -1
  153. package/dist/core/write-protect.js +5 -4
  154. package/dist/engine/compaction/utils.d.ts +6 -0
  155. package/dist/engine/compaction/utils.js +19 -19
  156. package/dist/engine/loop/types.d.ts +6 -0
  157. package/dist/index.d.ts +17 -6
  158. package/dist/index.js +17 -6
  159. package/dist/orchestration/goal.js +2 -1
  160. package/dist/orchestration/run-spec.js +3 -5
  161. package/dist/orchestration/run-workflow-tool.js +2 -2
  162. package/dist/prompt-assembly/event-registry.js +3 -3
  163. package/dist/prompt-assembly/tool-catalog.d.ts +2 -2
  164. package/dist/stores/file/checkpoint-store.js +1 -1
  165. package/dist/stores/file/permission-rule-store.d.ts +28 -23
  166. package/dist/stores/file/permission-rule-store.js +64 -16
  167. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  168. package/dist/tools/fs/fs-bash.d.ts +1 -1
  169. package/dist/tools/fs/fs-bash.js +7 -11
  170. package/dist/tools/fs/fs-pdf.d.ts +1 -1
  171. package/dist/tools/fs/fs-read.js +2 -2
  172. package/dist/tools/fs/fs-search-tools.d.ts +0 -7
  173. package/dist/tools/fs/fs-search-tools.js +3 -16
  174. package/dist/tools/fs/fs-write.js +4 -6
  175. package/dist/tools/fs/notebook.d.ts +1 -1
  176. package/dist/tools/fs/repo-map.js +2 -2
  177. package/dist/tools/fs/safety.d.ts +3 -2
  178. package/dist/tools/fs/safety.js +6 -5
  179. package/dist/tools/monitor.js +2 -2
  180. package/dist/tools/scheduler-tools.js +5 -8
  181. package/dist/tools/task-list.js +5 -8
  182. package/dist/tools/todo.js +2 -2
  183. package/dist/tools/web.js +3 -6
  184. package/dist/tools/worktree.js +3 -4
  185. package/package.json +4 -1
  186. package/test/export-surface.snapshot.json +189 -17
@@ -1,8 +1,32 @@
1
1
  import { homedir } from "node:os";
2
- import { parsePermissionRule } from "./permission-rules.js";
2
+ import { parsePermissionRule } from "./permission-rule-syntax.js";
3
3
  import { compileReadDeny } from "../tools/fs/read-deny.js";
4
4
  import { carriesShellRedirection, parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
5
5
  import { inlineUntrusted } from "./untrusted-text.js";
6
+ import { pathTargetOf } from "./tool-registry.js";
7
+ export const RULE_BEHAVIORS = ["deny", "ask", "allow"];
8
+ const RULE_BEHAVIOR_SET = new Set(RULE_BEHAVIORS);
9
+ export function isRuleBehavior(v) {
10
+ return RULE_BEHAVIOR_SET.has(v);
11
+ }
12
+ export const RULE_BEHAVIOR_PRECEDENCE = {
13
+ deny: 0,
14
+ ask: 1,
15
+ allow: 2,
16
+ };
17
+ export const RULE_BEHAVIORS_BY_PRECEDENCE = [...RULE_BEHAVIORS].sort((a, b) => RULE_BEHAVIOR_PRECEDENCE[a] - RULE_BEHAVIOR_PRECEDENCE[b]);
18
+ export function readRowBehavior(v) {
19
+ if (v === undefined)
20
+ return "allow";
21
+ return isRuleBehavior(v) ? v : undefined;
22
+ }
23
+ export const COMMAND_RULE_TOOL = "Bash";
24
+ export const READ_RULE_TOOL = "Read";
25
+ export function ruleToolGrammarOf(toolName) {
26
+ if (toolName === COMMAND_RULE_TOOL)
27
+ return "command";
28
+ return pathTargetOf(toolName) !== undefined ? "path" : undefined;
29
+ }
6
30
  export const MAX_RULE_TEXT_CHARS = 512;
7
31
  export const BARE_INTERPRETER_NAMES = new Set([
8
32
  "node", "deno", "bun", "python", "python2", "python3", "perl", "ruby", "php", "osascript",
@@ -187,7 +211,11 @@ export function renderUntrustedCommandText(text, maxLen = DISCLOSED_RULE_TEXT_MA
187
211
  export function hasUnrenderableCharacters(text) {
188
212
  return CONTROL_CHARS_RE.test(text);
189
213
  }
190
- export function parseAllowRuleText(text, opts) {
214
+ export function parseRuleText(text, behavior) {
215
+ if (!isRuleBehavior(behavior)) {
216
+ return reject("invalid.grammar", `rule "${text}" was parsed under behavior ${escapeForDisclosure(behavior)}, which is not one of ${RULE_BEHAVIORS.join("/")}`);
217
+ }
218
+ const tighten = behavior !== "allow";
191
219
  if (text.length > MAX_RULE_TEXT_CHARS) {
192
220
  return reject("invalid.too_long", `rule text exceeds ${MAX_RULE_TEXT_CHARS} characters`);
193
221
  }
@@ -198,28 +226,11 @@ export function parseAllowRuleText(text, opts) {
198
226
  if (parsed.ruleContent === undefined) {
199
227
  return reject("invalid.grammar", `"${text}" is not a Tool(content) rule — a bare tool name claims the whole tool and is not a command rule`);
200
228
  }
201
- if (parsed.toolName === "Read") {
202
- const content = parsed.ruleContent;
203
- const m = /^\/\/(.+)\/\*\*$/.exec(content);
204
- if (m === null) {
205
- return reject("invalid.grammar", `only the Read(//abs-dir/**) directory form is a rule this version reads ("${text}") — relative, single-file, single-slash and no-tail spellings have no rule form`);
206
- }
207
- const bodyR = m[1];
208
- if (bodyR.includes("*")) {
209
- return reject("unsupported.wildcard", `a \`*\` inside the directory body matches anything at that position, which has no rule form ("${text}")`);
229
+ if (parsed.toolName !== COMMAND_RULE_TOOL) {
230
+ if (ruleToolGrammarOf(parsed.toolName) !== "path") {
231
+ return reject("unsupported.tool", `only Bash command rules and rules on path-targeting tools (Read, Edit, Write, …) have a rule form in this version (got "${parsed.toolName}")`);
210
232
  }
211
- const segments = bodyR.split("/");
212
- if (segments.some((s) => s === "" || s.trim() === "")) {
213
- return reject("invalid.grammar", `the directory body of "${text}" is not in the canonical spelling (an empty or whitespace-only path segment) — the lexical normal form is the only accepted spelling`);
214
- }
215
- if (segments.some((s) => s === "." || s === "..")) {
216
- return reject("invalid.path_traversal", `the directory body of "${text}" carries a \`.\` or \`..\` segment — the lexical normal form is the only accepted spelling, and a traversal segment is not normalized away`);
217
- }
218
- const dir = "/" + bodyR;
219
- return { rule: { rule: formatAllowRuleText(dir, "subpath"), tool: "Read", match: "subpath", command: dir } };
220
- }
221
- if (parsed.toolName !== "Bash") {
222
- return reject("unsupported.tool", `only Bash and Read rules are supported in this version (got "${parsed.toolName}")`);
233
+ return parsePathRuleContent(text, parsed.toolName, parsed.ruleContent, behavior);
223
234
  }
224
235
  const content = parsed.ruleContent;
225
236
  const prefixBody = /^(.+):\*$/.exec(content)?.[1];
@@ -256,7 +267,7 @@ export function parseAllowRuleText(text, opts) {
256
267
  return reject("unsupported.wildcard", `a \`<chain> *\` pattern names more than one command, and the trailing-star form has no chain reading — the compound-prefix form spells one with the \`:*\` marker ("${text}")`);
257
268
  }
258
269
  const head = commandBasename(shape.names[shape.names.length - 1] ?? "");
259
- if ((match === "prefix" || match === "wildcard") && BARE_INTERPRETER_NAMES.has(head) && opts?.direction !== "tighten") {
270
+ if ((match === "prefix" || match === "wildcard") && BARE_INTERPRETER_NAMES.has(head) && !tighten) {
260
271
  return reject("invalid.bare_interpreter_prefix", shape.segments.length > 1
261
272
  ? `prefix rule "${text}" leaves its final segment open-ended under the interpreter "${head}" — such a rule authorizes running arbitrary programs, which one approval click cannot be read as having granted (an exact rule naming the whole command line is accepted)`
262
273
  : `prefix rule "${text}" is headed by the interpreter "${head}" — such a rule authorizes running arbitrary programs, which one approval click cannot be read as having granted (an exact rule naming the whole command line is accepted)`);
@@ -265,12 +276,132 @@ export function parseAllowRuleText(text, opts) {
265
276
  if (command === undefined) {
266
277
  return reject("invalid.unbalanced_quotes", `rule "${text}" has unbalanced quoting, so the command it names is not determinable`);
267
278
  }
268
- return { rule: { rule: formatAllowRuleText(command, match), tool: "Bash", match, command } };
279
+ return { rule: { behavior, rule: formatRuleText(command, match, COMMAND_RULE_TOOL), tool: COMMAND_RULE_TOOL, match, command } };
269
280
  }
270
- export function formatAllowRuleText(command, match) {
281
+ export function formatRuleText(command, match, tool) {
271
282
  if (match === "subpath")
272
- return `Read(//${command.startsWith("/") ? command.slice(1) : command}/**)`;
273
- return `Bash(${command}${match === "prefix" ? ":*" : match === "wildcard" ? " *" : ""})`;
283
+ return `${tool}(//${command.startsWith("/") ? command.slice(1) : command}/**)`;
284
+ if (match === "path")
285
+ return `${tool}(${command})`;
286
+ return `${tool}(${command}${match === "prefix" ? ":*" : match === "wildcard" ? " *" : ""})`;
287
+ }
288
+ function parsePathRuleContent(text, tool, content, behavior) {
289
+ const dirForm = /^\/\/(.+)\/\*\*$/.exec(content);
290
+ if (dirForm !== null) {
291
+ const bodyR = dirForm[1];
292
+ if (bodyR.includes("*")) {
293
+ return reject("unsupported.wildcard", `a \`*\` inside the directory body matches anything at that position, which has no rule form ("${text}")`);
294
+ }
295
+ const segments = bodyR.split("/");
296
+ if (segments.some((sg) => sg === "" || sg.trim() === "")) {
297
+ return reject("invalid.grammar", `the directory body of "${text}" is not in the canonical spelling (an empty or whitespace-only path segment) — the lexical normal form is the only accepted spelling`);
298
+ }
299
+ if (segments.some((sg) => sg === "." || sg === "..")) {
300
+ return reject("invalid.path_traversal", `the directory body of "${text}" carries a \`.\` or \`..\` segment — the lexical normal form is the only accepted spelling, and a traversal segment is not normalized away`);
301
+ }
302
+ if (behavior === "allow" && tool !== READ_RULE_TOOL) {
303
+ return reject("unsupported.tool", `an allow rule on a path tool speaks for the Read directory form only — a standing approval of a WRITE by directory is not a rule form ("${text}"); write it as a deny or an ask, or approve the call`);
304
+ }
305
+ const dir = "/" + bodyR;
306
+ return { rule: { behavior, rule: formatRuleText(dir, "subpath", tool), tool, match: "subpath", command: dir } };
307
+ }
308
+ if (behavior === "allow") {
309
+ return tool === READ_RULE_TOOL
310
+ ? reject("invalid.grammar", `only the Read(//abs-dir/**) directory form is a rule this version reads ("${text}") — relative, single-file, single-slash and no-tail spellings have no rule form`)
311
+ : reject("unsupported.tool", `an allow rule on "${tool}" has no rule form — only the Read directory form is a standing approval by path ("${text}"); write it as a deny or an ask, or approve the call`);
312
+ }
313
+ if (content.trim() !== content || content === "") {
314
+ return reject("invalid.grammar", `the path pattern of "${text}" is empty or carries surrounding whitespace — the pattern is stored as spelled, so the spelling must be the one meant`);
315
+ }
316
+ if (content.startsWith("!"))
317
+ return reject("invalid.grammar", `a negated path pattern ("${text}") is not a rule — a rule says what it reaches, never what it excepts`);
318
+ if (/[?[\]{}]/.test(content)) {
319
+ return reject("unsupported.wildcard", `the path pattern of "${text}" carries a glob class (\`?\`, \`[]\`, \`{}\`) this lane does not read — only \`*\` inside a segment and \`**\` as a whole segment have a rule form`);
320
+ }
321
+ const body = content.startsWith("//") ? content.slice(2) : content.startsWith("~/") ? content.slice(2) : content.startsWith("/") ? content.slice(1) : content.startsWith("./") ? content.slice(2) : content;
322
+ const segments = body.split("/");
323
+ if (segments.some((sg) => sg === "" || sg.trim() === "")) {
324
+ return reject("invalid.grammar", `the path pattern of "${text}" carries an empty path segment — the lexical normal form is the only accepted spelling`);
325
+ }
326
+ if (segments.some((sg) => sg === "." || sg === "..")) {
327
+ return reject("invalid.path_traversal", `the path pattern of "${text}" carries a \`.\` or \`..\` segment — the lexical normal form is the only accepted spelling, and a traversal segment is not normalized away`);
328
+ }
329
+ if (segments.some((sg) => sg.includes("**") && sg !== "**")) {
330
+ return reject("unsupported.wildcard", `\`**\` in the path pattern of "${text}" must stand as a whole segment (\`a/**/b\`), never inside one`);
331
+ }
332
+ return { rule: { behavior, rule: formatRuleText(content, "path", tool), tool, match: "path", command: content } };
333
+ }
334
+ function resolvePathPattern(pattern, bases) {
335
+ if (pattern.startsWith("//"))
336
+ return "/" + pattern.slice(2);
337
+ const under = (base, rest) => {
338
+ const normal = base === undefined ? undefined : lexicalNormalAbsolutePathOf(base);
339
+ if (normal === undefined)
340
+ return undefined;
341
+ return (normal === "/" ? "" : normal) + "/" + rest;
342
+ };
343
+ if (pattern.startsWith("~/"))
344
+ return under(bases.home ?? homedir(), pattern.slice(2));
345
+ if (pattern.startsWith("/"))
346
+ return under(bases.root, pattern.slice(1));
347
+ return under(bases.cwd ?? bases.root, pattern.startsWith("./") ? pattern.slice(2) : pattern);
348
+ }
349
+ export function isUsablePathBase(base) {
350
+ return base !== undefined && lexicalNormalAbsolutePathOf(base) !== undefined;
351
+ }
352
+ function segmentGlobMatches(glob, segment) {
353
+ if (!glob.includes("*"))
354
+ return glob === segment;
355
+ const parts = glob.split("*");
356
+ let at = 0;
357
+ for (let i = 0; i < parts.length; i++) {
358
+ const part = parts[i];
359
+ if (i === 0) {
360
+ if (!segment.startsWith(part))
361
+ return false;
362
+ at = part.length;
363
+ continue;
364
+ }
365
+ if (i === parts.length - 1)
366
+ return part === "" || (segment.endsWith(part) && segment.length - part.length >= at);
367
+ const k = segment.indexOf(part, at);
368
+ if (k === -1)
369
+ return false;
370
+ at = k + part.length;
371
+ }
372
+ return true;
373
+ }
374
+ function globSegmentsReach(globs, target) {
375
+ const seen = new Set();
376
+ const width = target.length + 1;
377
+ const walk = (g, t) => {
378
+ if (g === globs.length)
379
+ return true;
380
+ const key = g * width + t;
381
+ if (seen.has(key))
382
+ return false;
383
+ seen.add(key);
384
+ if (globs[g] === "**")
385
+ return walk(g + 1, t) || (t < target.length && walk(g, t + 1));
386
+ if (t >= target.length)
387
+ return false;
388
+ return segmentGlobMatches(globs[g], target[t]) && walk(g + 1, t + 1);
389
+ };
390
+ return walk(0, 0);
391
+ }
392
+ export function pathRuleReaches(rule, target, bases) {
393
+ if (rule.match === "subpath")
394
+ return directoryRuleAdmits(rule, target);
395
+ if (rule.match !== "path")
396
+ return false;
397
+ if (!isLexicalNormalAbsoluteDir(target) && target !== "/")
398
+ return false;
399
+ const resolved = resolvePathPattern(rule.command, bases);
400
+ if (resolved === undefined)
401
+ return false;
402
+ const globs = resolved.split("/").filter((sg) => sg !== "");
403
+ const segments = target.split("/").filter((sg) => sg !== "");
404
+ return globSegmentsReach(globs, segments);
274
405
  }
275
406
  function hasUnescapedStar(s) {
276
407
  for (let i = 0; i < s.length; i++) {
@@ -290,8 +421,117 @@ export function ruleAdmitsCommand(rule, command) {
290
421
  export function ruleAdmitsProgramRun(rule, command) {
291
422
  return admitsUnder(rule, command, PROGRAM_RUNS_READING);
292
423
  }
424
+ export function ruleReachesProgramRun(rule, command) {
425
+ if (rule.match === "subpath" || rule.match === "path")
426
+ return false;
427
+ if (ruleAdmitsProgramRun(rule, command))
428
+ return true;
429
+ const spelled = stripRedirections(stripFdDuplications(command) ?? command) ?? command;
430
+ if (spelled !== command && ruleAdmitsProgramRun(rule, spelled))
431
+ return true;
432
+ const segments = ruleLaneSegmentsOf(spelled);
433
+ if (segments === undefined)
434
+ return false;
435
+ return segments.some((segment) => ruleAdmitsProgramRun(rule, segment));
436
+ }
437
+ function stripRedirections(command) {
438
+ const isSpace = (c) => c === " " || c === "\t";
439
+ const isRedirect = (c) => c === "<" || c === ">";
440
+ const isConnector = (c) => c === "|" || c === "&" || c === ";";
441
+ let out = "";
442
+ let quote;
443
+ let i = 0;
444
+ while (i < command.length) {
445
+ const ch = command[i];
446
+ if (quote !== undefined) {
447
+ out += ch;
448
+ if (ch === quote)
449
+ quote = undefined;
450
+ i++;
451
+ continue;
452
+ }
453
+ if (ch === "'" || ch === '"') {
454
+ quote = ch;
455
+ out += ch;
456
+ i++;
457
+ continue;
458
+ }
459
+ if (isRedirect(ch)) {
460
+ let k = out.length;
461
+ while (k > 0 && /\d/.test(out[k - 1]))
462
+ k--;
463
+ if (k < out.length && (k === 0 || isSpace(out[k - 1]) || isConnector(out[k - 1])))
464
+ out = out.slice(0, k);
465
+ let j = i;
466
+ while (j < command.length && isRedirect(command[j]))
467
+ j++;
468
+ if (j < command.length && command[j] === "|" && command[i] === ">" && j === i + 1)
469
+ j++;
470
+ while (j < command.length && isSpace(command[j]))
471
+ j++;
472
+ if (j < command.length && isConnector(command[j]))
473
+ return undefined;
474
+ let q2;
475
+ while (j < command.length) {
476
+ const c = command[j];
477
+ if (q2 !== undefined) {
478
+ if (c === q2)
479
+ q2 = undefined;
480
+ j++;
481
+ continue;
482
+ }
483
+ if (c === "'" || c === '"') {
484
+ q2 = c;
485
+ j++;
486
+ continue;
487
+ }
488
+ if (isSpace(c) || isRedirect(c) || isConnector(c))
489
+ break;
490
+ j++;
491
+ }
492
+ if (q2 !== undefined)
493
+ return undefined;
494
+ if (out !== "" && !isSpace(out[out.length - 1]))
495
+ out += " ";
496
+ i = j;
497
+ continue;
498
+ }
499
+ out += ch;
500
+ i++;
501
+ }
502
+ if (quote !== undefined)
503
+ return undefined;
504
+ return out.trim();
505
+ }
506
+ function stripFdDuplications(command) {
507
+ let out = "";
508
+ let quote;
509
+ for (let i = 0; i < command.length; i++) {
510
+ const ch = command[i];
511
+ if (quote !== undefined) {
512
+ out += ch;
513
+ if (ch === quote)
514
+ quote = undefined;
515
+ continue;
516
+ }
517
+ if (ch === "'" || ch === '"') {
518
+ quote = ch;
519
+ out += ch;
520
+ continue;
521
+ }
522
+ const atWordStart = i === 0 || command[i - 1] === " " || command[i - 1] === "\t" || command[i - 1] === ";" || command[i - 1] === "|" || command[i - 1] === "&";
523
+ const m = (atWordStart ? /^\d*[<>]&\d+/ : /^[<>]&\d+/).exec(command.slice(i));
524
+ if (m !== null) {
525
+ i += m[0].length - 1;
526
+ out += " ";
527
+ continue;
528
+ }
529
+ out += ch;
530
+ }
531
+ return quote === undefined ? out : undefined;
532
+ }
293
533
  function admitsUnder(rule, command, reading) {
294
- if (rule.match === "subpath")
534
+ if (rule.match === "subpath" || rule.match === "path")
295
535
  return false;
296
536
  const shape = ruleLaneShapeOf(command, reading);
297
537
  if ("reject" in shape)
@@ -350,9 +590,8 @@ export function lexicalNormalAbsolutePathOf(path) {
350
590
  if (seg === "" || seg === ".")
351
591
  continue;
352
592
  if (seg === "..") {
353
- if (out.length === 0)
354
- return undefined;
355
- out.pop();
593
+ if (out.length > 0)
594
+ out.pop();
356
595
  continue;
357
596
  }
358
597
  out.push(seg);
@@ -366,7 +605,7 @@ function isLexicalNormalAbsoluteDir(path) {
366
605
  return segments.length > 0 && segments.every((s) => s !== "" && s !== "." && s !== "..");
367
606
  }
368
607
  export function directoryRuleAdmits(rule, path) {
369
- if (rule.tool !== "Read" || rule.match !== "subpath")
608
+ if (rule.match !== "subpath")
370
609
  return false;
371
610
  if (!isLexicalNormalAbsoluteDir(rule.command))
372
611
  return false;
@@ -483,12 +722,12 @@ export function isRuleLive(rule) {
483
722
  export function eligibleContext(rule, call) {
484
723
  return rule.tool === call.tool && scopeCoversCwd(rule.scope, call.cwd, call.sessionId);
485
724
  }
486
- export function eligiblePersisted(rule, call) {
487
- return isRuleLive(rule) && eligibleContext(rule, call);
725
+ export function eligiblePersisted(rule, call, behavior) {
726
+ return rule.behavior === behavior && isRuleLive(rule) && eligibleContext(rule, call);
488
727
  }
489
728
  function firstEligibleAdmittingRule(rules, commandText, call) {
490
729
  for (const rule of rules) {
491
- if (!eligiblePersisted(rule, call))
730
+ if (!eligiblePersisted(rule, call, "allow"))
492
731
  continue;
493
732
  if (ruleAdmitsCommand(rule, commandText))
494
733
  return rule;
@@ -521,13 +760,54 @@ export function findAdmittingRule(rules, call) {
521
760
  }
522
761
  function firstEligibleDirectoryRule(rules, directory, call) {
523
762
  for (const rule of rules) {
524
- if (!eligiblePersisted(rule, { tool: "Read", cwd: call.cwd, sessionId: call.sessionId }))
763
+ if (!eligiblePersisted(rule, { tool: "Read", cwd: call.cwd, sessionId: call.sessionId }, "allow"))
525
764
  continue;
526
765
  if (directoryRuleAdmits(rule, directory))
527
766
  return rule;
528
767
  }
529
768
  return undefined;
530
769
  }
770
+ export function adjudicatePersistedRules(rules, call) {
771
+ for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
772
+ if (behavior === "allow") {
773
+ const covering = findAdmittingRule(rules, call);
774
+ if (covering !== undefined)
775
+ return { behavior, rules: covering };
776
+ continue;
777
+ }
778
+ for (const rule of rules) {
779
+ if (!eligiblePersisted(rule, call, behavior))
780
+ continue;
781
+ if (ruleReachesProgramRun(rule, call.command))
782
+ return { behavior, rules: [rule] };
783
+ }
784
+ }
785
+ return undefined;
786
+ }
787
+ export function adjudicatePersistedPathRules(rules, call, targets, bases) {
788
+ for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
789
+ if (behavior === "allow") {
790
+ if (targets.allow === undefined || call.tool !== READ_RULE_TOOL)
791
+ continue;
792
+ for (const rule of rules) {
793
+ if (!eligiblePersisted(rule, call, behavior))
794
+ continue;
795
+ if (directoryRuleAdmits(rule, targets.allow))
796
+ return { behavior, rules: [rule] };
797
+ }
798
+ continue;
799
+ }
800
+ if (targets.tighten === undefined)
801
+ continue;
802
+ for (const rule of rules) {
803
+ if (!eligiblePersisted(rule, call, behavior))
804
+ continue;
805
+ if (pathRuleReaches(rule, targets.tighten, bases))
806
+ return { behavior, rules: [rule] };
807
+ }
808
+ }
809
+ return undefined;
810
+ }
531
811
  export function segmentCoverageOf(command, rules, call) {
532
812
  const folded = foldSpacing(command);
533
813
  if (folded === undefined)
@@ -569,7 +849,7 @@ export function suggestRulesForCommand(command, ctx) {
569
849
  if (folded === undefined)
570
850
  return [];
571
851
  const offers = [];
572
- const exact = parseAllowRuleText(formatAllowRuleText(folded, "exact"));
852
+ const exact = parseRuleText(formatRuleText(folded, "exact", COMMAND_RULE_TOOL), "allow");
573
853
  if ("rule" in exact && exact.rule.match === "exact" && ruleAdmitsCommand(exact.rule, command)) {
574
854
  offers.push({ kind: "single", rule: exact.rule.rule, match: "exact", command: exact.rule.command });
575
855
  }
@@ -589,7 +869,7 @@ export function suggestRulesForCommand(command, ctx) {
589
869
  }
590
870
  if (denied)
591
871
  return undefined;
592
- const parsed = parseAllowRuleText(formatAllowRuleText(directory, "subpath"));
872
+ const parsed = parseRuleText(formatRuleText(directory, "subpath", READ_RULE_TOOL), "allow");
593
873
  if (!("rule" in parsed) || parsed.rule.match !== "subpath")
594
874
  return undefined;
595
875
  if (!directoryRuleAdmits(parsed.rule, directory))
@@ -600,7 +880,7 @@ export function suggestRulesForCommand(command, ctx) {
600
880
  if (offers.length === 1) {
601
881
  const body = bodyOf(tokensOf(folded));
602
882
  if (body !== undefined) {
603
- const parsed = parseAllowRuleText(formatAllowRuleText(body, "wildcard"));
883
+ const parsed = parseRuleText(formatRuleText(body, "wildcard", COMMAND_RULE_TOOL), "allow");
604
884
  if ("rule" in parsed && parsed.rule.match === "wildcard" && ruleAdmitsCommand(parsed.rule, command)) {
605
885
  offers.push({ kind: "single", rule: parsed.rule.rule, match: "wildcard", command: parsed.rule.command });
606
886
  }
@@ -627,12 +907,12 @@ export function suggestRulesForCommand(command, ctx) {
627
907
  return undefined;
628
908
  const body = bodyOf(tokensOf(segment));
629
909
  if (body !== undefined) {
630
- const parsed = parseAllowRuleText(formatAllowRuleText(body, "wildcard"));
910
+ const parsed = parseRuleText(formatRuleText(body, "wildcard", COMMAND_RULE_TOOL), "allow");
631
911
  if ("rule" in parsed && parsed.rule.match === "wildcard" && ruleAdmitsCommand(parsed.rule, segment)) {
632
912
  return { rule: parsed.rule.rule, match: "wildcard", command: parsed.rule.command };
633
913
  }
634
914
  }
635
- const segExact = parseAllowRuleText(formatAllowRuleText(segment, "exact"));
915
+ const segExact = parseRuleText(formatRuleText(segment, "exact", COMMAND_RULE_TOOL), "allow");
636
916
  if ("rule" in segExact && segExact.rule.match === "exact" && ruleAdmitsCommand(segExact.rule, segment)) {
637
917
  return { rule: segExact.rule.rule, match: "exact", command: segExact.rule.command };
638
918
  }
@@ -655,7 +935,7 @@ export function suggestRulesForCommand(command, ctx) {
655
935
  const batchRules = minted.slice(0, 5);
656
936
  if (batchRules.length > 0) {
657
937
  const parsedBatch = batchRules.flatMap((r) => {
658
- const p = parseAllowRuleText(r.rule);
938
+ const p = parseRuleText(r.rule, "allow");
659
939
  return "rule" in p ? [p.rule] : [];
660
940
  });
661
941
  const uncoveredDetail = [];
@@ -6,7 +6,7 @@
6
6
  * convergent merge). So the org layer has NO CRDT, no tombstones and no join: the server is the single
7
7
  * authority, a client holds a read-only versioned snapshot, an update is a whole replacement (revision
8
8
  * strictly ordered), and a rollback is a NEW revision carrying old content. Cross-layer tombstones are
9
- * structurally impossible — an org rule never enters `PersistedAllowRule`, and an org snapshot replace
9
+ * structurally impossible — an org rule never enters `PersistedRule`, and an org snapshot replace
10
10
  * never touches a personal store's bytes. The two layers meet only at the CONSUMPTION site, where org
11
11
  * deny/ask folds strict-side: **org deny > org ask (non-dismissable) > personal allow rule > bare ask**.
12
12
  *
@@ -35,7 +35,7 @@
35
35
  * accept revision N−1 after a restart. Signature / predecessor chaining belongs to the privilege-
36
36
  * separation ticket; the high-water mark is the floor that needs no key distribution.
37
37
  */
38
- import { type PersistedAllowRule, type RuleRejectCode } from "./permission-rule-model.js";
38
+ import { type PersistedRule, type RuleRejectCode } from "./permission-rule-model.js";
39
39
  import type { PermissionRuleStore } from "./permission-rule-provider.js";
40
40
  /** One org rule. There is structurally no allow bucket (design/179 §9: the org layer only tightens). */
41
41
  export interface OrgPermissionRule {
@@ -187,8 +187,9 @@ export declare function unenforceableOrgRules(rules: readonly OrgPermissionRule[
187
187
  /**
188
188
  * design/182 §7.2 — which org rule speaks for this command, if any. Deny outranks ask; within a
189
189
  * behavior the first textual match wins (reporting order only — all denies are the same one answer).
190
- * Matching uses the SAME parser and matcher the personal lane uses (tighten direction: an
191
- * interpreter-headed prefix deny like `Bash(node:*)` is a legitimately wide tightening and matches).
190
+ * Matching uses the SAME parser and the SAME tightening reach the personal deny/ask rows use
191
+ * ({@link ruleReachesProgramRun}: an interpreter-headed prefix deny like `Bash(node:*)` is a
192
+ * legitimately wide tightening and matches; a segment hit is a hit).
192
193
  *
193
194
  * The skip below is a defensive floor, and what stands behind it is worth stating precisely: rules that
194
195
  * arrive from the org partition's `resolve` ({@link createOrgRulePartition}) have passed {@link validateOrgSnapshot} on BOTH the install
@@ -212,8 +213,8 @@ export declare function orgRuleVerdictFor(rules: readonly OrgPermissionRule[], c
212
213
  * OVERRIDE it, not to self-declare its removal — so a shadowed rule reports `shadowed-by-org` and comes
213
214
  * back by itself when the org deny is withdrawn.
214
215
  *
215
- * The SAME reach the gate uses (`orgRuleReaches`), so the view and the decision cannot disagree about
216
- * whether a deny touches this rule at all. A personal rule may name a connector chain, and a deny on one
216
+ * The SAME reach the gate uses ({@link ruleReachesProgramRun}), so the view and the decision cannot
217
+ * disagree about whether a deny touches this rule at all. A personal rule may name a connector chain, and a deny on one
217
218
  * of its segments shadows it exactly as a deny on the whole does.
218
219
  *
219
220
  * WHAT `shadowed-by-org` DOES AND DOES NOT CLAIM: the predicate asks whether the deny reaches the
@@ -232,9 +233,9 @@ export declare function orgRuleVerdictFor(rules: readonly OrgPermissionRule[], c
232
233
  * reach it, and its `command` is a PATH that must not be fed to the shell lane's segment reader as
233
234
  * though it were a command line.
234
235
  */
235
- export declare function orgRuleShadows(orgRules: readonly OrgPermissionRule[], rule: Pick<PersistedAllowRule, "tool" | "command">): boolean;
236
+ export declare function orgRuleShadows(orgRules: readonly OrgPermissionRule[], rule: Pick<PersistedRule, "tool" | "command">): boolean;
236
237
  /** The shadow predicate with the org denies parsed ONCE — what a read over many rows uses. */
237
- export declare function compileOrgShadowPredicate(orgRules: readonly OrgPermissionRule[]): (rule: Pick<PersistedAllowRule, "tool" | "command">) => boolean;
238
+ export declare function compileOrgShadowPredicate(orgRules: readonly OrgPermissionRule[]): (rule: Pick<PersistedRule, "tool" | "command">) => boolean;
238
239
  /** Does this store carry a write face? Re-exported convenience for org-ticket integrations that need to
239
240
  * distinguish a read-only projection from a writable personal bucket without importing the writer seam. */
240
241
  export declare function isWritablePermissionRuleStore(store: PermissionRuleStore): boolean;
@@ -1,4 +1,4 @@
1
- import { escapeForDisclosure, parseAllowRuleText, ruleAdmitsProgramRun, ruleLaneSegmentsOf } from "./permission-rule-model.js";
1
+ import { escapeForDisclosure, parseRuleText, ruleReachesProgramRun } from "./permission-rule-model.js";
2
2
  import { writerOf } from "./permission-rule-store.js";
3
3
  export const ORG_UNAVAILABLE_DECISION_REASON = "org_unavailable";
4
4
  export const ORG_ADJUDICATION_TIMEOUT_MS = 15_000;
@@ -199,7 +199,7 @@ export function unenforceableOrgRules(rules) {
199
199
  for (const r of rules) {
200
200
  if (typeof r?.rule !== "string" || r.rule === "")
201
201
  continue;
202
- const parsed = parseAllowRuleText(r.rule, { direction: "tighten" });
202
+ const parsed = parseRuleText(r.rule, r.behavior);
203
203
  if ("reject" in parsed) {
204
204
  out.push({ rule: r.rule, code: parsed.reject.code });
205
205
  continue;
@@ -243,15 +243,14 @@ function validateOrgSnapshot(s, nowMs) {
243
243
  return undefined;
244
244
  }
245
245
  export function orgRuleVerdictFor(rules, call) {
246
- const segments = ruleLaneSegmentsOf(call.command);
247
246
  let ask;
248
247
  for (const r of rules) {
249
- const parsed = parseAllowRuleText(r.rule, { direction: "tighten" });
248
+ const parsed = parseRuleText(r.rule, r.behavior);
250
249
  if ("reject" in parsed)
251
250
  continue;
252
251
  if (parsed.rule.tool !== call.tool)
253
252
  continue;
254
- if (!orgRuleReaches(parsed.rule, call.command, segments))
253
+ if (!ruleReachesProgramRun(parsed.rule, call.command))
255
254
  continue;
256
255
  if (r.behavior === "deny")
257
256
  return { behavior: "deny", rule: r.rule };
@@ -259,13 +258,6 @@ export function orgRuleVerdictFor(rules, call) {
259
258
  }
260
259
  return ask;
261
260
  }
262
- function orgRuleReaches(rule, command, segments) {
263
- if (ruleAdmitsProgramRun(rule, command))
264
- return true;
265
- if (segments === undefined)
266
- return false;
267
- return segments.some((segment) => ruleAdmitsProgramRun(rule, segment));
268
- }
269
261
  export function orgRuleShadows(orgRules, rule) {
270
262
  return compileOrgShadowPredicate(orgRules)(rule);
271
263
  }
@@ -274,15 +266,14 @@ export function compileOrgShadowPredicate(orgRules) {
274
266
  for (const d of orgRules) {
275
267
  if (d.behavior !== "deny")
276
268
  continue;
277
- const parsed = parseAllowRuleText(d.rule, { direction: "tighten" });
269
+ const parsed = parseRuleText(d.rule, "deny");
278
270
  if (!("reject" in parsed))
279
271
  denies.push(parsed.rule);
280
272
  }
281
273
  return (rule) => {
282
274
  if (rule.tool !== "Bash" || denies.length === 0)
283
275
  return false;
284
- const segments = ruleLaneSegmentsOf(rule.command);
285
- return denies.some((d) => d.tool === rule.tool && orgRuleReaches(d, rule.command, segments));
276
+ return denies.some((d) => d.tool === rule.tool && ruleReachesProgramRun(d, rule.command));
286
277
  };
287
278
  }
288
279
  export function isWritablePermissionRuleStore(store) {
@@ -34,7 +34,7 @@
34
34
  * durable backend keeps refusing a session row loudly should one ever reach it — the route is the
35
35
  * store's, the fence stays the partition's.
36
36
  */
37
- import type { PersistedAllowRule, RuleScope } from "./permission-rule-model.js";
37
+ import type { PersistedRule, RuleScope } from "./permission-rule-model.js";
38
38
  import { type DurableRulePartitionProvider, type QuarantinedRuleAdd, type RuleOwner } from "./permission-rule-store.js";
39
39
  import { type OrgRulePartitionConfig, type OrgRuleResolution } from "./permission-rule-org.js";
40
40
  import type { SessionRulePartition } from "./permission-rule-session.js";
@@ -44,8 +44,10 @@ export type RuleSource = "org" | "user" | "project" | "session";
44
44
  /** The source of a row is a projection of its scope — `global` is the `user` source. */
45
45
  export declare function ruleSourceOf(scope: RuleScope): Exclude<RuleSource, "org">;
46
46
  /** One live personal rule in the effective view: the row itself, its source, and its standing under the
47
- * org partition (`shadowed-by-org` = an org deny reaches its command pattern; see `orgRuleShadows`). */
48
- export interface EffectivePermissionRule extends PersistedAllowRule {
47
+ * org partition (`shadowed-by-org` = an org deny reaches an ALLOW row's command pattern; see
48
+ * `orgRuleShadows` a personal deny/ask row is never shadowed: it only ever tightens, and the org
49
+ * partition's power over the personal store is to override an allow, not a refusal). */
50
+ export interface EffectivePermissionRule extends PersistedRule {
49
51
  source: Exclude<RuleSource, "org">;
50
52
  status: "live" | "shadowed-by-org";
51
53
  }
@@ -53,6 +55,7 @@ export interface EffectivePermissionRule extends PersistedAllowRule {
53
55
  * tombstone identity is still visible in the partition. Session rows never appear here (they die
54
56
  * with their session and leave no tombstone). */
55
57
  export interface RemovedPermissionRule {
58
+ behavior: import("./permission-rule-model.js").RuleBehavior;
56
59
  rule: string;
57
60
  scope: RuleScope;
58
61
  source: "user" | "project";
@@ -84,9 +87,10 @@ export interface EffectivePermissionRules {
84
87
  * The host-visible store, already anchored to one owner. One read verb, which never throws: each
85
88
  * partition reports its own failure in the view — the DURABLE partition's read failure as `unreadable`
86
89
  * (zero rows; the caller maps it to "unreadable": a loud disclosure, fail toward asking, exactly as the
87
- * old `list()` throw did), the SESSION partition's as zero session rows plus a `disclosures` line, the
88
- * ORG partition's as its `status`. The three are independent on purpose: the org verdict must stand
89
- * while a personal store is down, and the personal rows must stand while an org source is down.
90
+ * old `list()` throw did), the SESSION partition's read failure as `unreadable` too (it may hold a
91
+ * deny/ask row of this session, and zero rows would erase it served-but-non-canonical rows stay a
92
+ * `disclosures` line), the ORG partition's as its `status`. The org verdict stands while a personal
93
+ * partition is down, and the personal rows stand while an org source is down.
90
94
  */
91
95
  export interface PermissionRuleStore {
92
96
  effective(query?: {