@sema-agent/core 7.44.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 (159) hide show
  1. package/CHANGELOG.md +418 -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 +65 -38
  6. package/dist/agents/tool-filter.d.ts +82 -5
  7. package/dist/agents/tool-filter.js +134 -4
  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/agent-definition.d.ts +2 -2
  11. package/dist/core/allow-entry.d.ts +30 -4
  12. package/dist/core/allow-entry.js +32 -5
  13. package/dist/core/ask-origin.d.ts +23 -4
  14. package/dist/core/ask-origin.js +11 -5
  15. package/dist/core/checkpoint-store.d.ts +41 -4
  16. package/dist/core/decision-receipt.js +1 -1
  17. package/dist/core/effective-path-target.d.ts +143 -10
  18. package/dist/core/effective-path-target.js +113 -9
  19. package/dist/core/engine-notice.d.ts +25 -3
  20. package/dist/core/engine-notice.js +12 -6
  21. package/dist/core/gate-fold.js +3 -0
  22. package/dist/core/gate-lanes.js +11 -3
  23. package/dist/core/gate-outcome.d.ts +5 -0
  24. package/dist/core/gate-outcome.js +2 -0
  25. package/dist/core/governance-codes.d.ts +3 -1
  26. package/dist/core/governance-codes.js +3 -0
  27. package/dist/core/hooks.d.ts +5 -0
  28. package/dist/core/memory-engine/content-origin.d.ts +0 -34
  29. package/dist/core/memory-engine/content-origin.js +9 -4
  30. package/dist/core/memory-engine/layout.d.ts +5 -1
  31. package/dist/core/memory-engine/layout.js +1 -0
  32. package/dist/core/path-rule-binding.d.ts +65 -0
  33. package/dist/core/path-rule-binding.js +99 -0
  34. package/dist/core/permission-rule-model.d.ts +47 -18
  35. package/dist/core/permission-rule-model.js +38 -42
  36. package/dist/core/permission-rule-provider.d.ts +7 -0
  37. package/dist/core/permission-rule-provider.js +10 -3
  38. package/dist/core/permission-rule-store.d.ts +8 -0
  39. package/dist/core/permission-rule-store.js +6 -2
  40. package/dist/core/permission-rule-sync.js +2 -2
  41. package/dist/core/permission-rules.d.ts +22 -36
  42. package/dist/core/permission-rules.js +50 -40
  43. package/dist/core/real-approval.d.ts +9 -6
  44. package/dist/core/real-approval.js +5 -5
  45. package/dist/core/runner/child-tree.d.ts +110 -0
  46. package/dist/core/runner/child-tree.js +107 -0
  47. package/dist/core/runner/construction-doors.d.ts +5 -0
  48. package/dist/core/runner/construction-doors.js +6 -0
  49. package/dist/core/runner/contracts.d.ts +54 -2
  50. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  51. package/dist/core/runner/permission-rule-lanes.js +18 -12
  52. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  53. package/dist/core/runner/persisted-path-rows.js +31 -12
  54. package/dist/core/runner/prepare-ask-lane.d.ts +6 -0
  55. package/dist/core/runner/prepare-ask-lane.js +4 -2
  56. package/dist/core/runner/prepare-config-doors.js +3 -1
  57. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  58. package/dist/core/runner/prepare-execution-env.js +11 -3
  59. package/dist/core/runner/prepare-gate-stations.d.ts +6 -0
  60. package/dist/core/runner/prepare-gate-stations.js +3 -1
  61. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  62. package/dist/core/runner/prepare-hands-readface.js +6 -6
  63. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  64. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  65. package/dist/core/runner/prepare-listings.d.ts +4 -4
  66. package/dist/core/runner/prepare-listings.js +2 -22
  67. package/dist/core/runner/prepare-park-ask.d.ts +5 -0
  68. package/dist/core/runner/prepare-park-ask.js +6 -4
  69. package/dist/core/runner/prepare-policy-chain.d.ts +16 -2
  70. package/dist/core/runner/prepare-policy-chain.js +14 -5
  71. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  72. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  73. package/dist/core/runner/prepare-task.js +41 -18
  74. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  75. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  76. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  77. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  78. package/dist/core/runner/resume-apply.js +17 -3
  79. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  80. package/dist/core/runner/resume-policy-outcome.js +2 -1
  81. package/dist/core/runner/run-leg.js +5 -1
  82. package/dist/core/runner/run-notification-lane.js +2 -2
  83. package/dist/core/runner/runtask.d.ts +2 -0
  84. package/dist/core/runner/runtask.js +4 -2
  85. package/dist/core/runner/stream-steer-verb.js +2 -2
  86. package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
  87. package/dist/core/runner/sub-agent-deny-lists.js +121 -0
  88. package/dist/core/runner/synthetic-tools.d.ts +1 -1
  89. package/dist/core/runner/synthetic-tools.js +3 -2
  90. package/dist/core/runner/write-protection-station.d.ts +3 -2
  91. package/dist/core/runner/write-protection-station.js +1 -0
  92. package/dist/core/runner-deps.d.ts +14 -0
  93. package/dist/core/sensitive-path-policy.d.ts +8 -0
  94. package/dist/core/sensitive-path-policy.js +18 -2
  95. package/dist/core/session-policy-store.d.ts +6 -2
  96. package/dist/core/session-policy-store.js +2 -1
  97. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  98. package/dist/core/shell-wrapper-projection.js +56 -0
  99. package/dist/core/shell-wrapper-table.d.ts +144 -34
  100. package/dist/core/shell-wrapper-table.js +40 -20
  101. package/dist/core/skill-grant.d.ts +10 -2
  102. package/dist/core/skill-grant.js +20 -4
  103. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  104. package/dist/core/synthetic-read-verdict.js +3 -1
  105. package/dist/core/task-notification.d.ts +12 -1
  106. package/dist/core/task-notification.js +13 -1
  107. package/dist/core/task-spec.d.ts +5 -3
  108. package/dist/core/tool-catalog-entries.d.ts +21 -0
  109. package/dist/core/tool-catalog-entries.js +9 -0
  110. package/dist/core/tool-policy.d.ts +45 -12
  111. package/dist/core/tool-policy.js +18 -20
  112. package/dist/core/tool-spec.d.ts +23 -4
  113. package/dist/core/types.d.ts +1 -1
  114. package/dist/core/types.js +1 -1
  115. package/dist/core/write-protect.d.ts +4 -0
  116. package/dist/core/write-protect.js +30 -9
  117. package/dist/index.d.ts +10 -6
  118. package/dist/index.js +9 -5
  119. package/dist/orchestration/run-spec.js +8 -7
  120. package/dist/orchestration/workflow.js +3 -2
  121. package/dist/shell/lex.d.ts +7 -0
  122. package/dist/shell/lex.js +17 -0
  123. package/dist/tools/fs/bash-program-position.d.ts +8 -87
  124. package/dist/tools/fs/bash-program-position.js +33 -69
  125. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  126. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  127. package/dist/tools/fs/index.d.ts +3 -2
  128. package/dist/tools/fs/program-read-model.d.ts +694 -0
  129. package/dist/tools/fs/program-read-model.js +1252 -0
  130. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  131. package/dist/tools/fs/program-read-model.rows.js +613 -0
  132. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  133. package/dist/tools/fs/read-deny-home.js +62 -24
  134. package/dist/tools/fs/read-deny.d.ts +54 -14
  135. package/dist/tools/fs/read-deny.js +104 -50
  136. package/dist/tools/fs/removal-targets.d.ts +83 -0
  137. package/dist/tools/fs/removal-targets.js +236 -0
  138. package/dist/tools/fs/safety.d.ts +3 -0
  139. package/dist/tools/fs/safety.js +10 -3
  140. package/dist/tools/fs/shell-read-argv.d.ts +85 -0
  141. package/dist/tools/fs/shell-read-argv.js +386 -0
  142. package/dist/tools/fs/shell-read-bindings.d.ts +273 -0
  143. package/dist/tools/fs/shell-read-bindings.js +746 -0
  144. package/dist/tools/fs/shell-read-check.js +23 -15
  145. package/dist/tools/fs/shell-read-establish.js +79 -5
  146. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  147. package/dist/tools/fs/shell-read-judge.js +9 -1
  148. package/dist/tools/fs/shell-read-plan.d.ts +31 -7
  149. package/dist/tools/fs/shell-read-plan.js +299 -393
  150. package/dist/tools/fs/shell-read-word.d.ts +29 -26
  151. package/dist/tools/fs/shell-read-word.js +30 -38
  152. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -37
  153. package/dist/tools/fs/shell-read-write-targets.js +83 -136
  154. package/dist/tools/fs/shell-reentry-text.d.ts +38 -6
  155. package/dist/tools/fs/shell-reentry-text.js +90 -53
  156. package/package.json +1 -1
  157. package/test/export-surface.snapshot.json +49 -3
  158. package/wire-changes/core@7.45.0.json +1084 -0
  159. package/wire-changes/core@7.46.0.json +1522 -0
@@ -0,0 +1,110 @@
1
+ import type { RunnerDeps } from "../types.js";
2
+ import type { ExecutionEnv } from "../../internal/harness.js";
3
+ import type { PathRuleBases } from "../permission-rule-model.js";
4
+ import type { ChildPlacement, InheritedGate, RunInternals } from "./contracts.js";
5
+ import type { ToolPolicy } from "../tool-policy.js";
6
+ import { CheckpointError } from "../checkpoint-store.js";
7
+ /**
8
+ * Whether this deployment can place a child agent in a tree OTHER than the caller's — the one fact behind
9
+ * every request of that shape: an explicit `cwd`, a workflow agent's `isolation: "worktree"`, and the
10
+ * delegation tool's per-call or per-definition `isolation: "worktree"`.
11
+ *
12
+ * Only an `executionEnvFactory` can: it builds one execution env per task, so a child can be rooted
13
+ * wherever the request says. A static `executionEnv` is ONE shared env with ONE cwd — every child that
14
+ * runs on it runs in the caller's root, whatever tree it was asked to work in. A request the deployment
15
+ * cannot honor is refused before the child starts (and, for a worktree, before one is minted); each site
16
+ * keeps its own code and sentence for the refusal, but none decides the condition itself.
17
+ */
18
+ export declare function deploymentCanRerootChild(deps: Pick<RunnerDeps, "executionEnvFactory">): boolean;
19
+ /**
20
+ * Where a child the caller asked to place at `asked` actually landed (`childRoot`, the root its environment gave it) — the
21
+ * ONE judgment every reader of that fact takes
22
+ * (the delegation tool's worktree request, and the anchors of the rules the child inherits):
23
+ * · `honored` — the environment's root is where the child was asked to be (canonical on both sides);
24
+ * · `caller_tree` — it is not, and it lies inside the caller's own tree (`callerRoot`): the same filesystem, the wrong
25
+ * directory — a worktree request then fails (the child would write the caller's checkout);
26
+ * · `elsewhere` — neither: another filesystem namespace (a sandbox the factory minted). The child runs there, it is not
27
+ * in the worktree it was offered, and it takes the caller's path rules again at its own anchors.
28
+ * A path the environment cannot canonicalize is compared as spelled. Residual: two namespaces that hold the same path
29
+ * string, with a factory claiming to honor it, read `honored`.
30
+ */
31
+ export declare function childPlacementOf(env: Pick<ExecutionEnv, "canonicalPath">, childRoot: string, asked: string | undefined, callerRoot: string): Promise<ChildPlacement>;
32
+ /** A delegated leg's placement: the place asked for is the hint (a minted worktree, an explicit cwd, the caller's own cwd)
33
+ * — or, for a worktree the factory mints itself (`isolation`), that worktree on the caller's host (a remote environment's
34
+ * is another namespace). A leg no delegation caller placed (no `callerRoot`) has no placement and reads nothing. */
35
+ export declare function childPlacementOnLeg(env: ExecutionEnv, internals: Pick<RunInternals, "isolation" | "parentCwd"> & {
36
+ readonly callerRoot: string;
37
+ }, root: string): Promise<ChildPlacement>;
38
+ /** The refusal of a worktree the caller minted for a child its environment placed in the caller's own tree. */
39
+ export declare function worktreeNotHonored(cwd: string): Error & {
40
+ code: "isolation.unavailable";
41
+ };
42
+ /**
43
+ * The chain's top leg's anchors as THIS leg reads them (the person's global / session path rows and every `~/` row anchor
44
+ * there): the chain's own (`live` as recorded, else the checkpoint's `seed` rebased — {@link recordedAnchors}), unless this
45
+ * leg is a child placed `elsewhere` or no chain names one — then this leg is the top. A recorded root that is not an
46
+ * absolute path anchors nothing: the rows it would anchor read `unreadable`.
47
+ */
48
+ export declare function chainTopAnchors(cfg: {
49
+ leg: PathRuleBases;
50
+ live: InheritedGate["pathRuleAnchors"];
51
+ seed: InheritedGate["pathRuleAnchors"];
52
+ placement: ChildPlacement | undefined;
53
+ rebase: (p: string) => string;
54
+ }): PathRuleBases;
55
+ /** The installation records a leg reads: the live chain's (as recorded), else the checkpoint's (through the workspace rebase). */
56
+ export interface ChainInstallations {
57
+ readonly live: InheritedGate["pathRuleInstallations"];
58
+ readonly seed: InheritedGate["pathRuleInstallations"];
59
+ }
60
+ /** Why no usable record says where a chain entry was installed: no record (a chain written before the record existed), a
61
+ * record that does not cover the chain position for position, or a member that names no absolute root. */
62
+ export type ChainInstallationGap = "absent" | "misaligned" | "invalid";
63
+ /**
64
+ * The chain entries a leg could not install where they were installed: each UNINSTALLED entry (a host re-supplying a
65
+ * chain) whose path rules depend on the installing leg, with no usable record of that leg. Such a chain is refused —
66
+ * installing those entries anywhere else (the chain's top leg, this leg) would move what they protect. Entries whose rules
67
+ * do not depend on it, and installed entries, never need a record.
68
+ */
69
+ export declare function chainInstallationGaps(chain: readonly ToolPolicy[], installations: ChainInstallations, rebase: (p: string) => string): Array<{
70
+ readonly position: number;
71
+ readonly gap: ChainInstallationGap;
72
+ }>;
73
+ /** The refusal of a chain with {@link chainInstallationGaps} (`resume.parent_constraint_mismatch`, `detail.reason` naming
74
+ * the first gap's form); `pending` says the checkpoint is still pending (the refusal came before the claim). */
75
+ export declare function chainInstallationRefusal(gaps: ReadonlyArray<{
76
+ readonly position: number;
77
+ readonly gap: ChainInstallationGap;
78
+ }>, pending: boolean): CheckpointError;
79
+ /**
80
+ * How a leg holds a caller policy — its own (`TaskSpec.toolPolicy` / `RunnerDeps.toolPolicy`) or one its delegation chain
81
+ * hands it: a policy is installed where it ENTERS the chain, and a child holds its ancestor's installation (one object,
82
+ * the same anchors) — its own caller slot included when that slot names the policy an ancestor installed. Installed again:
83
+ * on a child placed `elsewhere` (another filesystem namespace — the ancestor's anchors name another namespace's files), at
84
+ * this leg's anchors; a chain entry handed uninstalled (a host re-supplying a chain), where its installing leg recorded it
85
+ * (`InheritedGate.pathRuleInstallations`, live or from the checkpoint). An uninstalled entry whose rules depend on that
86
+ * leg and whose record is not usable refuses the leg ({@link chainInstallationRefusal}); one whose rules do not, reads
87
+ * the same wherever it is installed (the chain's top leg).
88
+ */
89
+ export declare function chainPolicyOnLeg(cfg: {
90
+ leg: PathRuleBases;
91
+ chainTop: PathRuleBases;
92
+ placement: ChildPlacement | undefined;
93
+ chain: readonly ToolPolicy[];
94
+ installations: ChainInstallations;
95
+ rebase: (p: string) => string;
96
+ }): {
97
+ own: (policy: ToolPolicy | undefined) => ToolPolicy | undefined;
98
+ chained: (policy: ToolPolicy, position: number) => ToolPolicy;
99
+ };
100
+ /** The installation record of a chain entry (its root and home), `null` for an entry with no path rules installed. */
101
+ export declare function installationRecordOf(policy: ToolPolicy): {
102
+ readonly root: string;
103
+ readonly home?: string;
104
+ } | null;
105
+ /** What a leg hands its children about anchors (data half): the chain's top leg's anchors — the person's global / session
106
+ * rows and every `~/` row anchor there — and where each entry handed down was installed, position for position (a chain
107
+ * re-supplied uninstalled is installed there again; absent when no entry is an installation). */
108
+ export declare function chainAnchorRecords(chainTop: PathRuleBases, chain: readonly {
109
+ readonly policy: ToolPolicy;
110
+ }[]): Pick<InheritedGate, "pathRuleAnchors" | "pathRuleInstallations">;
@@ -0,0 +1,107 @@
1
+ import { isRemoteExecutionEnv } from "../remote-env.js";
2
+ import { isAbsolutePathForm } from "../../tools/fs/safety.js";
3
+ import { installPolicyAnchors, installationAnchorsOf, installedFrom } from "../path-rule-binding.js";
4
+ import { CheckpointError } from "../checkpoint-store.js";
5
+ export function deploymentCanRerootChild(deps) {
6
+ return deps.executionEnvFactory !== undefined;
7
+ }
8
+ export async function childPlacementOf(env, childRoot, asked, callerRoot) {
9
+ const canon = async (p) => {
10
+ try {
11
+ const r = await env.canonicalPath(p);
12
+ return r.ok ? r.value : p;
13
+ }
14
+ catch {
15
+ return p;
16
+ }
17
+ };
18
+ const root = await canon(childRoot);
19
+ if (asked !== undefined && (await canon(asked)) === root)
20
+ return "honored";
21
+ const tree = await canon(callerRoot);
22
+ const sep = tree.includes("\\") && !tree.includes("/") ? "\\" : "/";
23
+ return root === tree || root.startsWith(tree.endsWith(sep) ? tree : tree + sep) ? "caller_tree" : "elsewhere";
24
+ }
25
+ export function childPlacementOnLeg(env, internals, root) {
26
+ const asked = internals.isolation === "worktree" ? (isRemoteExecutionEnv(env) ? undefined : root) : internals.parentCwd;
27
+ return childPlacementOf(env, root, asked, internals.callerRoot);
28
+ }
29
+ export function worktreeNotHonored(cwd) {
30
+ return Object.assign(new Error(`isolation "worktree" was requested but cannot be honored: the execution environment factory placed the sub-agent at "${cwd}", inside the caller's own tree, instead of the worktree it was offered. The sub-agent was not started (fail-closed — no silent fallback to the shared working tree).`), { code: "isolation.unavailable" });
31
+ }
32
+ const AS_RECORDED = (p) => p;
33
+ function recordedAnchors(r, rebase) {
34
+ if (r === null || r === undefined)
35
+ return undefined;
36
+ const read = (v) => {
37
+ if (typeof v !== "string")
38
+ return undefined;
39
+ const p = rebase(v);
40
+ return isAbsolutePathForm(p) ? p : undefined;
41
+ };
42
+ const [root, home] = [read(r.root), read(r.home)];
43
+ return { ...(root !== undefined ? { root } : {}), ...(home !== undefined ? { home } : {}) };
44
+ }
45
+ export function chainTopAnchors(cfg) {
46
+ if (cfg.placement === "elsewhere")
47
+ return cfg.leg;
48
+ return (cfg.live !== undefined ? recordedAnchors(cfg.live, AS_RECORDED) : recordedAnchors(cfg.seed, cfg.rebase)) ?? cfg.leg;
49
+ }
50
+ const GAP_REASON = { absent: "installation_record_absent", misaligned: "installation_record_misaligned", invalid: "installation_record_invalid" };
51
+ function dependsOnInstallation(policy) {
52
+ if (installedFrom(policy) !== undefined)
53
+ return false;
54
+ return typeof policy.pathRules === "function" && policy.pathRules({}).some((row) => row.unanchored !== undefined);
55
+ }
56
+ function recordedInstallation(installations, chainLength, position, rebase) {
57
+ const [record, read] = installations.live !== undefined ? [installations.live, AS_RECORDED] : [installations.seed, rebase];
58
+ if (record === undefined)
59
+ return "absent";
60
+ if (record.length !== chainLength)
61
+ return "misaligned";
62
+ const a = recordedAnchors(record[position], read);
63
+ return a?.root === undefined ? "invalid" : a;
64
+ }
65
+ export function chainInstallationGaps(chain, installations, rebase) {
66
+ return chain.flatMap((policy, position) => {
67
+ if (!dependsOnInstallation(policy))
68
+ return [];
69
+ const r = recordedInstallation(installations, chain.length, position, rebase);
70
+ return typeof r === "string" ? [{ position, gap: r }] : [];
71
+ });
72
+ }
73
+ export function chainInstallationRefusal(gaps, pending) {
74
+ const first = gaps[0];
75
+ const form = { absent: "absent (a chain written before installation records existed, or one built without them)", misaligned: "misaligned (it does not cover the chain entry for entry)", invalid: "invalid for that entry (it names no absolute root)" }[first.gap];
76
+ return new CheckpointError("resume.parent_constraint_mismatch", `the parent-constraint chain carries ${gaps.length} polic${gaps.length === 1 ? "y" : "ies"} (entry ${gaps.map((g) => g.position).join(", ")}) whose path rules resolve against the leg that installed ${gaps.length === 1 ? "it" : "them"}, and the installation record is ${form} — installing ${gaps.length === 1 ? "it" : "them"} anywhere else would move what ${gaps.length === 1 ? "it protects" : "they protect"}, so the chain is refused` +
77
+ (pending ? " before the claim (the checkpoint stays pending). A task parked before 7.46.0 under such a chain cannot be resumed by this version: let a worker of the version that parked it finish it, or let it expire and submit the task again." : "."), { reason: GAP_REASON[first.gap] });
78
+ }
79
+ export function chainPolicyOnLeg(cfg) {
80
+ if (cfg.placement !== "elsewhere") {
81
+ const gaps = chainInstallationGaps(cfg.chain, cfg.installations, cfg.rebase);
82
+ if (gaps.length > 0)
83
+ throw chainInstallationRefusal(gaps, false);
84
+ }
85
+ const recorded = (position) => {
86
+ const r = recordedInstallation(cfg.installations, cfg.chain.length, position, cfg.rebase);
87
+ return typeof r === "string" ? undefined : r;
88
+ };
89
+ const chained = (policy, position) => (cfg.placement === "elsewhere" ? installPolicyAnchors(policy, cfg.leg) : installedFrom(policy) !== undefined ? policy : installPolicyAnchors(policy, recorded(position) ?? cfg.chainTop)) ?? policy;
90
+ const own = (policy) => {
91
+ if (policy === undefined)
92
+ return undefined;
93
+ const position = cfg.placement === "elsewhere" ? -1 : cfg.chain.findIndex((c) => (installedFrom(c) ?? c) === (installedFrom(policy) ?? policy));
94
+ return position >= 0 ? chained(cfg.chain[position], position) : installPolicyAnchors(policy, cfg.leg);
95
+ };
96
+ return { own, chained };
97
+ }
98
+ export function installationRecordOf(policy) {
99
+ const a = installationAnchorsOf(policy);
100
+ return a?.root === undefined ? null : { root: a.root, ...(a.home !== undefined ? { home: a.home } : {}) };
101
+ }
102
+ export function chainAnchorRecords(chainTop, chain) {
103
+ return {
104
+ ...(chainTop.root !== undefined ? { pathRuleAnchors: { root: chainTop.root, ...(chainTop.home !== undefined ? { home: chainTop.home } : {}) } } : {}),
105
+ ...(chain.some((pc) => installationAnchorsOf(pc.policy)?.root !== undefined) ? { pathRuleInstallations: chain.map((pc) => installationRecordOf(pc.policy)) } : {}),
106
+ };
107
+ }
@@ -0,0 +1,5 @@
1
+ import type { RunnerDeps } from "../types.js";
2
+ /** The retired-seat door ({@link assertNoRetiredRunnerDeps}), then the seats whose value is screened at construction and
3
+ * never again: the declared configuration root must be the absolute path of an existing directory — a root that names no
4
+ * directory guards nothing, so it is refused here rather than read as absent. */
5
+ export declare function assertRunnerDepsDoors(deps: RunnerDeps): void;
@@ -0,0 +1,6 @@
1
+ import { checkedAgentConfigDir } from "../agent-config-dir.js";
2
+ import { assertNoRetiredRunnerDeps } from "./retired-runner-deps.js";
3
+ export function assertRunnerDepsDoors(deps) {
4
+ assertNoRetiredRunnerDeps(deps);
5
+ checkedAgentConfigDir(deps.agentConfigDir, "RunnerDeps.agentConfigDir");
6
+ }
@@ -31,7 +31,7 @@ import type { MaterializedA2a } from "../a2a.js";
31
31
  import type { CompactionForkContext, CompactionPhaseDurations, MaybeCompactOptions, RapidRefillState } from "../auto-compaction.js";
32
32
  import type { AutoModeArmingRecipe } from "../auto-mode-arming.js";
33
33
  import type { AutoModeDecider, AutoModeDenialTracker, DenialLimitFallbackFace } from "../auto-mode.js";
34
- import type { SettlementKind } from "../gate-outcome.js";
34
+ import type { HostDecider, SettlementKind } from "../gate-outcome.js";
35
35
  import type { CacheBreakDetector, ToolFingerprintInput } from "../cache-break-detector.js";
36
36
  import type { Checkpoint, CheckpointState, CheckpointStore, CheckpointToken, PendingSteerEntry, PlatformLimitReason, ReopenReason, ResourceLedger, ResourceLimitReason, ResumeOutcome, SerializedCheckpointState } from "../checkpoint-store.js";
37
37
  import type { ExecutionOutcomeRecordWord } from "../checkpoint-execution-record.js";
@@ -391,6 +391,10 @@ export interface Prepared {
391
391
  /** The per-task logical cwd ref when a real shell is mounted (else undefined). The Runner
392
392
  * reads `cwdRef.current` after each tool to detect a `cd` move and emit `workspace_changed`. */
393
393
  cwdRef?: CwdRef;
394
+ /** The root the hand tools resolve root-relative spellings against (present with hands; read-only hands included). */
395
+ handsRoot?: string;
396
+ /** The leg's target-reading stamp (present with hands): the resume belts stamp the executed bytes' request with it. */
397
+ stampCallTarget?: import("../effective-path-target.js").CallTargetStamp;
394
398
  /** The run's TOOL-PROGRESS lane seat, late-bound for the same reason `harnessRef` is (the tool band is
395
399
  * built during prepare; the lane needs `pushContent` and `ident`, which exist only after it returns).
396
400
  * The driver binds `current` right after `pushContent` exists. Always present on a completed prepare —
@@ -442,7 +446,7 @@ export interface Prepared {
442
446
  toolCallId: string;
443
447
  toolName: string;
444
448
  args: unknown;
445
- decidedBy: "person" | "sla_timeout" | "policy";
449
+ decidedBy: HostDecider;
446
450
  }) => Promise<{
447
451
  readonly refused: string;
448
452
  } | {
@@ -1415,6 +1419,29 @@ export interface InheritedGate {
1415
1419
  * had a governance surface (their opaque posture is untouched). Serializable; persists on checkpoints.
1416
1420
  */
1417
1421
  orgAdmissionGoverned?: true;
1422
+ /**
1423
+ * The chain's TOP leg's root and environment home (data half, serializable; persists on the checkpoint's data half):
1424
+ * the anchors of the person's global and session path rows and of every `~/` row on this chain — a child judges them
1425
+ * where the chain was installed, not at its own root (a project row anchors at its own scope root). Emitted by every leg
1426
+ * that hands a chain down (a top leg emits its own). Absent on an older chain ⇒ the child is its own top. A child placed
1427
+ * in ANOTHER filesystem namespace (its environment did not put it where it was asked, outside the caller's tree) ignores
1428
+ * it and takes its own leg as the top.
1429
+ */
1430
+ pathRuleAnchors?: {
1431
+ readonly root: string;
1432
+ readonly home?: string;
1433
+ };
1434
+ /**
1435
+ * Where each `parentConstraints` entry was INSTALLED (data half, serializable; persists on the checkpoint's data half):
1436
+ * position for position with `parentConstraints`, the root and home its path rules are anchored at (`null` for an entry
1437
+ * with no path rules). A chain re-supplied with uninstalled policies (a resume in another process, a host-built chain)
1438
+ * installs each where this records — never at another leg's anchors. Absent on an older chain: an uninstalled entry is
1439
+ * installed at the chain's top leg (`pathRuleAnchors`).
1440
+ */
1441
+ pathRuleInstallations?: ReadonlyArray<{
1442
+ readonly root: string;
1443
+ readonly home?: string;
1444
+ } | null>;
1418
1445
  /** Live ancestor caller-policy constraints (opaque half), ancestors first. `policy` is the ancestor's
1419
1446
  * RESOLVED caller policy (`spec.toolPolicy ?? deps.toolPolicy` — the same slot the ancestor itself
1420
1447
  * enforces, so a deps-level baseline is inherited too).
@@ -2135,6 +2162,20 @@ export interface RunInternals {
2135
2162
  * callers (ctx-injection posture, same as `parentCwd`).
2136
2163
  */
2137
2164
  requestedCwd?: string;
2165
+ /**
2166
+ * The SPAWNING run's own root, filled ONLY by core delegation callers beside `parentCwd` (ctx-injection posture): with
2167
+ * it the child's prepare judges where its environment actually placed it against where it was asked
2168
+ * (`childPlacementOf`, child-tree.ts: honored / inside the caller's tree / elsewhere) — the fact the
2169
+ * worktree request below and the anchors of the rules the child inherits read. Absent ⇒ no placement is judged (a top
2170
+ * leg, a caller that does not delegate through core).
2171
+ */
2172
+ callerRoot?: string;
2173
+ /**
2174
+ * `parentCwd` is a worktree the caller minted for THIS child (the delegation tool's `isolation: "worktree"`): a child
2175
+ * its environment placed inside the caller's own tree instead is refused before it starts (`isolation.unavailable`).
2176
+ * Filled ONLY by the core delegation tool; never a {@link TaskSpec} field.
2177
+ */
2178
+ worktreeOffered?: true;
2138
2179
  /**
2139
2180
  * Subagent steer verb (dogfood finding 2026-07-03, "中途插话"): the host run's opt-in
2140
2181
  * SUBAGENT-STEER-HANDLE sink. When set, `createSubagentTool` runs each child via `runTaskStream`
@@ -2235,6 +2276,10 @@ export interface RunInternals {
2235
2276
  * non-isolated run reports its plain root with `isolated: false` — the observation face stays complete, while
2236
2277
  * the honest non-claim keeps a consumer from mistaking a SHARED tree for its own worktree.
2237
2278
  */
2279
+ /** Where a delegated child's environment placed it against where its caller asked (`childPlacementOf`, child-tree.ts):
2280
+ * `honored` — where it was asked; `caller_tree` — elsewhere inside the caller's own tree; `elsewhere` — another
2281
+ * filesystem namespace. */
2282
+ export type ChildPlacement = "honored" | "caller_tree" | "elsewhere";
2238
2283
  export interface ResolvedWorkspace {
2239
2284
  /** The task's effective working root — the same value every fence / LSP / prompt consumer uses. */
2240
2285
  cwd: string;
@@ -2245,6 +2290,9 @@ export interface ResolvedWorkspace {
2245
2290
  * or names an unrelated directory — on the host. `isolated && !remote` is the only combination under
2246
2291
  * which `cwd` is a host path a consumer may diff / merge / remove. */
2247
2292
  remote: boolean;
2293
+ /** Where a delegated child's environment placed it against where its caller asked; absent on a leg no delegation
2294
+ * caller placed. */
2295
+ placement?: ChildPlacement;
2248
2296
  }
2249
2297
  /**
2250
2298
  * The resolver's answer, with the WITHHELD cause beside the value.
@@ -2748,6 +2796,10 @@ export interface RunnerSelfSeat {
2748
2796
  toolPolicy?: RunnerDeps["toolPolicy"];
2749
2797
  hooks?: RunnerDeps["hooks"];
2750
2798
  };
2799
+ /** Whether a child task THIS runner runs can be placed in a tree other than the caller's (an explicit cwd, a worktree):
2800
+ * true only when its deps carry an `executionEnvFactory` (live read — `swapDeps` replaces the deps). The delegation tool asks
2801
+ * the runner that will run the child, not the one that ran the parent leg. Absent on a hand-built seat ⇒ read as "cannot say" ⇒ no; a hand-built seat answers for the runner its `runTask` forwards to. */
2802
+ canRerootChild?(): boolean;
2751
2803
  /** Fire `RunnerDeps.onTaskOutcome` through the swallow-guarded chokepoint (thin compositions emit at their terminal state). */
2752
2804
  emitTaskOutcome(outcome: TaskOutcome): void;
2753
2805
  /** Run a task to completion. `internals` is the TRUSTED run-scoped channel (never a TaskSpec field). */
@@ -28,8 +28,10 @@
28
28
  import type { AskRuleEvidence, ToolCallRequest } from "../tool-policy.js";
29
29
  import { persistedRuleMandateOf, type RuleClearanceFacts } from "../ask-origin.js";
30
30
  import { type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit, type PersistedRuleUnreadable } from "../hooks.js";
31
- import type { PersistedRule, PersistedRuleVerdict, RuleOffer, SegmentCoverage } from "../permission-rule-model.js";
32
- import { type ToolCallLike } from "../tool-registry.js";
31
+ import type { PersistedRule, PersistedRuleVerdict, RuleOffer, ScopeRootIdentity, SegmentCoverage } from "../permission-rule-model.js";
32
+ import { type PathRuleBases } from "../permission-rule-model.js";
33
+ import { type HeadLeads } from "../path-rule-binding.js";
34
+ import type { ToolCallLike } from "../tool-registry.js";
33
35
  import { type OrgRuleResolution } from "../permission-rule-org.js";
34
36
  import { type EffectivePermissionRules, type PermissionRuleStoreProvider } from "../permission-rule-provider.js";
35
37
  /** The one SHELL tool the persisted-rule lane's COMMAND grammar speaks for (the rule-offer face mints for it and the
@@ -87,28 +89,25 @@ export declare function persistedRuleHitOf(verdict: PersistedRuleVerdict | undef
87
89
  * question nobody can answer. Every other verdict is {@link persistedRuleHitOf}'s hit. */
88
90
  export declare function persistedRuleAnswerOf(verdict: PersistedRuleVerdict | undefined): PersistedRuleHit | PersistedRuleUnreadable | undefined;
89
91
  /**
90
- * Widened to every path-targeting tool — the persisted-rule lane's PATH arm: the call's
91
- * target path in the SAME lexical identity the rule family is defined over, resolved by the ONE reader every
92
- * fence and every tool goes through (`effectivePathTargetOf`), so the lane judges the path the tool will
93
- * really open. The tool's own declaration decides which base a relative spelling takes and what an ABSENT
94
- * slot means — the lane no longer guesses `liveCwd ?? root` for every tool, which was wrong for the two
95
- * search tools in three separate shapes (see effective-path-target.ts). Still zero IO.
92
+ * The persisted-rule lane's PATH arm, for every path-targeting tool (and the entry lists' path rows): the call's target
93
+ * read through the ONE reader every lane shares ({@link callTargetViewsOf} — the reading's views, else the call's own
94
+ * lexical view), its rows anchored by `ctx.anchorsOf` (a stored row: a project row at its scope root, the rest at the
95
+ * chain's top leg; a list's row: where the list was installed) and judged where their heads lead too (`ctx.leads`). The
96
+ * precedence walk, the admission predicates and the Read-only allow are the model's (`adjudicatePersistedPathRules`); which
97
+ * project rows apply is judged against the leg's own root (`ctx.root`). An ALLOW here clears ASKS only — the read fences
98
+ * judge inside the tool exactly as they would with no rule anywhere — and an unresolvable path is no verdict on the allow
99
+ * side and the fail-closed ask on the tightening side.
96
100
  *
97
- * TWO readings of the call's path slot, one per direction: the tightening arms judge
98
- * the PROTECTIVE target (the object face's declaration, the catalog's for the name as the floor — reading
99
- * the catalog for an undeclared same-name tool can only tighten a deny); the allow arm judges the DECLARED
100
- * target (the face's OWN declaration only — a same-name tool that declares no path target is unconfirmable
101
- * and no grant admits it). The precedence walk, the admission predicates and the Read-only allow are the
102
- * model's (`adjudicatePersistedPathRules`). An ALLOW here clears ASKS only — the read fences (workspace
103
- * containment, the sensitive-read deny face) judge inside the tool exactly as they would with no rule
104
- * anywhere — and an unresolvable path is simply no verdict (fail toward asking on the allow side, no
105
- * refusal on the tightening side: a rule about a path speaks about no other shape).
101
+ * TWO readings of the call's path slot, one per direction: the tightening arms judge the PROTECTIVE reading (the object
102
+ * face's declaration, the catalog's for the name as the floor); the allow arm the DECLARED one (the face's OWN
103
+ * declaration only — a same-name tool that declares no path target is unconfirmable and no grant admits it).
106
104
  */
107
- export declare function pathRuleLaneAnswer(table: readonly PersistedRule[], req: Pick<ToolCallRequest, "toolName" | "args" | "face">, ctx: {
105
+ export declare function pathRuleLaneAnswer(table: readonly PersistedRule[], req: Pick<ToolCallRequest, "toolName" | "args" | "face" | "targetReading" | "root" | "cwd">, ctx: {
108
106
  root: string | undefined;
109
107
  sessionId: string | undefined;
110
- liveCwd: string | undefined;
111
- home: string | undefined;
108
+ anchorsOf: (rule: PersistedRule) => PathRuleBases;
109
+ leads: ReadonlyMap<string, string | undefined>;
110
+ scopeIdentity?: ScopeRootIdentity;
112
111
  }): PersistedRuleHit | PersistedRuleUnreadable | undefined;
113
112
  /** The two lanes prepare hands the gate. Each is `undefined` when its partition is not configured, so
114
113
  * a deployment without one keeps a decision path byte-identical to a build without the feature. */
@@ -152,15 +151,23 @@ export declare function createPermissionRuleLanes(cfg: {
152
151
  principal: string | undefined;
153
152
  sessionId: string;
154
153
  /** The isolation-aware task root — the same value the file fence, the prompt cwd and the session rules
155
- * use, so a project-scoped rule agrees with every other place that says "this project". */
154
+ * use, so a project-scoped rule agrees with every other place that says "this project" (which project rows apply). */
156
155
  root: string;
156
+ /** The same root's canonical spelling (`undefined`: none could be read): a project row applies when its scope root covers
157
+ * either spelling of it — the row's root resolved where it leads ({@link projectScopeIdentity}). */
158
+ rootCanonical: string | undefined;
159
+ /** The anchors of the person's global and session rows and of every `~/` row: the root and home of the chain's top leg
160
+ * (a project row anchors at its own scope root). */
161
+ chainTop: PathRuleBases;
162
+ /** The leg's resolution of the locations rule heads pass through (the rows' twins). */
163
+ leads: HeadLeads;
157
164
  /** The LIVE tracked working directory: the base a relative cd resolves against
158
165
  * after an earlier observable `cd`; read per call. */
159
166
  liveCwd: () => string | undefined;
160
- /** The EXECUTION ENVIRONMENT's home directory (`ExecutionEnv.homeDir`, adapter-declared), the base a
161
- * `~/` path rule resolves against. `undefined` = the adapter declares none: a `~/` deny/ask row then reads
162
- * UNREADABLE for every call (a fail-closed ask), never the engine process's home. A declared value that is
163
- * not an absolute path is refused HERE, loudly (a bad declaration is a wiring defect, not an absence). */
167
+ /** The EXECUTION ENVIRONMENT's home directory (`ExecutionEnv.homeDir`, adapter-declared), screened here: a declared
168
+ * value that is not an absolute path is refused, loudly (a bad declaration is a wiring defect, not an absence). The
169
+ * `~/` rows anchor at the chain's top leg's home (`chainTop.home`); `undefined` there = no home was declared: a `~/`
170
+ * deny/ask row then reads UNREADABLE for every call (a fail-closed ask), never the engine process's home. */
164
171
  home: string | undefined;
165
172
  /** True iff the reserved question-tool NAME resolves to the ENGINE's own content-ask tool on this leg. */
166
173
  questionToolMounted: boolean;
@@ -173,6 +180,8 @@ export declare function createPermissionRuleLanes(cfg: {
173
180
  /** The person is told their store (or the named sources of it) was skipped for a call (the run's one notice,
174
181
  * `ruleStoreUnreadableNotice`). */
175
182
  onStoreUnreadable: (at: "call", sources?: EffectivePermissionRules["unreadableSources"]) => void;
183
+ /** The person is told the store served rows that were left out (the run's one `permission.rule_row_dropped`). */
184
+ onRowsDropped: (rows: EffectivePermissionRules["droppedRows"]) => void;
176
185
  /** The run's cut: a read it interrupts is abandoned quietly — a cancelled wait is no fact about the store. */
177
186
  signal?: AbortSignal;
178
187
  /** The allow layer's auto strip over a stored ALLOW row (true ⇒ out of this call's table BEFORE the coverage is chosen). */
@@ -2,8 +2,8 @@ import { persistedRuleMandateOf, ruleClearanceOf } from "../ask-origin.js";
2
2
  import {} from "../hooks.js";
3
3
  import { adjudicatePersistedNameRules, adjudicatePersistedPathRules, adjudicatePersistedRules, callPathFaceOf, faceAskSpeaksFor, laneToolNamesOf, nameRuleSubjectOf, ruleToolGrammarOf, segmentCoverageOf, suggestRuleForName, suggestRulesForCommand } from "../permission-rule-model.js";
4
4
  import { isAbsolutePathForm } from "../../tools/fs/safety.js";
5
- import { effectivePathTargetOf } from "../effective-path-target.js";
6
- import { declaredPathTargetOf, protectivePathTargetOf } from "../tool-registry.js";
5
+ import { callTargetViewsOf } from "../effective-path-target.js";
6
+ import { bindPathRules, persistedRowAnchors, projectScopeIdentity } from "../path-rule-binding.js";
7
7
  import { heldBy, seatOf, wireOf } from "../tool-name-readings.js";
8
8
  import { orgRuleVerdictFor, settleOrgVerdictWithin } from "../permission-rule-org.js";
9
9
  import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js";
@@ -13,6 +13,7 @@ import { strongestAnswer } from "../persisted-rule-arms.js";
13
13
  import { gateWaitFailureOf, withGateTimeout } from "../gate-bounded-await.js";
14
14
  import { ORG_ADJUDICATION_TIMEOUT_MS } from "../permission-rule-org.js";
15
15
  export const PERSISTED_RULE_TOOL = "Bash";
16
+ const NO_LEADS = new Map();
16
17
  const HANDOFF_BOUND = 256;
17
18
  export function inheritedAskRuleEvidence(provider) {
18
19
  const org = provider?.partitions.org === true ? "not_adjudicated" : "not_wired";
@@ -40,12 +41,11 @@ export function persistedRuleAnswerOf(verdict) {
40
41
  return persistedRuleHitOf(verdict);
41
42
  }
42
43
  export function pathRuleLaneAnswer(table, req, ctx) {
43
- const bases = { ...(ctx.root !== undefined ? { root: ctx.root } : {}), ...(ctx.liveCwd !== undefined ? { cwd: ctx.liveCwd } : {}), ...(ctx.home !== undefined ? { home: ctx.home } : {}) };
44
- const tighten = effectivePathTargetOf(req, protectivePathTargetOf(req), bases);
45
- const allow = effectivePathTargetOf(req, declaredPathTargetOf(req), bases);
44
+ const tighten = callTargetViewsOf(req, "tighten");
45
+ const allow = callTargetViewsOf(req, "allow");
46
46
  if (tighten === undefined && allow === undefined)
47
47
  return undefined;
48
- return persistedRuleAnswerOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId, face: callPathFaceOf(req) }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) }, bases));
48
+ return persistedRuleAnswerOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId, face: callPathFaceOf(req), ...(ctx.scopeIdentity !== undefined ? { scopeIdentity: ctx.scopeIdentity } : {}) }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) }, ctx.anchorsOf, ctx.leads));
49
49
  }
50
50
  export function orgRuleLane(cfg) {
51
51
  return {
@@ -141,8 +141,12 @@ export function createPermissionRuleLanes(cfg) {
141
141
  }
142
142
  if (view.unreadable !== undefined)
143
143
  skipped(view.unreadable, view.unreadableSources);
144
+ cfg.onRowsDropped(view.droppedRows);
144
145
  try {
145
- const answer = strongestAnswer(judged.map(([n, grammar]) => judgeAgainst(view.rules.filter((r) => r.behavior !== "allow" || cfg.stripsAllow?.(r, req) !== true), { ...req, toolName: n }, grammar)));
146
+ const table = view.rules.filter((r) => r.behavior !== "allow" || cfg.stripsAllow?.(r, req) !== true);
147
+ const leads = judged.some(([, g]) => g === "path") ? await cfg.leads.resolve(bindPathRules(table.filter((r) => r.match === "path" || r.match === "subpath").map((r) => ({ parsed: r, ruleText: r.rule, rule: r })), (spec) => anchorsOf(spec.rule))) : NO_LEADS;
148
+ const scopeIdentity = await projectScopeIdentity(table, cfg.rootCanonical, cfg.leads);
149
+ const answer = strongestAnswer(judged.map(([n, grammar]) => judgeAgainst(table, { ...req, toolName: n }, grammar, leads, scopeIdentity)));
146
150
  return view.unreadable !== undefined && (answer === undefined || "segmentCoverage" in answer) ? { unreadable: true } : answer;
147
151
  }
148
152
  catch (err) {
@@ -151,20 +155,22 @@ export function createPermissionRuleLanes(cfg) {
151
155
  return { unreadable: true, reason: why };
152
156
  }
153
157
  };
154
- const judgeAgainst = (table, req, grammar) => {
158
+ const anchorsOf = (rule) => persistedRowAnchors(rule.scope, cfg.chainTop);
159
+ const judgeAgainst = (table, req, grammar, leads, scopeIdentity) => {
155
160
  const liveCwd = cfg.liveCwd();
161
+ const scope = scopeIdentity !== undefined ? { scopeIdentity } : {};
156
162
  if (grammar === "path")
157
- return pathRuleLaneAnswer(table, req, { root: cfg.root, sessionId: cfg.sessionId, liveCwd, home: cfg.home });
163
+ return pathRuleLaneAnswer(table, req, { root: cfg.root, sessionId: cfg.sessionId, anchorsOf, leads, ...scope });
158
164
  if (grammar === "name")
159
- return persistedRuleAnswerOf(adjudicatePersistedNameRules(table, { tool: req.toolName, name: nameRuleSubjectOf(req.toolName, req.args), cwd: cfg.root, sessionId: cfg.sessionId }));
165
+ return persistedRuleAnswerOf(adjudicatePersistedNameRules(table, { tool: req.toolName, name: nameRuleSubjectOf(req.toolName, req.args), cwd: cfg.root, sessionId: cfg.sessionId, ...scope }));
160
166
  const command = req.args?.command;
161
167
  if (typeof command !== "string")
162
168
  return undefined;
163
169
  const execCwd = liveCwd !== undefined ? { execCwd: liveCwd } : {};
164
- const verdict = adjudicatePersistedRules(table, { tool: req.toolName, command, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
170
+ const verdict = adjudicatePersistedRules(table, { tool: req.toolName, command, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd, ...scope });
165
171
  if (verdict !== undefined)
166
172
  return persistedRuleAnswerOf(verdict);
167
- const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
173
+ const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd, ...scope });
168
174
  return coverage !== undefined ? { segmentCoverage: coverage } : undefined;
169
175
  };
170
176
  const skipped = (why, sources) => (cfg.onDisclosure(why), cfg.onStoreUnreadable("call", sources), { unreadable: true });
@@ -3,14 +3,16 @@
3
3
  * per call).
4
4
  */
5
5
  import { type EffectivePermissionRules, type PermissionRuleStoreProvider } from "../permission-rule-provider.js";
6
- import type { PathRuleDenyRow } from "../../tools/fs/read-deny.js";
6
+ import { type PathRuleBases } from "../permission-rule-model.js";
7
+ import { type BoundPathRule, type HeadLeads } from "../path-rule-binding.js";
7
8
  import type { RunnerDeps } from "../runner-deps.js";
8
9
  /** The person's persisted PATH rows a leg's read face takes at prepare ({@link persistedPathRowsOf}). */
9
10
  export interface PersistedPathRows {
10
11
  /** Every live `Read(//dir/**)` allow row's directory (lexical, absolute): a standing yes to reads under it. */
11
12
  readonly readDirectories: readonly string[];
12
- /** Every live face-keyed path DENY row (`Read(...)`, `Edit(...)`), as a deny-table row resolved at the task's bases. */
13
- readonly denyRows: readonly PathRuleDenyRow[];
13
+ /** Every live face-keyed path DENY row (`Read(...)`, `Edit(...)`), bound at its anchors (a project row at its scope root,
14
+ * the rest at the chain's top leg). */
15
+ readonly denyRows: readonly BoundPathRule[];
14
16
  }
15
17
  /**
16
18
  * The person's persisted PATH rows, read ONCE at prepare from the same store query the gate's lanes read per call: a
@@ -22,19 +24,28 @@ export interface PersistedPathRows {
22
24
  * rows. A read that failed (or did not answer in time) ⇒ no rows, and a partition the read could not read ⇒ none of ITS
23
25
  * rows (the partition that was read contributes its own): what could not be read is skipped for this run's tables — its
24
26
  * directories widen nothing and its deny rows judge nothing here — disclosed on the trace and announced to the person
25
- * (`onUnread`); the next prepare reads again, as it would for a row the store gained mid-run. A deny row whose
26
- * pattern needs a base the task cannot supply (`~/` with no declared home) is left to the per-call lane and disclosed.
27
+ * (`onUnread`); the next prepare reads again, as it would for a row the store gained mid-run. A deny row is anchored by
28
+ * its scope (`persistedRowAnchors`: a project row at its own scope root, a global / session row at the chain's top leg,
29
+ * `~/` at the top leg's home); one whose pattern needs an anchor the chain does not supply (`~/` with no declared home) is
30
+ * left to the per-call lane and disclosed.
27
31
  */
28
32
  export declare function persistedPathRowsOf(cfg: {
29
33
  provider: PermissionRuleStoreProvider | undefined;
30
34
  localOwnerDeclared: boolean;
31
35
  principal: string | undefined;
32
36
  sessionId: string;
37
+ /** The leg's own root: which project rows apply to this leg (either spelling of it — {@link projectScopeIdentity}). */
33
38
  root: string;
34
- home: string | undefined;
39
+ rootCanonical: string | undefined;
40
+ /** The leg's resolution of locations (a project row's scope root, where it leads). */
41
+ leads: HeadLeads;
42
+ /** The anchors of the global / session rows and of `~/`: the chain's top leg's root and home. */
43
+ chainTop: PathRuleBases;
35
44
  onDisclosure: (message: string) => void;
36
45
  /** The person is told the store (or the named sources of it) was skipped ({@link ruleStoreUnreadableNotice}). */
37
46
  onUnread: (at: "prepare", sources?: EffectivePermissionRules["unreadableSources"]) => void;
47
+ /** The person is told rows were left out ({@link ruleRowsDroppedNotice}). */
48
+ onRowsDropped: (rows: EffectivePermissionRules["droppedRows"]) => void;
38
49
  /** The run's cut: a read that has not answered by then (or by the gate's await bound) reads as unreadable. */
39
50
  signal?: AbortSignal;
40
51
  }): Promise<PersistedPathRows>;
@@ -48,6 +59,16 @@ export declare function ruleStoreUnreadableNotice(onNotice: RunnerDeps["onNotice
48
59
  readonly sessionId: string;
49
60
  readonly runId: string;
50
61
  }): (at: "prepare" | "call", sources?: EffectivePermissionRules["unreadableSources"]) => void;
62
+ /**
63
+ * The run's ONE `permission.rule_row_dropped` notice (audience user): the person's store served project-scoped rows whose
64
+ * scope root is not a complete absolute path, left out of the checks; minted by whichever read met them first (the
65
+ * prepare read of the path rows, or a call's own read on the rule lane), once per run. The sentence names no path; the
66
+ * rows (bounded) ride the detail. The trace keeps every read's line.
67
+ */
68
+ export declare function ruleRowsDroppedNotice(onNotice: RunnerDeps["onNotice"], ids: {
69
+ readonly sessionId: string;
70
+ readonly runId: string;
71
+ }): (rows: EffectivePermissionRules["droppedRows"]) => void;
51
72
  /** {@link persistedPathRowsOf} for one task's prepare: only where a read face mounts (hands), disclosed on the run's trace,
52
73
  * cut by the run's own signals. `undefined` = nothing to read, returned synchronously: a run with no person's store (or
53
74
  * no hands) gains no suspension point on its prepare. */
@@ -56,9 +77,12 @@ export declare function persistedPathRowsOfTask(t: {
56
77
  principal: string | undefined;
57
78
  sessionId: string;
58
79
  root: string;
59
- home: string | undefined;
80
+ rootCanonical: string | undefined;
81
+ leads: HeadLeads;
82
+ chainTop: PathRuleBases;
60
83
  taskId: string;
61
84
  handsEnabled: boolean;
62
85
  signals: readonly AbortSignal[];
63
86
  onUnread: (at: "prepare", sources?: EffectivePermissionRules["unreadableSources"]) => void;
87
+ onRowsDropped: (rows: EffectivePermissionRules["droppedRows"]) => void;
64
88
  }): Promise<PersistedPathRows> | undefined;