@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.
- package/CHANGELOG.md +246 -1
- package/dist/agents/extra-tool-names.d.ts +65 -0
- package/dist/agents/extra-tool-names.js +124 -0
- package/dist/agents/subagent.d.ts +17 -5
- package/dist/agents/subagent.js +33 -25
- package/dist/agents/tool-filter.d.ts +17 -1
- package/dist/agents/tool-filter.js +7 -0
- package/dist/core/agent-config-dir.d.ts +20 -0
- package/dist/core/agent-config-dir.js +49 -0
- package/dist/core/allow-entry.d.ts +5 -3
- package/dist/core/allow-entry.js +3 -1
- package/dist/core/ask-origin.js +1 -1
- package/dist/core/checkpoint-store.d.ts +39 -2
- package/dist/core/effective-path-target.d.ts +136 -4
- package/dist/core/effective-path-target.js +110 -2
- package/dist/core/engine-notice.d.ts +5 -0
- package/dist/core/gate-fold.js +3 -0
- package/dist/core/gate-lanes.js +9 -3
- package/dist/core/governance-codes.d.ts +3 -1
- package/dist/core/governance-codes.js +3 -0
- package/dist/core/hooks.d.ts +3 -0
- package/dist/core/path-rule-binding.d.ts +65 -0
- package/dist/core/path-rule-binding.js +99 -0
- package/dist/core/permission-rule-model.d.ts +47 -18
- package/dist/core/permission-rule-model.js +38 -42
- package/dist/core/permission-rule-provider.d.ts +7 -0
- package/dist/core/permission-rule-provider.js +10 -3
- package/dist/core/permission-rule-store.d.ts +8 -0
- package/dist/core/permission-rule-store.js +6 -2
- package/dist/core/permission-rule-sync.js +2 -2
- package/dist/core/permission-rules.d.ts +22 -40
- package/dist/core/permission-rules.js +50 -43
- package/dist/core/runner/child-tree.d.ts +97 -0
- package/dist/core/runner/child-tree.js +104 -0
- package/dist/core/runner/construction-doors.d.ts +5 -0
- package/dist/core/runner/construction-doors.js +6 -0
- package/dist/core/runner/contracts.d.ts +46 -0
- package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
- package/dist/core/runner/permission-rule-lanes.js +18 -12
- package/dist/core/runner/persisted-path-rows.d.ts +31 -7
- package/dist/core/runner/persisted-path-rows.js +31 -12
- package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
- package/dist/core/runner/prepare-ask-lane.js +4 -2
- package/dist/core/runner/prepare-execution-env.d.ts +5 -2
- package/dist/core/runner/prepare-execution-env.js +10 -3
- package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
- package/dist/core/runner/prepare-gate-stations.js +2 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
- package/dist/core/runner/prepare-hands-readface.js +6 -6
- package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
- package/dist/core/runner/prepare-inherited-gate.js +8 -2
- package/dist/core/runner/prepare-park-ask.d.ts +3 -0
- package/dist/core/runner/prepare-park-ask.js +5 -3
- package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
- package/dist/core/runner/prepare-policy-chain.js +11 -4
- package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
- package/dist/core/runner/prepare-suspend-saga.js +7 -1
- package/dist/core/runner/prepare-task.js +37 -18
- package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
- package/dist/core/runner/prepare-turn-wiring.js +2 -1
- package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
- package/dist/core/runner/prepare-workspace-restore.js +1 -0
- package/dist/core/runner/resume-apply.js +17 -3
- package/dist/core/runner/resume-checkpoint-screen.js +13 -0
- package/dist/core/runner/run-leg.js +5 -1
- package/dist/core/runner/runtask.js +2 -2
- package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
- package/dist/core/runner/write-protection-station.d.ts +3 -2
- package/dist/core/runner/write-protection-station.js +1 -0
- package/dist/core/runner-deps.d.ts +14 -0
- package/dist/core/sensitive-path-policy.d.ts +8 -0
- package/dist/core/sensitive-path-policy.js +18 -2
- package/dist/core/shell-wrapper-projection.d.ts +11 -0
- package/dist/core/shell-wrapper-projection.js +56 -0
- package/dist/core/shell-wrapper-table.d.ts +144 -34
- package/dist/core/shell-wrapper-table.js +40 -20
- package/dist/core/synthetic-read-verdict.d.ts +3 -0
- package/dist/core/synthetic-read-verdict.js +3 -1
- package/dist/core/tool-catalog-entries.d.ts +21 -0
- package/dist/core/tool-catalog-entries.js +9 -0
- package/dist/core/tool-policy.d.ts +30 -8
- package/dist/core/tool-policy.js +9 -7
- package/dist/core/tool-spec.d.ts +3 -2
- package/dist/core/write-protect.d.ts +4 -0
- package/dist/core/write-protect.js +30 -9
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/orchestration/run-spec.js +8 -7
- package/dist/orchestration/workflow.js +1 -1
- package/dist/shell/lex.d.ts +7 -0
- package/dist/shell/lex.js +17 -0
- package/dist/tools/fs/bash-program-position.d.ts +8 -87
- package/dist/tools/fs/bash-program-position.js +33 -69
- package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
- package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
- package/dist/tools/fs/index.d.ts +3 -2
- package/dist/tools/fs/program-read-model.d.ts +694 -0
- package/dist/tools/fs/program-read-model.js +1252 -0
- package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
- package/dist/tools/fs/program-read-model.rows.js +613 -0
- package/dist/tools/fs/read-deny-home.d.ts +19 -1
- package/dist/tools/fs/read-deny-home.js +62 -24
- package/dist/tools/fs/read-deny.d.ts +54 -14
- package/dist/tools/fs/read-deny.js +104 -50
- package/dist/tools/fs/removal-targets.d.ts +7 -6
- package/dist/tools/fs/removal-targets.js +22 -22
- package/dist/tools/fs/safety.d.ts +3 -0
- package/dist/tools/fs/safety.js +10 -3
- package/dist/tools/fs/shell-read-argv.d.ts +85 -0
- package/dist/tools/fs/shell-read-argv.js +386 -0
- package/dist/tools/fs/shell-read-bindings.d.ts +273 -0
- package/dist/tools/fs/shell-read-bindings.js +746 -0
- package/dist/tools/fs/shell-read-check.js +23 -15
- package/dist/tools/fs/shell-read-establish.js +79 -5
- package/dist/tools/fs/shell-read-judge.d.ts +11 -1
- package/dist/tools/fs/shell-read-judge.js +9 -1
- package/dist/tools/fs/shell-read-plan.d.ts +31 -7
- package/dist/tools/fs/shell-read-plan.js +299 -393
- package/dist/tools/fs/shell-read-word.d.ts +29 -26
- package/dist/tools/fs/shell-read-word.js +30 -38
- package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
- package/dist/tools/fs/shell-read-write-targets.js +83 -136
- package/dist/tools/fs/shell-reentry-text.d.ts +38 -6
- package/dist/tools/fs/shell-reentry-text.js +90 -53
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +17 -3
- 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)
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
package/dist/core/gate-fold.js
CHANGED
|
@@ -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);
|
package/dist/core/gate-lanes.js
CHANGED
|
@@ -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
|
|
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(
|
|
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,
|
|
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",
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -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
|
+
}
|