@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.
Files changed (139) hide show
  1. package/CHANGELOG.md +298 -1
  2. package/dist/agents/extra-tool-names.d.ts +65 -0
  3. package/dist/agents/extra-tool-names.js +124 -0
  4. package/dist/agents/subagent.d.ts +17 -5
  5. package/dist/agents/subagent.js +33 -25
  6. package/dist/agents/tool-filter.d.ts +17 -1
  7. package/dist/agents/tool-filter.js +7 -0
  8. package/dist/core/agent-config-dir.d.ts +20 -0
  9. package/dist/core/agent-config-dir.js +49 -0
  10. package/dist/core/allow-entry.d.ts +6 -4
  11. package/dist/core/allow-entry.js +6 -5
  12. package/dist/core/ask-origin.d.ts +35 -12
  13. package/dist/core/ask-origin.js +24 -7
  14. package/dist/core/checkpoint-store.d.ts +39 -2
  15. package/dist/core/effective-path-target.d.ts +136 -4
  16. package/dist/core/effective-path-target.js +110 -2
  17. package/dist/core/engine-notice.d.ts +12 -4
  18. package/dist/core/gate-fold.js +5 -2
  19. package/dist/core/gate-lanes.js +11 -4
  20. package/dist/core/gate-outcome.d.ts +4 -2
  21. package/dist/core/gate-outcome.js +2 -2
  22. package/dist/core/governance-codes.d.ts +3 -1
  23. package/dist/core/governance-codes.js +3 -0
  24. package/dist/core/hooks.d.ts +3 -0
  25. package/dist/core/path-rule-binding.d.ts +65 -0
  26. package/dist/core/path-rule-binding.js +99 -0
  27. package/dist/core/permission-rule-model.d.ts +59 -24
  28. package/dist/core/permission-rule-model.js +48 -44
  29. package/dist/core/permission-rule-provider.d.ts +7 -0
  30. package/dist/core/permission-rule-provider.js +10 -3
  31. package/dist/core/permission-rule-store.d.ts +8 -0
  32. package/dist/core/permission-rule-store.js +6 -2
  33. package/dist/core/permission-rule-sync.js +2 -2
  34. package/dist/core/permission-rules.d.ts +26 -45
  35. package/dist/core/permission-rules.js +77 -57
  36. package/dist/core/runner/child-tree.d.ts +97 -0
  37. package/dist/core/runner/child-tree.js +104 -0
  38. package/dist/core/runner/construction-doors.d.ts +5 -0
  39. package/dist/core/runner/construction-doors.js +6 -0
  40. package/dist/core/runner/contracts.d.ts +46 -0
  41. package/dist/core/runner/gate-exit.d.ts +4 -1
  42. package/dist/core/runner/gate-exit.js +5 -1
  43. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  44. package/dist/core/runner/permission-rule-lanes.js +18 -12
  45. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  46. package/dist/core/runner/persisted-path-rows.js +31 -12
  47. package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
  48. package/dist/core/runner/prepare-ask-lane.js +4 -2
  49. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  50. package/dist/core/runner/prepare-execution-env.js +10 -3
  51. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  52. package/dist/core/runner/prepare-gate-stations.js +2 -1
  53. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  54. package/dist/core/runner/prepare-hands-readface.js +11 -11
  55. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  56. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  57. package/dist/core/runner/prepare-park-ask.d.ts +3 -0
  58. package/dist/core/runner/prepare-park-ask.js +5 -3
  59. package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
  60. package/dist/core/runner/prepare-policy-chain.js +13 -5
  61. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  62. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  63. package/dist/core/runner/prepare-task.js +37 -18
  64. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  65. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  66. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  67. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  68. package/dist/core/runner/resume-apply.js +17 -3
  69. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  70. package/dist/core/runner/run-leg.js +5 -1
  71. package/dist/core/runner/runtask.js +2 -2
  72. package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
  73. package/dist/core/runner/write-protection-station.d.ts +3 -2
  74. package/dist/core/runner/write-protection-station.js +1 -0
  75. package/dist/core/runner-deps.d.ts +14 -0
  76. package/dist/core/sensitive-path-policy.d.ts +8 -0
  77. package/dist/core/sensitive-path-policy.js +18 -2
  78. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  79. package/dist/core/shell-wrapper-projection.js +56 -0
  80. package/dist/core/shell-wrapper-table.d.ts +144 -34
  81. package/dist/core/shell-wrapper-table.js +40 -20
  82. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  83. package/dist/core/synthetic-read-verdict.js +3 -1
  84. package/dist/core/tool-catalog-entries.d.ts +21 -0
  85. package/dist/core/tool-catalog-entries.js +12 -2
  86. package/dist/core/tool-face.d.ts +10 -1
  87. package/dist/core/tool-name-readings.d.ts +8 -0
  88. package/dist/core/tool-name-readings.js +2 -0
  89. package/dist/core/tool-policy.d.ts +40 -17
  90. package/dist/core/tool-policy.js +11 -9
  91. package/dist/core/tool-registry.d.ts +9 -0
  92. package/dist/core/tool-registry.js +10 -0
  93. package/dist/core/tool-spec.d.ts +3 -2
  94. package/dist/core/write-protect.d.ts +14 -3
  95. package/dist/core/write-protect.js +41 -10
  96. package/dist/index.d.ts +6 -4
  97. package/dist/index.js +5 -3
  98. package/dist/orchestration/run-spec.js +8 -7
  99. package/dist/orchestration/workflow.js +1 -1
  100. package/dist/shell/lex.d.ts +22 -0
  101. package/dist/shell/lex.js +75 -13
  102. package/dist/tools/fs/bash-program-position.d.ts +14 -90
  103. package/dist/tools/fs/bash-program-position.js +41 -74
  104. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  105. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  106. package/dist/tools/fs/index.d.ts +3 -2
  107. package/dist/tools/fs/program-read-model.d.ts +711 -0
  108. package/dist/tools/fs/program-read-model.js +1259 -0
  109. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  110. package/dist/tools/fs/program-read-model.rows.js +624 -0
  111. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  112. package/dist/tools/fs/read-deny-home.js +62 -24
  113. package/dist/tools/fs/read-deny.d.ts +54 -14
  114. package/dist/tools/fs/read-deny.js +104 -50
  115. package/dist/tools/fs/removal-targets.d.ts +7 -6
  116. package/dist/tools/fs/removal-targets.js +34 -24
  117. package/dist/tools/fs/safety.d.ts +3 -0
  118. package/dist/tools/fs/safety.js +10 -3
  119. package/dist/tools/fs/shell-read-argv.d.ts +86 -0
  120. package/dist/tools/fs/shell-read-argv.js +388 -0
  121. package/dist/tools/fs/shell-read-bindings.d.ts +300 -0
  122. package/dist/tools/fs/shell-read-bindings.js +755 -0
  123. package/dist/tools/fs/shell-read-check.js +23 -15
  124. package/dist/tools/fs/shell-read-establish.js +82 -8
  125. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  126. package/dist/tools/fs/shell-read-judge.js +9 -1
  127. package/dist/tools/fs/shell-read-plan.d.ts +33 -8
  128. package/dist/tools/fs/shell-read-plan.js +309 -407
  129. package/dist/tools/fs/shell-read-word.d.ts +60 -26
  130. package/dist/tools/fs/shell-read-word.js +47 -37
  131. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
  132. package/dist/tools/fs/shell-read-write-targets.js +85 -137
  133. package/dist/tools/fs/shell-reentry-text.d.ts +136 -6
  134. package/dist/tools/fs/shell-reentry-text.js +203 -53
  135. package/dist/tools/monitor.d.ts +3 -3
  136. package/package.json +1 -1
  137. package/test/export-surface.snapshot.json +25 -5
  138. package/wire-changes/core@7.46.0.json +1522 -0
  139. 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
- return heldValues("policy.persistedGrammar", { get: (n) => (ruleToolGrammarOf(n) !== undefined ? n : undefined) }, req);
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 = pathFamilyOf(bases);
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 = pathFamilyOf(bases);
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 = pathFamilyOf(bases);
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
- return cwd !== undefined && pathWithinRoot(cwd, scope.root);
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, targets, bases) {
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 (targets.allow?.kind !== "path" || call.tool !== READ_RULE_TOOL)
937
+ if (views.allow === undefined || call.tool !== READ_RULE_TOOL)
920
938
  continue;
921
- for (const rule of rules) {
922
- if (!eligiblePersisted(rule, call, behavior))
923
- continue;
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
- const tighten = targets.tighten;
930
- if (tighten === undefined || tighten.kind === "refused")
944
+ if (views.tighten === undefined)
931
945
  continue;
932
- let unreadable;
933
- for (const rule of rules) {
934
- if (!speaks(rule, behavior))
935
- continue;
936
- if (tighten.kind === "unreadable") {
937
- if (rule.match === "path" || rule.match === "subpath")
938
- unreadable ??= { behavior: "ask", rules: [rule], unreadable: tighten.reason };
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", cwd: call.cwd, sessionId: call.sessionId }) &&
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
- for (const r of [...sessionRead, ...durableRead.rules]) {
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" && typeof sc.root === "string" && sc.root !== "") ||
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" && typeof sc.root === "string" && sc.root !== "");
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: typeof root === "string" && root !== "" ? { kind: "project", root } : undefined };
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 with `pathRuleReaches` on the call's PROTECTIVE path target (design/388: the
34
- * object face's declaration, the catalog's for the name as the floor). The bases a pattern resolves
35
- * against are the deployment's (`pathBases`); a pattern whose base the policy was not given is refused at
36
- * compile time (`unsupported.path_base`) rather than left to reach nothing at run time — a deny that
37
- * silently matches nothing is the failure form this module's header calls the most dangerous one.
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 ParsedRule, type PathRuleBases } from "./permission-rule-model.js";
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 was not given (`/x` needs
97
- * `pathBases.root`; a relative pattern needs `pathBases.cwd` or `root`) — refused rather than compiled
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 bases a path-form deny/ask pattern resolves against at match time ({@link PathRuleBases}): `root`
147
- * for `/…` (the task's project root), `cwd` for a relative pattern (the root stands in when absent — the
148
- * word's own meaning), `home` for `~/…` (the EXECUTION ENVIRONMENT's home — never the engine process's
149
- * own; #644). Facts of the DEPLOYMENT and the task, never of the rule — a pattern is stored as spelled.
150
- * A rule whose base is absent (or not an absolute path) here is refused at compile time
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
- * A call's own target resolves against these bases as given; when they hold neither `root` nor `cwd`,
153
- * against the request's (`ToolCallRequest.root` / `.cwd`, stamped by the Runner) — never a mix of the two
154
- * sources. A target neither supplies the base for is unreadable: every path rule able to reach the call
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;