@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
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { heldBy } from "./tool-name-readings.js";
|
|
2
|
+
import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
|
|
3
|
+
import { protocolOf } from "./protocol-table.js";
|
|
4
|
+
import { COARSE_SHELL_TOOLS } from "./tool-policy.js";
|
|
5
|
+
import { ALLOW_RULE_MEMBERS } from "./allow-rule-shadow.js";
|
|
6
|
+
import { COMMAND_RULE_TOOL, READ_RULE_TOOL, findAdmittingRule, laneToolNamesOf, parseRuleText, programRunReachOf, ruleToolGrammarOf } from "./permission-rule-model.js";
|
|
7
|
+
import { pathRuleLaneAnswer } from "./runner/permission-rule-lanes.js";
|
|
8
|
+
import { splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
9
|
+
import { mandateDisclosureOf } from "./ask-origin.js";
|
|
10
|
+
import { inlineUntrusted } from "./untrusted-text.js";
|
|
11
|
+
import { deliverEngineNotice } from "./engine-notice.js";
|
|
12
|
+
export const ALLOW_ENTRY_INERT_REASONS = Object.freeze(["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar"]);
|
|
13
|
+
function ruleRowOf(parsed) {
|
|
14
|
+
return { ...parsed, scope: { kind: "global" }, adds: [{ dot: { actor: "list", counter: 1 }, origin: "user", createdAt: "" }] };
|
|
15
|
+
}
|
|
16
|
+
const TOOL_NAME_SPELLING = /^[^\s()]+$/;
|
|
17
|
+
export function compileAllowEntry(raw, behavior) {
|
|
18
|
+
if (raw !== raw.trim())
|
|
19
|
+
return "grammar";
|
|
20
|
+
const parsed = parsePermissionRule(raw);
|
|
21
|
+
const name = parsed.toolName;
|
|
22
|
+
if (!TOOL_NAME_SPELLING.test(name))
|
|
23
|
+
return "grammar";
|
|
24
|
+
if (name === "*")
|
|
25
|
+
return "wildcard_all";
|
|
26
|
+
if (parsed.ruleContent === undefined) {
|
|
27
|
+
const ns = protocolOf(name);
|
|
28
|
+
if (ns !== undefined) {
|
|
29
|
+
if (name.slice(ns.prefix.length).split("__")[0]?.includes("*") === true)
|
|
30
|
+
return "namespace_wildcard";
|
|
31
|
+
if (isNamespacedCoveringRuleName(name))
|
|
32
|
+
return { kind: "namespace", pattern: name };
|
|
33
|
+
return { kind: "tool", name };
|
|
34
|
+
}
|
|
35
|
+
return COARSE_SHELL_TOOLS.includes(name) ? { kind: "shell", name } : { kind: "tool", name };
|
|
36
|
+
}
|
|
37
|
+
if (name === COMMAND_RULE_TOOL) {
|
|
38
|
+
const allowRead = parseRuleText(raw, behavior);
|
|
39
|
+
if ("rule" in allowRead)
|
|
40
|
+
return { kind: "command", rule: ruleRowOf(allowRead.rule) };
|
|
41
|
+
if (behavior === "allow" && allowRead.reject.code === "invalid.bare_interpreter_prefix") {
|
|
42
|
+
const wide = parseRuleText(raw, "deny");
|
|
43
|
+
if ("rule" in wide)
|
|
44
|
+
return { kind: "command", rule: ruleRowOf({ ...wide.rule, behavior: "allow" }) };
|
|
45
|
+
}
|
|
46
|
+
return "grammar";
|
|
47
|
+
}
|
|
48
|
+
if (ruleToolGrammarOf(name) === "path") {
|
|
49
|
+
const read = parseRuleText(raw, behavior);
|
|
50
|
+
if ("rule" in read) {
|
|
51
|
+
if (read.rule.match === "subpath" && read.rule.tool === READ_RULE_TOOL && behavior === "allow")
|
|
52
|
+
return { kind: "read_dir", rule: ruleRowOf(read.rule) };
|
|
53
|
+
if (behavior === "deny")
|
|
54
|
+
return { kind: "path", rule: ruleRowOf(read.rule) };
|
|
55
|
+
}
|
|
56
|
+
return "path_pattern";
|
|
57
|
+
}
|
|
58
|
+
if (/^domain:/.test(parsed.ruleContent))
|
|
59
|
+
return "domain";
|
|
60
|
+
return "content_unsupported";
|
|
61
|
+
}
|
|
62
|
+
export function compileAllowList(entries, behavior) {
|
|
63
|
+
const voided = behavior === "allow" && entries.some((e) => compileAllowEntry(e, behavior) === "wildcard_all");
|
|
64
|
+
const rows = [];
|
|
65
|
+
const inert = [];
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const compiled = voided ? "wildcard_all" : compileAllowEntry(entry, behavior);
|
|
68
|
+
if (typeof compiled === "string")
|
|
69
|
+
inert.push({ entry, reason: compiled });
|
|
70
|
+
else
|
|
71
|
+
rows.push({ entry, compiled });
|
|
72
|
+
}
|
|
73
|
+
return { rows, inert };
|
|
74
|
+
}
|
|
75
|
+
function answersTo(req, has) {
|
|
76
|
+
return heldBy("allowEntry.names", { has }, req);
|
|
77
|
+
}
|
|
78
|
+
export const AUTO_STRIPPED_PROGRAMS = Object.freeze([
|
|
79
|
+
"python", "python3", "python2", "node", "deno", "tsx", "ruby", "perl", "php", "lua", "npx", "bunx", "npm run", "yarn run", "pnpm run", "bun run", "bash", "sh", "ssh",
|
|
80
|
+
"zsh", "fish", "eval", "exec", "env", "xargs", "sudo",
|
|
81
|
+
]);
|
|
82
|
+
function shellContentStripped(content) {
|
|
83
|
+
if (content === undefined || content === "" || /^[\s*]+$/.test(content))
|
|
84
|
+
return true;
|
|
85
|
+
const n = content.trim().toLowerCase();
|
|
86
|
+
if (n === "*")
|
|
87
|
+
return true;
|
|
88
|
+
for (const program of AUTO_STRIPPED_PROGRAMS) {
|
|
89
|
+
const g = program.toLowerCase();
|
|
90
|
+
if (n === g || n === `${g}:*` || n === `${g} *` || n === `${g}*`)
|
|
91
|
+
return true;
|
|
92
|
+
if (n.startsWith(`${g} `) && n.endsWith("*")) {
|
|
93
|
+
const tail = n.slice(g.length + 1);
|
|
94
|
+
if (tail.startsWith("-") && !(/^python[\d.]*$/.test(g) && /^-m\s+\w+\.[\w.]+(\s*:|\s+)$/.test(tail.slice(0, -1))))
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
function shellRuleContent(rule) {
|
|
101
|
+
const split = splitShellCompoundSegments(rule.command, { trailingTerminator: "keep", redirection: "reject" });
|
|
102
|
+
const body = "reject" in split ? rule.command : (split.segments[split.segments.length - 1] ?? rule.command).trim();
|
|
103
|
+
return rule.match === "prefix" ? `${body}:*` : rule.match === "wildcard" ? `${body} *` : body;
|
|
104
|
+
}
|
|
105
|
+
export function autoStripped(compiled, call) {
|
|
106
|
+
switch (compiled.kind) {
|
|
107
|
+
case "shell":
|
|
108
|
+
return true;
|
|
109
|
+
case "tool":
|
|
110
|
+
return call !== undefined && answersTo(call, (n) => COARSE_SHELL_TOOLS.includes(n));
|
|
111
|
+
case "command":
|
|
112
|
+
return shellContentStripped(shellRuleContent(compiled.rule));
|
|
113
|
+
case "namespace":
|
|
114
|
+
case "read_dir":
|
|
115
|
+
case "path":
|
|
116
|
+
return false;
|
|
117
|
+
default: {
|
|
118
|
+
const exhaustive = compiled;
|
|
119
|
+
return exhaustive;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function autoStrippedRule(rule, call) {
|
|
124
|
+
return rule.behavior === "allow" && rule.tool === COMMAND_RULE_TOOL && autoStripped({ kind: "command", rule }, call);
|
|
125
|
+
}
|
|
126
|
+
function nameRowReaches(compiled, req) {
|
|
127
|
+
const covers = compiled.kind === "namespace" ? (n) => namespacedRuleNameCovers(compiled.pattern, n) : compiled.kind === "tool" || compiled.kind === "shell" ? (n) => n === compiled.name : undefined;
|
|
128
|
+
return covers !== undefined && answersTo(req, covers);
|
|
129
|
+
}
|
|
130
|
+
function commandOf(req) {
|
|
131
|
+
const tool = laneToolNamesOf(req).find((n) => ruleToolGrammarOf(n) === "command");
|
|
132
|
+
const command = req.args?.command;
|
|
133
|
+
return tool !== undefined && typeof command === "string" ? { tool, command } : undefined;
|
|
134
|
+
}
|
|
135
|
+
export function judgeAllowEntries(rows, req, behavior, ctx) {
|
|
136
|
+
const live = rows.filter((r) => r.behavior === behavior);
|
|
137
|
+
if (live.length === 0)
|
|
138
|
+
return undefined;
|
|
139
|
+
for (const row of live)
|
|
140
|
+
if (nameRowReaches(row.compiled, req))
|
|
141
|
+
return [row];
|
|
142
|
+
const call = commandOf(req);
|
|
143
|
+
const liveCwd = ctx.liveCwd();
|
|
144
|
+
if (call !== undefined) {
|
|
145
|
+
const byRule = new Map();
|
|
146
|
+
for (const r of live)
|
|
147
|
+
if (r.compiled.kind === "command")
|
|
148
|
+
byRule.set(r.compiled.rule, r);
|
|
149
|
+
if (behavior === "allow") {
|
|
150
|
+
const covering = findAdmittingRule([...byRule.keys()], { tool: call.tool, command: call.command, cwd: ctx.root, ...(ctx.sessionId !== undefined ? { sessionId: ctx.sessionId } : {}), ...(liveCwd !== undefined ? { execCwd: liveCwd } : {}) });
|
|
151
|
+
const hits = (covering ?? []).flatMap((rule) => byRule.get(rule) ?? []);
|
|
152
|
+
if (hits.length > 0)
|
|
153
|
+
return hits;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
for (const [rule, row] of byRule) {
|
|
157
|
+
if (programRunReachOf(rule, call.command).reach !== "not_reached")
|
|
158
|
+
return [row];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const pathKind = behavior === "allow" ? "read_dir" : "path";
|
|
163
|
+
const pathRows = live.filter((r) => r.compiled.kind === pathKind);
|
|
164
|
+
if (pathRows.length > 0) {
|
|
165
|
+
const table = pathRows.map((r) => r.compiled.rule);
|
|
166
|
+
for (const name of laneToolNamesOf(req).filter((n) => ruleToolGrammarOf(n) === "path")) {
|
|
167
|
+
const answer = pathRuleLaneAnswer(table, { ...req, toolName: name }, { root: ctx.root, sessionId: ctx.sessionId, liveCwd, home: ctx.home });
|
|
168
|
+
const spoke = answer !== undefined && ("unreadable" in answer ? behavior === "deny" : "behavior" in answer && answer.behavior === behavior);
|
|
169
|
+
if (spoke) {
|
|
170
|
+
const rule = answer !== undefined && "rules" in answer ? answer.rules[0]?.rule : undefined;
|
|
171
|
+
return [pathRows.find((r) => r.compiled.rule.rule === rule) ?? pathRows[0]];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
export function allowingRows(rows, req, mode) {
|
|
178
|
+
return mode === "auto" ? rows.filter((r) => !autoStripped(r.compiled, req)) : rows;
|
|
179
|
+
}
|
|
180
|
+
export class AllowEntryTable {
|
|
181
|
+
held = [];
|
|
182
|
+
seed(entries) {
|
|
183
|
+
const { rows, inert } = compileAllowList(entries, "allow");
|
|
184
|
+
for (const r of rows)
|
|
185
|
+
if (!this.held.some((h) => h.entry === r.entry))
|
|
186
|
+
this.held.push({ behavior: "allow", entry: r.entry, compiled: r.compiled });
|
|
187
|
+
return inert;
|
|
188
|
+
}
|
|
189
|
+
entries() {
|
|
190
|
+
return this.held.map((r) => r.entry);
|
|
191
|
+
}
|
|
192
|
+
lane(ctx) {
|
|
193
|
+
return {
|
|
194
|
+
allowing: (req, mode) => {
|
|
195
|
+
const hit = judgeAllowEntries(allowingRows(this.held, req, mode), req, "allow", ctx);
|
|
196
|
+
return hit === undefined ? undefined : { entries: hit.map((r) => r.entry) };
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export function seedAllowEntryTables(tables, lists) {
|
|
202
|
+
tables.request.seed(lists.inherited ?? []);
|
|
203
|
+
const requestInert = tables.request.seed(lists.request ?? []);
|
|
204
|
+
tables.deployment.seed(lists.deployment ?? []);
|
|
205
|
+
return [...lists.skillInert.map((e) => ({ source: "skill", ...e })), ...requestInert.map((e) => ({ source: "request", ...e }))];
|
|
206
|
+
}
|
|
207
|
+
export function withDeploymentAllowedTools(deps) {
|
|
208
|
+
if (deps.allowedTools === undefined)
|
|
209
|
+
return deps;
|
|
210
|
+
const allowedTools = screenAllowedToolsValue(deps.allowedTools, "RunnerDeps.allowedTools");
|
|
211
|
+
const inert = compileAllowList(allowedTools ?? [], "allow").inert;
|
|
212
|
+
if (inert.length > 0)
|
|
213
|
+
deliverEngineNotice(deps.onNotice, allowedToolsNotGrantedNotice(inert.map((e) => ({ source: "deployment", ...e })), undefined));
|
|
214
|
+
return { ...deps, allowedTools };
|
|
215
|
+
}
|
|
216
|
+
export const INVALID_PERMISSION_RULES_CODE = "config.invalid_permission_rules";
|
|
217
|
+
export function screenAllowedToolsValue(value, seat) {
|
|
218
|
+
if (value === undefined)
|
|
219
|
+
return undefined;
|
|
220
|
+
const issues = [];
|
|
221
|
+
if (!Array.isArray(value)) {
|
|
222
|
+
issues.push({ rule: "", code: "invalid.entry_type", message: `${seat} must be a list of permission-rule strings (got ${value === null ? "null" : typeof value})` });
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const list = value;
|
|
226
|
+
list.forEach((entry, i) => {
|
|
227
|
+
if (typeof entry !== "string")
|
|
228
|
+
issues.push({ rule: "", code: "invalid.entry_type", message: `${seat}[${i}] must be a permission-rule string (got ${entry === null ? "null" : typeof entry})` });
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (issues.length > 0) {
|
|
232
|
+
const e = new Error(`${seat}: ${issues.length} invalid entr${issues.length === 1 ? "y" : "ies"} — refusing rather than running without them:\n${issues.map((i) => ` [${i.code}] ${i.message}`).join("\n")}`);
|
|
233
|
+
e.code = INVALID_PERMISSION_RULES_CODE;
|
|
234
|
+
e.issues = issues;
|
|
235
|
+
throw e;
|
|
236
|
+
}
|
|
237
|
+
return Object.freeze([...value]);
|
|
238
|
+
}
|
|
239
|
+
const SHADOW_ENTRIES_MAX = 5;
|
|
240
|
+
const SHADOW_ENTRY_MAX_CHARS = 200;
|
|
241
|
+
export function allowRuleShadowOf(member, entries) {
|
|
242
|
+
const shown = entries.slice(0, SHADOW_ENTRIES_MAX).map((e) => inlineUntrusted(e, SHADOW_ENTRY_MAX_CHARS));
|
|
243
|
+
return { member, entries: shown, ...(entries.length > shown.length ? { omitted: entries.length - shown.length } : {}) };
|
|
244
|
+
}
|
|
245
|
+
const MEMBER_SET = new Set(ALLOW_RULE_MEMBERS);
|
|
246
|
+
export function readAllowRuleShadows(value) {
|
|
247
|
+
try {
|
|
248
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
249
|
+
return undefined;
|
|
250
|
+
const list = value;
|
|
251
|
+
const out = [];
|
|
252
|
+
for (const r of list) {
|
|
253
|
+
if (typeof r !== "object" || r === null)
|
|
254
|
+
return undefined;
|
|
255
|
+
const { member, entries, omitted } = r;
|
|
256
|
+
if (!MEMBER_SET.has(member) || !Array.isArray(entries) || !entries.every((e) => typeof e === "string"))
|
|
257
|
+
return undefined;
|
|
258
|
+
const shadow = allowRuleShadowOf(member, entries);
|
|
259
|
+
const carried = typeof omitted === "number" && Number.isInteger(omitted) && omitted > 0 ? omitted : 0;
|
|
260
|
+
out.push(carried > 0 ? { ...shadow, omitted: (shadow.omitted ?? 0) + carried } : shadow);
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const SHADOW_REASON = {
|
|
269
|
+
content_question: "the call is the engine's own question to a person",
|
|
270
|
+
hook: "a PreToolUse hook asks about this call — the deployment's own screening is answered by a person",
|
|
271
|
+
ancestor: "the task that delegated this one owns this question",
|
|
272
|
+
unverifiable_delete: "this removal could not be verified safe — it is cleared by confirming this call, never by an allow rule",
|
|
273
|
+
transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule",
|
|
274
|
+
org_rule: "an organization policy rule requires approval for this call",
|
|
275
|
+
governance_unavailable: "this deployment is org-governed and cannot currently read its organization policy",
|
|
276
|
+
rule_unreadable: "this call could not be read against a deny or ask rule, so whether one forbids it is unknown",
|
|
277
|
+
ask_rule: mandateDisclosureOf(undefined),
|
|
278
|
+
denial_limit_fallback: "the auto-mode classifier refused too many calls in a row, so a person decides",
|
|
279
|
+
requires_user_interaction: "this tool's server declares that a call must be put to a person",
|
|
280
|
+
operator_forced: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule",
|
|
281
|
+
shell_floor: mandateDisclosureOf("operator_always"),
|
|
282
|
+
write_protection_unresolved: mandateDisclosureOf("write_protection_unresolved"),
|
|
283
|
+
probe: mandateDisclosureOf("probe_mandate"),
|
|
284
|
+
write_protection: mandateDisclosureOf("write_protection"),
|
|
285
|
+
tool_marks_mcp: mandateDisclosureOf("tool_marks"),
|
|
286
|
+
tool_marks_a2a: mandateDisclosureOf("tool_marks"),
|
|
287
|
+
policy_rra: "the deployment's policy requires a real approval for this call",
|
|
288
|
+
artifact_shared_read: "this read targets another task's shared artifact — it is cleared by confirming this call",
|
|
289
|
+
mode: "the permission mode puts this question to a person",
|
|
290
|
+
unstated: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule",
|
|
291
|
+
ordinary: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule",
|
|
292
|
+
};
|
|
293
|
+
function memberPhrase(s) {
|
|
294
|
+
const listed = `${s.entries.join(", ")}${s.omitted !== undefined ? ` and ${s.omitted} more` : ""}`;
|
|
295
|
+
switch (s.member) {
|
|
296
|
+
case "hook":
|
|
297
|
+
return "a PreToolUse hook's allow";
|
|
298
|
+
case "deployment":
|
|
299
|
+
return `the deployment's allowedTools (${listed})`;
|
|
300
|
+
case "persisted":
|
|
301
|
+
return s.entries.length + (s.omitted ?? 0) === 1 ? `a persisted allow rule (${listed})` : `persisted allow rules (${listed})`;
|
|
302
|
+
case "skill":
|
|
303
|
+
return `an active skill's allowed-tools (${listed})`;
|
|
304
|
+
case "request":
|
|
305
|
+
return `this run's allowedTools (${listed})`;
|
|
306
|
+
default: {
|
|
307
|
+
const exhaustive = s.member;
|
|
308
|
+
return exhaustive;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
export function allowRuleShadowSentence(shadows, ask) {
|
|
313
|
+
const one = shadows.length === 1 && (shadows[0].member === "hook" || shadows[0].entries.length + (shadows[0].omitted ?? 0) === 1);
|
|
314
|
+
const why = ask.mandate !== undefined
|
|
315
|
+
? mandateDisclosureOf(ask.mandate)
|
|
316
|
+
: ask.authoredBy === "deployment"
|
|
317
|
+
? "this deployment requires a person's confirmation for this call"
|
|
318
|
+
: SHADOW_REASON[ask.askClass ?? "ordinary"];
|
|
319
|
+
return `${shadows.map(memberPhrase).join("; ")} ${one ? "matches this call but does not clear the ask" : "match this call but do not clear the ask"} — ${why}`;
|
|
320
|
+
}
|
|
321
|
+
export function cardDisclosureOf(decision, fallbackMessage) {
|
|
322
|
+
const base = decision.message ?? fallbackMessage;
|
|
323
|
+
const shadows = readAllowRuleShadows(decision.allowRuleShadowed);
|
|
324
|
+
if (shadows === undefined)
|
|
325
|
+
return { message: base };
|
|
326
|
+
return { message: `${base} (${allowRuleShadowSentence(shadows, decision)})`, allowRuleShadowed: shadows };
|
|
327
|
+
}
|
|
328
|
+
export const ALLOWED_TOOL_NOT_GRANTED_CODE = "permission.allowed_tool_not_granted";
|
|
329
|
+
const NOTICE_ENTRIES_SHOWN = 5;
|
|
330
|
+
export function allowedToolsNotGrantedNotice(inert, sessionId) {
|
|
331
|
+
const where = (e) => (e.source === "skill" ? `skill ${inlineUntrusted(e.skill, 80)} ${e.list}` : e.source === "request" ? "this run's allowedTools" : "the deployment's allowedTools");
|
|
332
|
+
const shown = inert.slice(0, NOTICE_ENTRIES_SHOWN).map((e) => `${where(e)} "${inlineUntrusted(e.entry, 120)}" (${e.reason})`);
|
|
333
|
+
const rest = inert.length - shown.length;
|
|
334
|
+
return {
|
|
335
|
+
code: ALLOWED_TOOL_NOT_GRANTED_CODE,
|
|
336
|
+
message: `${inert.length} allowed-tools entr${inert.length === 1 ? "y grants" : "ies grant"} or refuse${inert.length === 1 ? "s" : ""} nothing — ${shown.join("; ")}${rest > 0 ? `; and ${rest} more` : ""}. ` +
|
|
337
|
+
"A list holding `*` voids the list, `mcp__*` names no server, and a write-path or domain entry has no allow reading; name the tools (or `mcp__<server>`) instead.",
|
|
338
|
+
detail: { ...(sessionId !== undefined ? { sessionId } : {}), entries: inert.map((e) => ({ ...e })) },
|
|
339
|
+
};
|
|
340
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The allow layer's DISCLOSURE vocabulary — which member matched a call and could not clear its ask. A leaf: the decision
|
|
3
|
+
* union (`PermissionResult`), the approval request (`AskRequest`), the park row's risk descriptor and the allow layer all
|
|
4
|
+
* name these, and nothing here names them back.
|
|
5
|
+
*/
|
|
6
|
+
/** The allow layer's members, in the order the layer reads them: a PreToolUse hook's explicit `allow`, the deployment's
|
|
7
|
+
* `RunnerDeps.allowedTools`, a person's stored allow rows, an active skill's `allowed-tools`, the request's
|
|
8
|
+
* `TaskSpec.allowedTools`. The closed set {@link AllowRuleShadow.member} names. */
|
|
9
|
+
export declare const ALLOW_RULE_MEMBERS: readonly ["hook", "deployment", "persisted", "skill", "request"];
|
|
10
|
+
export type AllowRuleMember = (typeof ALLOW_RULE_MEMBERS)[number];
|
|
11
|
+
/** One allow-layer member that matched a call and could not clear its ask: the member, and the entries it matched as their
|
|
12
|
+
* source spells them (sanitized and capped at the mint; a hook names none — its word is about the call, not an entry).
|
|
13
|
+
* `omitted` counts entries beyond the cap, never dropped silently. */
|
|
14
|
+
export interface AllowRuleShadow {
|
|
15
|
+
readonly member: AllowRuleMember;
|
|
16
|
+
readonly entries: readonly string[];
|
|
17
|
+
readonly omitted?: number;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ALLOW_RULE_MEMBERS = Object.freeze(["hook", "deployment", "persisted", "skill", "request"]);
|
|
@@ -166,8 +166,8 @@ export declare function stampAskOrigin<T extends Extract<PermissionResult, {
|
|
|
166
166
|
* approver and this task's rule lane never adjudicates it;
|
|
167
167
|
* · the bit already stamped on the ask, which no layer below the mint may clear (tightening-only).
|
|
168
168
|
*
|
|
169
|
-
* And the SHADOW doors, which are not mandates:
|
|
170
|
-
*
|
|
169
|
+
* And the SHADOW doors, which are not mandates: an allow-layer member already matched and is outranked
|
|
170
|
+
* (`allowRuleShadowed`), or the person's explicit "ask me each time" rule matched
|
|
171
171
|
* (`matchedAskRule`). A rule minted from such a card silences nothing, so the lane offers none — but
|
|
172
172
|
* the reason is that a RULE is already speaking, not that the question is beyond rules, and a surface
|
|
173
173
|
* that auto-answers by posture is not crossing a mandate when it answers one.
|
|
@@ -181,7 +181,7 @@ export declare function stampAskOrigin<T extends Extract<PermissionResult, {
|
|
|
181
181
|
* · `mandated` — the FACT the ask carries as its own member. Read off the whole mandate set, so which
|
|
182
182
|
* door is DISPLAYED cannot change it: an ask that is both shadowed and mandated is mandated;
|
|
183
183
|
* · `ruleMayClear` — the ENFORCEMENT reading the gate's clearing arms take. It is `mandated` plus the
|
|
184
|
-
* person's explicit `ask` rule, and deliberately NOT `word !== undefined`: `
|
|
184
|
+
* person's explicit `ask` rule, and deliberately NOT `word !== undefined`: `allowRuleShadowed`
|
|
185
185
|
* is an ACCOUNT of a refusal that already happened, display-only by contract, and reading it as a
|
|
186
186
|
* blocker would let a policy forwarding that metadata refuse a call nothing is blocking. A
|
|
187
187
|
* display word is never an authority;
|
|
@@ -205,8 +205,8 @@ export interface RuleClearanceFacts {
|
|
|
205
205
|
/** The table word a gate stamped beside that bit (see {@link import("./tool-policy.js").AskRequest.mandate}). */
|
|
206
206
|
mandate?: PersistedRuleMandate;
|
|
207
207
|
requiresRealApproval?: boolean;
|
|
208
|
-
/**
|
|
209
|
-
|
|
208
|
+
/** Allow-layer members matched and did not clear the ask (see {@link import("./allow-rule-shadow.js").AllowRuleShadow}). */
|
|
209
|
+
allowRuleShadowed?: readonly import("./allow-rule-shadow.js").AllowRuleShadow[];
|
|
210
210
|
decisionReason?: import("./tool-policy.js").DecisionReason;
|
|
211
211
|
/** An EXPLICIT `ask` permission rule matched this call (DSL, stamped at the rule policy's three
|
|
212
212
|
* matched-ask exits — bare, covering and param). */
|
package/dist/core/ask-origin.js
CHANGED
|
@@ -32,7 +32,7 @@ export function ruleClearanceOf(ask, marks) {
|
|
|
32
32
|
const word = (() => {
|
|
33
33
|
if (ask?.requiresRealApproval === true)
|
|
34
34
|
return "mandated";
|
|
35
|
-
if (ask?.
|
|
35
|
+
if (Array.isArray(ask?.allowRuleShadowed) && ask.allowRuleShadowed.length > 0)
|
|
36
36
|
return "shadowed";
|
|
37
37
|
if (ask?.decisionReason === "hook")
|
|
38
38
|
return "mandated";
|
package/dist/core/auto-mode.d.ts
CHANGED
|
@@ -414,8 +414,9 @@ export interface AutoModeDenialTracker {
|
|
|
414
414
|
* ONCE (a second call with the same member arms nothing), so the hand-out is a recorded fact, not a
|
|
415
415
|
* convention. */
|
|
416
416
|
armTimedWindow(fallback: UnarmedDenialLimitFallback): DenialLimitFallback;
|
|
417
|
-
/** A classifier `allow`,
|
|
418
|
-
*
|
|
417
|
+
/** A classifier `allow`, a person's allow of a fallback ask (CC `yR`), or — in auto mode — an allow rule or the
|
|
418
|
+
* read-only reader clearing the ask (the reference resets the streak when its pipeline answers allow): consecutive → 0
|
|
419
|
+
* and the timed-card mark cleared. A hook's allow does not call this. */
|
|
419
420
|
recordAllow(): void;
|
|
420
421
|
/** The current counts (diagnostics / pins). `windowArmedAt` = the epoch-ms instant
|
|
421
422
|
* {@link armTimedWindow} handed the CURRENT streak's fallback a non-zero window to a live approver
|
|
@@ -150,22 +150,14 @@ export interface RiskDescriptor {
|
|
|
150
150
|
* {@link summary} (a path name can carry the same reordering hazard as a command) + capped.
|
|
151
151
|
* Omitted when none derivable. */
|
|
152
152
|
touchedPaths?: string[];
|
|
153
|
-
/**
|
|
154
|
-
* the matched
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
|
|
161
|
-
* validator-screened text the render coincides with the previous `inlineUntrusted` treatment
|
|
162
|
-
* (the two differ only on the `\p{Cf}` class, and both fold the width-bearing controls it admits
|
|
163
|
-
* (a tab) identically); for a Cf carrier this durable seat STRIPS what the synchronous ask's
|
|
164
|
-
* `persistedRuleShadowed` preserves — same value, two byte-forms, diverging exactly on `\p{Cf}`,
|
|
165
|
-
* the durable face stripping MORE (the safe direction). This seat carries NO cross-face
|
|
166
|
-
* same-value contract (unlike `probeReason`/`probeCause` below), which is why the one-face move
|
|
167
|
-
* is legal — a byte-pinning consumer must read per face.) */
|
|
168
|
-
shadowedRule?: string;
|
|
153
|
+
/** The allow-layer members that MATCHED this call and could not clear the ask — one record per member (the member and
|
|
154
|
+
* the entries it matched), threaded to the mint so the durable-park route carries the same disclosure the synchronous
|
|
155
|
+
* card does: the inbox renders "their rule is alive, just outranked" instead of the person concluding it silently broke.
|
|
156
|
+
* Each entry is rendered through the same display baseline as {@link summary} and capped (the bytes may be a person's
|
|
157
|
+
* stored text, a skill's or a deployment's list — model-authored or a custom provider's — and can carry `\p{Cf}`; this
|
|
158
|
+
* durable seat strips what the synchronous card keeps, the safe direction, and carries no cross-face same-value
|
|
159
|
+
* contract, so a byte-pinning consumer reads per face). */
|
|
160
|
+
allowRuleShadowed?: readonly import("./allow-rule-shadow.js").AllowRuleShadow[];
|
|
169
161
|
/** WHY the reversibility probe did not clear this call, in the probe's own
|
|
170
162
|
* words — the cause the tighten acted on, which until now existed only inside the probe and left the
|
|
171
163
|
* card showing WHAT is gated with no account of WHY. Present only for a `"maybe"`-tier tighten whose
|
|
@@ -380,7 +372,7 @@ export declare function safeDataValue(obj: Record<string, unknown>, key: string)
|
|
|
380
372
|
*
|
|
381
373
|
* **Redaction is load-bearing** (the `summary`/`touchedPaths` are PERSISTED + surfaced to a human inbox):
|
|
382
374
|
* every model-controlled value in the DESCRIPTOR-OWNED display family (`summary` both arms,
|
|
383
|
-
* `touchedPaths`, `
|
|
375
|
+
* `touchedPaths`, `allowRuleShadowed`) goes through
|
|
384
376
|
* {@link import("./permission-rule-model.js").renderUntrustedCommandText} — which STRIPS the `\p{Cf}`
|
|
385
377
|
* format class (directional controls / zero-width formatters, the display-reordering hazard the
|
|
386
378
|
* `hasBidiControls` bit warns about; #438) and then applies the shared inline form (folds CR/LF/Unicode
|
|
@@ -408,8 +400,8 @@ export declare function buildRiskDescriptor(input: {
|
|
|
408
400
|
shellGated?: boolean;
|
|
409
401
|
/** The resolved doctrine to persist when `shellGated` (see {@link RiskDescriptor.shellGateDoctrine}). */
|
|
410
402
|
shellGateDoctrine?: ShellGateDoctrine;
|
|
411
|
-
/** The matched-but-
|
|
412
|
-
|
|
403
|
+
/** The matched-but-not-cleared allow-layer members to persist (see {@link RiskDescriptor.allowRuleShadowed}). */
|
|
404
|
+
allowRuleShadowed?: readonly import("./allow-rule-shadow.js").AllowRuleShadow[];
|
|
413
405
|
/** The reversibility probe's cause for not clearing this call (see
|
|
414
406
|
* {@link RiskDescriptor.probeReason}). Re-neutralized here like its shadowed-rule sibling: this
|
|
415
407
|
* function is exported, so a caller reaching it directly must get the same redaction the gate applies. */
|
|
@@ -165,7 +165,7 @@ export function buildRiskDescriptor(input) {
|
|
|
165
165
|
axes,
|
|
166
166
|
toolName,
|
|
167
167
|
...(input.shellGated && input.shellGateDoctrine !== undefined ? { shellGateDoctrine: input.shellGateDoctrine } : {}),
|
|
168
|
-
...(input.
|
|
168
|
+
...(input.allowRuleShadowed !== undefined && input.allowRuleShadowed.length > 0 ? { allowRuleShadowed: input.allowRuleShadowed.map((r) => ({ member: r.member, entries: r.entries.map((e) => renderUntrustedCommandText(e, 200)), ...(r.omitted !== undefined ? { omitted: r.omitted } : {}) })) } : {}),
|
|
169
169
|
...(input.probeReason !== undefined ? { probeReason: inlineUntrusted(input.probeReason, PROBE_REASON_MAX) } : {}),
|
|
170
170
|
...(() => {
|
|
171
171
|
if (input.probeCause === undefined)
|
|
@@ -7,8 +7,10 @@ export declare const isEngineRefusal: (d: unknown) => boolean;
|
|
|
7
7
|
* compile error rather than a drift between the type and the screen. Module-private (the package publishes the type).
|
|
8
8
|
* `skill`: an active skill's list spoke — the allow layer's skill member cleared the call (`allowed-tools`), or this
|
|
9
9
|
* run's deny row refused it (`disallowed-tools`); distinct from `rule` (a deployment policy) and `persisted_rule` (a
|
|
10
|
-
* person's stored row) because the authority is the skill the run activated.
|
|
11
|
-
|
|
10
|
+
* person's stored row) because the authority is the skill the run activated. `deployment_rule` / `request_rule`: an entry
|
|
11
|
+
* of the deployment's `RunnerDeps.allowedTools` / of this run's `TaskSpec.allowedTools` cleared the call (the allow layer's
|
|
12
|
+
* two entry-list members) — one word per author, so "who said this needs no asking" is read off the decision. */
|
|
13
|
+
declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only", "skill", "deployment_rule", "request_rule"];
|
|
12
14
|
export type DecisionReason = (typeof DECISION_REASONS)[number];
|
|
13
15
|
export declare const RETIRED_WINDOW_MEMBER_DENY_MESSAGE: string;
|
|
14
16
|
export declare const carriesRetiredWindow: (fallback: unknown) => boolean;
|
|
@@ -8,7 +8,7 @@ export const refusedOutOfContract = (message) => {
|
|
|
8
8
|
return refused;
|
|
9
9
|
};
|
|
10
10
|
export const isEngineRefusal = (d) => typeof d === "object" && d !== null && ENGINE_REFUSALS.has(d);
|
|
11
|
-
const DECISION_REASONS = Object.freeze(["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only", "skill"]);
|
|
11
|
+
const DECISION_REASONS = Object.freeze(["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only", "skill", "deployment_rule", "request_rule"]);
|
|
12
12
|
const DECISION_ACTIONS = Object.keys({ allow: 0, deny: 0, ask: 0 });
|
|
13
13
|
const GUARDS = Object.keys({ unverifiable_delete: 0, transcript_write: 0, artifact_shared_read: 0 });
|
|
14
14
|
const RULE_UNREADABLE_WORDS = Object.keys({ content_deny: 0, content_ask: 0, persisted_call: 0 });
|
|
@@ -57,11 +57,14 @@ export interface EngineNotice {
|
|
|
57
57
|
* at least one type is listed name-only (`nameOnly`) or not at all (`dropped`). Audience `operator`.
|
|
58
58
|
* `detail: { budgetBytes, total, kept, nameOnly: [name], dropped: [name], sessionId }`; the same
|
|
59
59
|
* firing rule as its skills sibling below (once per delivered frame, on the carrying message's receipt).
|
|
60
|
-
* - `"
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
60
|
+
* - `"permission.allowed_tool_not_granted"` — an allow list holds entries that grant (or refuse) nothing: a loaded skill's
|
|
61
|
+
* `allowed-tools` / `disallowed-tools`, the request's `TaskSpec.allowedTools`, the deployment's `RunnerDeps.allowedTools`.
|
|
62
|
+
* `*` (it voids an allow list; on a deny list it reads as nothing and the rest still refuses), `mcp__*` (no server
|
|
63
|
+
* named), a write-path or domain entry, a shape the rule grammar does not read. The run goes on and asks for those tools
|
|
64
|
+
* as usual. Audience `user`; once per prepared task for the skill and request lists (`detail.sessionId` present), once
|
|
65
|
+
* per Runner for the deployment's (at construction, no session). `detail: { sessionId?, entries: [{ source, entry,
|
|
66
|
+
* reason, skill?, list? }] }` (`source` ∈ `skill` / `request` / `deployment`; `skill` and `list` only on a skill's;
|
|
67
|
+
* `reason` ∈ `wildcard_all` / `namespace_wildcard` / `path_pattern` / `domain` / `content_unsupported` / `grammar`).
|
|
65
68
|
* - `"skills.listing_truncated"` — the `<skills>` disclosure block did not fit its byte budget, so
|
|
66
69
|
* at least one skill is listed without its description (`nameOnly`) or, in the pathological case,
|
|
67
70
|
* not listed at all (`dropped`); the `Skill` tool still serves every one of them. Audience
|
package/dist/core/gate-fold.js
CHANGED
|
@@ -173,7 +173,7 @@ export async function runGateFold(pass) {
|
|
|
173
173
|
pass.deniedBy = "write_protection";
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
|
-
if (asksUnstated(pass.decision, permissionSeatAt(input, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput)?.seat, input.permissionMode?.()
|
|
176
|
+
if (asksUnstated(pass.decision, permissionSeatAt(input, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput)?.seat, input.permissionMode?.())) {
|
|
177
177
|
pass.decision = { action: "ask", message: permissionSeatAt(input, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput)?.question ?? unstatedAskMessage(toolName), ...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}) };
|
|
178
178
|
pass.tightenedBy = "unstated";
|
|
179
179
|
}
|
|
@@ -1,53 +1,5 @@
|
|
|
1
1
|
import { type GatePass } from "./runner/gate-exit.js";
|
|
2
|
-
import type {
|
|
3
|
-
/**
|
|
4
|
-
* design/252 review r3 — read a foreign {@link OrgGateVerdict} the way the personal-rule answer is read:
|
|
5
|
-
* OWN DATA properties only, never the prototype chain, never an accessor.
|
|
6
|
-
*
|
|
7
|
-
* Unlike the personal-rule normalizer below, this one accepts ANY non-array object as the carrier —
|
|
8
|
-
* not only a plain record. The two seams tighten in opposite directions when a shape is refused: the
|
|
9
|
-
* personal lane is a LOOSENING seam, so refusing a class instance degrades toward asking; this answer
|
|
10
|
-
* carries the org's DENY, and folding a structurally valid `{status:"available", verdict:{behavior:
|
|
11
|
-
* "deny"}}` class instance into `unavailable` would LOOSEN it — from a deny nobody can approve into a
|
|
12
|
-
* real-approval ask a person can clear. A class instance's fields are its own data properties, so the
|
|
13
|
-
* own-data read below already gives the full pollution guarantee (inherited members never authorize);
|
|
14
|
-
* the prototype test added nothing here but the downgrade.
|
|
15
|
-
*
|
|
16
|
-
* The two seams are the same class of trust boundary and were not being read the same way. What that
|
|
17
|
-
* cost here is worse than on the personal lane, because this answer is GOVERNANCE:
|
|
18
|
-
* · an inherited `revision` (a polluted `Object.prototype`) stamped a fabricated snapshot version onto
|
|
19
|
-
* a human's approval request — a WRONG record, which is worse than an absent one;
|
|
20
|
-
* · an inherited `verdict` invented an org rule, and with it an org ask or deny the lane never gave;
|
|
21
|
-
* · an answer that is not a record at all read as "available, nothing to say", i.e. the governance
|
|
22
|
-
* fail-open the availability contract exists to prevent.
|
|
23
|
-
* Anything this function cannot read as a well-formed answer becomes `unavailable` — the fail-CLOSED
|
|
24
|
-
* word, never the empty one.
|
|
25
|
-
*/
|
|
26
|
-
export declare function normalizeOrgGateVerdict(answer: unknown, unreadable: string): OrgGateVerdict;
|
|
27
|
-
/**
|
|
28
|
-
* Normalize the accepted {@link ToolGateInput.persistedRules} answers into one reading.
|
|
29
|
-
*
|
|
30
|
-
* `{}` = a clean negative (no rule admits this call). `{ unreadable: true }` = the lane could not read
|
|
31
|
-
* its source. `{ hit }` = a match, whose `rules` is the non-empty coverage set (design/375). An answer
|
|
32
|
-
* outside every accepted shape — a number, `null`, an object with neither `rules` nor `unreadable`, an
|
|
33
|
-
* EMPTY `rules` array, the retired pre-375 `{ rule, dots? }` single-rule object — is read as a clean
|
|
34
|
-
* negative rather than a match: this is a LOOSENING seam, so an answer nobody can name degrades toward
|
|
35
|
-
* asking, never toward an allow built on it. All-or-nothing across the set's TEXTS for the same
|
|
36
|
-
* reason: a set with one unreadable member is a different (smaller) claim than the lane made, and a
|
|
37
|
-
* decision must not stand on a claim nobody made — one bad member drops the whole answer to the clean
|
|
38
|
-
* negative. Per-member DOTS stay individually optional (identity lost ⇒ `"not_reported"`, exactly the
|
|
39
|
-
* single-rule contract).
|
|
40
|
-
*
|
|
41
|
-
* The hit's dots are COPIED, not aliased. The array travels onto an ask that may sit in front of a
|
|
42
|
-
* person for a long time; a lane that retains and mutates its own array would otherwise change what the
|
|
43
|
-
* approver is looking at, and what an audit later reads, after the evidence was stamped.
|
|
44
|
-
*/
|
|
45
|
-
export declare function normalizePersistedRuleHit(hit: PersistedRuleAnswer): {
|
|
46
|
-
hit?: PersistedRuleHit;
|
|
47
|
-
unreadable?: true;
|
|
48
|
-
reason?: string;
|
|
49
|
-
coverage?: readonly import("./permission-rule-model.js").SegmentCoverage[];
|
|
50
|
-
};
|
|
2
|
+
import type { ToolGateResult } from "./hooks.js";
|
|
51
3
|
/**
|
|
52
4
|
* Run the lanes over the pass: the org layer over the fold's verdict, then — for a surviving ask — the rewrite
|
|
53
5
|
* merge, the rule lane, the stamp, the classifier, the sandbox leg, the parks, the content lane, the resolution
|