@sema-agent/core 7.8.0 → 7.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -1
- package/dist/agents/subagent.d.ts +1 -1
- package/dist/core/ask-origin.d.ts +12 -1
- package/dist/core/ask-origin.js +5 -1
- package/dist/core/checkpoint-store.d.ts +34 -28
- package/dist/core/gate-lanes.js +49 -23
- package/dist/core/gate-outcome.d.ts +8 -4
- package/dist/core/gate-outcome.js +2 -1
- package/dist/core/hooks.d.ts +56 -31
- 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 +254 -55
- package/dist/core/permission-rule-model.js +323 -43
- package/dist/core/permission-rule-org.d.ts +9 -8
- package/dist/core/permission-rule-org.js +6 -15
- 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 +55 -33
- package/dist/core/permission-rules.js +65 -55
- package/dist/core/persisted-rule-arms.d.ts +56 -0
- package/dist/core/persisted-rule-arms.js +48 -0
- package/dist/core/runner/contracts.d.ts +21 -2
- package/dist/core/runner/permission-rule-lanes.d.ts +33 -26
- package/dist/core/runner/permission-rule-lanes.js +27 -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/tool-end-body.d.ts +12 -5
- package/dist/core/runner/tool-end-body.js +5 -5
- 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/store-contracts/permission-rule-sync-contract.js +29 -10
- package/dist/core/tool-roster.d.ts +9 -0
- package/dist/core/tool-roster.js +15 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- 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/notebook.d.ts +1 -1
- package/dist/tools/fs/safety.d.ts +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +43 -11
|
@@ -1,8 +1,32 @@
|
|
|
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
|
+
export const RULE_BEHAVIORS = ["deny", "ask", "allow"];
|
|
8
|
+
const RULE_BEHAVIOR_SET = new Set(RULE_BEHAVIORS);
|
|
9
|
+
export function isRuleBehavior(v) {
|
|
10
|
+
return RULE_BEHAVIOR_SET.has(v);
|
|
11
|
+
}
|
|
12
|
+
export const RULE_BEHAVIOR_PRECEDENCE = {
|
|
13
|
+
deny: 0,
|
|
14
|
+
ask: 1,
|
|
15
|
+
allow: 2,
|
|
16
|
+
};
|
|
17
|
+
export const RULE_BEHAVIORS_BY_PRECEDENCE = [...RULE_BEHAVIORS].sort((a, b) => RULE_BEHAVIOR_PRECEDENCE[a] - RULE_BEHAVIOR_PRECEDENCE[b]);
|
|
18
|
+
export function readRowBehavior(v) {
|
|
19
|
+
if (v === undefined)
|
|
20
|
+
return "allow";
|
|
21
|
+
return isRuleBehavior(v) ? v : undefined;
|
|
22
|
+
}
|
|
23
|
+
export const COMMAND_RULE_TOOL = "Bash";
|
|
24
|
+
export const READ_RULE_TOOL = "Read";
|
|
25
|
+
export function ruleToolGrammarOf(toolName) {
|
|
26
|
+
if (toolName === COMMAND_RULE_TOOL)
|
|
27
|
+
return "command";
|
|
28
|
+
return pathTargetOf(toolName) !== undefined ? "path" : undefined;
|
|
29
|
+
}
|
|
6
30
|
export const MAX_RULE_TEXT_CHARS = 512;
|
|
7
31
|
export const BARE_INTERPRETER_NAMES = new Set([
|
|
8
32
|
"node", "deno", "bun", "python", "python2", "python3", "perl", "ruby", "php", "osascript",
|
|
@@ -187,7 +211,11 @@ export function renderUntrustedCommandText(text, maxLen = DISCLOSED_RULE_TEXT_MA
|
|
|
187
211
|
export function hasUnrenderableCharacters(text) {
|
|
188
212
|
return CONTROL_CHARS_RE.test(text);
|
|
189
213
|
}
|
|
190
|
-
export function
|
|
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++) {
|
|
@@ -290,8 +421,117 @@ export function ruleAdmitsCommand(rule, command) {
|
|
|
290
421
|
export function ruleAdmitsProgramRun(rule, command) {
|
|
291
422
|
return admitsUnder(rule, command, PROGRAM_RUNS_READING);
|
|
292
423
|
}
|
|
424
|
+
export function ruleReachesProgramRun(rule, command) {
|
|
425
|
+
if (rule.match === "subpath" || rule.match === "path")
|
|
426
|
+
return false;
|
|
427
|
+
if (ruleAdmitsProgramRun(rule, command))
|
|
428
|
+
return true;
|
|
429
|
+
const spelled = stripRedirections(stripFdDuplications(command) ?? command) ?? command;
|
|
430
|
+
if (spelled !== command && ruleAdmitsProgramRun(rule, spelled))
|
|
431
|
+
return true;
|
|
432
|
+
const segments = ruleLaneSegmentsOf(spelled);
|
|
433
|
+
if (segments === undefined)
|
|
434
|
+
return false;
|
|
435
|
+
return segments.some((segment) => ruleAdmitsProgramRun(rule, segment));
|
|
436
|
+
}
|
|
437
|
+
function stripRedirections(command) {
|
|
438
|
+
const isSpace = (c) => c === " " || c === "\t";
|
|
439
|
+
const isRedirect = (c) => c === "<" || c === ">";
|
|
440
|
+
const isConnector = (c) => c === "|" || c === "&" || c === ";";
|
|
441
|
+
let out = "";
|
|
442
|
+
let quote;
|
|
443
|
+
let i = 0;
|
|
444
|
+
while (i < command.length) {
|
|
445
|
+
const ch = command[i];
|
|
446
|
+
if (quote !== undefined) {
|
|
447
|
+
out += ch;
|
|
448
|
+
if (ch === quote)
|
|
449
|
+
quote = undefined;
|
|
450
|
+
i++;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (ch === "'" || ch === '"') {
|
|
454
|
+
quote = ch;
|
|
455
|
+
out += ch;
|
|
456
|
+
i++;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (isRedirect(ch)) {
|
|
460
|
+
let k = out.length;
|
|
461
|
+
while (k > 0 && /\d/.test(out[k - 1]))
|
|
462
|
+
k--;
|
|
463
|
+
if (k < out.length && (k === 0 || isSpace(out[k - 1]) || isConnector(out[k - 1])))
|
|
464
|
+
out = out.slice(0, k);
|
|
465
|
+
let j = i;
|
|
466
|
+
while (j < command.length && isRedirect(command[j]))
|
|
467
|
+
j++;
|
|
468
|
+
if (j < command.length && command[j] === "|" && command[i] === ">" && j === i + 1)
|
|
469
|
+
j++;
|
|
470
|
+
while (j < command.length && isSpace(command[j]))
|
|
471
|
+
j++;
|
|
472
|
+
if (j < command.length && isConnector(command[j]))
|
|
473
|
+
return undefined;
|
|
474
|
+
let q2;
|
|
475
|
+
while (j < command.length) {
|
|
476
|
+
const c = command[j];
|
|
477
|
+
if (q2 !== undefined) {
|
|
478
|
+
if (c === q2)
|
|
479
|
+
q2 = undefined;
|
|
480
|
+
j++;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (c === "'" || c === '"') {
|
|
484
|
+
q2 = c;
|
|
485
|
+
j++;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (isSpace(c) || isRedirect(c) || isConnector(c))
|
|
489
|
+
break;
|
|
490
|
+
j++;
|
|
491
|
+
}
|
|
492
|
+
if (q2 !== undefined)
|
|
493
|
+
return undefined;
|
|
494
|
+
if (out !== "" && !isSpace(out[out.length - 1]))
|
|
495
|
+
out += " ";
|
|
496
|
+
i = j;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
out += ch;
|
|
500
|
+
i++;
|
|
501
|
+
}
|
|
502
|
+
if (quote !== undefined)
|
|
503
|
+
return undefined;
|
|
504
|
+
return out.trim();
|
|
505
|
+
}
|
|
506
|
+
function stripFdDuplications(command) {
|
|
507
|
+
let out = "";
|
|
508
|
+
let quote;
|
|
509
|
+
for (let i = 0; i < command.length; i++) {
|
|
510
|
+
const ch = command[i];
|
|
511
|
+
if (quote !== undefined) {
|
|
512
|
+
out += ch;
|
|
513
|
+
if (ch === quote)
|
|
514
|
+
quote = undefined;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (ch === "'" || ch === '"') {
|
|
518
|
+
quote = ch;
|
|
519
|
+
out += ch;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const atWordStart = i === 0 || command[i - 1] === " " || command[i - 1] === "\t" || command[i - 1] === ";" || command[i - 1] === "|" || command[i - 1] === "&";
|
|
523
|
+
const m = (atWordStart ? /^\d*[<>]&\d+/ : /^[<>]&\d+/).exec(command.slice(i));
|
|
524
|
+
if (m !== null) {
|
|
525
|
+
i += m[0].length - 1;
|
|
526
|
+
out += " ";
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
out += ch;
|
|
530
|
+
}
|
|
531
|
+
return quote === undefined ? out : undefined;
|
|
532
|
+
}
|
|
293
533
|
function admitsUnder(rule, command, reading) {
|
|
294
|
-
if (rule.match === "subpath")
|
|
534
|
+
if (rule.match === "subpath" || rule.match === "path")
|
|
295
535
|
return false;
|
|
296
536
|
const shape = ruleLaneShapeOf(command, reading);
|
|
297
537
|
if ("reject" in shape)
|
|
@@ -350,9 +590,8 @@ export function lexicalNormalAbsolutePathOf(path) {
|
|
|
350
590
|
if (seg === "" || seg === ".")
|
|
351
591
|
continue;
|
|
352
592
|
if (seg === "..") {
|
|
353
|
-
if (out.length
|
|
354
|
-
|
|
355
|
-
out.pop();
|
|
593
|
+
if (out.length > 0)
|
|
594
|
+
out.pop();
|
|
356
595
|
continue;
|
|
357
596
|
}
|
|
358
597
|
out.push(seg);
|
|
@@ -366,7 +605,7 @@ function isLexicalNormalAbsoluteDir(path) {
|
|
|
366
605
|
return segments.length > 0 && segments.every((s) => s !== "" && s !== "." && s !== "..");
|
|
367
606
|
}
|
|
368
607
|
export function directoryRuleAdmits(rule, path) {
|
|
369
|
-
if (rule.
|
|
608
|
+
if (rule.match !== "subpath")
|
|
370
609
|
return false;
|
|
371
610
|
if (!isLexicalNormalAbsoluteDir(rule.command))
|
|
372
611
|
return false;
|
|
@@ -483,12 +722,12 @@ export function isRuleLive(rule) {
|
|
|
483
722
|
export function eligibleContext(rule, call) {
|
|
484
723
|
return rule.tool === call.tool && scopeCoversCwd(rule.scope, call.cwd, call.sessionId);
|
|
485
724
|
}
|
|
486
|
-
export function eligiblePersisted(rule, call) {
|
|
487
|
-
return isRuleLive(rule) && eligibleContext(rule, call);
|
|
725
|
+
export function eligiblePersisted(rule, call, behavior) {
|
|
726
|
+
return rule.behavior === behavior && isRuleLive(rule) && eligibleContext(rule, call);
|
|
488
727
|
}
|
|
489
728
|
function firstEligibleAdmittingRule(rules, commandText, call) {
|
|
490
729
|
for (const rule of rules) {
|
|
491
|
-
if (!eligiblePersisted(rule, call))
|
|
730
|
+
if (!eligiblePersisted(rule, call, "allow"))
|
|
492
731
|
continue;
|
|
493
732
|
if (ruleAdmitsCommand(rule, commandText))
|
|
494
733
|
return rule;
|
|
@@ -521,13 +760,54 @@ export function findAdmittingRule(rules, call) {
|
|
|
521
760
|
}
|
|
522
761
|
function firstEligibleDirectoryRule(rules, directory, call) {
|
|
523
762
|
for (const rule of rules) {
|
|
524
|
-
if (!eligiblePersisted(rule, { tool: "Read", cwd: call.cwd, sessionId: call.sessionId }))
|
|
763
|
+
if (!eligiblePersisted(rule, { tool: "Read", cwd: call.cwd, sessionId: call.sessionId }, "allow"))
|
|
525
764
|
continue;
|
|
526
765
|
if (directoryRuleAdmits(rule, directory))
|
|
527
766
|
return rule;
|
|
528
767
|
}
|
|
529
768
|
return undefined;
|
|
530
769
|
}
|
|
770
|
+
export function adjudicatePersistedRules(rules, call) {
|
|
771
|
+
for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
|
|
772
|
+
if (behavior === "allow") {
|
|
773
|
+
const covering = findAdmittingRule(rules, call);
|
|
774
|
+
if (covering !== undefined)
|
|
775
|
+
return { behavior, rules: covering };
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
for (const rule of rules) {
|
|
779
|
+
if (!eligiblePersisted(rule, call, behavior))
|
|
780
|
+
continue;
|
|
781
|
+
if (ruleReachesProgramRun(rule, call.command))
|
|
782
|
+
return { behavior, rules: [rule] };
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return undefined;
|
|
786
|
+
}
|
|
787
|
+
export function adjudicatePersistedPathRules(rules, call, targets, bases) {
|
|
788
|
+
for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
|
|
789
|
+
if (behavior === "allow") {
|
|
790
|
+
if (targets.allow === undefined || call.tool !== READ_RULE_TOOL)
|
|
791
|
+
continue;
|
|
792
|
+
for (const rule of rules) {
|
|
793
|
+
if (!eligiblePersisted(rule, call, behavior))
|
|
794
|
+
continue;
|
|
795
|
+
if (directoryRuleAdmits(rule, targets.allow))
|
|
796
|
+
return { behavior, rules: [rule] };
|
|
797
|
+
}
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (targets.tighten === undefined)
|
|
801
|
+
continue;
|
|
802
|
+
for (const rule of rules) {
|
|
803
|
+
if (!eligiblePersisted(rule, call, behavior))
|
|
804
|
+
continue;
|
|
805
|
+
if (pathRuleReaches(rule, targets.tighten, bases))
|
|
806
|
+
return { behavior, rules: [rule] };
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
531
811
|
export function segmentCoverageOf(command, rules, call) {
|
|
532
812
|
const folded = foldSpacing(command);
|
|
533
813
|
if (folded === undefined)
|
|
@@ -569,7 +849,7 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
569
849
|
if (folded === undefined)
|
|
570
850
|
return [];
|
|
571
851
|
const offers = [];
|
|
572
|
-
const exact =
|
|
852
|
+
const exact = parseRuleText(formatRuleText(folded, "exact", COMMAND_RULE_TOOL), "allow");
|
|
573
853
|
if ("rule" in exact && exact.rule.match === "exact" && ruleAdmitsCommand(exact.rule, command)) {
|
|
574
854
|
offers.push({ kind: "single", rule: exact.rule.rule, match: "exact", command: exact.rule.command });
|
|
575
855
|
}
|
|
@@ -589,7 +869,7 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
589
869
|
}
|
|
590
870
|
if (denied)
|
|
591
871
|
return undefined;
|
|
592
|
-
const parsed =
|
|
872
|
+
const parsed = parseRuleText(formatRuleText(directory, "subpath", READ_RULE_TOOL), "allow");
|
|
593
873
|
if (!("rule" in parsed) || parsed.rule.match !== "subpath")
|
|
594
874
|
return undefined;
|
|
595
875
|
if (!directoryRuleAdmits(parsed.rule, directory))
|
|
@@ -600,7 +880,7 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
600
880
|
if (offers.length === 1) {
|
|
601
881
|
const body = bodyOf(tokensOf(folded));
|
|
602
882
|
if (body !== undefined) {
|
|
603
|
-
const parsed =
|
|
883
|
+
const parsed = parseRuleText(formatRuleText(body, "wildcard", COMMAND_RULE_TOOL), "allow");
|
|
604
884
|
if ("rule" in parsed && parsed.rule.match === "wildcard" && ruleAdmitsCommand(parsed.rule, command)) {
|
|
605
885
|
offers.push({ kind: "single", rule: parsed.rule.rule, match: "wildcard", command: parsed.rule.command });
|
|
606
886
|
}
|
|
@@ -627,12 +907,12 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
627
907
|
return undefined;
|
|
628
908
|
const body = bodyOf(tokensOf(segment));
|
|
629
909
|
if (body !== undefined) {
|
|
630
|
-
const parsed =
|
|
910
|
+
const parsed = parseRuleText(formatRuleText(body, "wildcard", COMMAND_RULE_TOOL), "allow");
|
|
631
911
|
if ("rule" in parsed && parsed.rule.match === "wildcard" && ruleAdmitsCommand(parsed.rule, segment)) {
|
|
632
912
|
return { rule: parsed.rule.rule, match: "wildcard", command: parsed.rule.command };
|
|
633
913
|
}
|
|
634
914
|
}
|
|
635
|
-
const segExact =
|
|
915
|
+
const segExact = parseRuleText(formatRuleText(segment, "exact", COMMAND_RULE_TOOL), "allow");
|
|
636
916
|
if ("rule" in segExact && segExact.rule.match === "exact" && ruleAdmitsCommand(segExact.rule, segment)) {
|
|
637
917
|
return { rule: segExact.rule.rule, match: "exact", command: segExact.rule.command };
|
|
638
918
|
}
|
|
@@ -655,7 +935,7 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
655
935
|
const batchRules = minted.slice(0, 5);
|
|
656
936
|
if (batchRules.length > 0) {
|
|
657
937
|
const parsedBatch = batchRules.flatMap((r) => {
|
|
658
|
-
const p =
|
|
938
|
+
const p = parseRuleText(r.rule, "allow");
|
|
659
939
|
return "rule" in p ? [p.rule] : [];
|
|
660
940
|
});
|
|
661
941
|
const uncoveredDetail = [];
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* convergent merge). So the org layer has NO CRDT, no tombstones and no join: the server is the single
|
|
7
7
|
* authority, a client holds a read-only versioned snapshot, an update is a whole replacement (revision
|
|
8
8
|
* strictly ordered), and a rollback is a NEW revision carrying old content. Cross-layer tombstones are
|
|
9
|
-
* structurally impossible — an org rule never enters `
|
|
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,9 @@ export declare function unenforceableOrgRules(rules: readonly OrgPermissionRule[
|
|
|
187
187
|
/**
|
|
188
188
|
* design/182 §7.2 — which org rule speaks for this command, if any. Deny outranks ask; within a
|
|
189
189
|
* behavior the first textual match wins (reporting order only — all denies are the same one answer).
|
|
190
|
-
* Matching uses the SAME parser and
|
|
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 ruleReachesProgramRun}: an interpreter-headed prefix deny like `Bash(node:*)` is a
|
|
192
|
+
* legitimately wide tightening and matches; a segment hit is a hit).
|
|
192
193
|
*
|
|
193
194
|
* The skip below is a defensive floor, and what stands behind it is worth stating precisely: rules that
|
|
194
195
|
* arrive from the org partition's `resolve` ({@link createOrgRulePartition}) have passed {@link validateOrgSnapshot} on BOTH the install
|
|
@@ -212,8 +213,8 @@ export declare function orgRuleVerdictFor(rules: readonly OrgPermissionRule[], c
|
|
|
212
213
|
* OVERRIDE it, not to self-declare its removal — so a shadowed rule reports `shadowed-by-org` and comes
|
|
213
214
|
* back by itself when the org deny is withdrawn.
|
|
214
215
|
*
|
|
215
|
-
* The SAME reach the gate uses (
|
|
216
|
-
* whether a deny touches this rule at all. A personal rule may name a connector chain, and a deny on one
|
|
216
|
+
* The SAME reach the gate uses ({@link ruleReachesProgramRun}), so the view and the decision cannot
|
|
217
|
+
* disagree about whether a deny touches this rule at all. A personal rule may name a connector chain, and a deny on one
|
|
217
218
|
* of its segments shadows it exactly as a deny on the whole does.
|
|
218
219
|
*
|
|
219
220
|
* WHAT `shadowed-by-org` DOES AND DOES NOT CLAIM: the predicate asks whether the deny reaches the
|
|
@@ -232,9 +233,9 @@ export declare function orgRuleVerdictFor(rules: readonly OrgPermissionRule[], c
|
|
|
232
233
|
* reach it, and its `command` is a PATH that must not be fed to the shell lane's segment reader as
|
|
233
234
|
* though it were a command line.
|
|
234
235
|
*/
|
|
235
|
-
export declare function orgRuleShadows(orgRules: readonly OrgPermissionRule[], rule: Pick<
|
|
236
|
+
export declare function orgRuleShadows(orgRules: readonly OrgPermissionRule[], rule: Pick<PersistedRule, "tool" | "command">): boolean;
|
|
236
237
|
/** The shadow predicate with the org denies parsed ONCE — what a read over many rows uses. */
|
|
237
|
-
export declare function compileOrgShadowPredicate(orgRules: readonly OrgPermissionRule[]): (rule: Pick<
|
|
238
|
+
export declare function compileOrgShadowPredicate(orgRules: readonly OrgPermissionRule[]): (rule: Pick<PersistedRule, "tool" | "command">) => boolean;
|
|
238
239
|
/** Does this store carry a write face? Re-exported convenience for org-ticket integrations that need to
|
|
239
240
|
* distinguish a read-only projection from a writable personal bucket without importing the writer seam. */
|
|
240
241
|
export declare function isWritablePermissionRuleStore(store: PermissionRuleStore): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { escapeForDisclosure,
|
|
1
|
+
import { escapeForDisclosure, parseRuleText, ruleReachesProgramRun } from "./permission-rule-model.js";
|
|
2
2
|
import { writerOf } from "./permission-rule-store.js";
|
|
3
3
|
export const ORG_UNAVAILABLE_DECISION_REASON = "org_unavailable";
|
|
4
4
|
export const ORG_ADJUDICATION_TIMEOUT_MS = 15_000;
|
|
@@ -199,7 +199,7 @@ export function unenforceableOrgRules(rules) {
|
|
|
199
199
|
for (const r of rules) {
|
|
200
200
|
if (typeof r?.rule !== "string" || r.rule === "")
|
|
201
201
|
continue;
|
|
202
|
-
const parsed =
|
|
202
|
+
const parsed = parseRuleText(r.rule, r.behavior);
|
|
203
203
|
if ("reject" in parsed) {
|
|
204
204
|
out.push({ rule: r.rule, code: parsed.reject.code });
|
|
205
205
|
continue;
|
|
@@ -243,15 +243,14 @@ function validateOrgSnapshot(s, nowMs) {
|
|
|
243
243
|
return undefined;
|
|
244
244
|
}
|
|
245
245
|
export function orgRuleVerdictFor(rules, call) {
|
|
246
|
-
const segments = ruleLaneSegmentsOf(call.command);
|
|
247
246
|
let ask;
|
|
248
247
|
for (const r of rules) {
|
|
249
|
-
const parsed =
|
|
248
|
+
const parsed = parseRuleText(r.rule, r.behavior);
|
|
250
249
|
if ("reject" in parsed)
|
|
251
250
|
continue;
|
|
252
251
|
if (parsed.rule.tool !== call.tool)
|
|
253
252
|
continue;
|
|
254
|
-
if (!
|
|
253
|
+
if (!ruleReachesProgramRun(parsed.rule, call.command))
|
|
255
254
|
continue;
|
|
256
255
|
if (r.behavior === "deny")
|
|
257
256
|
return { behavior: "deny", rule: r.rule };
|
|
@@ -259,13 +258,6 @@ export function orgRuleVerdictFor(rules, call) {
|
|
|
259
258
|
}
|
|
260
259
|
return ask;
|
|
261
260
|
}
|
|
262
|
-
function orgRuleReaches(rule, command, segments) {
|
|
263
|
-
if (ruleAdmitsProgramRun(rule, command))
|
|
264
|
-
return true;
|
|
265
|
-
if (segments === undefined)
|
|
266
|
-
return false;
|
|
267
|
-
return segments.some((segment) => ruleAdmitsProgramRun(rule, segment));
|
|
268
|
-
}
|
|
269
261
|
export function orgRuleShadows(orgRules, rule) {
|
|
270
262
|
return compileOrgShadowPredicate(orgRules)(rule);
|
|
271
263
|
}
|
|
@@ -274,15 +266,14 @@ export function compileOrgShadowPredicate(orgRules) {
|
|
|
274
266
|
for (const d of orgRules) {
|
|
275
267
|
if (d.behavior !== "deny")
|
|
276
268
|
continue;
|
|
277
|
-
const parsed =
|
|
269
|
+
const parsed = parseRuleText(d.rule, "deny");
|
|
278
270
|
if (!("reject" in parsed))
|
|
279
271
|
denies.push(parsed.rule);
|
|
280
272
|
}
|
|
281
273
|
return (rule) => {
|
|
282
274
|
if (rule.tool !== "Bash" || denies.length === 0)
|
|
283
275
|
return false;
|
|
284
|
-
|
|
285
|
-
return denies.some((d) => d.tool === rule.tool && orgRuleReaches(d, rule.command, segments));
|
|
276
|
+
return denies.some((d) => d.tool === rule.tool && ruleReachesProgramRun(d, rule.command));
|
|
286
277
|
};
|
|
287
278
|
}
|
|
288
279
|
export function isWritablePermissionRuleStore(store) {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* durable backend keeps refusing a session row loudly should one ever reach it — the route is the
|
|
35
35
|
* store's, the fence stays the partition's.
|
|
36
36
|
*/
|
|
37
|
-
import type {
|
|
37
|
+
import type { PersistedRule, RuleScope } from "./permission-rule-model.js";
|
|
38
38
|
import { type DurableRulePartitionProvider, type QuarantinedRuleAdd, type RuleOwner } from "./permission-rule-store.js";
|
|
39
39
|
import { type OrgRulePartitionConfig, type OrgRuleResolution } from "./permission-rule-org.js";
|
|
40
40
|
import type { SessionRulePartition } from "./permission-rule-session.js";
|
|
@@ -44,8 +44,10 @@ export type RuleSource = "org" | "user" | "project" | "session";
|
|
|
44
44
|
/** The source of a row is a projection of its scope — `global` is the `user` source. */
|
|
45
45
|
export declare function ruleSourceOf(scope: RuleScope): Exclude<RuleSource, "org">;
|
|
46
46
|
/** One live personal rule in the effective view: the row itself, its source, and its standing under the
|
|
47
|
-
* org partition (`shadowed-by-org` = an org deny reaches
|
|
48
|
-
|
|
47
|
+
* org partition (`shadowed-by-org` = an org deny reaches an ALLOW row's command pattern; see
|
|
48
|
+
* `orgRuleShadows` — a personal deny/ask row is never shadowed: it only ever tightens, and the org
|
|
49
|
+
* partition's power over the personal store is to override an allow, not a refusal). */
|
|
50
|
+
export interface EffectivePermissionRule extends PersistedRule {
|
|
49
51
|
source: Exclude<RuleSource, "org">;
|
|
50
52
|
status: "live" | "shadowed-by-org";
|
|
51
53
|
}
|
|
@@ -53,6 +55,7 @@ export interface EffectivePermissionRule extends PersistedAllowRule {
|
|
|
53
55
|
* tombstone identity is still visible in the partition. Session rows never appear here (they die
|
|
54
56
|
* with their session and leave no tombstone). */
|
|
55
57
|
export interface RemovedPermissionRule {
|
|
58
|
+
behavior: import("./permission-rule-model.js").RuleBehavior;
|
|
56
59
|
rule: string;
|
|
57
60
|
scope: RuleScope;
|
|
58
61
|
source: "user" | "project";
|
|
@@ -84,9 +87,10 @@ export interface EffectivePermissionRules {
|
|
|
84
87
|
* The host-visible store, already anchored to one owner. One read verb, which never throws: each
|
|
85
88
|
* partition reports its own failure in the view — the DURABLE partition's read failure as `unreadable`
|
|
86
89
|
* (zero rows; the caller maps it to "unreadable": a loud disclosure, fail toward asking, exactly as the
|
|
87
|
-
* old `list()` throw did), the SESSION partition's
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
+
* old `list()` throw did), the SESSION partition's read failure as `unreadable` too (it may hold a
|
|
91
|
+
* deny/ask row of this session, and zero rows would erase it — served-but-non-canonical rows stay a
|
|
92
|
+
* `disclosures` line), the ORG partition's as its `status`. The org verdict stands while a personal
|
|
93
|
+
* partition is down, and the personal rows stand while an org source is down.
|
|
90
94
|
*/
|
|
91
95
|
export interface PermissionRuleStore {
|
|
92
96
|
effective(query?: {
|