@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,11 +1,22 @@
1
1
  /**
2
- * design/179 §2 — the persisted ALLOW rule model and its matcher.
3
- *
4
- * A persisted allow rule is the standing form of ONE recorded human approval: the person confirmed a
5
- * command once on an approval card and asked not to be asked again, and the rule is what that answer
6
- * becomes. This module owns the rule's SHAPE (text grammar, canonical spelling, scope) and its MATCH
7
- * semantics; who may mint one, where it is stored, and where in the gate it is consumed live in
8
- * `permission-rule-store.ts`, `permission-rule-consent.ts` and `hooks.ts` respectively.
2
+ * design/179 §2 — the persisted permission-rule model and its matchers.
3
+ *
4
+ * A persisted rule is one person's standing instruction about a tool call, in one of THREE behaviors
5
+ * ({@link RULE_BEHAVIORS}): an `allow` is the standing form of ONE recorded human approval (the person
6
+ * confirmed a command once on an approval card and asked not to be asked again); a `deny` and an `ask`
7
+ * are the standing forms of "never run this" and "ask me every time" the same content-form rules
8
+ * upstream reads out of its settings' deny/ask lists. ONE grammar, ONE canonical spelling and ONE
9
+ * matcher family serve all three; the behavior is a FIELD beside the text, never part of it, so
10
+ * `Bash(rm -r:*)` is one spelling whether it allows, denies or asks. This module owns the rule's SHAPE
11
+ * (text grammar, canonical spelling, scope, behavior) and its MATCH semantics; who may mint one, where
12
+ * it is stored, and where in the gate it is consumed live in `permission-rule-store.ts`,
13
+ * `permission-rule-consent.ts` and `gate-lanes.ts` respectively.
14
+ *
15
+ * The two DIRECTIONS read a command differently, on purpose: an allow must cover the WHOLE call
16
+ * ({@link ruleAdmitsCommand} — one recorded yes clears exactly what it spells), while a deny/ask
17
+ * speaks about any PROGRAM RUN the call may perform ({@link ruleReachesProgramRun} — whole command or
18
+ * any segment of a chain, the wider tightening reading). Precedence across behaviors is the upstream
19
+ * one, deny > ask > allow ({@link RULE_BEHAVIOR_PRECEDENCE}, the one table every adjudication reads).
9
20
  *
10
21
  * ## The floor: what a rule may name, and what a rule may match
11
22
  *
@@ -58,6 +69,48 @@
58
69
  * here) and no leading-env-assignment stripping (`FOO=1 git status` simply does not match
59
70
  * `Bash(git status:*)`) — the last two are strict-side divergences from upstream, registered as such.
60
71
  */
72
+ import type { AssertAllKeysHandled } from "./ask-origin.js";
73
+ /**
74
+ * The closed set of rule BEHAVIORS — what a matching rule says about the call. The upstream vocabulary
75
+ * verbatim (`allow` / `deny` / `ask`, the three settings lists), in PRECEDENCE order: when rules of more
76
+ * than one behavior speak for one call, the earliest member here decides ({@link RULE_BEHAVIOR_PRECEDENCE}
77
+ * is that order as a table, and {@link adjudicatePersistedRules} is the one place that walks it).
78
+ *
79
+ * The type is derived FROM the array so a member added to either side is a compile error; a value read
80
+ * off a wire or a file goes through {@link isRuleBehavior} (present but outside the set = refused, never
81
+ * defaulted) or {@link readRowBehavior} (the ONE reading of a row that predates the field).
82
+ */
83
+ export declare const RULE_BEHAVIORS: readonly ["deny", "ask", "allow"];
84
+ export type RuleBehavior = (typeof RULE_BEHAVIORS)[number];
85
+ /** Membership test for {@link RuleBehavior}. Exported because the word crosses process boundaries (a
86
+ * store row, a sync payload, a settings entry) and a host validating one must not hand-roll the set. */
87
+ export declare function isRuleBehavior(v: unknown): v is RuleBehavior;
88
+ /**
89
+ * The precedence table: the rank at which each behavior decides a call when several rules speak (lower
90
+ * decides first). Upstream's arbiter walks deny, then ask, then allow — an explicit deny is never
91
+ * outvoted by an allow, and a standing "ask me each time" is never silenced by a remembered yes. Every
92
+ * adjudication over a mixed rule table orders its walk by THIS table (never by a hand-written if-chain),
93
+ * so the rank of a behavior is stated once.
94
+ */
95
+ export declare const RULE_BEHAVIOR_PRECEDENCE: {
96
+ readonly deny: 0;
97
+ readonly ask: 1;
98
+ readonly allow: 2;
99
+ };
100
+ /** Compile-time fence over the precedence table: `never` while every behavior has a rank. */
101
+ export type PrecedenceTableCoversEveryBehavior = AssertAllKeysHandled<Exclude<RuleBehavior, keyof typeof RULE_BEHAVIOR_PRECEDENCE>>;
102
+ /** The behaviors in precedence order — the walk every mixed-table adjudication takes. */
103
+ export declare const RULE_BEHAVIORS_BY_PRECEDENCE: readonly RuleBehavior[];
104
+ /**
105
+ * The ONE reading of a persisted row's behavior at every seam that may meet a row written before the
106
+ * field existed — the file store's schema migration, the sync wire's inbound picker, a checkpoint's
107
+ * rehydrated session rows. An ABSENT behavior reads `"allow"`: every row an engine without the field
108
+ * ever wrote was an allow, so that reading is the row's own meaning, not a default. A PRESENT value
109
+ * outside the closed set is refused (`undefined`) — a word this engine cannot read must not become an
110
+ * allow by falling through (the loud-bad-value doctrine), and must not become a deny either (a
111
+ * fabricated tightening is still a fabrication).
112
+ */
113
+ export declare function readRowBehavior(v: unknown): RuleBehavior | undefined;
61
114
  /** The tools the rule lane speaks for. `"Bash"` is the historical member; `"Read"` (design/382 §2.2)
62
115
  * speaks EXACTLY ONE grammar — the double-slash directory form `Read(//abs/**)`, the directory
63
116
  * read-authorization a compound card's cd segment mints — and its admission is the CLOSED two-point
@@ -70,7 +123,19 @@
70
123
  * silence the classifier's questions, never a mandated one), so a recorded human yes is exactly what
71
124
  * clears it. The org/hook/matchedAskRule immunities stay: those conjuncts live in the gate's
72
125
  * persisted-rule lane and do not loosen with this set. */
73
- export type PersistedRuleTool = "Bash" | "Read";
126
+ export type PersistedRuleTool = string;
127
+ /** The one shell tool the command grammar speaks for. */
128
+ export declare const COMMAND_RULE_TOOL = "Bash";
129
+ /** The one tool whose path family has an ALLOW form (the `Read(//abs-dir/**)` directory grant). */
130
+ export declare const READ_RULE_TOOL = "Read";
131
+ /**
132
+ * Which tools a rule may name — the model's tool set is DERIVED, never a hand-kept list: the shell tool
133
+ * takes the COMMAND grammar; every tool the catalog declares a path target for (Read / Edit / Write /
134
+ * Glob / NotebookEdit / …, `pathTargetOf`) takes the PATH grammar. A rule on any other tool has no
135
+ * content this model can judge and is refused (`unsupported.tool`). The tool set therefore moves with the
136
+ * catalog's declarations, exactly as the protective fences' covered set does.
137
+ */
138
+ export declare function ruleToolGrammarOf(toolName: string): "command" | "path" | undefined;
74
139
  /**
75
140
  * Match forms. Four members, two of them one predicate:
76
141
  * · `exact` — admits the one string the rule spells.
@@ -82,13 +147,19 @@ export type PersistedRuleTool = "Bash" | "Read";
82
147
  * on the single-command body. Kept as a fully supported COMPATIBILITY READ — stored rules are never
83
148
  * migrated or rewritten — and it remains the ONLY spelling of the compound-prefix form
84
149
  * (`Bash(cd /tmp && adb pull:*)`): the trailing-star form deliberately has no compound reading
85
- * (see {@link parseAllowRuleText}), because upstream's own trailing-star pattern is inert on a
150
+ * (see {@link parseRuleText}), because upstream's own trailing-star pattern is inert on a
86
151
  * compound and importing/minting one as a chain licence would grant what nobody approved.
87
- * · `subpath` — the Read directory form (design/382 §2.2): `Read(//abs/**)`, admitting reads under
88
- * one absolute directory. Never admits any command; its one predicate is
89
- * {@link directoryRuleAdmits}.
152
+ * · `subpath` — the directory form (design/382 §2.2): `Tool(//abs-dir/**)`, reaching every path under
153
+ * one absolute directory. Never admits any command; its one predicate is {@link directoryRuleAdmits}.
154
+ * As an ALLOW it exists for the Read tool only (a standing yes to reads under a directory); as a
155
+ * deny/ask it is legal on every path-targeting tool.
156
+ * · `path` — the PATH PATTERN form, deny/ask only: upstream's file-rule spelling over a tool's path
157
+ * target (`Edit(~/.sema/settings.json)`, `Write(src/**)`, `Read(//etc/*)`), stored AS SPELLED and
158
+ * resolved at match time against the call's bases (`//` absolute, `~/` home, `/` project root,
159
+ * otherwise the working directory). Its predicate is {@link pathRuleReaches}. Not an allow form: a
160
+ * standing approval by path pattern would be a wider grant than any card ever shows a person.
90
161
  */
91
- export type PersistedRuleMatch = "exact" | "prefix" | "wildcard" | "subpath";
162
+ export type PersistedRuleMatch = "exact" | "prefix" | "wildcard" | "subpath" | "path";
92
163
  /**
93
164
  * Where a rule applies — design/382 §4.1, the THREE consent dimensions.
94
165
  *
@@ -143,13 +214,24 @@ export interface RuleAdd {
143
214
  createdAt: string;
144
215
  }
145
216
  /**
146
- * A persisted ALLOW rule for one tool.
147
- *
148
- * `adds` is a real set, not a scalar: two concurrently approved records for the same (rule, scope)
149
- * each redeem their own dot, and a rule is live iff at least one of its adds survives the tombstones.
150
- * Organization rules have the opposite polarity (deny/ask only) and never enter this type.
217
+ * A persisted rule for one tool, in one of the three behaviors.
218
+ *
219
+ * The logical identity of a rule is the TRIPLE (behavior, rule text, scope): the same text may stand as
220
+ * an allow and as a deny in one bucket (upstream keeps the three lists side by side and lets precedence
221
+ * decide), so a tombstone, a quarantine row and a join group all key on the triple — a key on (text,
222
+ * scope) alone would let a delete aimed at the allow take the deny with it.
223
+ *
224
+ * `adds` is a real set, not a scalar: two concurrently approved records for the same identity each
225
+ * redeem their own dot, and a rule is live iff at least one of its adds survives the tombstones.
226
+ * Organization rules are a separate partition (a server-published snapshot, no CRDT) and never enter
227
+ * this type; the personal deny/ask rows here are the PERSON's own standing instructions (their settings'
228
+ * deny/ask lists, imported through the same consent door the allow list takes).
151
229
  */
152
- export interface PersistedAllowRule {
230
+ export interface PersistedRule {
231
+ /** What a match of this rule says about the call — REQUIRED on every row this engine writes. A row
232
+ * read without one is a pre-field row and reads `allow` through {@link readRowBehavior}, at the
233
+ * reading seams only (the store's schema migration, the sync picker, a checkpoint's session rows). */
234
+ behavior: RuleBehavior;
153
235
  /** Canonical rule text: `Bash(git status)` (exact) or `Bash(git status:*)` (prefix). A prefix body
154
236
  * may itself be a connector chain (`Bash(cd /tmp && adb pull:*)`) — see the module note on
155
237
  * connectors for what such a rule reaches. */
@@ -170,6 +252,8 @@ export interface PersistedAllowRule {
170
252
  * in no tombstone for the same (rule, scope).
171
253
  */
172
254
  export interface RuleTombstone {
255
+ /** The identity's behavior member — a tombstone deletes adds of ONE (behavior, text, scope). */
256
+ behavior: RuleBehavior;
173
257
  rule: string;
174
258
  scope: RuleScope;
175
259
  removedDots: RuleDot[];
@@ -185,8 +269,11 @@ export interface RuleReject {
185
269
  code: RuleRejectCode;
186
270
  message: string;
187
271
  }
188
- /** The parsed, canonical shape of one rule text — everything but the scope and the provenance. */
189
- export interface ParsedAllowRule {
272
+ /** The parsed, canonical shape of one rule text under one behavior — everything but the scope and the
273
+ * provenance. The behavior is the parser's INPUT (it decides which validator floor applies), carried
274
+ * out so a consumer holding the parse never re-derives which list the text came from. */
275
+ export interface ParsedRule {
276
+ behavior: RuleBehavior;
190
277
  rule: string;
191
278
  tool: PersistedRuleTool;
192
279
  match: PersistedRuleMatch;
@@ -209,7 +296,7 @@ export declare const MAX_RULE_TEXT_CHARS = 512;
209
296
  * an EXACT rule naming a whole interpreter command line stays legal, since it authorizes one command.
210
297
  *
211
298
  * TWO READERS, and they consult this table with DIFFERENT questions — do not read either as the other.
212
- * {@link parseAllowRuleText} asks "is this rule TEXT's own head a row?" and compares the basename as
299
+ * {@link parseRuleText} asks "is this rule TEXT's own head a row?" and compares the basename as
213
300
  * SPELLED, so an alias spelling (`SSH host:*`, `ssh.exe host:*`) is a different name to it and enters
214
301
  * the store like any other rule. {@link genericPrefixBody} asks "should the engine PROPOSE a body with
215
302
  * this head?" and normalizes first ({@link screenedHeadName}: case, trailing periods, executable
@@ -391,23 +478,46 @@ export declare function hasUnrenderableCharacters(text: string): boolean;
391
478
  * product's settings, reloading a store file, and any future backend. A shape refused here cannot enter
392
479
  * the store through any door.
393
480
  *
394
- * `direction` (design/182 §7, additive): the bare-interpreter-prefix refusal is an ALLOW-side argument
395
- * one approval click cannot be read as authorizing arbitrary programs. An org deny/ask rule has the
396
- * opposite polarity: `Bash(node:*)` as a DENY is a legitimately wide tightening, and refusing it would
397
- * silently disarm exactly the policy an administrator most wants. `"tighten"` therefore admits the
398
- * interpreter-headed prefix form and changes nothing else; the default stays the allow-side floor.
481
+ * `behavior` is REQUIRED — there is no default, because the two directions have different floors and
482
+ * a defaulted allow is exactly the silent-widening shape a validator must not have: the
483
+ * bare-interpreter-prefix refusal is an ALLOW-side argument (one approval click cannot be read as
484
+ * authorizing arbitrary programs), while a deny/ask has the opposite polarity — `Bash(node:*)` as a DENY
485
+ * is a legitimately wide tightening, and refusing it would silently disarm exactly the rule a person
486
+ * most wants. The tightening behaviors therefore admit the interpreter-headed prefix form and change
487
+ * nothing else; the allow floor is unchanged.
399
488
  */
400
- export declare function parseAllowRuleText(text: string, opts?: {
401
- direction?: "allow" | "tighten";
402
- }): {
403
- rule: ParsedAllowRule;
489
+ export declare function parseRuleText(text: string, behavior: RuleBehavior): {
490
+ rule: ParsedRule;
404
491
  } | {
405
492
  reject: RuleReject;
406
493
  };
407
- /** The canonical spelling of a rule for a (already folded) command and match form. For `subpath` the
408
- * command is the lexical-normal absolute directory (one leading slash), re-spelled in the
409
- * double-slash directory form. */
410
- export declare function formatAllowRuleText(command: string, match: PersistedRuleMatch): string;
494
+ /** The canonical spelling of a rule for a (already folded) command and match form on `tool`. For
495
+ * `subpath` the command is the lexical-normal absolute directory (one leading slash), re-spelled in the
496
+ * double-slash directory form; for `path` it is the pattern as spelled. */
497
+ export declare function formatRuleText(command: string, match: PersistedRuleMatch, tool: PersistedRuleTool): string;
498
+ /**
499
+ * The bases a path pattern resolves against at match time — facts of the CALL, never of the rule:
500
+ * `home` for `~/…` (absent ⇒ `os.homedir()`), `root` for `/…` (the task's project root), `cwd` for a
501
+ * relative pattern (the live working directory; absent ⇒ `root`). A pattern whose base is absent
502
+ * resolves to nothing and reaches nothing.
503
+ */
504
+ export interface PathRuleBases {
505
+ readonly root?: string;
506
+ readonly cwd?: string;
507
+ readonly home?: string;
508
+ }
509
+ /** Is this base usable by {@link resolvePathPattern} — an absolute path with a lexical normal form? A rule
510
+ * compiler asks this at compile time so a pattern is refused rather than compiled inert. */
511
+ export declare function isUsablePathBase(base: string | undefined): boolean;
512
+ /**
513
+ * The PATH family's tightening predicate: does this deny/ask rule reach the call's target path? The
514
+ * target is the caller's ALREADY-RESOLVED lexical-normal absolute path (the same identity the read/write
515
+ * fences judge with). A `subpath` rule reaches the directory and everything under it
516
+ * ({@link directoryRuleAdmits}); a `path` rule resolves its spelling against the call's bases and
517
+ * matches segment-wise (`*` within a segment, `**` across segments), reaching the path it names and
518
+ * everything under a directory it names. A command-family rule reaches no path.
519
+ */
520
+ export declare function pathRuleReaches(rule: Pick<PersistedRule, "match" | "command">, target: string, bases: PathRuleBases): boolean;
411
521
  /**
412
522
  * Does this rule's command pattern admit `command`?
413
523
  *
@@ -425,7 +535,7 @@ export declare function formatAllowRuleText(command: string, match: PersistedRul
425
535
  * same count equality is the containment: `Bash(a && b:*)` admits `a && b x`, never `a && b x && c`.
426
536
  * An EXACT rule has no such reach by construction: it admits one string, the one it spells.
427
537
  */
428
- export declare function ruleAdmitsCommand(rule: Pick<PersistedAllowRule, "match" | "command">, command: string): boolean;
538
+ export declare function ruleAdmitsCommand(rule: Pick<PersistedRule, "match" | "command">, command: string): boolean;
429
539
  /**
430
540
  * The same match, asked the DENY/ASK layer's question — "does this rule speak about this program run?".
431
541
  *
@@ -440,7 +550,38 @@ export declare function ruleAdmitsCommand(rule: Pick<PersistedAllowRule, "match"
440
550
  * fewer. A shape only the ALLOW side could read would be a standing approval no published policy could
441
551
  * see — the inversion this whole ticket exists to prevent.
442
552
  */
443
- export declare function ruleAdmitsProgramRun(rule: Pick<PersistedAllowRule, "match" | "command">, command: string): boolean;
553
+ export declare function ruleAdmitsProgramRun(rule: Pick<PersistedRule, "match" | "command">, command: string): boolean;
554
+ /**
555
+ * Does one TIGHTENING (deny/ask) rule speak about this call — as a whole, or about any single program
556
+ * run in it? The ONE reach predicate of the tightening direction, shared by the org partition, the
557
+ * personal deny/ask rows and the rule DSL's content-form deny/ask rules, so no two of them can disagree
558
+ * about what a deny reaches.
559
+ *
560
+ * The allow lane asks a different question and gets a different answer, deliberately. An allow rule is
561
+ * one recorded human yes and must cover the WHOLE call to clear it: `Bash(npm test)` does not clear
562
+ * `npm test && curl evil.example`, because nobody said yes to the second half. A deny/ask rule is a
563
+ * standing statement about a program run, and a call that MAY perform that run is a call the rule
564
+ * speaks about — so a segment hit is a hit. Deliberately an OVER-approximation, in the tightening
565
+ * direction: the segments are the runs the text can reach, not the runs that will happen, so a
566
+ * short-circuited branch (`true || curl evil.example`) and a commented-out tail
567
+ * (`echo ok # ; curl evil.example`) are judged as though they ran. Answering "does this definitely
568
+ * execute" needs evaluation, not parsing; over-firing costs an approval prompt, and the opposite error
569
+ * costs the rule. Reading the compound as one unmatched blob is what once made `deny Bash(curl:*)`
570
+ * silent on `npm test && curl evil.example`: a rule standing on paper and matching nothing in fact.
571
+ *
572
+ * `ruleAdmitsProgramRun`, not `ruleAdmitsCommand`, on BOTH arms — this direction asks which programs
573
+ * the call runs, and the matching predicate refuses a quoted operator that is ordinary argument text
574
+ * (a `&` inside a URL); inheriting that refusal here silenced a deny on the commonest curl spelling.
575
+ * EVERY segment is judged, including the sole segment of a one-element list: the two readings differ
576
+ * on terminator handling, so `curl evil.example;` has a single segment `curl evil.example` that the
577
+ * whole-command arm refuses and this arm must still see (a trailing `;` once carried a chain past the
578
+ * fence). A command the lane cannot read at all (a redirection, a substitution) has no segments; the
579
+ * whole-command arm still runs and answers false, as it always did — those shapes are refused upstream.
580
+ *
581
+ * A path-family rule (`subpath` / `path`) reaches no command: its predicates are {@link directoryRuleAdmits}
582
+ * and {@link pathRuleReaches}.
583
+ */
584
+ export declare function ruleReachesProgramRun(rule: Pick<PersistedRule, "match" | "command">, command: string): boolean;
444
585
  /** design/382 §3.3-5 — one BREADTH note about a rule a person is about to persist: which wide shape
445
586
  * it has, said in words a surface can show beside the input line. A warning is never a refusal —
446
587
  * the closed refusal set is untouched — and never adjudication input. */
@@ -478,7 +619,7 @@ export interface EditedRuleBreadthWarning {
478
619
  * The upstream editor validates edited text but shows no breadth evidence at all; this seat is a
479
620
  * superset disclosure (registered divergence, design/382 §6-4).
480
621
  */
481
- export declare function ruleBreadthWarningsOf(rule: Pick<PersistedAllowRule, "match" | "command">): readonly EditedRuleBreadthWarning[];
622
+ export declare function ruleBreadthWarningsOf(rule: Pick<PersistedRule, "match" | "command">): readonly EditedRuleBreadthWarning[];
482
623
  /**
483
624
  * The segments of `command` as the deny/ask layer must judge them, or `undefined` for a command this
484
625
  * lane cannot read.
@@ -501,8 +642,8 @@ export declare function pathWithinRoot(path: string, root: string): boolean;
501
642
  /**
502
643
  * The LEXICAL NORMALIZER for a CALL's target path (design/382 §2.2 — the rule family's one path
503
644
  * identity is the lexical normal form, zero IO): collapse slash runs and `.` segments, resolve `..`
504
- * segments textually, and answer the one-leading-slash absolute spelling or `undefined` when no
505
- * such spelling exists (a relative path, a `..` climbing past the root, an empty result). RULE
645
+ * segments textually (a `..` at the root stays at the root, as POSIX resolves it), and answer the
646
+ * one-leading-slash absolute spelling or `undefined` for a relative path. RULE
506
647
  * bodies never come through here (their canonical spelling is unique and non-normal spellings are
507
648
  * refused at the validator); this is for the comparison INPUT a caller derives from a live call.
508
649
  */
@@ -520,13 +661,16 @@ export declare function lexicalNormalAbsolutePathOf(path: string): string | unde
520
661
  * caller cannot smuggle a second path identity through the comparison. Containment is the
521
662
  * word-boundary one ({@link pathWithinRoot}: `/a` never reaches `/ab`).
522
663
  *
523
- * What it never does, stated as loudly as what it does (§2.5): admits READS ONLY — a rule of this
524
- * family never admits a write, never a whole Bash command, never a non-cd segment — and it clears
525
- * ASKS only, never a filesystem fence: workspace containment, the sensitive-read deny face and every
526
- * violation refusal judge exactly as they would without the rule (a fence refusal is structural, not
527
- * an ask, so there is nothing here for it to clear).
664
+ * What it never does, stated as loudly as what it does (§2.5): as an ALLOW it admits READS ONLY — the
665
+ * TOOL conjunct is the caller's (every consumer judges eligibility with the call's tool, and
666
+ * {@link adjudicatePersistedPathRules} takes the allow arm for the Read tool alone), so a directory allow
667
+ * never admits a write, never a whole Bash command, never a non-cd segment and it clears ASKS only,
668
+ * never a filesystem fence: workspace containment, the sensitive-read deny face and every violation
669
+ * refusal judge exactly as they would without the rule (a fence refusal is structural, not an ask, so
670
+ * there is nothing here for it to clear). As a DENY/ASK the same containment refuses or questions every
671
+ * path under the directory, on whichever path tool the rule names.
528
672
  */
529
- export declare function directoryRuleAdmits(rule: Pick<PersistedAllowRule, "tool" | "match" | "command">, path: string): boolean;
673
+ export declare function directoryRuleAdmits(rule: Pick<PersistedRule, "match" | "command">, path: string): boolean;
530
674
  /** Does a rule's scope cover a task running in `cwd` (and, for the session dimension, in the session
531
675
  * named by `sessionId`)? A project rule needs a cwd to compare against; without one it covers nothing
532
676
  * (fail-closed). A session rule (design/382 §4.3) covers a call iff the call's `sessionId` equals the
@@ -539,7 +683,7 @@ export declare function directoryRuleAdmits(rule: Pick<PersistedAllowRule, "tool
539
683
  export declare function scopeCoversCwd(scope: RuleScope, cwd: string | undefined, sessionId?: string): boolean;
540
684
  /** Is this rule live — i.e. does it still carry at least one add? Deleted adds are removed by the store
541
685
  * when tombstones are applied, so a rule with an empty `adds` is a rule that no longer exists. */
542
- export declare function isRuleLive(rule: PersistedAllowRule): boolean;
686
+ export declare function isRuleLive(rule: PersistedRule): boolean;
543
687
  /**
544
688
  * design/375 §5.1 — MAY this rule speak about this call's CONTEXT? Tool identity plus scope coverage,
545
689
  * and nothing about the rule's liveness: the CONTEXT question is the half a not-yet-persisted rule (a
@@ -551,7 +695,7 @@ export declare function isRuleLive(rule: PersistedAllowRule): boolean;
551
695
  * cwd to compare against covers nothing (fail-closed, {@link scopeCoversCwd}).
552
696
  *
553
697
  * `scope` rides beside the rule rather than inside it for the PROPOSED half's sake: a
554
- * {@link ParsedAllowRule} carries no scope, so a proposal is judged against the scope it would LAND in.
698
+ * {@link ParsedRule} carries no scope, so a proposal is judged against the scope it would LAND in.
555
699
  *
556
700
  * design/382 §4.3 — the call context carries a THIRD axis, `sessionId` (optional: an existing caller
557
701
  * that threads only `{tool, cwd}` still compiles and still means what it meant — no session rule can
@@ -576,13 +720,16 @@ export declare function eligibleContext(rule: {
576
720
  * conjunction arm and its coverage-set selection here, the suggestion face's coverage judgement and the
577
721
  * consent prepare's ({@link segmentCoverageOf} is the shared instrument). One predicate, so a rule that
578
722
  * one consumer refuses cannot be a rule another counts: the historically inlined trio of checks
579
- * (live / tool / scope) is this function, verbatim.
723
+ * (live / tool / scope) is this function, verbatim — plus the BEHAVIOR conjunct: a mixed table holds
724
+ * allow, deny and ask rows, and a consumer asking "which allow rules cover this" must not be answered
725
+ * by a deny row that happens to spell the same text. The behavior is a REQUIRED argument for the same
726
+ * reason the parser's is: a defaulted allow here is a silent widening.
580
727
  */
581
- export declare function eligiblePersisted(rule: PersistedAllowRule, call: {
728
+ export declare function eligiblePersisted(rule: PersistedRule, call: {
582
729
  tool: string;
583
730
  cwd: string | undefined;
584
731
  sessionId?: string;
585
- }): boolean;
732
+ }, behavior: RuleBehavior): boolean;
586
733
  /**
587
734
  * Find the rules that admit this command for a task in `cwd`, or `undefined`.
588
735
  *
@@ -611,8 +758,12 @@ export declare function eligiblePersisted(rule: PersistedAllowRule, call: {
611
758
  *
612
759
  * Order is defined only for reporting (what the decision names); no rule is stronger than another —
613
760
  * they are all the same one answer, "the person already said yes to this".
761
+ *
762
+ * ALLOW rows only: this is the allow direction's question, and a deny/ask row in the same table is not
763
+ * coverage (the eligibility predicate's behavior conjunct). The mixed-table walk that orders the three
764
+ * behaviors is {@link adjudicatePersistedRules}.
614
765
  */
615
- export declare function findAdmittingRule(rules: readonly PersistedAllowRule[], call: {
766
+ export declare function findAdmittingRule(rules: readonly PersistedRule[], call: {
616
767
  tool: string;
617
768
  command: string;
618
769
  cwd: string | undefined;
@@ -622,7 +773,55 @@ export declare function findAdmittingRule(rules: readonly PersistedAllowRule[],
622
773
  * `./x` against the moved cursor, not the task root). Defaults to `cwd`; the scope/eligibility
623
774
  * axis stays `cwd` either way. */
624
775
  execCwd?: string;
625
- }): readonly PersistedAllowRule[] | undefined;
776
+ }): readonly PersistedRule[] | undefined;
777
+ /** The answer of {@link adjudicatePersistedRules}: which behavior decided, and the rule(s) it stands
778
+ * on — a one-element set for a deny/ask (the first table-order rule of the deciding behavior that
779
+ * reaches the call) and the allow direction's coverage SET ({@link findAdmittingRule}). */
780
+ export interface PersistedRuleVerdict {
781
+ readonly behavior: RuleBehavior;
782
+ readonly rules: readonly PersistedRule[];
783
+ }
784
+ /**
785
+ * The ONE adjudication over a MIXED persisted-rule table for a shell call: walk the behaviors in
786
+ * {@link RULE_BEHAVIOR_PRECEDENCE} order and answer with the first behavior whose eligible rules speak
787
+ * for the call — deny before ask before allow, the upstream arbiter's order across its three lists.
788
+ *
789
+ * Each behavior is judged with ITS direction's instrument: a deny/ask row through
790
+ * {@link ruleReachesProgramRun} (the wide tightening reading: whole command or any program run in
791
+ * it), an allow through {@link findAdmittingRule} (the narrow reading: the whole call, or every segment
792
+ * of it, covered by recorded yeses). The asymmetry is the point and runs one way only: a deny reads
793
+ * MORE of a command than an allow does, so an exact allow naming a whole chain can never outrun a deny
794
+ * on one of its parts. Eligibility (live, this tool, scope covers the call, THIS behavior) is the one
795
+ * predicate every consumer shares. `undefined` ⇒ no rule of any behavior speaks.
796
+ */
797
+ export declare function adjudicatePersistedRules(rules: readonly PersistedRule[], call: {
798
+ tool: string;
799
+ command: string;
800
+ cwd: string | undefined;
801
+ sessionId?: string;
802
+ execCwd?: string;
803
+ }): PersistedRuleVerdict | undefined;
804
+ /**
805
+ * The path-family sibling of {@link adjudicatePersistedRules}: the same precedence walk over the rules
806
+ * of one path-targeting tool, judged on the call's resolved target path.
807
+ *
808
+ * TWO targets, because the two directions read the call's path slot differently (design/388 B6/B17): the
809
+ * tightening arms judge the PROTECTIVE target (`protectivePathTargetOf` — the object face's declaration,
810
+ * the catalog's for the name as the floor: reading the catalog for an undeclared same-name tool can only
811
+ * tighten a deny) with {@link pathRuleReaches}; the allow arm judges the DECLARED target
812
+ * (`declaredPathTargetOf` — the face's OWN declaration only: a same-name tool that declares no path
813
+ * target is unconfirmable, and no grant admits it) with {@link directoryRuleAdmits}, and for the Read tool
814
+ * ONLY — a directory allow is a standing yes to reads, never to a write. Either target may be absent
815
+ * (no resolvable path in that reading); the arm that needs it then speaks for nothing.
816
+ */
817
+ export declare function adjudicatePersistedPathRules(rules: readonly PersistedRule[], call: {
818
+ tool: string;
819
+ cwd: string | undefined;
820
+ sessionId?: string;
821
+ }, targets: {
822
+ readonly tighten?: string;
823
+ readonly allow?: string;
824
+ }, bases: PathRuleBases): PersistedRuleVerdict | undefined;
626
825
  /**
627
826
  * design/375 §5.2 — the per-segment coverage table for `command`: which segments an eligible rule
628
827
  * already admits, judged with the SAME split and the same eligibility predicates as the gate's
@@ -654,9 +853,9 @@ export declare function findAdmittingRule(rules: readonly PersistedAllowRule[],
654
853
  * adjudication inputs.
655
854
  */
656
855
  export declare function segmentCoverageOf(command: string, rules: {
657
- persisted: readonly PersistedAllowRule[];
856
+ persisted: readonly PersistedRule[];
658
857
  proposed?: readonly {
659
- rule: ParsedAllowRule;
858
+ rule: ParsedRule;
660
859
  scope: RuleScope;
661
860
  }[];
662
861
  }, call: {