@sema-agent/core 7.8.0 → 7.9.1
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 +75 -1
- package/dist/agents/subagent.d.ts +1 -1
- package/dist/core/ask-origin.d.ts +16 -1
- package/dist/core/ask-origin.js +5 -1
- package/dist/core/checkpoint-store.d.ts +36 -35
- package/dist/core/effective-path-target.d.ts +43 -0
- package/dist/core/effective-path-target.js +56 -0
- package/dist/core/engine-notice.d.ts +8 -0
- package/dist/core/fs-write-gate-policy.js +2 -1
- package/dist/core/gate-lanes.d.ts +1 -0
- package/dist/core/gate-lanes.js +60 -28
- package/dist/core/gate-outcome.d.ts +8 -4
- package/dist/core/gate-outcome.js +2 -1
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +66 -32
- package/dist/core/mcp-injection-drop.d.ts +74 -0
- package/dist/core/mcp-injection-drop.js +27 -0
- package/dist/core/permission-rule-consent.d.ts +29 -36
- package/dist/core/permission-rule-consent.js +110 -60
- package/dist/core/permission-rule-model.d.ts +311 -96
- package/dist/core/permission-rule-model.js +309 -50
- package/dist/core/permission-rule-org.d.ts +19 -12
- package/dist/core/permission-rule-org.js +13 -16
- package/dist/core/permission-rule-provider.d.ts +10 -6
- package/dist/core/permission-rule-provider.js +12 -8
- package/dist/core/permission-rule-session.d.ts +7 -6
- package/dist/core/permission-rule-session.js +29 -10
- package/dist/core/permission-rule-store.d.ts +40 -18
- package/dist/core/permission-rule-store.js +68 -42
- package/dist/core/permission-rule-sync.d.ts +9 -3
- package/dist/core/permission-rule-sync.js +29 -25
- package/dist/core/permission-rule-syntax.d.ts +30 -0
- package/dist/core/permission-rule-syntax.js +44 -0
- package/dist/core/permission-rules.d.ts +56 -33
- package/dist/core/permission-rules.js +88 -55
- package/dist/core/persisted-rule-arms.d.ts +61 -0
- package/dist/core/persisted-rule-arms.js +50 -0
- package/dist/core/runner/active-skill-scope.js +2 -1
- package/dist/core/runner/contracts.d.ts +21 -2
- package/dist/core/runner/permission-rule-lanes.d.ts +42 -27
- package/dist/core/runner/permission-rule-lanes.js +25 -21
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-safety-scan.js +8 -2
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/resume-admission.d.ts +53 -0
- package/dist/core/runner/resume-admission.js +83 -0
- package/dist/core/runner/resume-apply.d.ts +50 -0
- package/dist/core/runner/resume-apply.js +184 -0
- package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
- package/dist/core/runner/resume-checkpoint-screen.js +108 -0
- package/dist/core/runner/resume-claim.d.ts +32 -0
- package/dist/core/runner/resume-claim.js +27 -0
- package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
- package/dist/core/runner/resume-internals-and-config.js +50 -0
- package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
- package/dist/core/runner/resume-policy-outcome.js +127 -0
- package/dist/core/runner/resume-preflight.d.ts +40 -0
- package/dist/core/runner/resume-preflight.js +122 -0
- package/dist/core/runner/resume-review-outcome.d.ts +30 -0
- package/dist/core/runner/resume-review-outcome.js +88 -0
- package/dist/core/runner/run-harness-handlers.js +1 -1
- package/dist/core/runner/runtask.d.ts +20 -0
- package/dist/core/runner/runtask.js +85 -734
- package/dist/core/runner/session-rule-policy.js +2 -1
- package/dist/core/runner/tool-end-body.d.ts +12 -5
- package/dist/core/runner/tool-end-body.js +5 -5
- package/dist/core/runner/tool-face-overlay.js +22 -3
- package/dist/core/runner/tool-output-projection.d.ts +9 -6
- package/dist/core/runner/tool-output-projection.js +1 -18
- package/dist/core/runner/turn-attachments.d.ts +2 -2
- package/dist/core/sensitive-path-policy.js +5 -3
- package/dist/core/shell-lexer.d.ts +47 -0
- package/dist/core/shell-lexer.js +478 -0
- package/dist/core/shell-scan.d.ts +60 -0
- package/dist/core/shell-scan.js +183 -0
- package/dist/core/shell-wrapper-table.d.ts +297 -0
- package/dist/core/shell-wrapper-table.js +58 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
- package/dist/core/tool-catalog-entries.js +6 -6
- package/dist/core/tool-face.d.ts +80 -4
- package/dist/core/tool-face.js +10 -0
- package/dist/core/tool-policy.d.ts +1 -6
- package/dist/core/tool-registry.d.ts +8 -11
- package/dist/core/tool-registry.js +5 -2
- package/dist/core/tool-roster.d.ts +20 -2
- package/dist/core/tool-roster.js +36 -8
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -3
- package/dist/stores/file/checkpoint-store.js +1 -1
- package/dist/stores/file/permission-rule-store.d.ts +28 -23
- package/dist/stores/file/permission-rule-store.js +64 -16
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/fs-search-tools.d.ts +3 -2
- package/dist/tools/fs/fs-search-tools.js +17 -9
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/fs/notebook.d.ts +1 -1
- package/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/fs/search.d.ts +0 -8
- package/dist/tools/fs/search.js +0 -23
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +99 -11
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import { parsePermissionRule } from "./permission-
|
|
2
|
+
import { parsePermissionRule } from "./permission-rule-syntax.js";
|
|
3
3
|
import { compileReadDeny } from "../tools/fs/read-deny.js";
|
|
4
4
|
import { carriesShellRedirection, parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
5
5
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
6
|
+
import { pathTargetOf } from "./tool-registry.js";
|
|
7
|
+
import { readShellCommand } from "./shell-lexer.js";
|
|
8
|
+
export const RULE_BEHAVIORS = ["deny", "ask", "allow"];
|
|
9
|
+
const RULE_BEHAVIOR_SET = new Set(RULE_BEHAVIORS);
|
|
10
|
+
export function isRuleBehavior(v) {
|
|
11
|
+
return RULE_BEHAVIOR_SET.has(v);
|
|
12
|
+
}
|
|
13
|
+
export const RULE_BEHAVIOR_PRECEDENCE = {
|
|
14
|
+
deny: 0,
|
|
15
|
+
ask: 1,
|
|
16
|
+
allow: 2,
|
|
17
|
+
};
|
|
18
|
+
export const RULE_BEHAVIORS_BY_PRECEDENCE = [...RULE_BEHAVIORS].sort((a, b) => RULE_BEHAVIOR_PRECEDENCE[a] - RULE_BEHAVIOR_PRECEDENCE[b]);
|
|
19
|
+
export function readRowBehavior(v) {
|
|
20
|
+
if (v === undefined)
|
|
21
|
+
return "allow";
|
|
22
|
+
return isRuleBehavior(v) ? v : undefined;
|
|
23
|
+
}
|
|
24
|
+
export const COMMAND_RULE_TOOL = "Bash";
|
|
25
|
+
export const READ_RULE_TOOL = "Read";
|
|
26
|
+
export function ruleToolGrammarOf(toolName) {
|
|
27
|
+
if (toolName === COMMAND_RULE_TOOL)
|
|
28
|
+
return "command";
|
|
29
|
+
return pathTargetOf(toolName) !== undefined ? "path" : undefined;
|
|
30
|
+
}
|
|
6
31
|
export const MAX_RULE_TEXT_CHARS = 512;
|
|
7
32
|
export const BARE_INTERPRETER_NAMES = new Set([
|
|
8
33
|
"node", "deno", "bun", "python", "python2", "python3", "perl", "ruby", "php", "osascript",
|
|
@@ -120,7 +145,6 @@ function reject(code, message) {
|
|
|
120
145
|
}
|
|
121
146
|
const RULE_LANE_FLOOR = { pathPrefixedNameIsText: true };
|
|
122
147
|
const MATCH_READING = { terminator: "keep", quotedOperatorsAreText: false, redirection: "reject" };
|
|
123
|
-
const PROGRAM_RUNS_READING = { terminator: "strip", quotedOperatorsAreText: true, redirection: "reject" };
|
|
124
148
|
const OFFER_READING = { terminator: "keep", quotedOperatorsAreText: false, redirection: "tolerate" };
|
|
125
149
|
function commandBasename(name) {
|
|
126
150
|
return name.slice(name.lastIndexOf("/") + 1);
|
|
@@ -187,7 +211,11 @@ export function renderUntrustedCommandText(text, maxLen = DISCLOSED_RULE_TEXT_MA
|
|
|
187
211
|
export function hasUnrenderableCharacters(text) {
|
|
188
212
|
return CONTROL_CHARS_RE.test(text);
|
|
189
213
|
}
|
|
190
|
-
export function
|
|
214
|
+
export function parseRuleText(text, behavior) {
|
|
215
|
+
if (!isRuleBehavior(behavior)) {
|
|
216
|
+
return reject("invalid.grammar", `rule "${text}" was parsed under behavior ${escapeForDisclosure(behavior)}, which is not one of ${RULE_BEHAVIORS.join("/")}`);
|
|
217
|
+
}
|
|
218
|
+
const tighten = behavior !== "allow";
|
|
191
219
|
if (text.length > MAX_RULE_TEXT_CHARS) {
|
|
192
220
|
return reject("invalid.too_long", `rule text exceeds ${MAX_RULE_TEXT_CHARS} characters`);
|
|
193
221
|
}
|
|
@@ -198,28 +226,11 @@ export function parseAllowRuleText(text, opts) {
|
|
|
198
226
|
if (parsed.ruleContent === undefined) {
|
|
199
227
|
return reject("invalid.grammar", `"${text}" is not a Tool(content) rule — a bare tool name claims the whole tool and is not a command rule`);
|
|
200
228
|
}
|
|
201
|
-
if (parsed.toolName
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (m === null) {
|
|
205
|
-
return reject("invalid.grammar", `only the Read(//abs-dir/**) directory form is a rule this version reads ("${text}") — relative, single-file, single-slash and no-tail spellings have no rule form`);
|
|
206
|
-
}
|
|
207
|
-
const bodyR = m[1];
|
|
208
|
-
if (bodyR.includes("*")) {
|
|
209
|
-
return reject("unsupported.wildcard", `a \`*\` inside the directory body matches anything at that position, which has no rule form ("${text}")`);
|
|
229
|
+
if (parsed.toolName !== COMMAND_RULE_TOOL) {
|
|
230
|
+
if (ruleToolGrammarOf(parsed.toolName) !== "path") {
|
|
231
|
+
return reject("unsupported.tool", `only Bash command rules and rules on path-targeting tools (Read, Edit, Write, …) have a rule form in this version (got "${parsed.toolName}")`);
|
|
210
232
|
}
|
|
211
|
-
|
|
212
|
-
if (segments.some((s) => s === "" || s.trim() === "")) {
|
|
213
|
-
return reject("invalid.grammar", `the directory body of "${text}" is not in the canonical spelling (an empty or whitespace-only path segment) — the lexical normal form is the only accepted spelling`);
|
|
214
|
-
}
|
|
215
|
-
if (segments.some((s) => s === "." || s === "..")) {
|
|
216
|
-
return reject("invalid.path_traversal", `the directory body of "${text}" carries a \`.\` or \`..\` segment — the lexical normal form is the only accepted spelling, and a traversal segment is not normalized away`);
|
|
217
|
-
}
|
|
218
|
-
const dir = "/" + bodyR;
|
|
219
|
-
return { rule: { rule: formatAllowRuleText(dir, "subpath"), tool: "Read", match: "subpath", command: dir } };
|
|
220
|
-
}
|
|
221
|
-
if (parsed.toolName !== "Bash") {
|
|
222
|
-
return reject("unsupported.tool", `only Bash and Read rules are supported in this version (got "${parsed.toolName}")`);
|
|
233
|
+
return parsePathRuleContent(text, parsed.toolName, parsed.ruleContent, behavior);
|
|
223
234
|
}
|
|
224
235
|
const content = parsed.ruleContent;
|
|
225
236
|
const prefixBody = /^(.+):\*$/.exec(content)?.[1];
|
|
@@ -256,7 +267,7 @@ export function parseAllowRuleText(text, opts) {
|
|
|
256
267
|
return reject("unsupported.wildcard", `a \`<chain> *\` pattern names more than one command, and the trailing-star form has no chain reading — the compound-prefix form spells one with the \`:*\` marker ("${text}")`);
|
|
257
268
|
}
|
|
258
269
|
const head = commandBasename(shape.names[shape.names.length - 1] ?? "");
|
|
259
|
-
if ((match === "prefix" || match === "wildcard") && BARE_INTERPRETER_NAMES.has(head) &&
|
|
270
|
+
if ((match === "prefix" || match === "wildcard") && BARE_INTERPRETER_NAMES.has(head) && !tighten) {
|
|
260
271
|
return reject("invalid.bare_interpreter_prefix", shape.segments.length > 1
|
|
261
272
|
? `prefix rule "${text}" leaves its final segment open-ended under the interpreter "${head}" — such a rule authorizes running arbitrary programs, which one approval click cannot be read as having granted (an exact rule naming the whole command line is accepted)`
|
|
262
273
|
: `prefix rule "${text}" is headed by the interpreter "${head}" — such a rule authorizes running arbitrary programs, which one approval click cannot be read as having granted (an exact rule naming the whole command line is accepted)`);
|
|
@@ -265,12 +276,132 @@ export function parseAllowRuleText(text, opts) {
|
|
|
265
276
|
if (command === undefined) {
|
|
266
277
|
return reject("invalid.unbalanced_quotes", `rule "${text}" has unbalanced quoting, so the command it names is not determinable`);
|
|
267
278
|
}
|
|
268
|
-
return { rule: { rule:
|
|
279
|
+
return { rule: { behavior, rule: formatRuleText(command, match, COMMAND_RULE_TOOL), tool: COMMAND_RULE_TOOL, match, command } };
|
|
269
280
|
}
|
|
270
|
-
export function
|
|
281
|
+
export function formatRuleText(command, match, tool) {
|
|
271
282
|
if (match === "subpath")
|
|
272
|
-
return
|
|
273
|
-
|
|
283
|
+
return `${tool}(//${command.startsWith("/") ? command.slice(1) : command}/**)`;
|
|
284
|
+
if (match === "path")
|
|
285
|
+
return `${tool}(${command})`;
|
|
286
|
+
return `${tool}(${command}${match === "prefix" ? ":*" : match === "wildcard" ? " *" : ""})`;
|
|
287
|
+
}
|
|
288
|
+
function parsePathRuleContent(text, tool, content, behavior) {
|
|
289
|
+
const dirForm = /^\/\/(.+)\/\*\*$/.exec(content);
|
|
290
|
+
if (dirForm !== null) {
|
|
291
|
+
const bodyR = dirForm[1];
|
|
292
|
+
if (bodyR.includes("*")) {
|
|
293
|
+
return reject("unsupported.wildcard", `a \`*\` inside the directory body matches anything at that position, which has no rule form ("${text}")`);
|
|
294
|
+
}
|
|
295
|
+
const segments = bodyR.split("/");
|
|
296
|
+
if (segments.some((sg) => sg === "" || sg.trim() === "")) {
|
|
297
|
+
return reject("invalid.grammar", `the directory body of "${text}" is not in the canonical spelling (an empty or whitespace-only path segment) — the lexical normal form is the only accepted spelling`);
|
|
298
|
+
}
|
|
299
|
+
if (segments.some((sg) => sg === "." || sg === "..")) {
|
|
300
|
+
return reject("invalid.path_traversal", `the directory body of "${text}" carries a \`.\` or \`..\` segment — the lexical normal form is the only accepted spelling, and a traversal segment is not normalized away`);
|
|
301
|
+
}
|
|
302
|
+
if (behavior === "allow" && tool !== READ_RULE_TOOL) {
|
|
303
|
+
return reject("unsupported.tool", `an allow rule on a path tool speaks for the Read directory form only — a standing approval of a WRITE by directory is not a rule form ("${text}"); write it as a deny or an ask, or approve the call`);
|
|
304
|
+
}
|
|
305
|
+
const dir = "/" + bodyR;
|
|
306
|
+
return { rule: { behavior, rule: formatRuleText(dir, "subpath", tool), tool, match: "subpath", command: dir } };
|
|
307
|
+
}
|
|
308
|
+
if (behavior === "allow") {
|
|
309
|
+
return tool === READ_RULE_TOOL
|
|
310
|
+
? reject("invalid.grammar", `only the Read(//abs-dir/**) directory form is a rule this version reads ("${text}") — relative, single-file, single-slash and no-tail spellings have no rule form`)
|
|
311
|
+
: reject("unsupported.tool", `an allow rule on "${tool}" has no rule form — only the Read directory form is a standing approval by path ("${text}"); write it as a deny or an ask, or approve the call`);
|
|
312
|
+
}
|
|
313
|
+
if (content.trim() !== content || content === "") {
|
|
314
|
+
return reject("invalid.grammar", `the path pattern of "${text}" is empty or carries surrounding whitespace — the pattern is stored as spelled, so the spelling must be the one meant`);
|
|
315
|
+
}
|
|
316
|
+
if (content.startsWith("!"))
|
|
317
|
+
return reject("invalid.grammar", `a negated path pattern ("${text}") is not a rule — a rule says what it reaches, never what it excepts`);
|
|
318
|
+
if (/[?[\]{}]/.test(content)) {
|
|
319
|
+
return reject("unsupported.wildcard", `the path pattern of "${text}" carries a glob class (\`?\`, \`[]\`, \`{}\`) this lane does not read — only \`*\` inside a segment and \`**\` as a whole segment have a rule form`);
|
|
320
|
+
}
|
|
321
|
+
const body = content.startsWith("//") ? content.slice(2) : content.startsWith("~/") ? content.slice(2) : content.startsWith("/") ? content.slice(1) : content.startsWith("./") ? content.slice(2) : content;
|
|
322
|
+
const segments = body.split("/");
|
|
323
|
+
if (segments.some((sg) => sg === "" || sg.trim() === "")) {
|
|
324
|
+
return reject("invalid.grammar", `the path pattern of "${text}" carries an empty path segment — the lexical normal form is the only accepted spelling`);
|
|
325
|
+
}
|
|
326
|
+
if (segments.some((sg) => sg === "." || sg === "..")) {
|
|
327
|
+
return reject("invalid.path_traversal", `the path pattern of "${text}" carries a \`.\` or \`..\` segment — the lexical normal form is the only accepted spelling, and a traversal segment is not normalized away`);
|
|
328
|
+
}
|
|
329
|
+
if (segments.some((sg) => sg.includes("**") && sg !== "**")) {
|
|
330
|
+
return reject("unsupported.wildcard", `\`**\` in the path pattern of "${text}" must stand as a whole segment (\`a/**/b\`), never inside one`);
|
|
331
|
+
}
|
|
332
|
+
return { rule: { behavior, rule: formatRuleText(content, "path", tool), tool, match: "path", command: content } };
|
|
333
|
+
}
|
|
334
|
+
function resolvePathPattern(pattern, bases) {
|
|
335
|
+
if (pattern.startsWith("//"))
|
|
336
|
+
return "/" + pattern.slice(2);
|
|
337
|
+
const under = (base, rest) => {
|
|
338
|
+
const normal = base === undefined ? undefined : lexicalNormalAbsolutePathOf(base);
|
|
339
|
+
if (normal === undefined)
|
|
340
|
+
return undefined;
|
|
341
|
+
return (normal === "/" ? "" : normal) + "/" + rest;
|
|
342
|
+
};
|
|
343
|
+
if (pattern.startsWith("~/"))
|
|
344
|
+
return under(bases.home ?? homedir(), pattern.slice(2));
|
|
345
|
+
if (pattern.startsWith("/"))
|
|
346
|
+
return under(bases.root, pattern.slice(1));
|
|
347
|
+
return under(bases.cwd ?? bases.root, pattern.startsWith("./") ? pattern.slice(2) : pattern);
|
|
348
|
+
}
|
|
349
|
+
export function isUsablePathBase(base) {
|
|
350
|
+
return base !== undefined && lexicalNormalAbsolutePathOf(base) !== undefined;
|
|
351
|
+
}
|
|
352
|
+
function segmentGlobMatches(glob, segment) {
|
|
353
|
+
if (!glob.includes("*"))
|
|
354
|
+
return glob === segment;
|
|
355
|
+
const parts = glob.split("*");
|
|
356
|
+
let at = 0;
|
|
357
|
+
for (let i = 0; i < parts.length; i++) {
|
|
358
|
+
const part = parts[i];
|
|
359
|
+
if (i === 0) {
|
|
360
|
+
if (!segment.startsWith(part))
|
|
361
|
+
return false;
|
|
362
|
+
at = part.length;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (i === parts.length - 1)
|
|
366
|
+
return part === "" || (segment.endsWith(part) && segment.length - part.length >= at);
|
|
367
|
+
const k = segment.indexOf(part, at);
|
|
368
|
+
if (k === -1)
|
|
369
|
+
return false;
|
|
370
|
+
at = k + part.length;
|
|
371
|
+
}
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
function globSegmentsReach(globs, target) {
|
|
375
|
+
const seen = new Set();
|
|
376
|
+
const width = target.length + 1;
|
|
377
|
+
const walk = (g, t) => {
|
|
378
|
+
if (g === globs.length)
|
|
379
|
+
return true;
|
|
380
|
+
const key = g * width + t;
|
|
381
|
+
if (seen.has(key))
|
|
382
|
+
return false;
|
|
383
|
+
seen.add(key);
|
|
384
|
+
if (globs[g] === "**")
|
|
385
|
+
return walk(g + 1, t) || (t < target.length && walk(g, t + 1));
|
|
386
|
+
if (t >= target.length)
|
|
387
|
+
return false;
|
|
388
|
+
return segmentGlobMatches(globs[g], target[t]) && walk(g + 1, t + 1);
|
|
389
|
+
};
|
|
390
|
+
return walk(0, 0);
|
|
391
|
+
}
|
|
392
|
+
export function pathRuleReaches(rule, target, bases) {
|
|
393
|
+
if (rule.match === "subpath")
|
|
394
|
+
return directoryRuleAdmits(rule, target);
|
|
395
|
+
if (rule.match !== "path")
|
|
396
|
+
return false;
|
|
397
|
+
if (!isLexicalNormalAbsoluteDir(target) && target !== "/")
|
|
398
|
+
return false;
|
|
399
|
+
const resolved = resolvePathPattern(rule.command, bases);
|
|
400
|
+
if (resolved === undefined)
|
|
401
|
+
return false;
|
|
402
|
+
const globs = resolved.split("/").filter((sg) => sg !== "");
|
|
403
|
+
const segments = target.split("/").filter((sg) => sg !== "");
|
|
404
|
+
return globSegmentsReach(globs, segments);
|
|
274
405
|
}
|
|
275
406
|
function hasUnescapedStar(s) {
|
|
276
407
|
for (let i = 0; i < s.length; i++) {
|
|
@@ -287,11 +418,97 @@ function hasUnescapedStar(s) {
|
|
|
287
418
|
export function ruleAdmitsCommand(rule, command) {
|
|
288
419
|
return admitsUnder(rule, command, MATCH_READING);
|
|
289
420
|
}
|
|
290
|
-
export
|
|
291
|
-
|
|
421
|
+
export const PROGRAM_RUN_REACHES = ["reached", "not_reached", "unreadable"];
|
|
422
|
+
const REACHED = { reach: "reached" };
|
|
423
|
+
const NOT_REACHED = { reach: "not_reached" };
|
|
424
|
+
export function programRunReachOf(rule, command) {
|
|
425
|
+
if (rule.match === "subpath" || rule.match === "path")
|
|
426
|
+
return NOT_REACHED;
|
|
427
|
+
const body = readRuleBody(rule.command);
|
|
428
|
+
if (body === undefined)
|
|
429
|
+
return NOT_REACHED;
|
|
430
|
+
const shape = readCommandMemo(command);
|
|
431
|
+
const exact = rule.match === "exact";
|
|
432
|
+
if (body.segments.length > 1) {
|
|
433
|
+
const broken = shape.segments.find((s) => s.unreadable !== undefined);
|
|
434
|
+
if (broken !== undefined)
|
|
435
|
+
return { reach: "unreadable", reason: broken.unreadable };
|
|
436
|
+
const hidden = shape.segments.find((s) => s.peelUnreadable !== undefined);
|
|
437
|
+
const shaped = !shape.grouped && shape.segments.length === body.segments.length && shape.connectors.every((c, i) => c === body.connectors[i]);
|
|
438
|
+
if (!shaped)
|
|
439
|
+
return hidden === undefined ? NOT_REACHED : { reach: "unreadable", reason: hidden.peelUnreadable };
|
|
440
|
+
let outcome = REACHED;
|
|
441
|
+
for (let i = 0; i < body.segments.length; i++) {
|
|
442
|
+
const r = segmentReachOf(body.segments[i].argv, shape.segments[i], exact || i < body.segments.length - 1);
|
|
443
|
+
if (r.reach === "not_reached")
|
|
444
|
+
return hidden === undefined ? NOT_REACHED : { reach: "unreadable", reason: hidden.peelUnreadable };
|
|
445
|
+
if (r.reach === "unreadable")
|
|
446
|
+
outcome = r;
|
|
447
|
+
}
|
|
448
|
+
return outcome;
|
|
449
|
+
}
|
|
450
|
+
const bodyArgv = body.segments[0].argv;
|
|
451
|
+
let unreadable;
|
|
452
|
+
for (const segment of shape.segments) {
|
|
453
|
+
const r = segmentReachOf(bodyArgv, segment, exact);
|
|
454
|
+
if (r.reach === "reached")
|
|
455
|
+
return REACHED;
|
|
456
|
+
if (r.reach === "unreadable")
|
|
457
|
+
unreadable ??= r;
|
|
458
|
+
}
|
|
459
|
+
return unreadable ?? NOT_REACHED;
|
|
460
|
+
}
|
|
461
|
+
export function ruleReachesProgramRun(rule, command) {
|
|
462
|
+
return programRunReachOf(rule, command).reach === "reached";
|
|
463
|
+
}
|
|
464
|
+
function segmentReachOf(bodyArgv, segment, exact) {
|
|
465
|
+
if (segment.unreadable !== undefined)
|
|
466
|
+
return { reach: "unreadable", reason: segment.unreadable };
|
|
467
|
+
let unreadable;
|
|
468
|
+
for (const argv of [segment.argv, ...segment.peeled]) {
|
|
469
|
+
const r = argvReachOf(bodyArgv, argv, exact);
|
|
470
|
+
if (r.reach === "reached")
|
|
471
|
+
return REACHED;
|
|
472
|
+
if (r.reach === "unreadable")
|
|
473
|
+
unreadable ??= r;
|
|
474
|
+
}
|
|
475
|
+
if (segment.peelUnreadable !== undefined)
|
|
476
|
+
unreadable ??= { reach: "unreadable", reason: segment.peelUnreadable };
|
|
477
|
+
return unreadable ?? NOT_REACHED;
|
|
478
|
+
}
|
|
479
|
+
function argvReachOf(bodyArgv, argv, exact) {
|
|
480
|
+
for (let i = 0; i < bodyArgv.length; i++) {
|
|
481
|
+
const word = argv[i];
|
|
482
|
+
const wanted = bodyArgv[i];
|
|
483
|
+
if (word === undefined)
|
|
484
|
+
return NOT_REACHED;
|
|
485
|
+
if (word.expands !== false) {
|
|
486
|
+
if (word.raw === wanted.raw)
|
|
487
|
+
continue;
|
|
488
|
+
return { reach: "unreadable", reason: i === 0 ? "the program name is an expansion" : `an expansion (${word.raw}) stands where the rule reads a word` };
|
|
489
|
+
}
|
|
490
|
+
if (word.text !== wanted.text)
|
|
491
|
+
return NOT_REACHED;
|
|
492
|
+
}
|
|
493
|
+
if (!exact || argv.length === bodyArgv.length)
|
|
494
|
+
return REACHED;
|
|
495
|
+
const loose = argv.slice(bodyArgv.length).find((w) => w.expands === "many");
|
|
496
|
+
return loose === undefined ? NOT_REACHED : { reach: "unreadable", reason: `an unquoted expansion (${loose.raw}) may expand to nothing` };
|
|
497
|
+
}
|
|
498
|
+
function readRuleBody(body) {
|
|
499
|
+
const shape = readShellCommand(body);
|
|
500
|
+
if (shape.grouped || shape.segments.length === 0)
|
|
501
|
+
return undefined;
|
|
502
|
+
return shape.segments.every((s) => s.unreadable === undefined && s.argv.length > 0) ? shape : undefined;
|
|
503
|
+
}
|
|
504
|
+
let lastRead;
|
|
505
|
+
function readCommandMemo(command) {
|
|
506
|
+
if (lastRead?.command !== command)
|
|
507
|
+
lastRead = { command, shape: readShellCommand(command) };
|
|
508
|
+
return lastRead.shape;
|
|
292
509
|
}
|
|
293
510
|
function admitsUnder(rule, command, reading) {
|
|
294
|
-
if (rule.match === "subpath")
|
|
511
|
+
if (rule.match === "subpath" || rule.match === "path")
|
|
295
512
|
return false;
|
|
296
513
|
const shape = ruleLaneShapeOf(command, reading);
|
|
297
514
|
if ("reject" in shape)
|
|
@@ -332,10 +549,6 @@ export function ruleBreadthWarningsOf(rule) {
|
|
|
332
549
|
}
|
|
333
550
|
return [];
|
|
334
551
|
}
|
|
335
|
-
export function ruleLaneSegmentsOf(command) {
|
|
336
|
-
const shape = ruleLaneShapeOf(command, PROGRAM_RUNS_READING);
|
|
337
|
-
return "reject" in shape ? undefined : shape.segments;
|
|
338
|
-
}
|
|
339
552
|
export function pathWithinRoot(path, root) {
|
|
340
553
|
if (path === root)
|
|
341
554
|
return true;
|
|
@@ -350,9 +563,8 @@ export function lexicalNormalAbsolutePathOf(path) {
|
|
|
350
563
|
if (seg === "" || seg === ".")
|
|
351
564
|
continue;
|
|
352
565
|
if (seg === "..") {
|
|
353
|
-
if (out.length
|
|
354
|
-
|
|
355
|
-
out.pop();
|
|
566
|
+
if (out.length > 0)
|
|
567
|
+
out.pop();
|
|
356
568
|
continue;
|
|
357
569
|
}
|
|
358
570
|
out.push(seg);
|
|
@@ -366,7 +578,7 @@ function isLexicalNormalAbsoluteDir(path) {
|
|
|
366
578
|
return segments.length > 0 && segments.every((s) => s !== "" && s !== "." && s !== "..");
|
|
367
579
|
}
|
|
368
580
|
export function directoryRuleAdmits(rule, path) {
|
|
369
|
-
if (rule.
|
|
581
|
+
if (rule.match !== "subpath")
|
|
370
582
|
return false;
|
|
371
583
|
if (!isLexicalNormalAbsoluteDir(rule.command))
|
|
372
584
|
return false;
|
|
@@ -483,12 +695,12 @@ export function isRuleLive(rule) {
|
|
|
483
695
|
export function eligibleContext(rule, call) {
|
|
484
696
|
return rule.tool === call.tool && scopeCoversCwd(rule.scope, call.cwd, call.sessionId);
|
|
485
697
|
}
|
|
486
|
-
export function eligiblePersisted(rule, call) {
|
|
487
|
-
return isRuleLive(rule) && eligibleContext(rule, call);
|
|
698
|
+
export function eligiblePersisted(rule, call, behavior) {
|
|
699
|
+
return rule.behavior === behavior && isRuleLive(rule) && eligibleContext(rule, call);
|
|
488
700
|
}
|
|
489
701
|
function firstEligibleAdmittingRule(rules, commandText, call) {
|
|
490
702
|
for (const rule of rules) {
|
|
491
|
-
if (!eligiblePersisted(rule, call))
|
|
703
|
+
if (!eligiblePersisted(rule, call, "allow"))
|
|
492
704
|
continue;
|
|
493
705
|
if (ruleAdmitsCommand(rule, commandText))
|
|
494
706
|
return rule;
|
|
@@ -521,13 +733,60 @@ export function findAdmittingRule(rules, call) {
|
|
|
521
733
|
}
|
|
522
734
|
function firstEligibleDirectoryRule(rules, directory, call) {
|
|
523
735
|
for (const rule of rules) {
|
|
524
|
-
if (!eligiblePersisted(rule, { tool: "Read", cwd: call.cwd, sessionId: call.sessionId }))
|
|
736
|
+
if (!eligiblePersisted(rule, { tool: "Read", cwd: call.cwd, sessionId: call.sessionId }, "allow"))
|
|
525
737
|
continue;
|
|
526
738
|
if (directoryRuleAdmits(rule, directory))
|
|
527
739
|
return rule;
|
|
528
740
|
}
|
|
529
741
|
return undefined;
|
|
530
742
|
}
|
|
743
|
+
export function adjudicatePersistedRules(rules, call) {
|
|
744
|
+
for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
|
|
745
|
+
if (behavior === "allow") {
|
|
746
|
+
const covering = findAdmittingRule(rules, call);
|
|
747
|
+
if (covering !== undefined)
|
|
748
|
+
return { behavior, rules: covering };
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
let unreadable;
|
|
752
|
+
for (const rule of rules) {
|
|
753
|
+
if (!eligiblePersisted(rule, call, behavior))
|
|
754
|
+
continue;
|
|
755
|
+
const outcome = programRunReachOf(rule, call.command);
|
|
756
|
+
if (outcome.reach === "reached")
|
|
757
|
+
return { behavior, rules: [rule] };
|
|
758
|
+
if (outcome.reach === "unreadable")
|
|
759
|
+
unreadable ??= { behavior: "ask", rules: [rule], unreadable: outcome.reason };
|
|
760
|
+
}
|
|
761
|
+
if (unreadable !== undefined)
|
|
762
|
+
return unreadable;
|
|
763
|
+
}
|
|
764
|
+
return undefined;
|
|
765
|
+
}
|
|
766
|
+
export function adjudicatePersistedPathRules(rules, call, targets, bases) {
|
|
767
|
+
for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
|
|
768
|
+
if (behavior === "allow") {
|
|
769
|
+
if (targets.allow === undefined || call.tool !== READ_RULE_TOOL)
|
|
770
|
+
continue;
|
|
771
|
+
for (const rule of rules) {
|
|
772
|
+
if (!eligiblePersisted(rule, call, behavior))
|
|
773
|
+
continue;
|
|
774
|
+
if (directoryRuleAdmits(rule, targets.allow))
|
|
775
|
+
return { behavior, rules: [rule] };
|
|
776
|
+
}
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (targets.tighten === undefined)
|
|
780
|
+
continue;
|
|
781
|
+
for (const rule of rules) {
|
|
782
|
+
if (!eligiblePersisted(rule, call, behavior))
|
|
783
|
+
continue;
|
|
784
|
+
if (pathRuleReaches(rule, targets.tighten, bases))
|
|
785
|
+
return { behavior, rules: [rule] };
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return undefined;
|
|
789
|
+
}
|
|
531
790
|
export function segmentCoverageOf(command, rules, call) {
|
|
532
791
|
const folded = foldSpacing(command);
|
|
533
792
|
if (folded === undefined)
|
|
@@ -569,7 +828,7 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
569
828
|
if (folded === undefined)
|
|
570
829
|
return [];
|
|
571
830
|
const offers = [];
|
|
572
|
-
const exact =
|
|
831
|
+
const exact = parseRuleText(formatRuleText(folded, "exact", COMMAND_RULE_TOOL), "allow");
|
|
573
832
|
if ("rule" in exact && exact.rule.match === "exact" && ruleAdmitsCommand(exact.rule, command)) {
|
|
574
833
|
offers.push({ kind: "single", rule: exact.rule.rule, match: "exact", command: exact.rule.command });
|
|
575
834
|
}
|
|
@@ -589,7 +848,7 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
589
848
|
}
|
|
590
849
|
if (denied)
|
|
591
850
|
return undefined;
|
|
592
|
-
const parsed =
|
|
851
|
+
const parsed = parseRuleText(formatRuleText(directory, "subpath", READ_RULE_TOOL), "allow");
|
|
593
852
|
if (!("rule" in parsed) || parsed.rule.match !== "subpath")
|
|
594
853
|
return undefined;
|
|
595
854
|
if (!directoryRuleAdmits(parsed.rule, directory))
|
|
@@ -600,7 +859,7 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
600
859
|
if (offers.length === 1) {
|
|
601
860
|
const body = bodyOf(tokensOf(folded));
|
|
602
861
|
if (body !== undefined) {
|
|
603
|
-
const parsed =
|
|
862
|
+
const parsed = parseRuleText(formatRuleText(body, "wildcard", COMMAND_RULE_TOOL), "allow");
|
|
604
863
|
if ("rule" in parsed && parsed.rule.match === "wildcard" && ruleAdmitsCommand(parsed.rule, command)) {
|
|
605
864
|
offers.push({ kind: "single", rule: parsed.rule.rule, match: "wildcard", command: parsed.rule.command });
|
|
606
865
|
}
|
|
@@ -627,12 +886,12 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
627
886
|
return undefined;
|
|
628
887
|
const body = bodyOf(tokensOf(segment));
|
|
629
888
|
if (body !== undefined) {
|
|
630
|
-
const parsed =
|
|
889
|
+
const parsed = parseRuleText(formatRuleText(body, "wildcard", COMMAND_RULE_TOOL), "allow");
|
|
631
890
|
if ("rule" in parsed && parsed.rule.match === "wildcard" && ruleAdmitsCommand(parsed.rule, segment)) {
|
|
632
891
|
return { rule: parsed.rule.rule, match: "wildcard", command: parsed.rule.command };
|
|
633
892
|
}
|
|
634
893
|
}
|
|
635
|
-
const segExact =
|
|
894
|
+
const segExact = parseRuleText(formatRuleText(segment, "exact", COMMAND_RULE_TOOL), "allow");
|
|
636
895
|
if ("rule" in segExact && segExact.rule.match === "exact" && ruleAdmitsCommand(segExact.rule, segment)) {
|
|
637
896
|
return { rule: segExact.rule.rule, match: "exact", command: segExact.rule.command };
|
|
638
897
|
}
|
|
@@ -655,7 +914,7 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
655
914
|
const batchRules = minted.slice(0, 5);
|
|
656
915
|
if (batchRules.length > 0) {
|
|
657
916
|
const parsedBatch = batchRules.flatMap((r) => {
|
|
658
|
-
const p =
|
|
917
|
+
const p = parseRuleText(r.rule, "allow");
|
|
659
918
|
return "rule" in p ? [p.rule] : [];
|
|
660
919
|
});
|
|
661
920
|
const uncoveredDetail = [];
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* convergent merge). So the org layer has NO CRDT, no tombstones and no join: the server is the single
|
|
7
7
|
* authority, a client holds a read-only versioned snapshot, an update is a whole replacement (revision
|
|
8
8
|
* strictly ordered), and a rollback is a NEW revision carrying old content. Cross-layer tombstones are
|
|
9
|
-
* structurally impossible — an org rule never enters `
|
|
9
|
+
* structurally impossible — an org rule never enters `PersistedRule`, and an org snapshot replace
|
|
10
10
|
* never touches a personal store's bytes. The two layers meet only at the CONSUMPTION site, where org
|
|
11
11
|
* deny/ask folds strict-side: **org deny > org ask (non-dismissable) > personal allow rule > bare ask**.
|
|
12
12
|
*
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* accept revision N−1 after a restart. Signature / predecessor chaining belongs to the privilege-
|
|
36
36
|
* separation ticket; the high-water mark is the floor that needs no key distribution.
|
|
37
37
|
*/
|
|
38
|
-
import { type
|
|
38
|
+
import { type PersistedRule, type RuleRejectCode } from "./permission-rule-model.js";
|
|
39
39
|
import type { PermissionRuleStore } from "./permission-rule-provider.js";
|
|
40
40
|
/** One org rule. There is structurally no allow bucket (design/179 §9: the org layer only tightens). */
|
|
41
41
|
export interface OrgPermissionRule {
|
|
@@ -187,8 +187,10 @@ export declare function unenforceableOrgRules(rules: readonly OrgPermissionRule[
|
|
|
187
187
|
/**
|
|
188
188
|
* design/182 §7.2 — which org rule speaks for this command, if any. Deny outranks ask; within a
|
|
189
189
|
* behavior the first textual match wins (reporting order only — all denies are the same one answer).
|
|
190
|
-
* Matching uses the SAME parser and
|
|
191
|
-
* interpreter-headed prefix deny like `Bash(node:*)` is a
|
|
190
|
+
* Matching uses the SAME parser and the SAME tightening reach the personal deny/ask rows use
|
|
191
|
+
* ({@link programRunReachOf}: an interpreter-headed prefix deny like `Bash(node:*)` is a
|
|
192
|
+
* legitimately wide tightening and matches; a segment hit is a hit; a call the lexer cannot read
|
|
193
|
+
* against a rule is an ASK with the reason — fail closed, never silence).
|
|
192
194
|
*
|
|
193
195
|
* The skip below is a defensive floor, and what stands behind it is worth stating precisely: rules that
|
|
194
196
|
* arrive from the org partition's `resolve` ({@link createOrgRulePartition}) have passed {@link validateOrgSnapshot} on BOTH the install
|
|
@@ -201,10 +203,15 @@ export declare function unenforceableOrgRules(rules: readonly OrgPermissionRule[
|
|
|
201
203
|
export declare function orgRuleVerdictFor(rules: readonly OrgPermissionRule[], call: {
|
|
202
204
|
tool: string;
|
|
203
205
|
command: string;
|
|
204
|
-
}):
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
}): OrgRuleVerdict | undefined;
|
|
207
|
+
/** The org layer's answer for one call: which rule spoke, with what behavior — and, when the call could
|
|
208
|
+
* not be READ against the org's rules, the lexer's reason beside the first rule it could not judge
|
|
209
|
+
* (`behavior` is then `ask`: the org published a rule that may reach this call, and nobody can say). */
|
|
210
|
+
export interface OrgRuleVerdict {
|
|
211
|
+
readonly behavior: "deny" | "ask";
|
|
212
|
+
readonly rule: string;
|
|
213
|
+
readonly unreadable?: string;
|
|
214
|
+
}
|
|
208
215
|
/**
|
|
209
216
|
* design/182 §7.3 / design/389 — the SHADOW predicate the unified store's read face applies to every
|
|
210
217
|
* personal allow row: does an org DENY reach this rule's own command pattern? An org deny does not
|
|
@@ -212,8 +219,8 @@ export declare function orgRuleVerdictFor(rules: readonly OrgPermissionRule[], c
|
|
|
212
219
|
* OVERRIDE it, not to self-declare its removal — so a shadowed rule reports `shadowed-by-org` and comes
|
|
213
220
|
* back by itself when the org deny is withdrawn.
|
|
214
221
|
*
|
|
215
|
-
* The SAME reach the gate uses (
|
|
216
|
-
* whether a deny touches this rule at all. A personal rule may name a connector chain, and a deny on one
|
|
222
|
+
* The SAME reach the gate uses ({@link ruleReachesProgramRun}), so the view and the decision cannot
|
|
223
|
+
* disagree about whether a deny touches this rule at all. A personal rule may name a connector chain, and a deny on one
|
|
217
224
|
* of its segments shadows it exactly as a deny on the whole does.
|
|
218
225
|
*
|
|
219
226
|
* WHAT `shadowed-by-org` DOES AND DOES NOT CLAIM: the predicate asks whether the deny reaches the
|
|
@@ -232,9 +239,9 @@ export declare function orgRuleVerdictFor(rules: readonly OrgPermissionRule[], c
|
|
|
232
239
|
* reach it, and its `command` is a PATH that must not be fed to the shell lane's segment reader as
|
|
233
240
|
* though it were a command line.
|
|
234
241
|
*/
|
|
235
|
-
export declare function orgRuleShadows(orgRules: readonly OrgPermissionRule[], rule: Pick<
|
|
242
|
+
export declare function orgRuleShadows(orgRules: readonly OrgPermissionRule[], rule: Pick<PersistedRule, "tool" | "command">): boolean;
|
|
236
243
|
/** The shadow predicate with the org denies parsed ONCE — what a read over many rows uses. */
|
|
237
|
-
export declare function compileOrgShadowPredicate(orgRules: readonly OrgPermissionRule[]): (rule: Pick<
|
|
244
|
+
export declare function compileOrgShadowPredicate(orgRules: readonly OrgPermissionRule[]): (rule: Pick<PersistedRule, "tool" | "command">) => boolean;
|
|
238
245
|
/** Does this store carry a write face? Re-exported convenience for org-ticket integrations that need to
|
|
239
246
|
* distinguish a read-only projection from a writable personal bucket without importing the writer seam. */
|
|
240
247
|
export declare function isWritablePermissionRuleStore(store: PermissionRuleStore): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { escapeForDisclosure,
|
|
1
|
+
import { escapeForDisclosure, parseRuleText, programRunReachOf, ruleReachesProgramRun } from "./permission-rule-model.js";
|
|
2
2
|
import { writerOf } from "./permission-rule-store.js";
|
|
3
3
|
export const ORG_UNAVAILABLE_DECISION_REASON = "org_unavailable";
|
|
4
4
|
export const ORG_ADJUDICATION_TIMEOUT_MS = 15_000;
|
|
@@ -199,7 +199,7 @@ export function unenforceableOrgRules(rules) {
|
|
|
199
199
|
for (const r of rules) {
|
|
200
200
|
if (typeof r?.rule !== "string" || r.rule === "")
|
|
201
201
|
continue;
|
|
202
|
-
const parsed =
|
|
202
|
+
const parsed = parseRuleText(r.rule, r.behavior);
|
|
203
203
|
if ("reject" in parsed) {
|
|
204
204
|
out.push({ rule: r.rule, code: parsed.reject.code });
|
|
205
205
|
continue;
|
|
@@ -243,28 +243,26 @@ function validateOrgSnapshot(s, nowMs) {
|
|
|
243
243
|
return undefined;
|
|
244
244
|
}
|
|
245
245
|
export function orgRuleVerdictFor(rules, call) {
|
|
246
|
-
const segments = ruleLaneSegmentsOf(call.command);
|
|
247
246
|
let ask;
|
|
247
|
+
let unreadable;
|
|
248
248
|
for (const r of rules) {
|
|
249
|
-
const parsed =
|
|
249
|
+
const parsed = parseRuleText(r.rule, r.behavior);
|
|
250
250
|
if ("reject" in parsed)
|
|
251
251
|
continue;
|
|
252
252
|
if (parsed.rule.tool !== call.tool)
|
|
253
253
|
continue;
|
|
254
|
-
|
|
254
|
+
const outcome = programRunReachOf(parsed.rule, call.command);
|
|
255
|
+
if (outcome.reach === "not_reached")
|
|
255
256
|
continue;
|
|
257
|
+
if (outcome.reach === "unreadable") {
|
|
258
|
+
unreadable ??= { behavior: "ask", rule: r.rule, unreadable: outcome.reason };
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
256
261
|
if (r.behavior === "deny")
|
|
257
262
|
return { behavior: "deny", rule: r.rule };
|
|
258
263
|
ask ??= { behavior: "ask", rule: r.rule };
|
|
259
264
|
}
|
|
260
|
-
return ask;
|
|
261
|
-
}
|
|
262
|
-
function orgRuleReaches(rule, command, segments) {
|
|
263
|
-
if (ruleAdmitsProgramRun(rule, command))
|
|
264
|
-
return true;
|
|
265
|
-
if (segments === undefined)
|
|
266
|
-
return false;
|
|
267
|
-
return segments.some((segment) => ruleAdmitsProgramRun(rule, segment));
|
|
265
|
+
return unreadable ?? ask;
|
|
268
266
|
}
|
|
269
267
|
export function orgRuleShadows(orgRules, rule) {
|
|
270
268
|
return compileOrgShadowPredicate(orgRules)(rule);
|
|
@@ -274,15 +272,14 @@ export function compileOrgShadowPredicate(orgRules) {
|
|
|
274
272
|
for (const d of orgRules) {
|
|
275
273
|
if (d.behavior !== "deny")
|
|
276
274
|
continue;
|
|
277
|
-
const parsed =
|
|
275
|
+
const parsed = parseRuleText(d.rule, "deny");
|
|
278
276
|
if (!("reject" in parsed))
|
|
279
277
|
denies.push(parsed.rule);
|
|
280
278
|
}
|
|
281
279
|
return (rule) => {
|
|
282
280
|
if (rule.tool !== "Bash" || denies.length === 0)
|
|
283
281
|
return false;
|
|
284
|
-
|
|
285
|
-
return denies.some((d) => d.tool === rule.tool && orgRuleReaches(d, rule.command, segments));
|
|
282
|
+
return denies.some((d) => d.tool === rule.tool && ruleReachesProgramRun(d, rule.command));
|
|
286
283
|
};
|
|
287
284
|
}
|
|
288
285
|
export function isWritablePermissionRuleStore(store) {
|