@sema-agent/core 7.45.0 → 7.47.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 +298 -1
- package/dist/agents/extra-tool-names.d.ts +65 -0
- package/dist/agents/extra-tool-names.js +124 -0
- package/dist/agents/subagent.d.ts +17 -5
- package/dist/agents/subagent.js +33 -25
- package/dist/agents/tool-filter.d.ts +17 -1
- package/dist/agents/tool-filter.js +7 -0
- package/dist/core/agent-config-dir.d.ts +20 -0
- package/dist/core/agent-config-dir.js +49 -0
- package/dist/core/allow-entry.d.ts +6 -4
- package/dist/core/allow-entry.js +6 -5
- package/dist/core/ask-origin.d.ts +35 -12
- package/dist/core/ask-origin.js +24 -7
- package/dist/core/checkpoint-store.d.ts +39 -2
- package/dist/core/effective-path-target.d.ts +136 -4
- package/dist/core/effective-path-target.js +110 -2
- package/dist/core/engine-notice.d.ts +12 -4
- package/dist/core/gate-fold.js +5 -2
- package/dist/core/gate-lanes.js +11 -4
- package/dist/core/gate-outcome.d.ts +4 -2
- package/dist/core/gate-outcome.js +2 -2
- package/dist/core/governance-codes.d.ts +3 -1
- package/dist/core/governance-codes.js +3 -0
- package/dist/core/hooks.d.ts +3 -0
- package/dist/core/path-rule-binding.d.ts +65 -0
- package/dist/core/path-rule-binding.js +99 -0
- package/dist/core/permission-rule-model.d.ts +59 -24
- package/dist/core/permission-rule-model.js +48 -44
- package/dist/core/permission-rule-provider.d.ts +7 -0
- package/dist/core/permission-rule-provider.js +10 -3
- package/dist/core/permission-rule-store.d.ts +8 -0
- package/dist/core/permission-rule-store.js +6 -2
- package/dist/core/permission-rule-sync.js +2 -2
- package/dist/core/permission-rules.d.ts +26 -45
- package/dist/core/permission-rules.js +77 -57
- package/dist/core/runner/child-tree.d.ts +97 -0
- package/dist/core/runner/child-tree.js +104 -0
- package/dist/core/runner/construction-doors.d.ts +5 -0
- package/dist/core/runner/construction-doors.js +6 -0
- package/dist/core/runner/contracts.d.ts +46 -0
- package/dist/core/runner/gate-exit.d.ts +4 -1
- package/dist/core/runner/gate-exit.js +5 -1
- package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
- package/dist/core/runner/permission-rule-lanes.js +18 -12
- package/dist/core/runner/persisted-path-rows.d.ts +31 -7
- package/dist/core/runner/persisted-path-rows.js +31 -12
- package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
- package/dist/core/runner/prepare-ask-lane.js +4 -2
- package/dist/core/runner/prepare-execution-env.d.ts +5 -2
- package/dist/core/runner/prepare-execution-env.js +10 -3
- package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
- package/dist/core/runner/prepare-gate-stations.js +2 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
- package/dist/core/runner/prepare-hands-readface.js +11 -11
- package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
- package/dist/core/runner/prepare-inherited-gate.js +8 -2
- package/dist/core/runner/prepare-park-ask.d.ts +3 -0
- package/dist/core/runner/prepare-park-ask.js +5 -3
- package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
- package/dist/core/runner/prepare-policy-chain.js +13 -5
- package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
- package/dist/core/runner/prepare-suspend-saga.js +7 -1
- package/dist/core/runner/prepare-task.js +37 -18
- package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
- package/dist/core/runner/prepare-turn-wiring.js +2 -1
- package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
- package/dist/core/runner/prepare-workspace-restore.js +1 -0
- package/dist/core/runner/resume-apply.js +17 -3
- package/dist/core/runner/resume-checkpoint-screen.js +13 -0
- package/dist/core/runner/run-leg.js +5 -1
- package/dist/core/runner/runtask.js +2 -2
- package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
- package/dist/core/runner/write-protection-station.d.ts +3 -2
- package/dist/core/runner/write-protection-station.js +1 -0
- package/dist/core/runner-deps.d.ts +14 -0
- package/dist/core/sensitive-path-policy.d.ts +8 -0
- package/dist/core/sensitive-path-policy.js +18 -2
- package/dist/core/shell-wrapper-projection.d.ts +11 -0
- package/dist/core/shell-wrapper-projection.js +56 -0
- package/dist/core/shell-wrapper-table.d.ts +144 -34
- package/dist/core/shell-wrapper-table.js +40 -20
- package/dist/core/synthetic-read-verdict.d.ts +3 -0
- package/dist/core/synthetic-read-verdict.js +3 -1
- package/dist/core/tool-catalog-entries.d.ts +21 -0
- package/dist/core/tool-catalog-entries.js +12 -2
- package/dist/core/tool-face.d.ts +10 -1
- package/dist/core/tool-name-readings.d.ts +8 -0
- package/dist/core/tool-name-readings.js +2 -0
- package/dist/core/tool-policy.d.ts +40 -17
- package/dist/core/tool-policy.js +11 -9
- package/dist/core/tool-registry.d.ts +9 -0
- package/dist/core/tool-registry.js +10 -0
- package/dist/core/tool-spec.d.ts +3 -2
- package/dist/core/write-protect.d.ts +14 -3
- package/dist/core/write-protect.js +41 -10
- package/dist/index.d.ts +6 -4
- package/dist/index.js +5 -3
- package/dist/orchestration/run-spec.js +8 -7
- package/dist/orchestration/workflow.js +1 -1
- package/dist/shell/lex.d.ts +22 -0
- package/dist/shell/lex.js +75 -13
- package/dist/tools/fs/bash-program-position.d.ts +14 -90
- package/dist/tools/fs/bash-program-position.js +41 -74
- package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
- package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
- package/dist/tools/fs/index.d.ts +3 -2
- package/dist/tools/fs/program-read-model.d.ts +711 -0
- package/dist/tools/fs/program-read-model.js +1259 -0
- package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
- package/dist/tools/fs/program-read-model.rows.js +624 -0
- package/dist/tools/fs/read-deny-home.d.ts +19 -1
- package/dist/tools/fs/read-deny-home.js +62 -24
- package/dist/tools/fs/read-deny.d.ts +54 -14
- package/dist/tools/fs/read-deny.js +104 -50
- package/dist/tools/fs/removal-targets.d.ts +7 -6
- package/dist/tools/fs/removal-targets.js +34 -24
- package/dist/tools/fs/safety.d.ts +3 -0
- package/dist/tools/fs/safety.js +10 -3
- package/dist/tools/fs/shell-read-argv.d.ts +86 -0
- package/dist/tools/fs/shell-read-argv.js +388 -0
- package/dist/tools/fs/shell-read-bindings.d.ts +300 -0
- package/dist/tools/fs/shell-read-bindings.js +755 -0
- package/dist/tools/fs/shell-read-check.js +23 -15
- package/dist/tools/fs/shell-read-establish.js +82 -8
- package/dist/tools/fs/shell-read-judge.d.ts +11 -1
- package/dist/tools/fs/shell-read-judge.js +9 -1
- package/dist/tools/fs/shell-read-plan.d.ts +33 -8
- package/dist/tools/fs/shell-read-plan.js +309 -407
- package/dist/tools/fs/shell-read-word.d.ts +60 -26
- package/dist/tools/fs/shell-read-word.js +47 -37
- package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
- package/dist/tools/fs/shell-read-write-targets.js +85 -137
- package/dist/tools/fs/shell-reentry-text.d.ts +136 -6
- package/dist/tools/fs/shell-reentry-text.js +203 -53
- package/dist/tools/monitor.d.ts +3 -3
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +25 -5
- package/wire-changes/core@7.46.0.json +1522 -0
- package/wire-changes/core@7.47.0.json +412 -0
|
@@ -3,9 +3,10 @@ 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, protectivePathTargetOf } from "./tool-registry.js";
|
|
6
|
+
import { isShellFaceToolName, pathTargetOf, protectivePathTargetOf } from "./tool-registry.js";
|
|
7
7
|
import { heldValues } from "./tool-name-readings.js";
|
|
8
8
|
import { carriesDotSegment, isAbsoluteForFamily, isIdentityDir, isPathIdentity, isShellRootedSpellingUnmapped, isUncIdentity, pathFamilyOf, pathIdentityOf } from "../tools/fs/safety.js";
|
|
9
|
+
import { bindPathRules, pathRowsReach, rowsWhereTheyLead } from "./path-rule-binding.js";
|
|
9
10
|
import { readShellCommand } from "./shell-lexer.js";
|
|
10
11
|
import { readOnlyLineObjection, readOnlyShellVerdict } from "./read-only-shell.js";
|
|
11
12
|
export const RULE_BEHAVIORS = Object.freeze(["deny", "ask", "allow"]);
|
|
@@ -25,6 +26,8 @@ export function readRowBehavior(v) {
|
|
|
25
26
|
return isRuleBehavior(v) ? v : undefined;
|
|
26
27
|
}
|
|
27
28
|
export const COMMAND_RULE_TOOL = "Bash";
|
|
29
|
+
if (!isShellFaceToolName(COMMAND_RULE_TOOL))
|
|
30
|
+
throw new Error(`permission rules: the command rule word ${JSON.stringify(COMMAND_RULE_TOOL)} names no definition on the catalog's shell face`);
|
|
28
31
|
export const READ_RULE_TOOL = "Read";
|
|
29
32
|
export const PATH_RULE_FACES = Object.freeze({
|
|
30
33
|
[READ_RULE_TOOL]: Object.freeze({ deny: Object.freeze(["read", "write"]), ask: Object.freeze(["read"]) }),
|
|
@@ -55,7 +58,13 @@ export function nameRuleSubjectOf(toolName, args) {
|
|
|
55
58
|
return typeof value === "string" ? value : undefined;
|
|
56
59
|
}
|
|
57
60
|
export function laneToolNamesOf(req) {
|
|
58
|
-
|
|
61
|
+
const words = heldValues("policy.persistedGrammar", { get: ruleWordOf }, req);
|
|
62
|
+
return words.length <= 1 ? words : [...new Set(words)];
|
|
63
|
+
}
|
|
64
|
+
function ruleWordOf(name) {
|
|
65
|
+
if (isShellFaceToolName(name))
|
|
66
|
+
return COMMAND_RULE_TOOL;
|
|
67
|
+
return ruleToolGrammarOf(name) !== undefined ? name : undefined;
|
|
59
68
|
}
|
|
60
69
|
function isCommandMatch(match) {
|
|
61
70
|
return match === "exact" || match === "prefix" || match === "wildcard";
|
|
@@ -68,7 +77,7 @@ export const BARE_INTERPRETER_NAMES = new Set([
|
|
|
68
77
|
"bash", "sh", "zsh", "ksh", "csh", "tcsh", "dash", "fish", "cmd", "powershell", "pwsh", "busybox",
|
|
69
78
|
"source", ".", "trap", "eval", "exec", "command", "builtin",
|
|
70
79
|
"enable", "compgen", "complete", "bind", "mapfile", "readarray",
|
|
71
|
-
"env", "noglob", "xargs",
|
|
80
|
+
"env", "noglob", "nocorrect", "xargs",
|
|
72
81
|
"sudo", "doas", "pkexec", "su", "runuser", "ssh", "chroot", "setpriv",
|
|
73
82
|
"nohup", "nice", "ionice", "chrt", "taskset", "stdbuf", "timeout", "time", "watch",
|
|
74
83
|
"setsid", "flock", "unshare", "nsenter", "script", "numactl", "prlimit", "systemd-run", "parallel",
|
|
@@ -440,8 +449,8 @@ function baseValueOf(base, bases) {
|
|
|
440
449
|
}
|
|
441
450
|
}
|
|
442
451
|
}
|
|
443
|
-
function resolvePathPattern(pattern, bases, behavior) {
|
|
444
|
-
const family =
|
|
452
|
+
function resolvePathPattern(pattern, bases, behavior, treeFamily = pathFamilyOf(bases)) {
|
|
453
|
+
const family = treeFamily;
|
|
445
454
|
const tree = bases.root ?? bases.cwd;
|
|
446
455
|
if (pattern.startsWith("//")) {
|
|
447
456
|
const abs = pathIdentityOf("/" + pattern.slice(2), family, tree);
|
|
@@ -503,15 +512,15 @@ function globSegmentsReach(globs, target) {
|
|
|
503
512
|
};
|
|
504
513
|
return walk(0, 0);
|
|
505
514
|
}
|
|
506
|
-
export function pathRuleReachOf(rule, target, bases) {
|
|
515
|
+
export function pathRuleReachOf(rule, target, bases, treeFamily = pathFamilyOf(bases)) {
|
|
507
516
|
if (rule.match === "subpath")
|
|
508
|
-
return directoryRuleAdmits(rule, target, bases) ? REACHED : NOT_REACHED;
|
|
517
|
+
return directoryRuleAdmits(rule, target, bases, treeFamily) ? REACHED : NOT_REACHED;
|
|
509
518
|
if (rule.match !== "path")
|
|
510
519
|
return NOT_REACHED;
|
|
511
|
-
const family =
|
|
520
|
+
const family = treeFamily;
|
|
512
521
|
if (!isPathIdentity(target, family))
|
|
513
522
|
return NOT_REACHED;
|
|
514
|
-
const r = resolvePathPattern(rule.command, bases, rule.behavior);
|
|
523
|
+
const r = resolvePathPattern(rule.command, bases, rule.behavior, family);
|
|
515
524
|
if ("missingBase" in r) {
|
|
516
525
|
return { reach: "unreadable", reason: `the rule is relative to ${PATH_RULE_BASE_LABEL[r.missingBase]} and this call supplies no absolute \`${r.missingBase}\` base of this tree's family to resolve it against` };
|
|
517
526
|
}
|
|
@@ -706,10 +715,10 @@ function isLexicalNormalAbsoluteDir(path) {
|
|
|
706
715
|
const segments = path.split("/").slice(1);
|
|
707
716
|
return segments.length > 0 && segments.every((s) => s !== "" && s !== "." && s !== "..");
|
|
708
717
|
}
|
|
709
|
-
export function directoryRuleAdmits(rule, path, bases) {
|
|
718
|
+
export function directoryRuleAdmits(rule, path, bases, treeFamily = pathFamilyOf(bases)) {
|
|
710
719
|
if (rule.match !== "subpath")
|
|
711
720
|
return false;
|
|
712
|
-
const family =
|
|
721
|
+
const family = treeFamily;
|
|
713
722
|
if (carriesDotSegment(rule.command, family))
|
|
714
723
|
return false;
|
|
715
724
|
const dir = pathIdentityOf(rule.command, family, bases.root ?? bases.cwd);
|
|
@@ -820,21 +829,29 @@ function resolveCdSegmentDirectories(segments, cwd) {
|
|
|
820
829
|
return dir === undefined || isShellRootedSpellingUnmapped(pathFamilyOf(bases), dir) ? undefined : directoryIdentityOf(dir, bases);
|
|
821
830
|
});
|
|
822
831
|
}
|
|
823
|
-
export function scopeCoversCwd(scope, cwd, sessionId) {
|
|
832
|
+
export function scopeCoversCwd(scope, cwd, sessionId, identity) {
|
|
824
833
|
if (scope.kind === "global")
|
|
825
834
|
return true;
|
|
826
835
|
if (scope.kind === "session")
|
|
827
836
|
return sessionId !== undefined && sessionId === scope.sessionId;
|
|
828
|
-
|
|
837
|
+
if (cwd === undefined)
|
|
838
|
+
return false;
|
|
839
|
+
if (pathWithinRoot(cwd, scope.root))
|
|
840
|
+
return true;
|
|
841
|
+
return identity !== undefined && pathWithinRoot(identity.canonicalRoot, identity.canonicalOf(scope.root) ?? scope.root);
|
|
829
842
|
}
|
|
830
843
|
export function isRuleLive(rule) {
|
|
831
844
|
return rule.adds.length > 0;
|
|
832
845
|
}
|
|
833
846
|
export function eligibleContext(rule, call) {
|
|
834
|
-
return rule.tool === call.tool && scopeCoversCwd(rule.scope, call.cwd, call.sessionId);
|
|
847
|
+
return rule.tool === call.tool && scopeCoversCwd(rule.scope, call.cwd, call.sessionId, call.scopeIdentity);
|
|
835
848
|
}
|
|
836
849
|
export function eligiblePersisted(rule, call, behavior) {
|
|
837
|
-
return rule.behavior === behavior && isRuleLive(rule) && eligibleContext(rule, call);
|
|
850
|
+
return rule.behavior === behavior && isRuleLive(rule) && eligibleContext(rule, behavior === "allow" ? spelledScope(call) : call);
|
|
851
|
+
}
|
|
852
|
+
export function spelledScope(call) {
|
|
853
|
+
const { scopeIdentity: _identity, ...spelled } = call;
|
|
854
|
+
return spelled;
|
|
838
855
|
}
|
|
839
856
|
function firstEligibleAdmittingRule(rules, commandText, call) {
|
|
840
857
|
for (const rule of rules) {
|
|
@@ -911,41 +928,28 @@ export function adjudicatePersistedRules(rules, call) {
|
|
|
911
928
|
}
|
|
912
929
|
return undefined;
|
|
913
930
|
}
|
|
914
|
-
export function adjudicatePersistedPathRules(rules, call,
|
|
931
|
+
export function adjudicatePersistedPathRules(rules, call, views, anchorsOf, leads) {
|
|
915
932
|
const speaks = (rule, behavior) => eligiblePersisted(rule, call, behavior) ||
|
|
916
933
|
(behavior === "ask" && call.face !== undefined && PATH_RULE_FACES[rule.tool]?.ask?.includes(call.face) === true && eligiblePersisted(rule, { ...call, tool: rule.tool }, behavior));
|
|
934
|
+
const bind = (rows) => bindPathRules(rows.map((rule) => ({ parsed: rule, ruleText: rule.rule, rule })), (s) => anchorsOf(s.rule));
|
|
917
935
|
for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
|
|
918
936
|
if (behavior === "allow") {
|
|
919
|
-
if (
|
|
937
|
+
if (views.allow === undefined || call.tool !== READ_RULE_TOOL)
|
|
920
938
|
continue;
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
if (directoryRuleAdmits(rule, targets.allow.identity, bases))
|
|
925
|
-
return { behavior, rules: [rule] };
|
|
926
|
-
}
|
|
939
|
+
const hit = pathRowsReach(bind(rules.filter((rule) => rule.match === "subpath" && eligiblePersisted(rule, call, behavior))), views.allow, "allow");
|
|
940
|
+
if (hit !== undefined)
|
|
941
|
+
return { behavior, rules: [hit.row.spec.rule] };
|
|
927
942
|
continue;
|
|
928
943
|
}
|
|
929
|
-
|
|
930
|
-
if (tighten === undefined || tighten.kind === "refused")
|
|
944
|
+
if (views.tighten === undefined)
|
|
931
945
|
continue;
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
continue;
|
|
940
|
-
}
|
|
941
|
-
const outcome = pathRuleReachOf(rule, tighten.identity, bases);
|
|
942
|
-
if (outcome.reach === "reached")
|
|
943
|
-
return { behavior, rules: [rule] };
|
|
944
|
-
if (outcome.reach === "unreadable")
|
|
945
|
-
unreadable ??= { behavior: "ask", rules: [rule], unreadable: outcome.reason };
|
|
946
|
-
}
|
|
947
|
-
if (unreadable !== undefined)
|
|
948
|
-
return unreadable;
|
|
946
|
+
const rows = rules.filter((rule) => (rule.match === "path" || rule.match === "subpath") && speaks(rule, behavior));
|
|
947
|
+
if (rows.length === 0)
|
|
948
|
+
continue;
|
|
949
|
+
const hit = pathRowsReach(rowsWhereTheyLead(bind(rows), leads), views.tighten, "tighten");
|
|
950
|
+
if (hit === undefined)
|
|
951
|
+
continue;
|
|
952
|
+
return hit.unreadable !== undefined ? { behavior: "ask", rules: [hit.row.spec.rule], unreadable: hit.unreadable } : { behavior, rules: [hit.row.spec.rule] };
|
|
949
953
|
}
|
|
950
954
|
return undefined;
|
|
951
955
|
}
|
|
@@ -987,9 +991,9 @@ export function segmentCoverageOf(command, rules, call) {
|
|
|
987
991
|
(dir !== undefined && firstEligibleDirectoryRule(rules.persisted, dir, call) !== undefined) ||
|
|
988
992
|
proposed.some((p) => p.rule.tool === "Read"
|
|
989
993
|
? dir !== undefined &&
|
|
990
|
-
eligibleContext({ tool: p.rule.tool, scope: p.scope }, { tool: "Read"
|
|
994
|
+
eligibleContext({ tool: p.rule.tool, scope: p.scope }, { ...spelledScope(call), tool: "Read" }) &&
|
|
991
995
|
directoryRuleAdmits(p.rule, dir, basesOfCwd(call.execCwd ?? call.cwd))
|
|
992
|
-
: eligibleContext({ tool: p.rule.tool, scope: p.scope }, call) && ruleAdmitsCommand(p.rule, segment)),
|
|
996
|
+
: eligibleContext({ tool: p.rule.tool, scope: p.scope }, spelledScope(call)) && ruleAdmitsCommand(p.rule, segment)),
|
|
993
997
|
};
|
|
994
998
|
});
|
|
995
999
|
}
|
|
@@ -86,6 +86,13 @@ export interface EffectivePermissionRules {
|
|
|
86
86
|
* served that are not canonical session rows of this session). The org partition's lines ride
|
|
87
87
|
* `org.disclosures`. */
|
|
88
88
|
disclosures: string[];
|
|
89
|
+
/** The durable rows left out because their project scope root is not a complete absolute path (a foreign writer's or a
|
|
90
|
+
* hand edit's row): each row's text and its scope root as served (a non-string root as its type). Absent when none. The
|
|
91
|
+
* reader tells the person (`permission.rule_row_dropped`). */
|
|
92
|
+
droppedRows?: ReadonlyArray<{
|
|
93
|
+
readonly rule: string;
|
|
94
|
+
readonly scopeRoot: string;
|
|
95
|
+
}>;
|
|
89
96
|
}
|
|
90
97
|
/**
|
|
91
98
|
* The host-visible store, already anchored to one owner. One read verb, which never throws: each
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { escapeForDisclosure } from "./permission-rule-model.js";
|
|
2
|
-
import { PERMISSION_RULE_WRITER, errText, normalizePersistedRule, sameRuleIdentity, writerOf, } from "./permission-rule-store.js";
|
|
2
|
+
import { PERMISSION_RULE_WRITER, errText, isValidDurableScope, normalizePersistedRule, sameRuleIdentity, writerOf, } from "./permission-rule-store.js";
|
|
3
3
|
import { UNGOVERNED_ORG_RESOLUTION, compileOrgShadowPredicate, createOrgRulePartition } from "./permission-rule-org.js";
|
|
4
4
|
export function ruleSourceOf(scope) {
|
|
5
5
|
return scope.kind === "global" ? "user" : scope.kind;
|
|
@@ -80,7 +80,11 @@ class UnifiedPermissionRuleStore {
|
|
|
80
80
|
const sessionRead = "unreadable" in sessionRows ? [] : sessionRows;
|
|
81
81
|
const shadowed = compileOrgShadowPredicate(orgResolution.rules);
|
|
82
82
|
const rules = [];
|
|
83
|
-
|
|
83
|
+
const illegal = durableRead.rules.filter((r) => !isValidDurableScope(r.scope));
|
|
84
|
+
const droppedRows = illegal.map((r) => ({ rule: String(r.rule), scopeRoot: scopeRootAsServed(r.scope.root) }));
|
|
85
|
+
if (illegal.length > 0)
|
|
86
|
+
disclosures.push(`the persisted rule partition served ${illegal.length} row(s) whose project scope root is not a complete absolute path — dropped, not adjudicated: ${illegal.slice(0, 3).map((r) => `${escapeForDisclosure(r.rule)} (${escapeForDisclosure(r.scope.root)})`).join(", ")}`);
|
|
87
|
+
for (const r of [...sessionRead, ...durableRead.rules.filter((x) => !illegal.includes(x))]) {
|
|
84
88
|
rules.push({ ...r, source: ruleSourceOf(r.scope), status: r.behavior === "allow" && shadowed(r) ? "shadowed-by-org" : "live" });
|
|
85
89
|
}
|
|
86
90
|
const removed = [];
|
|
@@ -93,7 +97,7 @@ class UnifiedPermissionRuleStore {
|
|
|
93
97
|
continue;
|
|
94
98
|
removed.push({ behavior: t.behavior, rule: t.rule, scope: t.scope, source: ruleSourceOf(t.scope), status: "removed" });
|
|
95
99
|
}
|
|
96
|
-
return { rules, removed, org: orgResolution, rev: durableRead.rev, disclosures, ...failed };
|
|
100
|
+
return { rules, removed, org: orgResolution, rev: durableRead.rev, disclosures, ...failed, ...(droppedRows.length > 0 ? { droppedRows } : {}) };
|
|
97
101
|
}
|
|
98
102
|
async readSessionRows(sessionId, disclosures) {
|
|
99
103
|
if (this.session === undefined || sessionId === undefined)
|
|
@@ -116,6 +120,9 @@ class UnifiedPermissionRuleStore {
|
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
122
|
}
|
|
123
|
+
function scopeRootAsServed(root) {
|
|
124
|
+
return typeof root === "string" ? root : `(${root === null ? "null" : typeof root})`;
|
|
125
|
+
}
|
|
119
126
|
function routingWriter(durable, session) {
|
|
120
127
|
return {
|
|
121
128
|
nextDot: () => durable.nextDot(),
|
|
@@ -457,6 +457,14 @@ export declare function isValidConsentScope(scope: RuleScope): boolean;
|
|
|
457
457
|
* duty (design/382 §10): every such entrance, present or future, carries this face.
|
|
458
458
|
*/
|
|
459
459
|
export declare function isValidDurableScope(scope: RuleScope): boolean;
|
|
460
|
+
/**
|
|
461
|
+
* A PROJECT scope's root is a complete absolute path — `/…`, a drive-rooted `X:\…` / `X:/…`, a UNC `\host\…` — the one
|
|
462
|
+
* predicate every scope face reads (the consent face, the durable face, the sync wire's pick). It is what a project row's
|
|
463
|
+
* path patterns anchor at (its `/x` and relative words), so a root that is not one (`C:`, a relative spelling, empty)
|
|
464
|
+
* would let the row apply (containment is a prefix test) while its anchor reads nothing: such a row is refused at every
|
|
465
|
+
* write, dropped at every read, and disclosed.
|
|
466
|
+
*/
|
|
467
|
+
export declare function isProjectScopeRoot(root: unknown): root is string;
|
|
460
468
|
/** Is `dot` at or below the frontier? Absent actor ⇒ not below (a frontier never covers what it never saw). */
|
|
461
469
|
export declare function dotAtOrBelowFrontier(dot: RuleDot, frontier: RuleSyncFrontier): boolean;
|
|
462
470
|
/**
|
|
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { canonicalize } from "./canonical-json.js";
|
|
3
3
|
import { escapeForDisclosure, isRuleBehavior, isRuleLive, parseRuleText } from "./permission-rule-model.js";
|
|
4
4
|
import { thrownText } from "./thrown-value.js";
|
|
5
|
+
import { isAbsolutePathForm } from "../tools/fs/safety.js";
|
|
5
6
|
export function sameRuleOwner(a, b) {
|
|
6
7
|
return a.kind === "principal" ? b.kind === "principal" && a.principal === b.principal : b.kind === "local-owner";
|
|
7
8
|
}
|
|
@@ -143,12 +144,15 @@ export function assertWriteDeltaScopeDurable(delta) {
|
|
|
143
144
|
export function isValidConsentScope(scope) {
|
|
144
145
|
const sc = scope;
|
|
145
146
|
return (sc?.kind === "global" ||
|
|
146
|
-
(sc?.kind === "project" &&
|
|
147
|
+
(sc?.kind === "project" && isProjectScopeRoot(sc.root)) ||
|
|
147
148
|
(sc?.kind === "session" && typeof sc.sessionId === "string" && sc.sessionId !== ""));
|
|
148
149
|
}
|
|
149
150
|
export function isValidDurableScope(scope) {
|
|
150
151
|
const sc = scope;
|
|
151
|
-
return sc?.kind === "global" || (sc?.kind === "project" &&
|
|
152
|
+
return sc?.kind === "global" || (sc?.kind === "project" && isProjectScopeRoot(sc.root));
|
|
153
|
+
}
|
|
154
|
+
export function isProjectScopeRoot(root) {
|
|
155
|
+
return typeof root === "string" && isAbsolutePathForm(root);
|
|
152
156
|
}
|
|
153
157
|
function isValidDot(dot) {
|
|
154
158
|
const d = dot;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { escapeForDisclosure, isRuleBehavior, parseRuleText } from "./permission-rule-model.js";
|
|
2
|
-
import { applyTombstones, errText, joinFrontiers, joinRuleStates, normalizePersistedRule, readRuleWriteOutcome, ruleSyncVector, sameRuleIdentity, screenRuleSyncState, writerOf, } from "./permission-rule-store.js";
|
|
2
|
+
import { applyTombstones, errText, isProjectScopeRoot, joinFrontiers, joinRuleStates, normalizePersistedRule, readRuleWriteOutcome, ruleSyncVector, sameRuleIdentity, screenRuleSyncState, writerOf, } from "./permission-rule-store.js";
|
|
3
3
|
import { emitTrace } from "./trace.js";
|
|
4
4
|
export const PERMISSION_RULE_SYNC_PATH = "/v1/permission-rules/sync";
|
|
5
5
|
export const LOCAL_OWNER_UNSYNCABLE_CODE = "permission_rules.local_owner_unsyncable";
|
|
@@ -262,7 +262,7 @@ function pickScopeCaptured(v) {
|
|
|
262
262
|
return { kind, scope: { kind: "global" } };
|
|
263
263
|
if (kind === "project") {
|
|
264
264
|
const root = s?.root;
|
|
265
|
-
return { kind, scope:
|
|
265
|
+
return { kind, scope: isProjectScopeRoot(root) ? { kind: "project", root } : undefined };
|
|
266
266
|
}
|
|
267
267
|
if (kind === "session") {
|
|
268
268
|
const sessionId = s?.sessionId;
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
* (`programRunReachOf`: the whole command line or any program run in a connector chain — upstream's
|
|
23
23
|
* per-subcommand deny, the same instrument the org partition and the personal store's deny/ask rows use;
|
|
24
24
|
* a call the lexer cannot read against a rule is a fail-closed ask, never silence).
|
|
25
|
+
* The rule is written in the command word (`Bash`) and reaches every call on the catalog's SHELL FACE — `Bash`, and
|
|
26
|
+
* `Monitor`, which runs its `command` through the same execution seam — under any name the call answers to; a command
|
|
27
|
+
* spelling on another shell-face name (`Monitor(touch:*)`) is refused (`invalid.command_rule`, naming the `Bash(…)` form):
|
|
28
|
+
* upstream reads no such rule. A bare `Bash` rule is a tool-name rule and reaches the tool of that name only.
|
|
25
29
|
* It sits in the field-level tier of the evaluation order — right after the bare deny (resp. ask) — which
|
|
26
30
|
* is upstream's placement (`bs()` whole-tool deny → `AN(…,"deny")` field-level deny → `ph()` whole-tool
|
|
27
31
|
* ask → `AN(…,"ask")` field-level ask).
|
|
@@ -30,11 +34,13 @@
|
|
|
30
34
|
* `Edit(~/.sema/settings.json)` / `Write(src/**)` / `Read(//etc/**)` deny or ask compiles through the same
|
|
31
35
|
* validator (the model's path grammar — the `//abs-dir/**` directory form and upstream's pattern form:
|
|
32
36
|
* `//` absolute, `~/` home, `/` project root, otherwise working-directory relative; `*` within a segment,
|
|
33
|
-
* `**` across) and is judged
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
37
|
+
* `**` across) and is judged on the call's PROTECTIVE path target (design/388: the object face's declaration, the
|
|
38
|
+
* catalog's for the name as the floor), every view of it (`callTargetViewsOf`), by the one judgment every path rule
|
|
39
|
+
* meets (`pathRowsReach`). A pattern's ANCHORS are the rule's own fact, taken once where the policy is installed: the
|
|
40
|
+
* declared `pathBases` word by word, else the leg the Runner installs the policy on (`policyAnchors`) — never the
|
|
41
|
+
* call's live working directory. A declared base that is not a usable absolute path is refused at compile time
|
|
42
|
+
* (`unsupported.path_base`); a policy called outside a Runner leg with no declared base reads such a rule
|
|
43
|
+
* `unreadable` (the fail-closed ask).
|
|
38
44
|
* A `Read(...)` / `Edit(...)` deny or ask reaches the calls on its behavior's FACES (`PATH_RULE_FACES`), not only
|
|
39
45
|
* its own tool: a `Read` ask asks for a Grep / Glob rooted in its path, judged on that call's own target.
|
|
40
46
|
*
|
|
@@ -53,14 +59,8 @@
|
|
|
53
59
|
*/
|
|
54
60
|
import type { NamedToolPolicy } from "./tool-policy.js";
|
|
55
61
|
import { parsePermissionRule, type ParsedPermissionRule } from "./permission-rule-syntax.js";
|
|
56
|
-
import { type
|
|
57
|
-
import type { DenyFace, PathRuleDenyRow } from "../tools/fs/read-deny.js";
|
|
62
|
+
import { type PathRuleBases } from "./permission-rule-model.js";
|
|
58
63
|
export { parsePermissionRule, type ParsedPermissionRule };
|
|
59
|
-
/** [K-PLATFORM-SWEEP] MINOR#11: Bash's REAL non-primary arg names, accepted as generic param rules
|
|
60
|
-
* (`Bash(timeout:...)`). Exported SOLELY so a lockstep test can assert it against the live Bash tool
|
|
61
|
-
* schema — a schema param added without updating this set would compile to silently-never-matching
|
|
62
|
-
* rules (the exact drift class the Opus 复审 m note warned about). */
|
|
63
|
-
export declare const BASH_GENERIC_PARAMS: ReadonlySet<string>;
|
|
64
64
|
export interface PermissionRule {
|
|
65
65
|
/** CC 语法:`"Tool"` 或 `"Tool(param:pattern)"`。`Tool()`/`Tool(*)` 归一为裸规则;括号可 `\(`/`\)` 转义。 */
|
|
66
66
|
rule: string;
|
|
@@ -93,9 +93,10 @@ export interface PermissionRuleIssue {
|
|
|
93
93
|
* is an object/array (a rule on it can never match); the tool is unknown to the resolver a leg
|
|
94
94
|
* supplied (absent resolver ⇒ the catalog alone judges and an unknown tool stays permissive). */
|
|
95
95
|
| "unsupported.param_unknown" | "unsupported.param_non_scalar" | "unsupported.tool_unknown"
|
|
96
|
-
/** A path-form deny/ask whose spelling needs a base this policy
|
|
97
|
-
* `pathBases.root`; a relative pattern
|
|
98
|
-
* into a rule that reaches nothing.
|
|
96
|
+
/** A path-form deny/ask whose spelling needs a base this policy DECLARES as something other than an absolute path
|
|
97
|
+
* of its tree's family (`/x` reads `pathBases.root`; a relative pattern `pathBases.cwd`, else `root`; `~/` `home`)
|
|
98
|
+
* — a bad declared value, refused rather than compiled into a rule that reaches nothing. An undeclared base is no
|
|
99
|
+
* refusal: the rule is anchored where the policy is installed. */
|
|
99
100
|
| "unsupported.path_base" | "unsupported.mcp_paren"
|
|
100
101
|
/** A parenthesised rule whose NAME is a covering spelling in a non-MCP protocol namespace
|
|
101
102
|
* (`a2a__<peer>__*(x:y)`, `a2a__<peer>(x:y)`). The param DSL is keyed by EXACT tool name, and a
|
|
@@ -143,16 +144,17 @@ export interface PermissionRulePolicyOptions {
|
|
|
143
144
|
*/
|
|
144
145
|
ruleFaces?: (toolName: string) => RuleFaceView | undefined;
|
|
145
146
|
/**
|
|
146
|
-
* The
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
147
|
+
* The ANCHORS of this rule set's path patterns ({@link PathRuleBases}), word by word: `root` for `/…`, `cwd` for a
|
|
148
|
+
* relative pattern (the declared root stands in when absent — the word's own meaning), `home` for `~/…` (an
|
|
149
|
+
* execution environment's home — never the engine process's own; #644). A word declared here anchors the rules
|
|
150
|
+
* wherever the policy runs; a word left out is anchored where the Runner installs the policy — the root and the
|
|
151
|
+
* environment's home of the leg it enters the delegation chain on — and stays there for every child and every `cd`.
|
|
152
|
+
* A declared value that is not an absolute path of its tree's family is refused at compile time
|
|
151
153
|
* (`unsupported.path_base`, naming the base — {@link ruleBasesNeeded} is the same classification).
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* answers it with the fail-closed ask (`ruleUnreadable`).
|
|
154
|
+
* These are the RULES' anchors, never the call's: a call's target is read from the call (its target reading, else its
|
|
155
|
+
* own `root` / `cwd`); only a host calling the policy directly with a request that carries neither has its target
|
|
156
|
+
* resolved against `root` / `cwd` here. A target nothing locates is unreadable: every path rule able to reach the
|
|
157
|
+
* call answers it with the fail-closed ask (`ruleUnreadable`).
|
|
156
158
|
*/
|
|
157
159
|
pathBases?: PathRuleBases;
|
|
158
160
|
}
|
|
@@ -205,28 +207,7 @@ export interface RuleFaceView {
|
|
|
205
207
|
export declare function catalogRuleFaceView(toolName: string): RuleFaceView | undefined;
|
|
206
208
|
/** Exported for the lockstep guard only (the scalar vocabulary of a catalog tool) — not part of the rule DSL. */
|
|
207
209
|
export declare const fileToolParamVocabulary: (canonicalTool: string) => ReadonlySet<string> | undefined;
|
|
208
|
-
/**
|
|
209
|
-
* The deny-table row of ONE face-keyed path deny rule, resolved against `bases` — the row's reach IS the rule's (the same
|
|
210
|
-
* parsed rule, the same bases). Every source of such rows builds them here: a policy's compiled rules, and the person's
|
|
211
|
-
* persisted store rows. The caller holds the bases the pattern needs (`ruleBasesNeeded`): a row whose base is missing
|
|
212
|
-
* reads `unreadable`, which this row treats as a reach — a deny row that cannot be judged holds.
|
|
213
|
-
*/
|
|
214
|
-
export declare function pathRuleDenyRowOf(rule: {
|
|
215
|
-
readonly parsed: ParsedRule;
|
|
216
|
-
readonly ruleText: string;
|
|
217
|
-
readonly faces: readonly DenyFace[];
|
|
218
|
-
}, bases: PathRuleBases): PathRuleDenyRow;
|
|
219
210
|
/** Dry-run 校验/迁移报告:不 throw,返回全部 issues(含 `unsupported.*` 分类,
|
|
220
211
|
* 供从 CC settings 迁移的部署逐条对照)。 */
|
|
221
212
|
export declare function validatePermissionRules(rules: PermissionRule[], opts?: Pick<PermissionRulePolicyOptions, "primaryFieldGeneric" | "caps" | "ruleFaces" | "pathBases">): PermissionRuleIssue[];
|
|
222
|
-
/**
|
|
223
|
-
* Compile declarative permission rules into ONE {@link ToolPolicy} (design/127). Evaluation order
|
|
224
|
-
* inside the policy (CC `G1e` :595272 isomorph): bare deny → command deny → param deny → bare ask →
|
|
225
|
-
* command ask → param ask → bare allow → `defaultAction` (the content-form command tiers are the
|
|
226
|
-
* field-level tiers of upstream's arbiter, beside the generic param tiers). `check()` is idempotent and side-effect-free (combinePolicies 契约);
|
|
227
|
-
* all parsing/validation happens HERE, the hot path never re-parses.
|
|
228
|
-
*
|
|
229
|
-
* ⚠️ This policy states no allow: a call no rule reaches gets `defaultAction`, whose `"allow"` is "no objection" (states
|
|
230
|
-
* nothing, clears nothing). Express default-closed via `defaultAction`, never via a trailing always-ask policy.
|
|
231
|
-
*/
|
|
232
213
|
export declare function createPermissionRulePolicy(rules: PermissionRule[], opts?: PermissionRulePolicyOptions): NamedToolPolicy;
|