@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
@@ -20,9 +20,19 @@
20
20
  *
21
21
  * The fix is not a fourth arm in the lanes: it is that "which base does this tool resolve against, and what
22
22
  * does it target when the slot is empty" is a FACT OF THE TOOL, so the tool declares it once and both
23
- * sides read the declaration. {@link pathTargetBaseOf} is the shared SELECTION (which base), and each side
24
- * then applies its own identity to the selected base — lexical-normal here (the rule family's one path
25
- * identity, zero IO), canonical (symlink-resolving, IO) inside the tool.
23
+ * sides read the declaration. {@link pathTargetBaseOf} is the shared SELECTION (which base).
24
+ *
25
+ * ## The views of one target
26
+ *
27
+ * A target is seen three ways: its LEXICAL view (the spelling joined to the selected base, `.` / `..` folded —
28
+ * zero IO), its CANONICAL view (the same spelling resolved by the execution environment: symlinks, aliases of the
29
+ * tree, the disk's own case) and the HOPS of its leaf's symlink chain. {@link readPathViews} is the one producer of
30
+ * those views — the tool's own resolution and its deny judgment read it, and so does {@link readCallTarget}, which
31
+ * a Runner stamp point calls once per adjudicated call and hands to every rule lane as
32
+ * `ToolCallRequest.targetReading`. Every rule lane reads a call's target through ONE reader, {@link callTargetViewsOf}:
33
+ * the reading's views when the call carries one, else the call's own lexical view (a bare request). The judgment over
34
+ * those views is the deny tables' (`pathRowsReach`): any view a deny or ask row reaches is reached; an allow row admits a
35
+ * call only when it admits every view of the declared reading.
26
36
  *
27
37
  * ## What it does NOT decide
28
38
  *
@@ -35,7 +45,9 @@
35
45
  * rule family reaches it, and the rule grammar is derived from the catalog's path targets), so it has no
36
46
  * declaration to read; the day it declares one, it reads the same selector as everything else.
37
47
  */
38
- import { type PathFamily, type PathIdentity, type Win32NamespaceRefusal } from "../tools/fs/safety.js";
48
+ import { canonicalizeTarget, type PathFamily, type PathIdentity, type Win32NamespaceRefusal } from "../tools/fs/safety.js";
49
+ import type { TargetViews } from "../tools/fs/read-deny.js";
50
+ import type { ExecutionEnv } from "../internal/harness-types.js";
39
51
  import type { ToolCallPathTarget } from "./tool-face.js";
40
52
  import { type ToolCallLike } from "./tool-registry.js";
41
53
  /** The bases of ONE CALL a target resolves against: the task root, and the live tracked working directory
@@ -114,3 +126,123 @@ export declare function effectivePathSpellingOf(call: ToolCallLike, pt: ToolCall
114
126
  } | {
115
127
  readonly refused: Win32NamespaceRefusal;
116
128
  } | undefined;
129
+ /**
130
+ * ONE reading of a call's path slot as VIEWS. `path`: the LEXICAL view (the spelling joined to the selected base and
131
+ * folded — a native spelling of the tree's family), the CANONICAL view when the environment answered (absent ⇒ not asked
132
+ * or no answer: an absent view takes no part in a judgment and is never a refusal) and the HOPS of the leaf's symlink
133
+ * chain when the environment declares one. `unreadable` / `refused` read as in {@link PathTargetReading}.
134
+ */
135
+ export type CallTargetView = {
136
+ readonly kind: "path";
137
+ readonly lexical: string;
138
+ readonly canonical?: string;
139
+ readonly hops?: readonly string[];
140
+ } | {
141
+ readonly kind: "unreadable";
142
+ readonly reason: string;
143
+ } | {
144
+ readonly kind: "refused";
145
+ readonly reason: Win32NamespaceRefusal;
146
+ };
147
+ /**
148
+ * A call's TARGET READING: the views of each reading of its path slot — `protective`, the tightening direction's
149
+ * ({@link protectivePathTargetOf}), and `declared`, the allow direction's ({@link declaredPathTargetOf}; absent on a
150
+ * request that carries no face). It carries no root and no cwd: the call's own `root` / `cwd` are the only copies.
151
+ */
152
+ export interface CallTargetReading {
153
+ readonly protective?: CallTargetView;
154
+ readonly declared?: CallTargetView;
155
+ /** Where each location on the way to a rule row's head that this leg's lanes hold LEADS in the leg's environment (a
156
+ * link, a firmlink, an alias of the tree) — the rows' twins are judged there too. A fact of the judging leg, taken
157
+ * once per location per leg; absent when no row head leads elsewhere. */
158
+ readonly leads?: ReadonlyMap<string, string>;
159
+ }
160
+ /** The environment's answer the canonical view rests on — the throat's own, so the tool keeps reading the refusal it gave. */
161
+ export type CanonicalAnswer = Awaited<ReturnType<typeof canonicalizeTarget>>;
162
+ /** The three views of one spelling as {@link readPathViews} produces them. */
163
+ export interface PathViews {
164
+ readonly lexical: PathTargetReading;
165
+ /** `undefined` ⇒ the environment was not asked (none supplied, or a spelling this reading cannot locate). */
166
+ readonly canonical?: CanonicalAnswer;
167
+ readonly hops: readonly string[];
168
+ }
169
+ /**
170
+ * The ONE producer of a target's views: `spelled` against `base` (the base the tool's declaration selected), under the
171
+ * tree `bases` names. Lexical through the same resolution {@link effectivePathTargetOf} reads; canonical through the
172
+ * throat's own canonicalizer; the hops (when `opts.hops` and the canonical view answered) through the leaf's chain. The
173
+ * tool's resolution and deny judgment read this, and so does {@link readCallTarget}; an environment that THROWS
174
+ * propagates here (each caller holds its own posture: the tool refuses, the reading leaves the view absent).
175
+ */
176
+ export declare function readPathViews(env: ExecutionEnv | undefined, spelled: string, base: string | undefined, bases: ToolPathBases, signal?: AbortSignal, opts?: {
177
+ readonly hops?: boolean;
178
+ }): Promise<PathViews>;
179
+ /** The identity of an ABSOLUTE spelling under the tree `bases` names — the target side's one prelude (family, namespace
180
+ * screen, identity) for a path a judge already holds (a canonical key, a hop, a lexical view). `undefined` when the
181
+ * spelling is not an absolute path of that tree. */
182
+ export declare function absoluteTargetIdentityOf(spelled: string, bases: ToolPathBases): PathIdentity | undefined;
183
+ /**
184
+ * A call's target reading under ONE call's facts: its `root` / `cwd` (the bases the stamp point reads off the leg) and
185
+ * the execution environment `env` (absent ⇒ the lexical views alone — the pre-claim recheck, which has no environment).
186
+ * A canonical view the environment cannot answer (or throws on) is absent, never a refusal (an absent view does not vote).
187
+ * The two readings share their views when they select the same spelling against the same base (one canonicalization).
188
+ */
189
+ export declare function readCallTarget(env: ExecutionEnv | undefined, call: ToolCallLike & {
190
+ readonly root?: string | undefined;
191
+ readonly cwd?: string | undefined;
192
+ }, signal?: AbortSignal, leads?: ReadonlyMap<string, string | undefined>): Promise<CallTargetReading>;
193
+ /**
194
+ * `req` with its arguments replaced by `args` and — when `req` carries a reading of its own args — a reading of the NEW
195
+ * args taken the same way (same environment, same root / cwd, same face). Every seat that rewrites a request's arguments
196
+ * and hands it on (a composition's rewrite cascade, a re-judgment of the final or an edited form) goes through here, so the
197
+ * next layer never judges a request its reading does not describe. `signal` is the CURRENT call's: a reading never carries
198
+ * one call's cancellation into another's. `undefined` ⇒ nothing to re-read: use `{ ...req, args }`.
199
+ */
200
+ export declare function withArgsReading<R extends {
201
+ readonly args: unknown;
202
+ readonly targetReading?: CallTargetReading | undefined;
203
+ }>(req: R, args: unknown, signal: AbortSignal | undefined): Promise<R> | undefined;
204
+ /** The request's target reading, when it describes the request's own `args` (the object it was taken for); otherwise
205
+ * nothing — a rule lane then judges the lexical view it always judged. Every rule lane reads the reading through here. */
206
+ export declare function callTargetReadingOf(req: {
207
+ readonly args: unknown;
208
+ readonly targetReading?: CallTargetReading | undefined;
209
+ }): CallTargetReading | undefined;
210
+ /**
211
+ * THE one reader of a call's target for every rule lane, in one direction's reading (`tighten` = the protective reading,
212
+ * `allow` = the declared one): the views of the call's target reading when it carries one of its own args (lexical,
213
+ * canonical, the leaf chain's hops), else the call's own lexical view — resolved against the call's `root` / `cwd` when it
214
+ * carries either, else against `bare` (a host's direct call of a policy: the bases it declared). `undefined` = the call
215
+ * names no path in this reading (or a spelling the throat refuses — no rule speaks about it); an `unreadable` answer =
216
+ * the call names a path this reading cannot locate.
217
+ */
218
+ export declare function callTargetViewsOf(req: ToolCallLike & {
219
+ readonly root?: string | undefined;
220
+ readonly cwd?: string | undefined;
221
+ readonly targetReading?: CallTargetReading | undefined;
222
+ }, direction: "tighten" | "allow", bare?: ToolPathBases): TargetViews | undefined;
223
+ /**
224
+ * A leg's STAMP seat: `req` with a target reading of its own args, or `undefined` when there is nothing to stamp — the
225
+ * leg's lanes hold no path rule, the request already carries a reading of its args, or its args are not an object. The
226
+ * reading is taken under the request's own `root` / `cwd` when it carries them, else the leg's (the same values every
227
+ * Runner seat stamps), and memoized per args object within one call (its `toolCallId`), so every lane reading one call
228
+ * costs one reading. `undefined`
229
+ * rather than a resolved request, so a seat with nothing to do adds no suspension point.
230
+ */
231
+ export type CallTargetStamp = <R extends ToolCallLike & {
232
+ readonly toolCallId?: string;
233
+ readonly root?: string | undefined;
234
+ readonly cwd?: string | undefined;
235
+ readonly targetReading?: CallTargetReading | undefined;
236
+ }>(req: R, signal?: AbortSignal) => Promise<R & {
237
+ readonly targetReading: CallTargetReading;
238
+ }> | undefined;
239
+ /** The stamp seat of one leg: its execution environment, its root and live working directory, whether its lanes hold a
240
+ * path rule (read per call — an activated skill can add one), and where the heads of the rows its lanes hold lead in its
241
+ * environment (resolved once per location per leg). */
242
+ export declare function legCallTargetStamp(leg: {
243
+ readonly env: ExecutionEnv;
244
+ readonly root: string;
245
+ readonly cwd: () => string | undefined;
246
+ readonly judgesPaths: () => boolean;
247
+ readonly leads: () => Promise<ReadonlyMap<string, string | undefined>>;
248
+ }): CallTargetStamp;
@@ -1,5 +1,7 @@
1
- import { isAbsoluteForFamily, joinForFamily, pathFamilyOf, pathIdentityOf, win32NamespaceScreen } from "../tools/fs/safety.js";
2
- import { pathTargetValue } from "./tool-registry.js";
1
+ import { canonicalizeTarget, isAbsoluteForFamily, joinForFamily, pathFamilyOf, pathIdentityOf, spellingOfIdentity, win32NamespaceScreen } from "../tools/fs/safety.js";
2
+ import { declaredPathTargetOf, pathTargetValue, protectivePathTargetOf } from "./tool-registry.js";
3
+ import { symlinkChainHops } from "./path-identity.js";
4
+ import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, withGateTimeout } from "./gate-bounded-await.js";
3
5
  export function pathTargetBaseOf(pt, bases) {
4
6
  return (pt?.base ?? "cwd") === "root" ? bases.root : (bases.cwd ?? bases.root);
5
7
  }
@@ -73,3 +75,109 @@ function readingOf(r) {
73
75
  function unreadableBase(base) {
74
76
  return { kind: "unreadable", reason: base === undefined ? "the call's path is relative to a base that was not supplied" : `the base this call resolves against (${JSON.stringify(base)}) is not a fully-qualified path of this tree's family` };
75
77
  }
78
+ export async function readPathViews(env, spelled, base, bases, signal, opts) {
79
+ const lexical = resolveSpelling(spelled, base, bases);
80
+ const family = pathFamilyOf(bases);
81
+ if (env === undefined || family === undefined || lexical?.kind === "unreadable")
82
+ return { lexical, hops: [] };
83
+ const canonical = await canonicalizeTarget(env, spelled, signal, family, base);
84
+ const climbs = (s) => s !== undefined && (family === "win" ? /(^|[\\/])\.\.([\\/]|$)/ : /(^|\/)\.\.(\/|$)/).test(s);
85
+ const followsALink = canonical.ok && (climbs(spelled) || climbs(base) || lexical?.kind !== "path" || absoluteTargetIdentityOf(canonical.key, bases) !== lexical.identity);
86
+ const hops = opts?.hops === true && followsALink ? await withGateTimeout(symlinkChainHops(env, spelled, signal, family, base), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "canonicalChain").catch(() => []) : [];
87
+ return { lexical, canonical, hops };
88
+ }
89
+ export function absoluteTargetIdentityOf(spelled, bases) {
90
+ const r = pathIdentityOf(spelled, pathFamilyOf(bases), bases.root ?? bases.cwd);
91
+ return r.ok ? r.identity : undefined;
92
+ }
93
+ const READ_FOR = new WeakMap();
94
+ const REREAD = new WeakMap();
95
+ export async function readCallTarget(env, call, signal, leads) {
96
+ const bases = { ...(call.root !== undefined ? { root: call.root } : {}), ...(call.cwd !== undefined ? { cwd: call.cwd } : {}) };
97
+ const family = pathFamilyOf(bases);
98
+ const views = new Map();
99
+ const viewOf = (pt) => {
100
+ const sel = effectivePathSpellingOf(call, pt, bases);
101
+ if (sel === undefined)
102
+ return Promise.resolve(undefined);
103
+ if ("refused" in sel)
104
+ return Promise.resolve({ kind: "refused", reason: sel.refused });
105
+ const target = sel.spelled !== undefined ? { spelled: sel.spelled, base: sel.base } : sel.base !== undefined && isAbsoluteForFamily(family, sel.base) ? { spelled: sel.base, base: undefined } : undefined;
106
+ if (target === undefined)
107
+ return Promise.resolve(unreadableBase(sel.base));
108
+ const { spelled, base } = target;
109
+ const key = JSON.stringify([spelled, base ?? null]);
110
+ const prior = views.get(key);
111
+ if (prior !== undefined)
112
+ return prior;
113
+ const read = readPathViews(env, spelled, base, bases, signal, { hops: true })
114
+ .catch(() => ({ lexical: resolveSpelling(spelled, base, bases), hops: [] }))
115
+ .then((v) => viewFrom(v, family));
116
+ views.set(key, read);
117
+ return read;
118
+ };
119
+ const protective = await viewOf(protectivePathTargetOf(call));
120
+ const declared = call.face === undefined ? undefined : await viewOf(declaredPathTargetOf(call));
121
+ const led = leads === undefined ? undefined : [...leads].filter((e) => e[1] !== undefined && e[1] !== e[0]);
122
+ const reading = Object.freeze({ ...(protective !== undefined ? { protective } : {}), ...(declared !== undefined ? { declared } : {}), ...(led !== undefined && led.length > 0 ? { leads: new Map(led) } : {}) });
123
+ if (typeof call.args === "object" && call.args !== null)
124
+ READ_FOR.set(reading, call.args);
125
+ REREAD.set(reading, (args, rereadSignal) => readCallTarget(env, { ...call, args }, rereadSignal, leads));
126
+ return reading;
127
+ }
128
+ export function withArgsReading(req, args, signal) {
129
+ const reading = callTargetReadingOf(req);
130
+ const reread = reading === undefined ? undefined : REREAD.get(reading);
131
+ return reread === undefined ? undefined : reread(args, signal).then((targetReading) => ({ ...req, args, targetReading }));
132
+ }
133
+ function viewFrom(v, family) {
134
+ if (v.lexical === undefined)
135
+ return undefined;
136
+ if (v.lexical.kind !== "path")
137
+ return Object.freeze({ ...v.lexical });
138
+ const canonical = v.canonical?.ok === true ? v.canonical.key : undefined;
139
+ return Object.freeze({ kind: "path", lexical: spellingOfIdentity(v.lexical.identity, family), ...(canonical !== undefined ? { canonical } : {}), ...(v.hops.length > 0 ? { hops: Object.freeze([...v.hops]) } : {}) });
140
+ }
141
+ export function callTargetReadingOf(req) {
142
+ const r = req.targetReading;
143
+ return r !== undefined && READ_FOR.get(r) === req.args ? r : undefined;
144
+ }
145
+ export function callTargetViewsOf(req, direction, bare = {}) {
146
+ const reading = callTargetReadingOf(req);
147
+ if (reading !== undefined) {
148
+ const v = direction === "tighten" ? reading.protective : reading.declared;
149
+ if (v === undefined || v.kind === "refused")
150
+ return undefined;
151
+ if (v.kind === "unreadable")
152
+ return { spellings: [], unreadable: v.reason };
153
+ const spellings = [v.lexical];
154
+ for (const s of [v.canonical, ...(v.hops ?? [])])
155
+ if (s !== undefined && !spellings.includes(s))
156
+ spellings.push(s);
157
+ return { spellings };
158
+ }
159
+ const own = req.root !== undefined || req.cwd !== undefined ? { ...(req.root !== undefined ? { root: req.root } : {}), ...(req.cwd !== undefined ? { cwd: req.cwd } : {}) } : bare;
160
+ const target = effectivePathTargetOf(req, direction === "tighten" ? protectivePathTargetOf(req) : declaredPathTargetOf(req), own);
161
+ if (target === undefined || target.kind === "refused")
162
+ return undefined;
163
+ if (target.kind === "unreadable")
164
+ return { spellings: [], unreadable: target.reason };
165
+ return { spellings: [spellingOfIdentity(target.identity, pathFamilyOf(own))] };
166
+ }
167
+ export function legCallTargetStamp(leg) {
168
+ const taken = new WeakMap();
169
+ return (req, signal) => {
170
+ const args = req.args;
171
+ if (typeof args !== "object" || args === null || callTargetReadingOf(req) !== undefined || !leg.judgesPaths())
172
+ return undefined;
173
+ const root = req.root ?? leg.root;
174
+ const cwd = req.cwd ?? leg.cwd();
175
+ const prior = taken.get(args);
176
+ const reading = prior !== undefined && prior.root === root && prior.cwd === cwd && prior.toolName === req.toolName && prior.toolCallId === req.toolCallId && prior.face === req.face
177
+ ? prior.reading
178
+ : leg.leads().then((leads) => readCallTarget(leg.env, { toolName: req.toolName, args, ...(req.face !== undefined ? { face: req.face } : {}), root, ...(cwd !== undefined ? { cwd } : {}) }, signal, leads));
179
+ if (prior?.reading !== reading)
180
+ taken.set(args, { root, cwd, toolName: req.toolName, toolCallId: req.toolCallId, face: req.face, reading });
181
+ return reading.then((targetReading) => ({ ...req, targetReading }));
182
+ };
183
+ }
@@ -368,6 +368,11 @@ export interface EngineNotice {
368
368
  * (one call's own rule check — every later call reads again, so the rows apply as soon as a read succeeds). Once
369
369
  * per run, whichever read fails first; the trace keeps every read's line (`permission.rule_store_unreadable`).
370
370
  * `detail: { at, sessionId, runId }`. Audience `user`.
371
+ * - `"permission.rule_row_dropped"` — the person's store served project-scoped rows whose scope root is not a
372
+ * complete absolute path (only a foreign writer or a hand edit makes one; every engine write face refuses it): they
373
+ * take no part in this run's checks — a deny or ask row among them protects nothing — until they are rewritten or
374
+ * removed. Once per run, by whichever read met them first. `detail: { rows, count, sessionId, runId }` — `rows`
375
+ * the first eight `{ rule, scopeRoot }` (each bounded), `count` all of them. Audience `user`.
371
376
  * - `"config.durable_gate_unavailable"` — a durable-approval MANDATE is in force on a leg with NO
372
377
  * checkpoint store: the per-principal `forceDurableGate` entitlement, or the task's own
373
378
  * `durableApproval` opt-in with no live approver seat (`durableMandateSourceOf`, the predicate the
@@ -13,6 +13,9 @@ export async function runGateFold(pass) {
13
13
  const { input, toolName, toolCallId, callSignal, ledger, notifier, adjudicate } = pass;
14
14
  const boundedAwaitDeadlineMs = createGateDeadline(input, notifier);
15
15
  pass.req = { toolName, args: pass.currentInput, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
16
+ const stamped = input.stampCallTarget?.(pass.req, callSignal ?? input.abortSignal);
17
+ if (stamped !== undefined)
18
+ pass.req = await stamped;
16
19
  pass.decision = adjudicate ? refuseOutOfContractDecision(await (callSignal !== undefined ? adjudicate(pass.req, callSignal) : adjudicate(pass.req))) : { action: "allow" };
17
20
  {
18
21
  const refusedBy = ledger.fromDecision(pass.decision);
@@ -1,3 +1,4 @@
1
+ import { withArgsReading } from "./effective-path-target.js";
1
2
  import { attestClassifierDenyCause, classifierDenyCauseOf, decisionText, refuseOutOfContractDecision } from "./tool-policy.js";
2
3
  import { arbitrateByMode, classifyAsk, fileCommandOptionsSentence, persistedRuleMandateOf, ruleClearanceOf, safetyAxisOf, stampAskOrigin } from "./ask-origin.js";
3
4
  import { classifierParseFailureDenyMessage, denialLimitFallbackMessage, rungDidNotRule, unarmedWindow } from "./auto-mode.js";
@@ -118,7 +119,8 @@ export async function runGateLanes(pass) {
118
119
  pass.decision = orgAnswer.decision;
119
120
  const ruleCallUnreadableOf = (lane) => lane?.unreadable === true && lane.reason !== undefined;
120
121
  const readPersistedRules = (req) => readPersistedRuleLane(input.persistedRules, req, input.abortSignal);
121
- const laneReq = { ...pass.req, ...(pass.policyRewrite !== undefined ? { args: pass.policyRewrite } : {}) };
122
+ const reread = pass.policyRewrite !== undefined ? withArgsReading(pass.req, pass.policyRewrite, pass.callSignal ?? input.abortSignal) : undefined;
123
+ const laneReq = reread !== undefined ? await reread : { ...pass.req, ...(pass.policyRewrite !== undefined ? { args: pass.policyRewrite } : {}) };
122
124
  const laneAnswer = input.persistedRules !== undefined && wireOf("lanes.persistedLaneSkip", pass.req) !== ASK_USER_QUESTION_TOOL_NAME ? await readPersistedRules(laneReq) : undefined;
123
125
  {
124
126
  const tightened = applyPersistedTightening(pass.decision, laneAnswer);
@@ -139,6 +141,8 @@ export async function runGateLanes(pass) {
139
141
  if (pass.policyRewrite !== undefined) {
140
142
  pass.currentInput = pass.policyRewrite;
141
143
  pass.req.args = pass.policyRewrite;
144
+ if (laneReq.targetReading !== undefined)
145
+ pass.req.targetReading = laneReq.targetReading;
142
146
  }
143
147
  const ruleMayClear = () => ruleClearanceOf(pass.decision.action === "ask" ? pass.decision : undefined, mandateMarksNow()).ruleMayClear;
144
148
  const classMayClear = () => pass.decision.action === "ask" && classifyAsk(pass.decision, askOriginFacts(orgAnswer.org, ruleCallUnreadableOf(laneAnswer))).row.ruleMayClear;
@@ -576,8 +580,10 @@ export async function runGateLanes(pass) {
576
580
  break;
577
581
  }
578
582
  let editRuleCallUnreadable = false;
583
+ const editReread = withArgsReading(pass.req, editArgs, callSignal ?? input.abortSignal);
584
+ const editLaneReq = editReread !== undefined ? await editReread : { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
579
585
  if (input.persistedRules !== undefined && wireOf("lanes.persistedEditSkip", pass.req) !== ASK_USER_QUESTION_TOOL_NAME) {
580
- const editLane = await readPersistedRules({ toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) });
586
+ const editLane = await readPersistedRules(editLaneReq);
581
587
  editRuleCallUnreadable = ruleCallUnreadableOf(editLane);
582
588
  const editTightened = applyPersistedTightening(recheck, editLane);
583
589
  recheck = editTightened.decision;
@@ -588,7 +594,7 @@ export async function runGateLanes(pass) {
588
594
  break;
589
595
  }
590
596
  }
591
- const editSkillRefusal = skillRowRefusal(input.skillGrants, { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) });
597
+ const editSkillRefusal = skillRowRefusal(input.skillGrants, editLaneReq);
592
598
  if (editSkillRefusal !== undefined) {
593
599
  editDenied = editSkillRefusal;
594
600
  pass.deniedBy = "policy";
@@ -136,7 +136,7 @@ export type NoticeAudience = "user" | "operator";
136
136
  * the union's catalog ({@link EngineNotice.code}). The groups below say only which family a row joins,
137
137
  * so the catalog is not written a second time here and the two cannot drift.
138
138
  */
139
- export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "config.secret_env_scrubbed", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "permission.allowed_tool_not_granted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "classifier.unavailable", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "route.tool_call_id_folded", "context.tool_turn_repaired", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "task.tool_turn_incomplete", "approval.read_root_granted", "approval.read_root_grant_rejected", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed", "instructions.source_changed", "session.records_skipped", "memory.layer_locked_legacy", "server.model_unreachable"];
139
+ export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "config.secret_env_scrubbed", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "permission.rule_row_dropped", "permission.allowed_tool_not_granted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "classifier.unavailable", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "route.tool_call_id_folded", "context.tool_turn_repaired", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "task.tool_turn_incomplete", "approval.read_root_granted", "approval.read_root_grant_rejected", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed", "instructions.source_changed", "session.records_skipped", "memory.layer_locked_legacy", "server.model_unreachable"];
140
140
  /** A catalogued code (see {@link ENGINE_NOTICE_CODES}); {@link NOTICE_MINTER} says who mints it. NOT the type of
141
141
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
142
142
  * sink is a supported shape, and narrowing that field would break it. */
@@ -181,6 +181,7 @@ declare const NOTICE_AUDIENCE_TABLE: {
181
181
  readonly "permission.ask_released_by_mode": "user";
182
182
  readonly "permission.read_deny_unapplied": "user";
183
183
  readonly "permission.rule_store_unavailable": "user";
184
+ readonly "permission.rule_row_dropped": "user";
184
185
  readonly "permission.allowed_tool_not_granted": "user";
185
186
  readonly "memory.capture_opted_out": "user";
186
187
  readonly "memory.capture_optout_unpersisted": "user";
@@ -295,6 +296,7 @@ declare const NOTICE_MINTER_TABLE: {
295
296
  readonly "permission.ask_released_by_mode": "engine";
296
297
  readonly "permission.read_deny_unapplied": "engine";
297
298
  readonly "permission.rule_store_unavailable": "engine";
299
+ readonly "permission.rule_row_dropped": "engine";
298
300
  readonly "permission.allowed_tool_not_granted": "engine";
299
301
  readonly "peer.inbound_disposition": "engine";
300
302
  readonly "peer.held_settled": "engine";
@@ -144,6 +144,7 @@ export const ENGINE_NOTICE_CODES = Object.freeze([
144
144
  "permission.ask_released_by_mode",
145
145
  "permission.read_deny_unapplied",
146
146
  "permission.rule_store_unavailable",
147
+ "permission.rule_row_dropped",
147
148
  "permission.allowed_tool_not_granted",
148
149
  "peer.inbound_disposition",
149
150
  "peer.held_settled",
@@ -229,6 +230,7 @@ const NOTICE_AUDIENCE_TABLE = {
229
230
  "permission.ask_released_by_mode": "user",
230
231
  "permission.read_deny_unapplied": "user",
231
232
  "permission.rule_store_unavailable": "user",
233
+ "permission.rule_row_dropped": "user",
232
234
  "permission.allowed_tool_not_granted": "user",
233
235
  "memory.capture_opted_out": "user",
234
236
  "memory.capture_optout_unpersisted": "user",
@@ -324,6 +326,7 @@ const NOTICE_MINTER_TABLE = {
324
326
  "permission.ask_released_by_mode": "engine",
325
327
  "permission.read_deny_unapplied": "engine",
326
328
  "permission.rule_store_unavailable": "engine",
329
+ "permission.rule_row_dropped": "engine",
327
330
  "permission.allowed_tool_not_granted": "engine",
328
331
  "peer.inbound_disposition": "engine",
329
332
  "peer.held_settled": "engine",
@@ -1,6 +1,7 @@
1
1
  import type { ActorAssertion, DocumentContent, ImageContent, TextContent } from "../internal/llm.js";
2
2
  import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
3
3
  import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallFace, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
4
+ import type { CallTargetStamp } from "./effective-path-target.js";
4
5
  import type { GateOutcome } from "./gate-outcome.js";
5
6
  import { type PreToolUseScreening } from "./runner/gate-exit.js";
6
7
  export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./rule-lane-reads.js";
@@ -1176,6 +1177,8 @@ export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats, All
1176
1177
  * starting directory and answer for a run that has since `cd`ed. Absent ⇒ the contexts carry no
1177
1178
  * `cwd`, which is the honest answer for a run with no tracked one. */
1178
1179
  trackedCwd?: () => string | undefined;
1180
+ /** The leg's target-reading stamp, applied where the gate's request is minted over the final args (a rewrite re-reads it: `withArgsReading`). */
1181
+ stampCallTarget?: CallTargetStamp;
1179
1182
  /** The read-only env capability face put on every {@link HookToolContext} this gate call
1180
1183
  * builds ({@link HookEnvCapabilities}). Built ONCE per task by the runner (after the env is minted) and
1181
1184
  * passed through unchanged; omitted when the deployment wired no execution environment. */
@@ -0,0 +1,65 @@
1
+ import { type PathRuleBase, type PathRuleBases, type PersistedRule, type RuleBehavior, type RuleScope, type ScopeRootIdentity } from "./permission-rule-model.js";
2
+ import { type DenyFace, type JudgedPathRow, type PathRowReach } from "../tools/fs/read-deny.js";
3
+ import { type ToolPolicy } from "./tool-policy.js";
4
+ export { pathRowsReach, rowsWhereTheyLead, type PathRowsHit, type TargetViews } from "../tools/fs/read-deny.js";
5
+ /** One path rule as a binding reads it: the parsed rule, its text, and — for a deny rule that guards a face of the
6
+ * engine's deny tables — those faces (absent: the rule is judged on its own tool's calls only). */
7
+ export interface PathRuleSpec {
8
+ readonly parsed: Pick<PersistedRule, "match" | "command" | "behavior" | "tool">;
9
+ readonly ruleText: string;
10
+ readonly faces?: readonly DenyFace[];
11
+ }
12
+ /** A rule bound to its anchors: a deny-table row (`faces` empty for a rule that guards no table face), its three-valued
13
+ * reach over an absolute spelling, and the spec it was bound from. `unanchored` names the base words its pattern needs
14
+ * that the anchors do not supply (the rule reads `unreadable` everywhere: a gate lane fails closed on it; a deny table
15
+ * cannot judge it and leaves it to the gate). */
16
+ export interface BoundPathRule<S extends PathRuleSpec = PathRuleSpec> extends JudgedPathRow {
17
+ readonly spec: S;
18
+ /** The rule's behavior (a deny table takes the `deny` rows that guard a face). */
19
+ readonly behavior: RuleBehavior;
20
+ readonly reach: (absolutePath: string) => PathRowReach;
21
+ readonly unanchored?: readonly PathRuleBase[];
22
+ }
23
+ /**
24
+ * The ONE binding of path rules to their anchors (`anchors`: one set for every spec, or a set per spec). Each row resolves
25
+ * its pattern once against its anchors and reads an absolute spelling under the anchors' family — a row installed on a win
26
+ * root reads `C:\proj\x`, `C:/proj/x` and `c:\proj\x` as one path; a row whose anchors name no tree reads the spelling's
27
+ * own family. The head (where the row's reach starts) is spelled for the environment, so a leg can judge the row where
28
+ * the head leads.
29
+ */
30
+ export declare function bindPathRules<S extends PathRuleSpec>(specs: readonly S[], anchors: PathRuleBases | ((spec: S) => PathRuleBases)): BoundPathRule<S>[];
31
+ /** A caller policy's anchors on the leg it is installed on: the declared `pathBases` word by word, else the leg's — `/x`
32
+ * at the declared root (else the leg's root), a relative pattern at the declared cwd (else the `/x` anchor), `~/` at the
33
+ * declared home (else the leg environment's). `install` absent (a policy called outside a Runner leg): the declaration
34
+ * alone. */
35
+ export declare function policyAnchors(declared: PathRuleBases, install: PathRuleBases | undefined): PathRuleBases;
36
+ /** A stored row's anchors: a PROJECT row's `/x` and relative words at the row's own scope root (the project it was written
37
+ * for), a global or session row's at the chain's top leg; `~/` at the top leg's home for every row. */
38
+ export declare function persistedRowAnchors(scope: RuleScope, chainTop: PathRuleBases): PathRuleBases;
39
+ /** A leg's root as an identity for project coverage ({@link ScopeRootIdentity}): its canonical root, and each project
40
+ * row's scope root resolved through the leg's resolution (`leads`, each location once per leg) — so a row written under
41
+ * one spelling of the project's directory applies to a leg rooted at another spelling of it. `undefined` when the leg
42
+ * has no canonical root (the spellings alone are compared). */
43
+ export declare function projectScopeIdentity(rules: readonly Pick<PersistedRule, "scope">[], canonicalRoot: string | undefined, leads: HeadLeads): Promise<ScopeRootIdentity | undefined>;
44
+ /** A leg's resolution of the locations rule heads pass through: each location resolved once (twins are not refreshed
45
+ * within a leg — a link re-pointed mid-leg is met through the row as spelled, and at its new location from the next
46
+ * prepare; a location that does not resolve has no twin). `resolve` settles the parts of the given rows and answers
47
+ * every location settled so far. */
48
+ export interface HeadLeads {
49
+ resolve(rows: readonly Pick<JudgedPathRow, "head">[]): Promise<ReadonlyMap<string, string | undefined>>;
50
+ }
51
+ export declare function createHeadLeads(leadsTo: (part: string) => Promise<string | undefined>): HeadLeads;
52
+ /**
53
+ * INSTALL a policy on a leg: its path rules are anchored at `anchors` (the leg's root and home; the policy's declared
54
+ * `pathBases` still win word by word) — once, for every judgment of the policy from then on: its `check` (the copy of each
55
+ * request it hands its inner check carries `ToolCallRequest.pathRuleAnchors`), the deny-table rows
56
+ * (`ToolPolicy.pathRules` answers with this installation's rows whatever it is given), and every child the leg hands
57
+ * the policy to as a parent constraint. A policy with no path rules is returned as it is. Installing an installed policy
58
+ * installs what it was installed FROM; one policy installed at the same anchors is one object (identity is the
59
+ * installation). Every member the policy carries rides as it holds it; the decision its `check` answers is untouched.
60
+ */
61
+ export declare function installPolicyAnchors(policy: ToolPolicy | undefined, anchors: PathRuleBases): ToolPolicy | undefined;
62
+ /** The policy an installation was installed from (`undefined`: not an installation). */
63
+ export declare function installedFrom(policy: ToolPolicy | undefined): ToolPolicy | undefined;
64
+ /** The anchors an installation was installed at (`undefined`: not an installation). */
65
+ export declare function installationAnchorsOf(policy: ToolPolicy | undefined): PathRuleBases | undefined;
@@ -0,0 +1,99 @@
1
+ import { parsePermissionRule } from "./permission-rule-syntax.js";
2
+ import { isUsablePathBase, pathRuleHeadOf, pathRuleReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
3
+ import { pathFamilyOf, spellingOfIdentity } from "../tools/fs/safety.js";
4
+ import { absoluteTargetIdentityOf } from "./effective-path-target.js";
5
+ import { locationLeadsTo, ruleHeadParts } from "../tools/fs/read-deny.js";
6
+ import { carriedMembersOf } from "./tool-policy.js";
7
+ export { pathRowsReach, rowsWhereTheyLead } from "../tools/fs/read-deny.js";
8
+ const NOT_REACHED = { reach: "not_reached" };
9
+ export function bindPathRules(specs, anchors) {
10
+ return specs.map((spec) => {
11
+ const a = typeof anchors === "function" ? anchors(spec) : anchors;
12
+ const family = pathFamilyOf(a);
13
+ const head = pathRuleHeadOf(spec.parsed, a);
14
+ const unanchored = ruleBasesNeeded(spec.parsed).filter((b) => !isUsablePathBase(b === "cwd" ? (a.cwd ?? a.root) : a[b], family));
15
+ const reach = (absolutePath) => {
16
+ const tree = a.root !== undefined || a.cwd !== undefined ? a : { root: absolutePath };
17
+ const id = absoluteTargetIdentityOf(absolutePath, tree);
18
+ return id === undefined ? NOT_REACHED : pathRuleReachOf(spec.parsed, id, a, pathFamilyOf(tree));
19
+ };
20
+ return {
21
+ spec,
22
+ behavior: spec.parsed.behavior,
23
+ rule: spec.ruleText,
24
+ tool: spec.parsed.tool,
25
+ pattern: parsePermissionRule(spec.ruleText).ruleContent ?? "",
26
+ faces: spec.faces ?? [],
27
+ reach,
28
+ reaches: (absolutePath) => reach(absolutePath).reach !== "not_reached",
29
+ ...(head !== undefined ? { head: spellingOfIdentity(head, family) } : {}),
30
+ ...(unanchored.length > 0 ? { unanchored } : {}),
31
+ };
32
+ });
33
+ }
34
+ export function policyAnchors(declared, install) {
35
+ const root = declared.root ?? install?.root;
36
+ const cwd = declared.cwd ?? root;
37
+ const home = declared.home ?? install?.home;
38
+ return { ...(root !== undefined ? { root } : {}), ...(cwd !== undefined ? { cwd } : {}), ...(home !== undefined ? { home } : {}) };
39
+ }
40
+ export function persistedRowAnchors(scope, chainTop) {
41
+ const root = scope.kind === "project" ? scope.root : chainTop.root;
42
+ return { ...(root !== undefined ? { root, cwd: root } : {}), ...(chainTop.home !== undefined ? { home: chainTop.home } : {}) };
43
+ }
44
+ export async function projectScopeIdentity(rules, canonicalRoot, leads) {
45
+ if (canonicalRoot === undefined)
46
+ return undefined;
47
+ const roots = rules.flatMap((r) => (r.scope.kind === "project" ? [{ head: r.scope.root }] : []));
48
+ const led = roots.length > 0 ? await leads.resolve(roots) : new Map();
49
+ return { canonicalRoot, canonicalOf: (root) => locationLeadsTo(root, led) };
50
+ }
51
+ export function createHeadLeads(leadsTo) {
52
+ const pending = new Map();
53
+ const settled = new Map();
54
+ return {
55
+ async resolve(rows) {
56
+ const parts = ruleHeadParts(rows).filter((p) => !settled.has(p));
57
+ await Promise.all(parts.map((part) => {
58
+ let p = pending.get(part);
59
+ if (p === undefined) {
60
+ p = leadsTo(part).catch(() => undefined);
61
+ pending.set(part, p);
62
+ }
63
+ return p.then((led) => settled.set(part, led));
64
+ }));
65
+ return settled;
66
+ },
67
+ };
68
+ }
69
+ const INSTALLED = new WeakMap();
70
+ const INSTALLATIONS = new WeakMap();
71
+ export function installPolicyAnchors(policy, anchors) {
72
+ if (policy === undefined)
73
+ return undefined;
74
+ const from = INSTALLED.get(policy)?.from ?? policy;
75
+ const rules = from.pathRules;
76
+ if (rules === undefined)
77
+ return from;
78
+ const key = JSON.stringify([anchors.root ?? null, anchors.cwd ?? null, anchors.home ?? null]);
79
+ const byAnchors = INSTALLATIONS.get(from) ?? new Map();
80
+ INSTALLATIONS.set(from, byAnchors);
81
+ const prior = byAnchors.get(key);
82
+ if (prior !== undefined)
83
+ return prior;
84
+ const seated = Object.freeze({ ...anchors });
85
+ const installed = {
86
+ ...carriedMembersOf(from),
87
+ pathRules: () => rules(seated),
88
+ check: (req, signal) => from.check({ ...req, pathRuleAnchors: seated }, signal),
89
+ };
90
+ INSTALLED.set(installed, { from, anchors: seated });
91
+ byAnchors.set(key, installed);
92
+ return installed;
93
+ }
94
+ export function installedFrom(policy) {
95
+ return policy === undefined ? undefined : INSTALLED.get(policy)?.from;
96
+ }
97
+ export function installationAnchorsOf(policy) {
98
+ return policy === undefined ? undefined : INSTALLED.get(policy)?.anchors;
99
+ }