@sema-agent/core 7.41.0 → 7.42.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.
- package/CHANGELOG.md +90 -0
- package/dist/agents/subagent.js +2 -0
- package/dist/core/agent-definition.d.ts +1 -1
- package/dist/core/allow-entry.d.ts +241 -0
- package/dist/core/allow-entry.js +340 -0
- package/dist/core/allow-rule-shadow.d.ts +18 -0
- package/dist/core/allow-rule-shadow.js +1 -0
- package/dist/core/ask-origin.d.ts +5 -5
- package/dist/core/ask-origin.js +1 -1
- package/dist/core/auto-mode.d.ts +3 -2
- package/dist/core/checkpoint-store.d.ts +11 -19
- package/dist/core/checkpoint-store.js +1 -1
- package/dist/core/decision-receipt.d.ts +4 -2
- package/dist/core/decision-receipt.js +1 -1
- package/dist/core/engine-notice.d.ts +8 -5
- package/dist/core/gate-fold.js +1 -1
- package/dist/core/gate-lanes.d.ts +1 -49
- package/dist/core/gate-lanes.js +85 -223
- package/dist/core/governance-codes.d.ts +3 -3
- package/dist/core/governance-codes.js +3 -3
- package/dist/core/hooks.d.ts +13 -10
- package/dist/core/hooks.js +5 -3
- package/dist/core/mode-policy.d.ts +3 -3
- package/dist/core/mode-policy.js +2 -2
- package/dist/core/permission-rules.d.ts +24 -23
- package/dist/core/permission-rules.js +37 -33
- package/dist/core/reminder-disclosure.d.ts +53 -2
- package/dist/core/reminder-disclosure.js +49 -36
- package/dist/core/rule-lane-reads.d.ts +84 -0
- package/dist/core/rule-lane-reads.js +164 -0
- package/dist/core/runner/contracts.d.ts +4 -2
- package/dist/core/runner/gate-exit.d.ts +3 -3
- package/dist/core/runner/image.js +4 -3
- package/dist/core/runner/inline-image.d.ts +14 -0
- package/dist/core/runner/inline-image.js +26 -0
- package/dist/core/runner/permission-rule-lanes.d.ts +3 -1
- package/dist/core/runner/permission-rule-lanes.js +1 -1
- package/dist/core/runner/prepare-ask-lane.d.ts +0 -19
- package/dist/core/runner/prepare-ask-lane.js +2 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +2 -0
- package/dist/core/runner/prepare-inherited-gate.js +1 -0
- package/dist/core/runner/prepare-park-ask.js +3 -3
- package/dist/core/runner/prepare-policy-chain.d.ts +12 -0
- package/dist/core/runner/prepare-policy-chain.js +10 -9
- package/dist/core/runner/prepare-project-context.d.ts +13 -3
- package/dist/core/runner/prepare-project-context.js +5 -4
- package/dist/core/runner/prepare-task.js +8 -5
- package/dist/core/runner/prepare-turn-wiring.d.ts +16 -3
- package/dist/core/runner/prepare-turn-wiring.js +19 -18
- package/dist/core/runner/run-leg.js +1 -1
- package/dist/core/runner/runtask.js +2 -1
- package/dist/core/runner-deps.d.ts +11 -0
- package/dist/core/skill-grant.d.ts +8 -47
- package/dist/core/skill-grant.js +15 -136
- package/dist/core/synthetic-read-verdict.d.ts +108 -0
- package/dist/core/synthetic-read-verdict.js +111 -0
- package/dist/core/task-spec.d.ts +18 -0
- package/dist/core/tool-name-readings.d.ts +1 -1
- package/dist/core/tool-name-readings.js +1 -1
- package/dist/core/tool-policy.d.ts +25 -24
- package/dist/core/tool-policy.js +26 -17
- package/dist/core/tool-spec.d.ts +7 -0
- package/dist/core/trace.d.ts +16 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -0
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/tools/fs/dir-entries.d.ts +69 -0
- package/dist/tools/fs/dir-entries.js +60 -0
- package/dist/tools/fs/fs-pdf.d.ts +7 -11
- package/dist/tools/fs/fs-pdf.js +5 -14
- package/dist/tools/fs/fs-read.d.ts +173 -2
- package/dist/tools/fs/fs-read.js +399 -286
- package/dist/tools/fs/image-admission.d.ts +51 -0
- package/dist/tools/fs/image-admission.js +36 -0
- package/dist/tools/fs/search.js +3 -7
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +29 -1
- package/wire-changes/core@7.41.1.json +159 -0
- package/wire-changes/core@7.42.0.json +1107 -0
|
@@ -78,9 +78,9 @@ export interface ModeGateSeats {
|
|
|
78
78
|
* `readOnlyHint` ({@link AgentTool.mcpReadOnlyHint})? The mode gate's `readOnlyDeclared` seat over the leg's live tools. */
|
|
79
79
|
export declare function mountedToolDeclaresReadOnly(tools: readonly AgentTool[], toolName: string): boolean;
|
|
80
80
|
/** The ONE rule of the unstated question, read at the fold's last station and at the approved-edit round: an allow nobody
|
|
81
|
-
* stated (no `decisionReason
|
|
82
|
-
*
|
|
83
|
-
export declare function asksUnstated(d: PermissionResult, seat: "passthrough" | undefined, mode: PermissionMode | undefined
|
|
81
|
+
* stated (no `decisionReason`) on a tool that states no permission of its own, under a mode whose `unstated` answer is
|
|
82
|
+
* `ask`. Who may answer it is the allow layer's business (a hook's own `allow` is one of its members), not this rule's. */
|
|
83
|
+
export declare function asksUnstated(d: PermissionResult, seat: "passthrough" | undefined, mode: PermissionMode | undefined): boolean;
|
|
84
84
|
/** The unstated question's sentence (the reference implementation's passthrough sentence, without its product name). */
|
|
85
85
|
export declare const unstatedAskMessage: (toolName: string) => string;
|
|
86
86
|
/** One call's permission seat: `passthrough` (an allow nobody stated meets the mode's unstated answer) and the question
|
package/dist/core/mode-policy.js
CHANGED
|
@@ -42,8 +42,8 @@ export function modeAcceptsFileCommand(mode, toolName, args) {
|
|
|
42
42
|
export function mountedToolDeclaresReadOnly(tools, toolName) {
|
|
43
43
|
return tools.findLast((t) => t.name === toolName)?.mcpReadOnlyHint === true;
|
|
44
44
|
}
|
|
45
|
-
export function asksUnstated(d, seat, mode
|
|
46
|
-
return d.action === "allow" && d.decisionReason === undefined &&
|
|
45
|
+
export function asksUnstated(d, seat, mode) {
|
|
46
|
+
return d.action === "allow" && d.decisionReason === undefined && seat === "passthrough" && mode !== undefined && modePolicyOf(mode).unstated === "ask";
|
|
47
47
|
}
|
|
48
48
|
export const unstatedAskMessage = (toolName) => `The agent requested permission to use ${toolName}, but it has not been granted yet.`;
|
|
49
49
|
export function permissionSeatAt(input, args) {
|
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
* 架构边界(design/127 §0,重判过的「core 不长 rule interpreter」):这是纯函数式、opt-in、零
|
|
7
7
|
* run-loop 介入的「声明式数据→ToolPolicy」工厂(`createCoarseCommandNamePolicy` 同形态先例)。
|
|
8
8
|
* core 不读设置文件、不管 source 层级合并 —— Settings-loader 把各层合并成**单一规则表**后编译
|
|
9
|
-
* **一个** policy
|
|
10
|
-
*
|
|
9
|
+
* **一个** policy。
|
|
10
|
+
*
|
|
11
|
+
* ## Deny and ask only
|
|
12
|
+
* A policy fold says `deny` / `ask`. An allow rule is refused (`config.invalid_permission_rules`, one `unsupported.allow`
|
|
13
|
+
* issue per rule): "this does not need asking" lives on the allow layer (`TaskSpec.allowedTools`, `RunnerDeps.allowedTools`).
|
|
11
14
|
*
|
|
12
15
|
* ## The CONTENT-FORM command lane (the shell tool's own grammar)
|
|
13
16
|
*
|
|
@@ -21,9 +24,7 @@
|
|
|
21
24
|
* a call the lexer cannot read against a rule is a fail-closed ask, never silence).
|
|
22
25
|
* It sits in the field-level tier of the evaluation order — right after the bare deny (resp. ask) — which
|
|
23
26
|
* is upstream's placement (`bs()` whole-tool deny → `AN(…,"deny")` field-level deny → `ph()` whole-tool
|
|
24
|
-
* ask → `AN(…,"ask")` field-level ask
|
|
25
|
-
* a standing approval is a CONSENT record and lives in the persisted-rule store (the import door carries
|
|
26
|
-
* a settings allow list there); inside a policy fold an allow is "no objection" and would clear nothing.
|
|
27
|
+
* ask → `AN(…,"ask")` field-level ask).
|
|
27
28
|
*
|
|
28
29
|
* The PATH lane is the same shape for every tool the catalog declares a path target for: a
|
|
29
30
|
* `Edit(~/.sema/settings.json)` / `Write(src/**)` / `Read(//etc/**)` deny or ask compiles through the same
|
|
@@ -33,14 +34,12 @@
|
|
|
33
34
|
* object face's declaration, the catalog's for the name as the floor). The bases a pattern resolves
|
|
34
35
|
* against are the deployment's (`pathBases`); a pattern whose base the policy was not given is refused at
|
|
35
36
|
* compile time (`unsupported.path_base`) rather than left to reach nothing at run time — a deny that
|
|
36
|
-
* silently matches nothing is the failure form this module's header calls the most dangerous one.
|
|
37
|
-
* path-form ALLOW stays refused (`unsupported.file_glob`): the Read directory allow is the consent store's.
|
|
37
|
+
* silently matches nothing is the failure form this module's header calls the most dangerous one.
|
|
38
38
|
* A `Read(...)` / `Edit(...)` deny or ask reaches the calls on its behavior's FACES (`PATH_RULE_FACES`), not only
|
|
39
39
|
* its own tool: a `Read` ask asks for a Grep / Glob rooted in its path, judged on that call's own target.
|
|
40
40
|
*
|
|
41
41
|
* 🔴 与 CC 的有意差(design/127 r2):
|
|
42
|
-
* - CC 参数级 generic 道无 allow lane(gnn 只有 deny/ask)——
|
|
43
|
-
* 无效规则;auto-allow 只来自裸工具规则。
|
|
42
|
+
* - CC 参数级 generic 道无 allow lane(gnn 只有 deny/ask)—— 我们同样,且整个 policy 无 allow 臂。
|
|
44
43
|
* - CC 对尾缀垃圾静默降级为裸名(Lh `n !== len-1`)—— `parsePermissionRule` 保持 CC parity,但
|
|
45
44
|
* **工厂 validator strict**:降级即 invalid(fail-fast 世界观,写错的规则不能静默变宽)。
|
|
46
45
|
* - 已知主字段参数规则(如 `Bash(command:…)`)默认 **reject**:通配是 trim 后**整串 lexical**
|
|
@@ -60,7 +59,7 @@ export declare const BASH_GENERIC_PARAMS: ReadonlySet<string>;
|
|
|
60
59
|
export interface PermissionRule {
|
|
61
60
|
/** CC 语法:`"Tool"` 或 `"Tool(param:pattern)"`。`Tool()`/`Tool(*)` 归一为裸规则;括号可 `\(`/`\)` 转义。 */
|
|
62
61
|
rule: string;
|
|
63
|
-
behavior: "deny" | "ask"
|
|
62
|
+
behavior: "deny" | "ask";
|
|
64
63
|
/** Provenance:进 deny/ask 的 message 供审计(e.g. `"policySettings"`)。层级
|
|
65
64
|
* 优先级/tighten-only 语义在 Settings-loader,不在这里。 */
|
|
66
65
|
source?: string;
|
|
@@ -69,21 +68,25 @@ export interface PermissionRuleIssue {
|
|
|
69
68
|
rule: string;
|
|
70
69
|
/** `invalid.*` = 语法/语义错;`unsupported.*` = CC 合法但 v1 不支持的 lane(独立
|
|
71
70
|
* 分类,绝不静默 no-op —— `Bash(npm run:*)` 被当 generic 参数规则接受后永不匹配是最危险的失败形态)。 */
|
|
72
|
-
code: "invalid.empty_tool" | "invalid.paren" | "invalid.param_split" | "invalid.
|
|
73
|
-
/**
|
|
74
|
-
*
|
|
75
|
-
| "unsupported.
|
|
71
|
+
code: "invalid.empty_tool" | "invalid.paren" | "invalid.param_split" | "invalid.primary_field" | "invalid.cap_exceeded"
|
|
72
|
+
/** An ALLOW rule (any spelling): a policy fold says deny / ask only — "this does not need asking" is the allow
|
|
73
|
+
* layer's (`TaskSpec.allowedTools`, `RunnerDeps.allowedTools`). One issue per allow rule. */
|
|
74
|
+
| "unsupported.allow"
|
|
75
|
+
/** A `behavior` outside the closed set `deny | ask` (a JS caller's typo, an undefined member, a boxed string): refused —
|
|
76
|
+
* a rule that matched nothing because its verb was misspelled would be a deny that silently never fires. */
|
|
77
|
+
| "invalid.behavior"
|
|
76
78
|
/** A content-form Bash deny/ask rule the ONE rule validator refuses (a redirection, a substitution, a
|
|
77
79
|
* mid-pattern star, control characters…) — the message carries the validator's own code and sentence. */
|
|
78
80
|
| "invalid.command_rule"
|
|
81
|
+
/** An `allowedTools` value (the request's `TaskSpec.allowedTools`, the deployment's `RunnerDeps.allowedTools`) that is
|
|
82
|
+
* not a list of strings: one issue for a non-list, one per element that is not a string (`rule` is empty — there is
|
|
83
|
+
* no rule text to name). */
|
|
84
|
+
| "invalid.entry_type"
|
|
79
85
|
/** design/388 B15 — the closed rule-lint set over a tool's RULE FACE (derived scalar vocabulary; a
|
|
80
86
|
* declaration only narrows it): the named parameter is not one the tool carries; it is carried but
|
|
81
87
|
* is an object/array (a rule on it can never match); the tool is unknown to the resolver a leg
|
|
82
88
|
* supplied (absent resolver ⇒ the catalog alone judges and an unknown tool stays permissive). */
|
|
83
89
|
| "unsupported.param_unknown" | "unsupported.param_non_scalar" | "unsupported.tool_unknown"
|
|
84
|
-
/** A path-form ALLOW rule (`Edit(src/**)` / `Read(//x/**)` in an allow list): a standing approval by
|
|
85
|
-
* path is the consent store's (the Read directory form imports there); deny/ask path rules COMPILE. */
|
|
86
|
-
| "unsupported.file_glob"
|
|
87
90
|
/** A path-form deny/ask whose spelling needs a base this policy was not given (`/x` needs
|
|
88
91
|
* `pathBases.root`; a relative pattern needs `pathBases.cwd` or `root`) — refused rather than compiled
|
|
89
92
|
* into a rule that reaches nothing. */
|
|
@@ -119,9 +122,8 @@ export interface PermissionRulePolicyOptions {
|
|
|
119
122
|
/** Called once per dropped rule under `"skip"` (audit surface — silent drops are how a deployment
|
|
120
123
|
* discovers, months later, that its deny never existed). Default: one aggregated `console.warn`. */
|
|
121
124
|
onIssue?: (issue: PermissionRuleIssue) => void;
|
|
122
|
-
/** What the policy returns when NO rule matches. Default `"allow"` = 本 policy 无意见(
|
|
123
|
-
* default-closed 部署用 `"ask"`/`"deny"` —— 必须在这 INSIDE the policy(链尾
|
|
124
|
-
* ask policy 会在 fold 里吞掉本 policy 的显式 allow)。 */
|
|
125
|
+
/** What the policy returns when NO rule matches. Default `"allow"` = 本 policy 无意见(组合语义;不是同意)。
|
|
126
|
+
* default-closed 部署用 `"ask"`/`"deny"` —— 必须在这 INSIDE the policy(链尾 ask policy 会改写整条链的答案)。 */
|
|
125
127
|
defaultAction?: "allow" | "ask" | "deny";
|
|
126
128
|
/** 已知主字段参数规则(`Bash(command:…)` 等)的处置。默认 `"reject"`(整串 lexical ≠ 前缀/AST,
|
|
127
129
|
* 假安全感);`"allow-lexical"` 显式接受并按 generic 语义匹配。 */
|
|
@@ -214,8 +216,7 @@ export declare function validatePermissionRules(rules: PermissionRule[], opts?:
|
|
|
214
216
|
* field-level tiers of upstream's arbiter, beside the generic param tiers). `check()` is idempotent and side-effect-free (combinePolicies 契约);
|
|
215
217
|
* all parsing/validation happens HERE, the hot path never re-parses.
|
|
216
218
|
*
|
|
217
|
-
* ⚠️
|
|
218
|
-
*
|
|
219
|
-
* Express default-closed via `defaultAction`, never via a trailing always-ask policy.
|
|
219
|
+
* ⚠️ This policy states no allow: a call no rule reaches gets `defaultAction`, whose `"allow"` is "no objection" (states
|
|
220
|
+
* nothing, clears nothing). Express default-closed via `defaultAction`, never via a trailing always-ask policy.
|
|
220
221
|
*/
|
|
221
222
|
export declare function createPermissionRulePolicy(rules: PermissionRule[], opts?: PermissionRulePolicyOptions): NamedToolPolicy;
|
|
@@ -9,6 +9,21 @@ import { ruleDenialSentence } from "./rule-denial-sentence.js";
|
|
|
9
9
|
import { heldValues } from "./tool-name-readings.js";
|
|
10
10
|
export { parsePermissionRule };
|
|
11
11
|
export const BASH_GENERIC_PARAMS = new Set(catalogRuleFaceOf("Bash")?.params ?? []);
|
|
12
|
+
function describeBehavior(v) {
|
|
13
|
+
switch (typeof v) {
|
|
14
|
+
case "undefined": return "undefined";
|
|
15
|
+
case "string": {
|
|
16
|
+
const q = JSON.stringify(v);
|
|
17
|
+
return q.length > 40 ? q.slice(0, 37) + '..."' : q;
|
|
18
|
+
}
|
|
19
|
+
case "number":
|
|
20
|
+
case "boolean":
|
|
21
|
+
case "bigint": return String(v);
|
|
22
|
+
case "symbol": return "a symbol";
|
|
23
|
+
case "function": return "a function";
|
|
24
|
+
default: return v === null ? "null" : "an object";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
12
27
|
const DEFAULT_CAPS = {
|
|
13
28
|
maxRules: 256,
|
|
14
29
|
maxRuleLength: 512,
|
|
@@ -115,7 +130,7 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
115
130
|
return { face, judged: face !== undefined };
|
|
116
131
|
};
|
|
117
132
|
const byTool = new Map();
|
|
118
|
-
const namespacedCovering = { deny: [], ask: []
|
|
133
|
+
const namespacedCovering = { deny: [], ask: [] };
|
|
119
134
|
const issues = [];
|
|
120
135
|
const bad = (rule, code, message) => {
|
|
121
136
|
issues.push({ rule, code, message });
|
|
@@ -125,6 +140,15 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
125
140
|
}
|
|
126
141
|
for (const r of rules) {
|
|
127
142
|
const text = r.rule;
|
|
143
|
+
const behavior = r.behavior;
|
|
144
|
+
if (behavior !== "deny" && behavior !== "ask") {
|
|
145
|
+
if (behavior === "allow")
|
|
146
|
+
bad(text, "unsupported.allow", "an allow rule does not belong to a policy — allow entries live on the allow layer: put the entry in `TaskSpec.allowedTools` (a request's) or `RunnerDeps.allowedTools` (the deployment's); deny / ask rules stay here");
|
|
147
|
+
else
|
|
148
|
+
bad(text, "invalid.behavior", `a rule's behavior is one of "deny" | "ask" — got ${describeBehavior(behavior)}; a misspelled verb would compile to a rule that silently never fires`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const verb = behavior;
|
|
128
152
|
if (text.length > caps.maxRuleLength) {
|
|
129
153
|
bad(text, "invalid.cap_exceeded", `rule longer than ${caps.maxRuleLength} chars`);
|
|
130
154
|
continue;
|
|
@@ -161,10 +185,10 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
161
185
|
const entry = byTool.get(toolName) ?? { bare: {}, param: { deny: [], ask: [] }, content: { deny: [], ask: [] } };
|
|
162
186
|
if (parsed.ruleContent === undefined) {
|
|
163
187
|
if (isNamespacedCoveringRuleName(toolName)) {
|
|
164
|
-
namespacedCovering[
|
|
188
|
+
namespacedCovering[verb].push({ ruleName: toolName, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}) });
|
|
165
189
|
continue;
|
|
166
190
|
}
|
|
167
|
-
entry.bare[
|
|
191
|
+
entry.bare[verb] = { ruleText: text, source: r.source };
|
|
168
192
|
byTool.set(toolName, entry);
|
|
169
193
|
continue;
|
|
170
194
|
}
|
|
@@ -174,16 +198,12 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
174
198
|
const paramPart = colon > 0 ? content.slice(0, colon).trim() : "";
|
|
175
199
|
const looksLikeParamRule = colon > 0 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(paramPart) && BASH_GENERIC_PARAMS.has(paramPart);
|
|
176
200
|
if (!looksLikeParamRule || content.endsWith(":*")) {
|
|
177
|
-
|
|
178
|
-
bad(text, "unsupported.bash_prefix", "a Bash command ALLOW rule (`Bash(npm run:*)`, `Bash(git *)`) is a standing approval and belongs to the persisted-rule store (import it, or approve the command once and keep the rule) — a policy fold compiles command DENY/ASK rules only");
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
const parsedCommand = parseRuleText(text, r.behavior);
|
|
201
|
+
const parsedCommand = parseRuleText(text, verb);
|
|
182
202
|
if ("reject" in parsedCommand) {
|
|
183
203
|
bad(text, "invalid.command_rule", `${parsedCommand.reject.code}: ${parsedCommand.reject.message}`);
|
|
184
204
|
continue;
|
|
185
205
|
}
|
|
186
|
-
entry.content[
|
|
206
|
+
entry.content[verb].push({ parsed: parsedCommand.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}) });
|
|
187
207
|
byTool.set(toolName, entry);
|
|
188
208
|
continue;
|
|
189
209
|
}
|
|
@@ -196,11 +216,7 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
196
216
|
}
|
|
197
217
|
const face = resolved.face;
|
|
198
218
|
if (face !== undefined && face.pathTargeting === true && colon <= 0) {
|
|
199
|
-
|
|
200
|
-
bad(text, "unsupported.file_glob", "a path-form ALLOW rule (`Edit(src/**)`, `Read(//x/**)`) is a standing approval and belongs to the persisted-rule store (the Read directory form imports there) — a policy fold compiles path DENY/ASK rules only");
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
const parsedPath = parseRuleText(text, r.behavior);
|
|
219
|
+
const parsedPath = parseRuleText(text, verb);
|
|
204
220
|
if ("reject" in parsedPath) {
|
|
205
221
|
bad(text, "invalid.command_rule", `${parsedPath.reject.code}: ${parsedPath.reject.message}`);
|
|
206
222
|
continue;
|
|
@@ -213,8 +229,8 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
213
229
|
}
|
|
214
230
|
if (issues.length > 0 && issues[issues.length - 1].rule === text && issues[issues.length - 1].code === "unsupported.path_base")
|
|
215
231
|
continue;
|
|
216
|
-
const faces = PATH_RULE_FACES[toolName]?.[
|
|
217
|
-
entry.content[
|
|
232
|
+
const faces = PATH_RULE_FACES[toolName]?.[verb];
|
|
233
|
+
entry.content[verb].push({ parsed: parsedPath.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}), ...(faces !== undefined ? { faces } : {}) });
|
|
218
234
|
byTool.set(toolName, entry);
|
|
219
235
|
continue;
|
|
220
236
|
}
|
|
@@ -242,10 +258,6 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
242
258
|
bad(text, "invalid.param_split", "param and pattern must both be non-empty");
|
|
243
259
|
continue;
|
|
244
260
|
}
|
|
245
|
-
if (r.behavior === "allow") {
|
|
246
|
-
bad(text, "invalid.allow_param_rule", "param-level rules cannot `allow` (CC parity: the generic lane is deny/ask only; auto-allow comes from bare-tool rules)");
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
261
|
if (pattern.length > caps.maxPatternLength || countStars(pattern) > caps.maxStars) {
|
|
250
262
|
bad(text, "invalid.cap_exceeded", `pattern exceeds maxPatternLength ${caps.maxPatternLength} or maxStars ${caps.maxStars}`);
|
|
251
263
|
continue;
|
|
@@ -254,7 +266,7 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
254
266
|
bad(text, "invalid.primary_field", `"${param}" is ${toolName}'s primary field — the generic matcher is a trim+FULL-STRING lexical match (no shell/path semantics); '${toolName}(${param}:git push*)' would NOT match 'cd /x && git push'. Opt in with primaryFieldGeneric:"allow-lexical" if you understand this.`);
|
|
255
267
|
continue;
|
|
256
268
|
}
|
|
257
|
-
entry.param[
|
|
269
|
+
entry.param[verb].push({ param, pattern, behavior: verb, ruleText: text, source: r.source });
|
|
258
270
|
byTool.set(toolName, entry);
|
|
259
271
|
}
|
|
260
272
|
return { byTool, namespacedCovering, issues };
|
|
@@ -320,21 +332,19 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
320
332
|
}
|
|
321
333
|
}
|
|
322
334
|
const defaultAction = opts?.defaultAction ?? "allow";
|
|
323
|
-
const nameSets = { deny: [], ask: []
|
|
335
|
+
const nameSets = { deny: [], ask: [] };
|
|
324
336
|
for (const [toolName, entry] of byTool) {
|
|
325
337
|
if (entry.bare.deny !== undefined || entry.param.deny.length > 0 || entry.content.deny.length > 0)
|
|
326
338
|
nameSets.deny.push(toolName);
|
|
327
339
|
if (entry.bare.ask !== undefined || entry.param.ask.length > 0 || entry.content.ask.length > 0)
|
|
328
340
|
nameSets.ask.push(toolName);
|
|
329
|
-
if (entry.bare.allow !== undefined)
|
|
330
|
-
nameSets.allow.push(toolName);
|
|
331
341
|
}
|
|
332
|
-
for (const behavior of ["deny", "ask"
|
|
342
|
+
for (const behavior of ["deny", "ask"]) {
|
|
333
343
|
for (const r of namespacedCovering[behavior])
|
|
334
344
|
nameSets[behavior].push(r.ruleName);
|
|
335
345
|
}
|
|
336
346
|
const coveringHit = (lane, toolName) => lane.length === 0 ? undefined : lane.find((r) => namespacedRuleNameCovers(r.ruleName, toolName));
|
|
337
|
-
const hasCovering = namespacedCovering.deny.length > 0 || namespacedCovering.ask.length > 0
|
|
347
|
+
const hasCovering = namespacedCovering.deny.length > 0 || namespacedCovering.ask.length > 0;
|
|
338
348
|
const faceRules = {
|
|
339
349
|
deny: [...byTool.values()].flatMap((e) => e.content.deny.filter((r) => r.faces !== undefined)),
|
|
340
350
|
ask: [...byTool.values()].flatMap((e) => e.content.ask.filter((r) => r.faces !== undefined)),
|
|
@@ -387,7 +397,7 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
387
397
|
if (entries.length <= 1)
|
|
388
398
|
return entries[0];
|
|
389
399
|
const bare = {};
|
|
390
|
-
for (const b of ["deny", "ask"
|
|
400
|
+
for (const b of ["deny", "ask"]) {
|
|
391
401
|
const hit = entries.find((e) => e.bare[b] !== undefined)?.bare[b];
|
|
392
402
|
if (hit !== undefined)
|
|
393
403
|
bare[b] = hit;
|
|
@@ -448,12 +458,6 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
448
458
|
if (paramAsk) {
|
|
449
459
|
return { action: "ask", message: ruleAskMessage(paramAsk.ruleText, paramAsk.source), matchedAskRule: paramAsk.ruleText };
|
|
450
460
|
}
|
|
451
|
-
if (entry.bare.allow) {
|
|
452
|
-
return { action: "allow", decisionReason: "rule" };
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
if (covering && coveringHit(namespacedCovering.allow, req.toolName)) {
|
|
456
|
-
return { action: "allow", decisionReason: "rule" };
|
|
457
461
|
}
|
|
458
462
|
if (defaultAction === "allow")
|
|
459
463
|
return { action: "allow" };
|
|
@@ -150,7 +150,58 @@ export interface ReminderDisclosureOutcome {
|
|
|
150
150
|
throttled: boolean;
|
|
151
151
|
}
|
|
152
152
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
153
|
+
* What a planned disclosure will say, as a value: which sentences the trailer carries. The Read carrier
|
|
154
|
+
* renders it with {@link renderReminderDisclosureTrailer} (the bytes the pipeline has always appended);
|
|
155
|
+
* another carrier renders the same verdict in its own words. Undefined `reminder` and `envelopeTags` ⇒ no
|
|
156
|
+
* trailer.
|
|
157
|
+
*/
|
|
158
|
+
export interface ReminderDisclosureVerdict {
|
|
159
|
+
/** The `<system-reminder>`-family sentence: the marked copy (with whether the defuse arm rewrote bytes)
|
|
160
|
+
* or the bare copy. */
|
|
161
|
+
reminder?: {
|
|
162
|
+
form: "marked";
|
|
163
|
+
defused: boolean;
|
|
164
|
+
} | {
|
|
165
|
+
form: "bare";
|
|
166
|
+
};
|
|
167
|
+
/** The envelope-family sentence, with the REGISTRY tag names it names (never matched bytes). */
|
|
168
|
+
envelopeTags?: readonly string[];
|
|
169
|
+
}
|
|
170
|
+
/** The state a planned disclosure changes — applied by {@link commitReminderDisclosureEffects}, never by
|
|
171
|
+
* the plan: the counter keys to bump (one entry per bump, in pipeline order) and the throttle windows to
|
|
172
|
+
* advance (key → next-allowed epoch ms). */
|
|
173
|
+
export interface ReminderDisclosureEffects {
|
|
174
|
+
counts: readonly string[];
|
|
175
|
+
windows: ReadonlyArray<readonly [string, number]>;
|
|
176
|
+
}
|
|
177
|
+
/** {@link planReminderDisclosure}'s answer: the outcome the pipeline reports, the verdict a carrier renders,
|
|
178
|
+
* and the effects a delivery commits. */
|
|
179
|
+
export interface ReminderDisclosurePlan {
|
|
180
|
+
outcome: Omit<ReminderDisclosureOutcome, "trailer">;
|
|
181
|
+
verdict: ReminderDisclosureVerdict;
|
|
182
|
+
effects: ReminderDisclosureEffects;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Plan the four-step pipeline over one finalized external projection WITHOUT changing anything: the
|
|
186
|
+
* throttle windows are read (a due bare/envelope sentence is suppressed by a live window) but never
|
|
187
|
+
* advanced, and the counters are not bumped — both land in `effects`, which the caller commits when the
|
|
188
|
+
* projection is actually delivered ({@link commitReminderDisclosureEffects}). Two plans over the same key
|
|
189
|
+
* before either commits both see the window open; that is the defined meaning of "effects ride the
|
|
190
|
+
* delivery".
|
|
191
|
+
*/
|
|
192
|
+
export declare function planReminderDisclosure(input: ReminderDisclosureInput): ReminderDisclosurePlan;
|
|
193
|
+
/** Apply a plan's effects: bump each counter key, advance each throttle window. `windows` is the map the plan
|
|
194
|
+
* read (the outlet's per-closure throttle seat); either seat may be absent (library-direct mounts). */
|
|
195
|
+
export declare function commitReminderDisclosureEffects(effects: ReminderDisclosureEffects, seats: {
|
|
196
|
+
counts?: ReminderDisclosureCounts;
|
|
197
|
+
windows?: Map<string, number>;
|
|
198
|
+
}): void;
|
|
199
|
+
/** The trailer a verdict renders to on the tool-result carrier — a full trusted reminder block (self-marked),
|
|
200
|
+
* or undefined when the verdict carries no sentence. Byte-identical to the pipeline's historical trailer. */
|
|
201
|
+
export declare function renderReminderDisclosureTrailer(verdict: ReminderDisclosureVerdict, mark: string | undefined): string | undefined;
|
|
202
|
+
/**
|
|
203
|
+
* Run the four-step pipeline over one finalized external projection and commit its effects at once — the
|
|
204
|
+
* single-shot outlets (MCP, web) whose projection is delivered in the same breath. Plan + commit + render:
|
|
205
|
+
* the windows and counters move exactly as they always did.
|
|
155
206
|
*/
|
|
156
207
|
export declare function discloseReminderShaped(input: ReminderDisclosureInput): ReminderDisclosureOutcome;
|
|
@@ -6,7 +6,7 @@ export function bumpReminderDisclosureCount(counts, key) {
|
|
|
6
6
|
}
|
|
7
7
|
export function observeReminderMarkEcho(input) {
|
|
8
8
|
const { text, mark, outlet, counts } = input;
|
|
9
|
-
if (
|
|
9
|
+
if (!markEchoed(text, mark))
|
|
10
10
|
return false;
|
|
11
11
|
bumpReminderDisclosureCount(counts, `${outlet}.mark_echo`);
|
|
12
12
|
return true;
|
|
@@ -39,21 +39,20 @@ function envelopeTrailerBody(tags) {
|
|
|
39
39
|
function composeTrailerBody(reminderPart, envelopePart) {
|
|
40
40
|
return [reminderPart, envelopePart].filter((p) => p !== undefined).join(" ");
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
42
|
+
function markEchoed(text, mark) {
|
|
43
|
+
return mark !== undefined && mark !== "" && text.includes(mark);
|
|
44
|
+
}
|
|
45
|
+
export function planReminderDisclosure(input) {
|
|
46
|
+
const { mark, outlet } = input;
|
|
47
|
+
const counts = [];
|
|
48
|
+
const windows = [];
|
|
49
|
+
const effects = () => ({ counts, windows });
|
|
50
|
+
const untouchedOutcome = { segments: [...input.segments], form: undefined, defused: false, throttled: false };
|
|
51
51
|
if (mark === undefined)
|
|
52
|
-
return
|
|
52
|
+
return { outcome: untouchedOutcome, verdict: {}, effects: effects() };
|
|
53
53
|
const joined = input.segments.join("");
|
|
54
54
|
const scan = scanReminderShaped(joined, mark);
|
|
55
55
|
const envScan = scanEnvelopeShaped(joined);
|
|
56
|
-
const envelopePart = envScan.hit ? envelopeTrailerBody(envScan.tags) : undefined;
|
|
57
56
|
let segments = [...input.segments];
|
|
58
57
|
let defused = false;
|
|
59
58
|
if (input.defuseExactMark) {
|
|
@@ -62,24 +61,21 @@ export function discloseReminderShaped(input) {
|
|
|
62
61
|
defused = r.changed;
|
|
63
62
|
}
|
|
64
63
|
const marked = scan.hadCurrentMark || defused;
|
|
65
|
-
if (!marked)
|
|
66
|
-
|
|
64
|
+
if (!marked && markEchoed(joined, mark))
|
|
65
|
+
counts.push(`${outlet}.mark_echo`);
|
|
67
66
|
if (!marked && !scan.hit && !envScan.hit) {
|
|
68
|
-
|
|
69
|
-
return { ...clean, segments };
|
|
67
|
+
return { outcome: { ...untouchedOutcome, segments }, verdict: {}, effects: effects() };
|
|
70
68
|
}
|
|
71
69
|
if (marked) {
|
|
72
70
|
if (defused)
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
counts.push(`${outlet}.defused`);
|
|
72
|
+
counts.push(`${outlet}.marked`);
|
|
75
73
|
if (envScan.hit)
|
|
76
|
-
|
|
74
|
+
counts.push(`${outlet}.envelope`);
|
|
77
75
|
return {
|
|
78
|
-
segments,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
defused,
|
|
82
|
-
throttled: false,
|
|
76
|
+
outcome: { segments, form: "marked", defused, throttled: false },
|
|
77
|
+
verdict: { reminder: { form: "marked", defused }, ...(envScan.hit ? { envelopeTags: envScan.tags } : {}) },
|
|
78
|
+
effects: effects(),
|
|
83
79
|
};
|
|
84
80
|
}
|
|
85
81
|
const t = input.throttle;
|
|
@@ -96,27 +92,44 @@ export function discloseReminderShaped(input) {
|
|
|
96
92
|
const bare = verdict(scan.hit, throttleWindowKey("bare", t?.key ?? ""));
|
|
97
93
|
const envelope = verdict(envScan.hit, throttleWindowKey("envelope", t?.key ?? ""));
|
|
98
94
|
if (bare.suppressed)
|
|
99
|
-
|
|
95
|
+
counts.push(`${outlet}.bare_throttled`);
|
|
100
96
|
if (envelope.suppressed)
|
|
101
|
-
|
|
97
|
+
counts.push(`${outlet}.envelope_throttled`);
|
|
102
98
|
if (!bare.emit && !envelope.emit) {
|
|
103
|
-
return {
|
|
99
|
+
return { outcome: { segments, form: undefined, defused, throttled: true }, verdict: {}, effects: effects() };
|
|
104
100
|
}
|
|
105
101
|
if (t !== undefined) {
|
|
106
102
|
if (bare.consume !== undefined)
|
|
107
|
-
|
|
103
|
+
windows.push([bare.consume, now + BARE_REMINDER_DISCLOSURE_WINDOW_MS]);
|
|
108
104
|
if (envelope.consume !== undefined)
|
|
109
|
-
|
|
105
|
+
windows.push([envelope.consume, now + BARE_REMINDER_DISCLOSURE_WINDOW_MS]);
|
|
110
106
|
}
|
|
111
107
|
if (bare.emit)
|
|
112
|
-
|
|
108
|
+
counts.push(`${outlet}.bare`);
|
|
113
109
|
if (envelope.emit)
|
|
114
|
-
|
|
110
|
+
counts.push(`${outlet}.envelope`);
|
|
115
111
|
return {
|
|
116
|
-
segments,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
defused,
|
|
120
|
-
throttled: bare.suppressed || envelope.suppressed,
|
|
112
|
+
outcome: { segments, form: bare.emit ? "bare" : "envelope", defused, throttled: bare.suppressed || envelope.suppressed },
|
|
113
|
+
verdict: { ...(bare.emit ? { reminder: { form: "bare" } } : {}), ...(envelope.emit ? { envelopeTags: envScan.tags } : {}) },
|
|
114
|
+
effects: effects(),
|
|
121
115
|
};
|
|
122
116
|
}
|
|
117
|
+
export function commitReminderDisclosureEffects(effects, seats) {
|
|
118
|
+
for (const key of effects.counts)
|
|
119
|
+
bumpReminderDisclosureCount(seats.counts, key);
|
|
120
|
+
if (seats.windows !== undefined)
|
|
121
|
+
for (const [key, until] of effects.windows)
|
|
122
|
+
seats.windows.set(key, until);
|
|
123
|
+
}
|
|
124
|
+
export function renderReminderDisclosureTrailer(verdict, mark) {
|
|
125
|
+
const reminderPart = verdict.reminder === undefined ? undefined : verdict.reminder.form === "marked" ? markedTrailerBody(verdict.reminder.defused) : bareTrailerBody();
|
|
126
|
+
const envelopePart = verdict.envelopeTags !== undefined ? envelopeTrailerBody(verdict.envelopeTags) : undefined;
|
|
127
|
+
if (reminderPart === undefined && envelopePart === undefined)
|
|
128
|
+
return undefined;
|
|
129
|
+
return mintSystemReminder(composeTrailerBody(reminderPart, envelopePart), mark);
|
|
130
|
+
}
|
|
131
|
+
export function discloseReminderShaped(input) {
|
|
132
|
+
const plan = planReminderDisclosure(input);
|
|
133
|
+
commitReminderDisclosureEffects(plan.effects, { counts: input.counts, ...(input.throttle !== undefined ? { windows: input.throttle.windows } : {}) });
|
|
134
|
+
return { ...plan.outcome, trailer: renderReminderDisclosureTrailer(plan.verdict, input.mark) };
|
|
135
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two rule LANES' answers — the org overlay's and the person's persisted rules' — screened and read for one
|
|
3
|
+
* call, shared by every consumer that judges a call against them (the live gate's lane station, the resumed
|
|
4
|
+
* execution boundary, the synthetic-Read judge). One normalizer and one bounded read per lane: a second copy of
|
|
5
|
+
* either would be a second answer to "what did the lane say", and the gate's fail-closed (org) / skip
|
|
6
|
+
* (persisted store) readings would drift apart between consumers.
|
|
7
|
+
*/
|
|
8
|
+
import type { ToolCallRequest } from "./tool-policy.js";
|
|
9
|
+
import type { OrgGateVerdict, PersistedRuleAnswer, PersistedRuleHit } from "./hooks.js";
|
|
10
|
+
/**
|
|
11
|
+
* design/252 review r3 — read a foreign {@link OrgGateVerdict} the way the personal-rule answer is read:
|
|
12
|
+
* OWN DATA properties only, never the prototype chain, never an accessor.
|
|
13
|
+
*
|
|
14
|
+
* Unlike the personal-rule normalizer below, this one accepts ANY non-array object as the carrier —
|
|
15
|
+
* not only a plain record. The two seams tighten in opposite directions when a shape is refused: the
|
|
16
|
+
* personal lane is a LOOSENING seam, so refusing a class instance degrades toward asking; this answer
|
|
17
|
+
* carries the org's DENY, and folding a structurally valid `{status:"available", verdict:{behavior:
|
|
18
|
+
* "deny"}}` class instance into `unavailable` would LOOSEN it — from a deny nobody can approve into a
|
|
19
|
+
* real-approval ask a person can clear. A class instance's fields are its own data properties, so the
|
|
20
|
+
* own-data read below already gives the full pollution guarantee (inherited members never authorize);
|
|
21
|
+
* the prototype test added nothing here but the downgrade.
|
|
22
|
+
*
|
|
23
|
+
* The two seams are the same class of trust boundary and were not being read the same way. What that
|
|
24
|
+
* cost here is worse than on the personal lane, because this answer is GOVERNANCE:
|
|
25
|
+
* · an inherited `revision` (a polluted `Object.prototype`) stamped a fabricated snapshot version onto
|
|
26
|
+
* a human's approval request — a WRONG record, which is worse than an absent one;
|
|
27
|
+
* · an inherited `verdict` invented an org rule, and with it an org ask or deny the lane never gave;
|
|
28
|
+
* · an answer that is not a record at all read as "available, nothing to say", i.e. the governance
|
|
29
|
+
* fail-open the availability contract exists to prevent.
|
|
30
|
+
* Anything this function cannot read as a well-formed answer becomes `unavailable` — the fail-CLOSED
|
|
31
|
+
* word, never the empty one.
|
|
32
|
+
*/
|
|
33
|
+
export declare function normalizeOrgGateVerdict(answer: unknown, unreadable: string): OrgGateVerdict;
|
|
34
|
+
/**
|
|
35
|
+
* Normalize the accepted {@link ToolGateInput.persistedRules} answers into one reading.
|
|
36
|
+
*
|
|
37
|
+
* `{}` = a clean negative (no rule admits this call). `{ unreadable: true }` = the lane could not read
|
|
38
|
+
* its source. `{ hit }` = a match, whose `rules` is the non-empty coverage set (design/375). An answer
|
|
39
|
+
* outside every accepted shape — a number, `null`, an object with neither `rules` nor `unreadable`, an
|
|
40
|
+
* EMPTY `rules` array, the retired pre-375 `{ rule, dots? }` single-rule object — is read as a clean
|
|
41
|
+
* negative rather than a match: this is a LOOSENING seam, so an answer nobody can name degrades toward
|
|
42
|
+
* asking, never toward an allow built on it. All-or-nothing across the set's TEXTS for the same
|
|
43
|
+
* reason: a set with one unreadable member is a different (smaller) claim than the lane made, and a
|
|
44
|
+
* decision must not stand on a claim nobody made — one bad member drops the whole answer to the clean
|
|
45
|
+
* negative. Per-member DOTS stay individually optional (identity lost ⇒ `"not_reported"`, exactly the
|
|
46
|
+
* single-rule contract).
|
|
47
|
+
*
|
|
48
|
+
* The hit's dots are COPIED, not aliased. The array travels onto an ask that may sit in front of a
|
|
49
|
+
* person for a long time; a lane that retains and mutates its own array would otherwise change what the
|
|
50
|
+
* approver is looking at, and what an audit later reads, after the evidence was stamped.
|
|
51
|
+
*/
|
|
52
|
+
export declare function normalizePersistedRuleHit(hit: PersistedRuleAnswer): {
|
|
53
|
+
hit?: PersistedRuleHit;
|
|
54
|
+
unreadable?: true;
|
|
55
|
+
reason?: string;
|
|
56
|
+
coverage?: readonly import("./permission-rule-model.js").SegmentCoverage[];
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The ORG lane's one read for one call — every consumer's (the live gate's org layer, the synthetic-Read
|
|
60
|
+
* judge). The CALL is inside the boundary (a synchronous throw — a non-function face, a revoked proxy —
|
|
61
|
+
* happens before any `.catch` on its result could be installed), it is invoked directly (a lane that reads
|
|
62
|
+
* its own state before its first `await` sees the same turn), it is BOUNDED by the dependency timeout and
|
|
63
|
+
* the signal, and everything that is not a well-formed answer — a throw, a hang, a garbage value — reads
|
|
64
|
+
* as `unavailable`, the fail-closed word, never as "no org rule spoke".
|
|
65
|
+
*/
|
|
66
|
+
export declare function readOrgRuleLane(org: {
|
|
67
|
+
adjudicate: (req: ToolCallRequest) => Promise<OrgGateVerdict>;
|
|
68
|
+
}, req: ToolCallRequest, signal: AbortSignal | undefined, timeoutMs?: number): Promise<OrgGateVerdict>;
|
|
69
|
+
/**
|
|
70
|
+
* The persisted-rule lane's one read for one call — every consumer's (the live gate, the synthetic-Read
|
|
71
|
+
* judge), normalized once. A lane that throws, hangs past the dependency timeout, or answers something
|
|
72
|
+
* unreadable is a lane that could not answer: for the DECISION its rows take no part (the tightening arms
|
|
73
|
+
* do not fire — `applyPersistedTightening` reads `{ unreadable }` without a reason that way), and for
|
|
74
|
+
* the RECORD it is `"unavailable"`. A call the lane could not read against the rows (`reason`) fails closed
|
|
75
|
+
* in the arms. Absent lane ⇒ the clean negative.
|
|
76
|
+
*/
|
|
77
|
+
export declare function readPersistedRuleLane(lane: {
|
|
78
|
+
adjudicate: (req: ToolCallRequest) => PersistedRuleAnswer | Promise<PersistedRuleAnswer>;
|
|
79
|
+
} | undefined, req: ToolCallRequest, signal: AbortSignal | undefined, timeoutMs?: number): Promise<{
|
|
80
|
+
hit?: PersistedRuleHit;
|
|
81
|
+
unreadable?: true;
|
|
82
|
+
reason?: string;
|
|
83
|
+
coverage?: readonly import("./permission-rule-model.js").SegmentCoverage[];
|
|
84
|
+
}>;
|