@sema-agent/core 7.45.0 → 7.46.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 (127) hide show
  1. package/CHANGELOG.md +246 -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 +5 -3
  11. package/dist/core/allow-entry.js +3 -1
  12. package/dist/core/ask-origin.js +1 -1
  13. package/dist/core/checkpoint-store.d.ts +39 -2
  14. package/dist/core/effective-path-target.d.ts +136 -4
  15. package/dist/core/effective-path-target.js +110 -2
  16. package/dist/core/engine-notice.d.ts +5 -0
  17. package/dist/core/gate-fold.js +3 -0
  18. package/dist/core/gate-lanes.js +9 -3
  19. package/dist/core/governance-codes.d.ts +3 -1
  20. package/dist/core/governance-codes.js +3 -0
  21. package/dist/core/hooks.d.ts +3 -0
  22. package/dist/core/path-rule-binding.d.ts +65 -0
  23. package/dist/core/path-rule-binding.js +99 -0
  24. package/dist/core/permission-rule-model.d.ts +47 -18
  25. package/dist/core/permission-rule-model.js +38 -42
  26. package/dist/core/permission-rule-provider.d.ts +7 -0
  27. package/dist/core/permission-rule-provider.js +10 -3
  28. package/dist/core/permission-rule-store.d.ts +8 -0
  29. package/dist/core/permission-rule-store.js +6 -2
  30. package/dist/core/permission-rule-sync.js +2 -2
  31. package/dist/core/permission-rules.d.ts +22 -40
  32. package/dist/core/permission-rules.js +50 -43
  33. package/dist/core/runner/child-tree.d.ts +97 -0
  34. package/dist/core/runner/child-tree.js +104 -0
  35. package/dist/core/runner/construction-doors.d.ts +5 -0
  36. package/dist/core/runner/construction-doors.js +6 -0
  37. package/dist/core/runner/contracts.d.ts +46 -0
  38. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  39. package/dist/core/runner/permission-rule-lanes.js +18 -12
  40. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  41. package/dist/core/runner/persisted-path-rows.js +31 -12
  42. package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
  43. package/dist/core/runner/prepare-ask-lane.js +4 -2
  44. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  45. package/dist/core/runner/prepare-execution-env.js +10 -3
  46. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  47. package/dist/core/runner/prepare-gate-stations.js +2 -1
  48. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  49. package/dist/core/runner/prepare-hands-readface.js +6 -6
  50. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  51. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  52. package/dist/core/runner/prepare-park-ask.d.ts +3 -0
  53. package/dist/core/runner/prepare-park-ask.js +5 -3
  54. package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
  55. package/dist/core/runner/prepare-policy-chain.js +11 -4
  56. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  57. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  58. package/dist/core/runner/prepare-task.js +37 -18
  59. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  60. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  61. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  62. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  63. package/dist/core/runner/resume-apply.js +17 -3
  64. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  65. package/dist/core/runner/run-leg.js +5 -1
  66. package/dist/core/runner/runtask.js +2 -2
  67. package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
  68. package/dist/core/runner/write-protection-station.d.ts +3 -2
  69. package/dist/core/runner/write-protection-station.js +1 -0
  70. package/dist/core/runner-deps.d.ts +14 -0
  71. package/dist/core/sensitive-path-policy.d.ts +8 -0
  72. package/dist/core/sensitive-path-policy.js +18 -2
  73. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  74. package/dist/core/shell-wrapper-projection.js +56 -0
  75. package/dist/core/shell-wrapper-table.d.ts +144 -34
  76. package/dist/core/shell-wrapper-table.js +40 -20
  77. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  78. package/dist/core/synthetic-read-verdict.js +3 -1
  79. package/dist/core/tool-catalog-entries.d.ts +21 -0
  80. package/dist/core/tool-catalog-entries.js +9 -0
  81. package/dist/core/tool-policy.d.ts +30 -8
  82. package/dist/core/tool-policy.js +9 -7
  83. package/dist/core/tool-spec.d.ts +3 -2
  84. package/dist/core/write-protect.d.ts +4 -0
  85. package/dist/core/write-protect.js +30 -9
  86. package/dist/index.d.ts +5 -3
  87. package/dist/index.js +4 -2
  88. package/dist/orchestration/run-spec.js +8 -7
  89. package/dist/orchestration/workflow.js +1 -1
  90. package/dist/shell/lex.d.ts +7 -0
  91. package/dist/shell/lex.js +17 -0
  92. package/dist/tools/fs/bash-program-position.d.ts +8 -87
  93. package/dist/tools/fs/bash-program-position.js +33 -69
  94. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  95. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  96. package/dist/tools/fs/index.d.ts +3 -2
  97. package/dist/tools/fs/program-read-model.d.ts +694 -0
  98. package/dist/tools/fs/program-read-model.js +1252 -0
  99. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  100. package/dist/tools/fs/program-read-model.rows.js +613 -0
  101. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  102. package/dist/tools/fs/read-deny-home.js +62 -24
  103. package/dist/tools/fs/read-deny.d.ts +54 -14
  104. package/dist/tools/fs/read-deny.js +104 -50
  105. package/dist/tools/fs/removal-targets.d.ts +7 -6
  106. package/dist/tools/fs/removal-targets.js +22 -22
  107. package/dist/tools/fs/safety.d.ts +3 -0
  108. package/dist/tools/fs/safety.js +10 -3
  109. package/dist/tools/fs/shell-read-argv.d.ts +85 -0
  110. package/dist/tools/fs/shell-read-argv.js +386 -0
  111. package/dist/tools/fs/shell-read-bindings.d.ts +273 -0
  112. package/dist/tools/fs/shell-read-bindings.js +746 -0
  113. package/dist/tools/fs/shell-read-check.js +23 -15
  114. package/dist/tools/fs/shell-read-establish.js +79 -5
  115. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  116. package/dist/tools/fs/shell-read-judge.js +9 -1
  117. package/dist/tools/fs/shell-read-plan.d.ts +31 -7
  118. package/dist/tools/fs/shell-read-plan.js +299 -393
  119. package/dist/tools/fs/shell-read-word.d.ts +29 -26
  120. package/dist/tools/fs/shell-read-word.js +30 -38
  121. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
  122. package/dist/tools/fs/shell-read-write-targets.js +83 -136
  123. package/dist/tools/fs/shell-reentry-text.d.ts +38 -6
  124. package/dist/tools/fs/shell-reentry-text.js +90 -53
  125. package/package.json +1 -1
  126. package/test/export-surface.snapshot.json +17 -3
  127. package/wire-changes/core@7.46.0.json +1522 -0
@@ -76,7 +76,7 @@ import { type DenyFace } from "../tools/fs/read-deny.js";
76
76
  import type { AssertAllKeysHandled } from "./ask-origin.js";
77
77
  import { type ToolCallLike } from "./tool-registry.js";
78
78
  import { type PathFamily, type PathIdentity } from "../tools/fs/safety.js";
79
- import type { PathTargetReading } from "./effective-path-target.js";
79
+ import { type TargetViews } from "./path-rule-binding.js";
80
80
  /**
81
81
  * The closed set of rule BEHAVIORS — what a matching rule says about the call. The upstream vocabulary
82
82
  * verbatim (`allow` / `deny` / `ask`, the three settings lists), in PRECEDENCE order: when rules of more
@@ -144,7 +144,7 @@ export declare const READ_RULE_TOOL = "Read";
144
144
  * its tool's name as its reach (`Write(<path>)` is judged on the Write tool, a stricter reading the reference does not make).
145
145
  *
146
146
  * The two columns are enforced where each behavior can be: a deny row is exported to the engine's deny tables
147
- * (`ToolPolicy.pathDenyRules`, the person's rows at prepare), which refuse inside the tool, descend into a search's
147
+ * (`ToolPolicy.pathRules` bound where the policy is installed, the person's rows at prepare), which refuse inside the tool, descend into a search's
148
148
  * results and reach the shell's file commands; an ask is a question asked at the gate before the call runs, so it is
149
149
  * judged there, on the call's own path target only — it does not descend (a search rooted above the asked path is not
150
150
  * asked, and its results are not filtered).
@@ -600,7 +600,7 @@ export declare function isUsablePathBase(base: string | undefined, family: PathF
600
600
  * defect (a `~/` deny falling back to the engine host's home guarded the wrong directory; a `/…` deny with
601
601
  * no root reached nothing at all).
602
602
  */
603
- export declare function pathRuleReachOf(rule: Pick<PersistedRule, "match" | "command" | "behavior">, target: PathIdentity, bases: PathRuleBases): ProgramRunReachOutcome;
603
+ export declare function pathRuleReachOf(rule: Pick<PersistedRule, "match" | "command" | "behavior">, target: PathIdentity, bases: PathRuleBases, treeFamily?: PathFamily | undefined): ProgramRunReachOutcome;
604
604
  /**
605
605
  * Where a path rule's reach STARTS under the call's bases — its HEAD: the location the resolved pattern names before
606
606
  * its first segment carrying `*` (the whole path when none does), or a directory rule's directory. Everything the rule
@@ -772,7 +772,7 @@ export declare function lexicalNormalAbsolutePathOf(path: string, family: PathFa
772
772
  * there is nothing here for it to clear). As a DENY/ASK the same containment refuses or questions every
773
773
  * path under the directory, on whichever path tool the rule names.
774
774
  */
775
- export declare function directoryRuleAdmits(rule: Pick<PersistedRule, "match" | "command">, path: PathIdentity, bases: PathRuleBases): boolean;
775
+ export declare function directoryRuleAdmits(rule: Pick<PersistedRule, "match" | "command">, path: PathIdentity, bases: PathRuleBases, treeFamily?: PathFamily | undefined): boolean;
776
776
  /** The identity of a directory SPELLING (the cd resolver's `/`-rooted output) under the call's tree, for the
777
777
  * consumers that hand {@link directoryRuleAdmits} a spelling; a rule minted from it stores this form. */
778
778
  export declare function directoryIdentityOf(spelling: string, bases: PathRuleBases): PathIdentity | undefined;
@@ -785,7 +785,18 @@ export declare function directoryIdentityOf(spelling: string, bases: PathRuleBas
785
785
  * r3-② cwd posture), never inferred from the process. ONE function on purpose: this is the scope
786
786
  * conjunct of the single eligibility predicate ({@link eligibleContext}), and a sister predicate
787
787
  * would be the second fact source the 375 §5.1 discipline exists to forbid. */
788
- export declare function scopeCoversCwd(scope: RuleScope, cwd: string | undefined, sessionId?: string): boolean;
788
+ export declare function scopeCoversCwd(scope: RuleScope, cwd: string | undefined, sessionId?: string, identity?: ScopeRootIdentity): boolean;
789
+ /**
790
+ * A leg's root as an IDENTITY for project coverage: its canonical spelling, and the leg's resolution of a project row's
791
+ * scope root to the location it leads to. A DENY or ASK project row written under one spelling of a directory (a link the
792
+ * host spelled, a firmlink) covers a leg rooted at another spelling of the same directory: the row applies by where the two
793
+ * roots ARE, not only by how they are spelled. An allow row never reads it ({@link spelledScope}). A caller that holds no
794
+ * resolution (no leg, no environment) compares the spellings alone.
795
+ */
796
+ export interface ScopeRootIdentity {
797
+ readonly canonicalRoot: string;
798
+ readonly canonicalOf: (root: string) => string | undefined;
799
+ }
789
800
  /** Is this rule live — i.e. does it still carry at least one add? Deleted adds are removed by the store
790
801
  * when tombstones are applied, so a rule with an empty `adds` is a rule that no longer exists. */
791
802
  export declare function isRuleLive(rule: PersistedRule): boolean;
@@ -815,6 +826,7 @@ export declare function eligibleContext(rule: {
815
826
  tool: string;
816
827
  cwd: string | undefined;
817
828
  sessionId?: string;
829
+ scopeIdentity?: ScopeRootIdentity;
818
830
  }): boolean;
819
831
  /**
820
832
  * design/375 §5.1 — MAY this PERSISTED rule participate in admitting this call at all? The liveness
@@ -834,7 +846,16 @@ export declare function eligiblePersisted(rule: PersistedRule, call: {
834
846
  tool: string;
835
847
  cwd: string | undefined;
836
848
  sessionId?: string;
849
+ scopeIdentity?: ScopeRootIdentity;
837
850
  }, behavior: RuleBehavior): boolean;
851
+ /**
852
+ * The call context an ALLOW row's scope is judged in: the spellings alone. A deny or ask row applies wherever its project's
853
+ * directory is (its scope root resolved where it leads — more rows, only ever more protection); an allow row applies only
854
+ * where its scope root is SPELLED, so a link someone can re-point never carries a standing approval to another directory.
855
+ */
856
+ export declare function spelledScope<C extends {
857
+ scopeIdentity?: ScopeRootIdentity;
858
+ }>(call: C): Omit<C, "scopeIdentity">;
838
859
  /**
839
860
  * Find the rules that admit this command for a task in `cwd`, or `undefined`.
840
861
  *
@@ -914,19 +935,23 @@ export declare function adjudicatePersistedRules(rules: readonly PersistedRule[]
914
935
  cwd: string | undefined;
915
936
  sessionId?: string;
916
937
  execCwd?: string;
938
+ scopeIdentity?: ScopeRootIdentity;
917
939
  }): PersistedRuleVerdict | undefined;
918
940
  /**
919
941
  * The path-family sibling of {@link adjudicatePersistedRules}: the same precedence walk over the rules
920
- * of one path-targeting tool, judged on the call's resolved target path.
942
+ * of one path-targeting tool, judged on the call's target views by the one judgment every path rule meets
943
+ * ({@link pathRowsReach}: rows outer, views inner).
944
+ *
945
+ * TWO readings, because the two directions read the call's path slot differently (design/388 B6/B17): the
946
+ * tightening arms judge the PROTECTIVE reading's views (`views.tighten` — the object face's declaration, the catalog's
947
+ * for the name as the floor: reading the catalog for an undeclared same-name tool can only tighten a deny), every row
948
+ * also where its head leads (`leads`, the judging leg's resolution); the allow arm judges the DECLARED reading's views
949
+ * (`views.allow` — the face's OWN declaration only: a same-name tool that declares no path target is unconfirmable, and
950
+ * no grant admits it) with the directory rows, for the Read tool ONLY — a directory allow is a standing yes to reads,
951
+ * never to a write — and admits only when every view lies under the directory. Either reading may be absent (no
952
+ * resolvable path in that reading); the arm that needs it then speaks for nothing.
921
953
  *
922
- * TWO targets, because the two directions read the call's path slot differently (design/388 B6/B17): the
923
- * tightening arms judge the PROTECTIVE target (`protectivePathTargetOf` — the object face's declaration,
924
- * the catalog's for the name as the floor: reading the catalog for an undeclared same-name tool can only
925
- * tighten a deny) with {@link pathRuleReaches}; the allow arm judges the DECLARED target
926
- * (`declaredPathTargetOf` — the face's OWN declaration only: a same-name tool that declares no path
927
- * target is unconfirmable, and no grant admits it) with {@link directoryRuleAdmits}, and for the Read tool
928
- * ONLY — a directory allow is a standing yes to reads, never to a write. Either target may be absent
929
- * (no resolvable path in that reading); the arm that needs it then speaks for nothing.
954
+ * A row is anchored by `anchorsOf` (the person's rows: a project row at its scope root, the rest at the chain's top leg).
930
955
  *
931
956
  * `call.face` is the face the call's protective target is on (`read` / `write`): an ASK row speaks for the call when it
932
957
  * is the call's tool's row, or when {@link PATH_RULE_FACES} puts that face in the row's ask column (a `Read(<path>)` ask
@@ -938,10 +963,11 @@ export declare function adjudicatePersistedPathRules(rules: readonly PersistedRu
938
963
  cwd: string | undefined;
939
964
  sessionId?: string;
940
965
  face?: DenyFace | undefined;
941
- }, targets: {
942
- readonly tighten?: PathTargetReading;
943
- readonly allow?: PathTargetReading;
944
- }, bases: PathRuleBases): PersistedRuleVerdict | undefined;
966
+ scopeIdentity?: ScopeRootIdentity;
967
+ }, views: {
968
+ readonly tighten?: TargetViews;
969
+ readonly allow?: TargetViews;
970
+ }, anchorsOf: (rule: PersistedRule) => PathRuleBases, leads: ReadonlyMap<string, string | undefined>): PersistedRuleVerdict | undefined;
945
971
  /**
946
972
  * The name-family sibling of {@link adjudicatePersistedRules}: the same precedence walk over the rules of one name tool
947
973
  * ({@link NAME_RULE_FACES}), judged on the one string the call names (`nameRuleSubjectOf`). A rule of the tool speaks for the call
@@ -955,6 +981,7 @@ export declare function adjudicatePersistedNameRules(rules: readonly PersistedRu
955
981
  name: string | undefined;
956
982
  cwd: string | undefined;
957
983
  sessionId?: string;
984
+ scopeIdentity?: ScopeRootIdentity;
958
985
  }): PersistedRuleVerdict | undefined;
959
986
  /**
960
987
  * The ONE offer a name tool's approval card carries: the exact rule for the name the call carries (`Skill(<name>)`, the reference's
@@ -1009,6 +1036,8 @@ export declare function segmentCoverageOf(command: string, rules: {
1009
1036
  /** The relative-cd resolution base (see {@link findAdmittingRule}'s member of the same name) —
1010
1037
  * defaults to `cwd`; scope/eligibility stays `cwd`. */
1011
1038
  execCwd?: string;
1039
+ /** The leg's root as an identity for project coverage ({@link ScopeRootIdentity}). */
1040
+ scopeIdentity?: ScopeRootIdentity;
1012
1041
  }): readonly SegmentCoverage[] | undefined;
1013
1042
  /** One row of a per-segment coverage table (see {@link segmentCoverageOf}): the FOLDED, trimmed
1014
1043
  * segment text (a display/correlation seat, never adjudication input) and whether an eligible rule
@@ -6,6 +6,7 @@ import { inlineUntrusted } from "./untrusted-text.js";
6
6
  import { 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"]);
@@ -68,7 +69,7 @@ export const BARE_INTERPRETER_NAMES = new Set([
68
69
  "bash", "sh", "zsh", "ksh", "csh", "tcsh", "dash", "fish", "cmd", "powershell", "pwsh", "busybox",
69
70
  "source", ".", "trap", "eval", "exec", "command", "builtin",
70
71
  "enable", "compgen", "complete", "bind", "mapfile", "readarray",
71
- "env", "noglob", "xargs",
72
+ "env", "noglob", "nocorrect", "xargs",
72
73
  "sudo", "doas", "pkexec", "su", "runuser", "ssh", "chroot", "setpriv",
73
74
  "nohup", "nice", "ionice", "chrt", "taskset", "stdbuf", "timeout", "time", "watch",
74
75
  "setsid", "flock", "unshare", "nsenter", "script", "numactl", "prlimit", "systemd-run", "parallel",
@@ -440,8 +441,8 @@ function baseValueOf(base, bases) {
440
441
  }
441
442
  }
442
443
  }
443
- function resolvePathPattern(pattern, bases, behavior) {
444
- const family = pathFamilyOf(bases);
444
+ function resolvePathPattern(pattern, bases, behavior, treeFamily = pathFamilyOf(bases)) {
445
+ const family = treeFamily;
445
446
  const tree = bases.root ?? bases.cwd;
446
447
  if (pattern.startsWith("//")) {
447
448
  const abs = pathIdentityOf("/" + pattern.slice(2), family, tree);
@@ -503,15 +504,15 @@ function globSegmentsReach(globs, target) {
503
504
  };
504
505
  return walk(0, 0);
505
506
  }
506
- export function pathRuleReachOf(rule, target, bases) {
507
+ export function pathRuleReachOf(rule, target, bases, treeFamily = pathFamilyOf(bases)) {
507
508
  if (rule.match === "subpath")
508
- return directoryRuleAdmits(rule, target, bases) ? REACHED : NOT_REACHED;
509
+ return directoryRuleAdmits(rule, target, bases, treeFamily) ? REACHED : NOT_REACHED;
509
510
  if (rule.match !== "path")
510
511
  return NOT_REACHED;
511
- const family = pathFamilyOf(bases);
512
+ const family = treeFamily;
512
513
  if (!isPathIdentity(target, family))
513
514
  return NOT_REACHED;
514
- const r = resolvePathPattern(rule.command, bases, rule.behavior);
515
+ const r = resolvePathPattern(rule.command, bases, rule.behavior, family);
515
516
  if ("missingBase" in r) {
516
517
  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
518
  }
@@ -706,10 +707,10 @@ function isLexicalNormalAbsoluteDir(path) {
706
707
  const segments = path.split("/").slice(1);
707
708
  return segments.length > 0 && segments.every((s) => s !== "" && s !== "." && s !== "..");
708
709
  }
709
- export function directoryRuleAdmits(rule, path, bases) {
710
+ export function directoryRuleAdmits(rule, path, bases, treeFamily = pathFamilyOf(bases)) {
710
711
  if (rule.match !== "subpath")
711
712
  return false;
712
- const family = pathFamilyOf(bases);
713
+ const family = treeFamily;
713
714
  if (carriesDotSegment(rule.command, family))
714
715
  return false;
715
716
  const dir = pathIdentityOf(rule.command, family, bases.root ?? bases.cwd);
@@ -820,21 +821,29 @@ function resolveCdSegmentDirectories(segments, cwd) {
820
821
  return dir === undefined || isShellRootedSpellingUnmapped(pathFamilyOf(bases), dir) ? undefined : directoryIdentityOf(dir, bases);
821
822
  });
822
823
  }
823
- export function scopeCoversCwd(scope, cwd, sessionId) {
824
+ export function scopeCoversCwd(scope, cwd, sessionId, identity) {
824
825
  if (scope.kind === "global")
825
826
  return true;
826
827
  if (scope.kind === "session")
827
828
  return sessionId !== undefined && sessionId === scope.sessionId;
828
- return cwd !== undefined && pathWithinRoot(cwd, scope.root);
829
+ if (cwd === undefined)
830
+ return false;
831
+ if (pathWithinRoot(cwd, scope.root))
832
+ return true;
833
+ return identity !== undefined && pathWithinRoot(identity.canonicalRoot, identity.canonicalOf(scope.root) ?? scope.root);
829
834
  }
830
835
  export function isRuleLive(rule) {
831
836
  return rule.adds.length > 0;
832
837
  }
833
838
  export function eligibleContext(rule, call) {
834
- return rule.tool === call.tool && scopeCoversCwd(rule.scope, call.cwd, call.sessionId);
839
+ return rule.tool === call.tool && scopeCoversCwd(rule.scope, call.cwd, call.sessionId, call.scopeIdentity);
835
840
  }
836
841
  export function eligiblePersisted(rule, call, behavior) {
837
- return rule.behavior === behavior && isRuleLive(rule) && eligibleContext(rule, call);
842
+ return rule.behavior === behavior && isRuleLive(rule) && eligibleContext(rule, behavior === "allow" ? spelledScope(call) : call);
843
+ }
844
+ export function spelledScope(call) {
845
+ const { scopeIdentity: _identity, ...spelled } = call;
846
+ return spelled;
838
847
  }
839
848
  function firstEligibleAdmittingRule(rules, commandText, call) {
840
849
  for (const rule of rules) {
@@ -911,41 +920,28 @@ export function adjudicatePersistedRules(rules, call) {
911
920
  }
912
921
  return undefined;
913
922
  }
914
- export function adjudicatePersistedPathRules(rules, call, targets, bases) {
923
+ export function adjudicatePersistedPathRules(rules, call, views, anchorsOf, leads) {
915
924
  const speaks = (rule, behavior) => eligiblePersisted(rule, call, behavior) ||
916
925
  (behavior === "ask" && call.face !== undefined && PATH_RULE_FACES[rule.tool]?.ask?.includes(call.face) === true && eligiblePersisted(rule, { ...call, tool: rule.tool }, behavior));
926
+ const bind = (rows) => bindPathRules(rows.map((rule) => ({ parsed: rule, ruleText: rule.rule, rule })), (s) => anchorsOf(s.rule));
917
927
  for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
918
928
  if (behavior === "allow") {
919
- if (targets.allow?.kind !== "path" || call.tool !== READ_RULE_TOOL)
929
+ if (views.allow === undefined || call.tool !== READ_RULE_TOOL)
920
930
  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
- }
931
+ const hit = pathRowsReach(bind(rules.filter((rule) => rule.match === "subpath" && eligiblePersisted(rule, call, behavior))), views.allow, "allow");
932
+ if (hit !== undefined)
933
+ return { behavior, rules: [hit.row.spec.rule] };
927
934
  continue;
928
935
  }
929
- const tighten = targets.tighten;
930
- if (tighten === undefined || tighten.kind === "refused")
936
+ if (views.tighten === undefined)
931
937
  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;
938
+ const rows = rules.filter((rule) => (rule.match === "path" || rule.match === "subpath") && speaks(rule, behavior));
939
+ if (rows.length === 0)
940
+ continue;
941
+ const hit = pathRowsReach(rowsWhereTheyLead(bind(rows), leads), views.tighten, "tighten");
942
+ if (hit === undefined)
943
+ continue;
944
+ return hit.unreadable !== undefined ? { behavior: "ask", rules: [hit.row.spec.rule], unreadable: hit.unreadable } : { behavior, rules: [hit.row.spec.rule] };
949
945
  }
950
946
  return undefined;
951
947
  }
@@ -987,9 +983,9 @@ export function segmentCoverageOf(command, rules, call) {
987
983
  (dir !== undefined && firstEligibleDirectoryRule(rules.persisted, dir, call) !== undefined) ||
988
984
  proposed.some((p) => p.rule.tool === "Read"
989
985
  ? dir !== undefined &&
990
- eligibleContext({ tool: p.rule.tool, scope: p.scope }, { tool: "Read", cwd: call.cwd, sessionId: call.sessionId }) &&
986
+ eligibleContext({ tool: p.rule.tool, scope: p.scope }, { ...spelledScope(call), tool: "Read" }) &&
991
987
  directoryRuleAdmits(p.rule, dir, basesOfCwd(call.execCwd ?? call.cwd))
992
- : eligibleContext({ tool: p.rule.tool, scope: p.scope }, call) && ruleAdmitsCommand(p.rule, segment)),
988
+ : eligibleContext({ tool: p.rule.tool, scope: p.scope }, spelledScope(call)) && ruleAdmitsCommand(p.rule, segment)),
993
989
  };
994
990
  });
995
991
  }
@@ -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;
@@ -30,11 +30,13 @@
30
30
  * `Edit(~/.sema/settings.json)` / `Write(src/**)` / `Read(//etc/**)` deny or ask compiles through the same
31
31
  * validator (the model's path grammar — the `//abs-dir/**` directory form and upstream's pattern form:
32
32
  * `//` 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.
33
+ * `**` across) and is judged on the call's PROTECTIVE path target (design/388: the object face's declaration, the
34
+ * catalog's for the name as the floor), every view of it (`callTargetViewsOf`), by the one judgment every path rule
35
+ * meets (`pathRowsReach`). A pattern's ANCHORS are the rule's own fact, taken once where the policy is installed: the
36
+ * declared `pathBases` word by word, else the leg the Runner installs the policy on (`policyAnchors`) — never the
37
+ * call's live working directory. A declared base that is not a usable absolute path is refused at compile time
38
+ * (`unsupported.path_base`); a policy called outside a Runner leg with no declared base reads such a rule
39
+ * `unreadable` (the fail-closed ask).
38
40
  * A `Read(...)` / `Edit(...)` deny or ask reaches the calls on its behavior's FACES (`PATH_RULE_FACES`), not only
39
41
  * its own tool: a `Read` ask asks for a Grep / Glob rooted in its path, judged on that call's own target.
40
42
  *
@@ -53,8 +55,7 @@
53
55
  */
54
56
  import type { NamedToolPolicy } from "./tool-policy.js";
55
57
  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";
58
+ import { type PathRuleBases } from "./permission-rule-model.js";
58
59
  export { parsePermissionRule, type ParsedPermissionRule };
59
60
  /** [K-PLATFORM-SWEEP] MINOR#11: Bash's REAL non-primary arg names, accepted as generic param rules
60
61
  * (`Bash(timeout:...)`). Exported SOLELY so a lockstep test can assert it against the live Bash tool
@@ -93,9 +94,10 @@ export interface PermissionRuleIssue {
93
94
  * is an object/array (a rule on it can never match); the tool is unknown to the resolver a leg
94
95
  * supplied (absent resolver ⇒ the catalog alone judges and an unknown tool stays permissive). */
95
96
  | "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. */
97
+ /** A path-form deny/ask whose spelling needs a base this policy DECLARES as something other than an absolute path
98
+ * of its tree's family (`/x` reads `pathBases.root`; a relative pattern `pathBases.cwd`, else `root`; `~/` `home`)
99
+ * — a bad declared value, refused rather than compiled into a rule that reaches nothing. An undeclared base is no
100
+ * refusal: the rule is anchored where the policy is installed. */
99
101
  | "unsupported.path_base" | "unsupported.mcp_paren"
100
102
  /** A parenthesised rule whose NAME is a covering spelling in a non-MCP protocol namespace
101
103
  * (`a2a__<peer>__*(x:y)`, `a2a__<peer>(x:y)`). The param DSL is keyed by EXACT tool name, and a
@@ -143,16 +145,17 @@ export interface PermissionRulePolicyOptions {
143
145
  */
144
146
  ruleFaces?: (toolName: string) => RuleFaceView | undefined;
145
147
  /**
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
148
+ * The ANCHORS of this rule set's path patterns ({@link PathRuleBases}), word by word: `root` for `/…`, `cwd` for a
149
+ * relative pattern (the declared root stands in when absent — the word's own meaning), `home` for `~/…` (an
150
+ * execution environment's home — never the engine process's own; #644). A word declared here anchors the rules
151
+ * wherever the policy runs; a word left out is anchored where the Runner installs the policy — the root and the
152
+ * environment's home of the leg it enters the delegation chain on — and stays there for every child and every `cd`.
153
+ * A declared value that is not an absolute path of its tree's family is refused at compile time
151
154
  * (`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`).
155
+ * These are the RULES' anchors, never the call's: a call's target is read from the call (its target reading, else its
156
+ * own `root` / `cwd`); only a host calling the policy directly with a request that carries neither has its target
157
+ * resolved against `root` / `cwd` here. A target nothing locates is unreadable: every path rule able to reach the
158
+ * call answers it with the fail-closed ask (`ruleUnreadable`).
156
159
  */
157
160
  pathBases?: PathRuleBases;
158
161
  }
@@ -205,28 +208,7 @@ export interface RuleFaceView {
205
208
  export declare function catalogRuleFaceView(toolName: string): RuleFaceView | undefined;
206
209
  /** Exported for the lockstep guard only (the scalar vocabulary of a catalog tool) — not part of the rule DSL. */
207
210
  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
211
  /** Dry-run 校验/迁移报告:不 throw,返回全部 issues(含 `unsupported.*` 分类,
220
212
  * 供从 CC settings 迁移的部署逐条对照)。 */
221
213
  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
214
  export declare function createPermissionRulePolicy(rules: PermissionRule[], opts?: PermissionRulePolicyOptions): NamedToolPolicy;