@sema-agent/core 7.38.0 → 7.38.1
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 +33 -0
- package/dist/brain/tool-call-repair.js +1 -1
- package/dist/core/engine-notice.d.ts +4 -3
- package/dist/core/runner/prepare-artifact.d.ts +3 -2
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -3
- package/dist/core/runner/prepare-hands-readface.js +1 -0
- package/dist/core/runner/prepare-turn-wiring.d.ts +2 -2
- package/dist/tools/fs/fs-shared.d.ts +2 -1
- package/dist/tools/fs/fs-shared.js +1 -1
- package/dist/tools/fs/index.d.ts +13 -6
- package/dist/tools/fs/index.js +12 -4
- package/dist/tools/fs/read-deny-home.d.ts +98 -45
- package/dist/tools/fs/read-deny-home.js +294 -117
- package/dist/tools/fs/read-deny.d.ts +2 -2
- package/dist/tools/fs/safety.d.ts +2 -0
- package/dist/tools/fs/safety.js +1 -1
- package/dist/tools/fs/search.d.ts +4 -3
- package/dist/tools/fs/search.js +3 -3
- package/dist/tools/fs/shell-read-check.d.ts +9 -0
- package/dist/tools/fs/shell-read-check.js +3 -2
- package/dist/tools/fs/shell-read-establish.d.ts +3 -0
- package/dist/tools/fs/shell-read-establish.js +1 -1
- package/dist/tools/fs/shell-read-identity.d.ts +4 -3
- package/dist/tools/fs/shell-read-identity.js +2 -2
- package/package.json +1 -1
- package/wire-changes/core@7.38.1.json +56 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
## 7.38.1 — 2026-10-02
|
|
2
|
+
|
|
3
|
+
**Hard-breaking changes:** none.
|
|
4
|
+
|
|
5
|
+
**Closed sets:** none. **Package exports:** +0 / −0.
|
|
6
|
+
|
|
7
|
+
### Home-anchored read-deny rows are placed under the account home as well as the child's `HOME` (#1244; behavior (tighten: the read deny table); +0 exports) (@server @cli @client-core @test)
|
|
8
|
+
|
|
9
|
+
- **Before (v7.38.0).** The home-anchored rows of the read deny table (`~/.sema/settings.json`, `~/.ssh`, `~/.aws`, …) were placed under one home: the `HOME` the shell child receives (`ExecutionEnv.homeDir`; `NodeExecutionEnv` reads it off `shellEnv` over the inherited environment). A host that handed the child a temporary `HOME` moved the whole fence with it — the user's real home directory (the account database's, `os.userInfo().homedir`) held no row, and the structured read tools and the shell read station served `~/.sema/settings.json` there.
|
|
10
|
+
- **Now.** `declaredReadDenyHomes` returns the declared home and, when its lexically normalized spelling differs, the account home. The home set is fixed ONCE per placement — the Runner's (per run), the library-direct toolkit's own, or a host's explicit `readDenyHomes` taken as the home set (the account database is then not asked and no home is added) — and every home of the set, looked up or listed, is anchored at its spelling and at its canonical spelling where the environment answered one — and every judgment reads that set: a judge carries the home views of the placement it was compiled at, so a path spelled through a firmlink or a bind mount of a home is respelled at the home without asking the account again. The Runner hands its tool band that placement itself — the very home list it returned carries its views — so the band never reads the homes a second time; a band handed any other list with the placement's links (a copy) holds its home-anchored rows at every location and says so on `console.warn` (an engine defect, never a silent narrower view). The DECLARED home keeps the v7.38.0 law: its probes wait the gate bound, a placement whose declared home did not answer holds the rows at every location and is asked again, and its directory chain is asked again by every judgment until it answers (a home created after the run began is read once it exists); its views stay under every answer of the account lookup, also under no answer, so a user's deny rule and the write face meet its aliases as in v7.38.0. The ACCOUNT home (and every entry of an explicit list that is not the declared home) is best effort and bounded: its canonical spelling, its identity chain and where its rows' heads lead are read once per placement under one 2000 ms bound; what answered is kept and only what did not is lost, that home's alone — a canonical spelling that answered anchors the rows there at once; a chain that did not answer leaves a path through an alias of the home judged as spelled, a head that did not answer adds no linked location, until the next placement; a canonical spelling that did not answer leaves the rows at the home's spelling alone — the placement stays whole, and one `console.warn` line says so. In the chain a directory that is a home (read off the home's own canonical directory, where its identity answered) keeps the home's own spelling over an ancestor's. A head under a spelling that is no directory — a file, a symlink to a file, a device node, a socket: the environment's path resolution answers `not_directory` — has nothing below it (an answer; before, only a plain file counted: a `HOME` that is a symlink to a file left the placement not whole, replayed on every call). The `~` expansion base is unchanged: `ExecutionEnv.homeDir` stays the directory the child's own `~` names. An environment that declares no absolute home still places nothing (a home-anchored row then holds its run anywhere); the account home is no stand-in for it.
|
|
11
|
+
- **Who is hurt.** A read of a file under the account home that matches a home-anchored row (`~/.sema/settings.json`, `~/.ssh/…`, `~/.aws/…`) while the child's `HOME` is elsewhere: refused (`read_path_denied`) where it was served. A host that lists `readDenyHomes` itself sees the same placement, and its views now cover every listed home (v7.38.0 respelled aliases of the declared home only: an alias spelling of another listed home was served); its explicit list stays a legal channel and can be withdrawn once it carries nothing the default does not. A remote or sandboxed leg gets the engine host's account home as one more anchor: a row over a path that may not exist there. A deployment whose account lookup fails for a reason other than "no passwd row" gets the over-strict placement above (rows held at every location) until the lookup answers. A hung account home makes a placement wait up to 2 s more, once per placement (the Runner's tool band judges with the run's placement and reads no home again). A host that passes BOTH `readDenyHomes` and `readDenyHomeLinks` (a hand-built pair) is now taken for the engine's own handoff of a placement it cannot find: the home-anchored rows are held at every location and `console.warn` names it (v7.38.0 placed the rows at the listed homes); no sibling host makes that call — pass `readDenyHomes` alone and let the toolkit resolve where the heads lead. No sibling host lists one today: server's only home seat is the execution environment's `homeDir` (`boot/execution-env.ts`), so its deployments get the account home from this default with no change on its side.
|
|
12
|
+
- **Compensation.** None needed for the common case (`HOME` equals the account home: one home, nothing changes). A deployment that really reads a file under its own account home that a row names lists the row in `readDenyBuiltinExclude`.
|
|
13
|
+
- **Known limit (KL-1244-a).** The account lookup has three answers, the split the environment's `canonicalPath` already meets, and NOTHING is kept for the process: every placement asks afresh, and a placement's judgments never ask. A home places. **No account home** — `os.userInfo()` throws the libuv "no passwd row" error (`info.code === "ENOENT"`, a container that runs an arbitrary uid; also a directory service that is down and says so) or answers a non-absolute path — places the rows under the declared home alone, a whole placement; the missing row is not a fault and does not warn (a non-absolute answer warns once). A library-direct toolkit does not place again after a whole placement, so an account home that answers later is not placed for that toolkit's lifetime; the Runner's next run places it. **No answer** on the first placement — any other failure, whatever the error object looks like — says so once on `console.warn` and yields an EMPTY home list (`readDenyHomesOf`, `declaredReadDenyHomes`), the engine's rule for "no home known": the home-anchored rows hold their run at every location until the lookup answers (the project's own `.sema/settings.json` is refused too — over-strict, recoverable) and the placement is asked again at the next placement; the home views keep the declared home meanwhile. `readDenyHomesOf` returns `[]` both when the environment declares no home and when the lookup has no answer, so a host cannot tell them apart and should not cache the list: take it at every mount, or omit `readDenyHomes` and let the toolkit place itself. A toolkit whose table has no home-anchored row places nothing and its views read the declared home (or the list it is handed) without asking the account. A structured notice would be a closed-set row; a minor release carries it.
|
|
14
|
+
- **Known limit (KL-1244-b).** An auxiliary home loses, until the next placement, exactly the parts that did not answer within its 2000 ms bound: a chain that did not answer — paths through an alias of it are judged as spelled; a head that did not answer — no linked location for it; a canonical spelling that did not answer — the rows sit at its spelling alone (neither its canonical spelling nor its linked targets are guarded). The next placement is the Runner's next run; a library-direct toolkit does not place again for that. A directory the probes may not enter (EACCES) is no answer: as the declared home it leaves the placement not whole (replayed per call; the rows stay at its spellings). **Known limit (KL-1244-d).** The public `judgeShellRead` has no placement to read — the boundary's deny seats are the host's own compile and the input carries no home list — so its views read the declared home and the account home looked up for that call; rows the host placed at another home get no alias views there (as in v7.38.0, whose public judgment read the declared home only); an optional input seat comes with a minor. **Known limit (KL-1244-c).** A user's own `~/` persisted permission rule (`Read(~/.aws/**)` …) still resolves against the environment's declared home only (`prepare-task.ts`, `prepare-policy-chain.ts`, `permission-rule-lanes.ts`); under a temporary `HOME` such a rule does not cover the real home directory. The built-in read deny table does.
|
|
15
|
+
- Pins: `test/backlog1244-passwd-home.test.ts` — ① Read / `cat` / Grep / the library-direct toolkit over the account home with a temporary child `HOME` (baseline: served); ② the child's own `HOME` still guarded; ③ one home when both are the same; ④ the `~` expansion base is the child's `HOME`; ⑤ the lookup's answers — (i) ENOENT: declared home alone, whole, silent, the project's settings read; (ii) no answer on a direct toolkit: rows held anywhere, said once, placed again on the next read; (iii) the same on the Runner lane, per run; (iv) the views read the placement's homes — the declared home's chain under every answer, read once per placement, the account home's only when it answered; (v) every reader of the home list is empty under no answer, and a toolkit built from `readDenyHomesOf` holds the rows anywhere (the account home's file, the declared home's file and the project's settings refused, a plain file read), then carries both homes once the lookup answers; (vi) the `~` base is still the child's `HOME`; (vii) the failure is classified totally (an `info` / `message` getter or `toString` that throws, an unprintable value, a console that throws) and the tool call still ends with a frame; a non-absolute answer is a bad value, said once; ⑥ no declared home ⇒ no placement; ⑦ the account home reached through a mount that the declared home lies below (Read / `cat` / Grep refused; the view of a path through the mount is the account home's spelling), also when the declared home's own identity probe fails; ⑧ the placement read the account home, then the lookup fails (EMFILE): the alias spellings of both homes are still refused (toolkit and Runner), nothing is kept for the process and a placement's judgments ask the account 0 times, an account home that does not exist or is a plain file is an answer (the chain read once, the placement whole). `test/backlog1244-home-view-snapshot.test.ts` (baseline car-7381: 10 of 11 red) — T1–T3 a placement that answered (ENOENT / a bad value) then a failing lookup: the declared home's alias refused (toolkit, Runner: Read / `cat` / Grep); R3a an explicit `[H, A]` under a lookup that never answers: both aliases refused, the account asked 0 times; R3b an account home whose path probe hangs: the first judgment ends under 3 s, the declared home's alias and the account home's own file refused, the project's settings read, the Runner's run not failed; R3c an account home whose probe fails: probed by the placement only, the project's settings read; Q1 under no answer a user's Read deny rule and an Edit deny rule (the write face) meet the declared home's alias; Q2 a declared home created after the placement is read once it exists; N a declared home that is a symlink to a file, a device node or a socket: a whole placement, not replayed; a directory the probe may not enter is no answer; R2 an account home that answers after a whole placement is not placed by the toolkit (the account not asked), the Runner's next run places it; R4 the account home's probe answers the run's placement and fails afterwards: Read / Grep / Glob through its alias refused on the Runner lane, its chain read once per run; a carried copy of the list holds the rows anywhere and is named; a host's own list is read once by the toolkit itself; R6 a host's list `[H, A-link]` whose A-link answers its canonical spelling while its chain and heads are slow: Read / Grep of A-real refused, the anchors carry A-real; a symlinked declared home in a list is anchored at its canonical spelling too; a list of real directories is placed at exactly the list; R5 a symlinked account home whose canonical spelling answers at once and whose chain is slow: anchored at the real path (a Grep over the link, a Read and a `cat` of the real path refused), a canonical probe that hangs leaves the spelling alone, `readDenyHomesOf` lists the placement's anchors. `test/backlog-r2-default-tiers.test.ts` asserts the whole homes list.
|
|
16
|
+
|
|
17
|
+
### The text-repair lane's fallback tool-call id never carries the tool's name (#1219; behavior (the id's shape); +0 exports) (@server @test)
|
|
18
|
+
|
|
19
|
+
- **Before (v7.38.0).** `repairTextToolCalls` minted `repair_<tool name>_<body>`. A roster name is only length-bounded (1–128), so a non-ASCII name or a 120-character name made an id outside the admitted shape that every native lane's id is held to, with no fold and no notice.
|
|
20
|
+
- **Now.** The prefix is the constant `repair_` (as `toolu_` / `call_` are): `repair_<opaque body>`, in the admitted shape, kept verbatim by `admitToolCallId`; the id no longer names the tool. Off by default (`createOpenAIBrain({ repairTextToolCalls: true })`).
|
|
21
|
+
- **Who is hurt.** A consumer that parsed a repaired call's tool name out of its id, or matched `^repair_<name>_`. Nothing in the engine does.
|
|
22
|
+
- **Compensation.** Read the call's `name`.
|
|
23
|
+
- Pins: `test/backlog1219-repair-fallback-id.test.ts` (non-ASCII, 120-character, fold-separator and space names; the openai lane end to end); `test/brain-protocol-coverage.test.ts` re-pinned.
|
|
24
|
+
|
|
25
|
+
### Docs: which of a call's names a host's own by-name policy reads (#1220; docs only; +0 exports)
|
|
26
|
+
|
|
27
|
+
`docs/INTEGRATION-CORE.md` §4.6f: a tightening policy (deny, ask, require approval) reads the whole of `ruleNamesOf(req)`; a loosening one (allow, auto-approve) reads `toolName`, the wire name. No code change; `ruleNamesOf` was already exported.
|
|
28
|
+
|
|
29
|
+
### Contract sentence: the `crashed` cause of `task.user_steer_undrained` (docs only) (@server @cli @client-core)
|
|
30
|
+
|
|
31
|
+
- `USER_STEER_UNDRAINED_CAUSES` member `crashed` now reads "accepted by the serving layer and not confirmed consumed before the process died" (was "never consumed"). A live leg gives the serving layer no evidence that a steer reached the model, so the arm cannot claim the steer was never consumed; a consumer must not tell the person to re-send without checking the run's transcript. No code, word or shape change. The consumption evidence itself is a separate change (#1273).
|
|
32
|
+
|
|
33
|
+
|
|
1
34
|
## 7.38.0 — 2026-10-01
|
|
2
35
|
|
|
3
36
|
**Hard-breaking changes:** none.
|
|
@@ -395,7 +395,7 @@ export interface EngineNotice {
|
|
|
395
395
|
* `detail: { steer, cause, taskId?, sessionId?, runId? }` / `{ followUp, taskId?, sessionId?, runId? }`
|
|
396
396
|
* (`cause` on the steer row only, closed `USER_STEER_UNDRAINED_CAUSES`: `"leg_end"` — this engine's arm, the
|
|
397
397
|
* leg ended with it still owed; `"crashed"` — minted by the SERVING LAYER, not this engine, before the
|
|
398
|
-
* session's next run, for steers it had accepted
|
|
398
|
+
* session's next run, for steers it had accepted whose consumption was not confirmed before the process died: their queue was the
|
|
399
399
|
* serving layer's, so only it can count them. One code for both, so a consumer de-duplicating on `code` keeps
|
|
400
400
|
* one row; `sessionId` = #433's routing half for these two `"user"`-audience rows; `runId` = #499's
|
|
401
401
|
* per-run identity, the key this account is actually about — see
|
|
@@ -952,10 +952,11 @@ export declare function settleOwedUserInput(account: OwedUserInputAccount, rowRe
|
|
|
952
952
|
runId?: string;
|
|
953
953
|
}, onNotice: ((notice: EngineNotice) => void) | undefined): void;
|
|
954
954
|
/**
|
|
955
|
-
* Why a person's steer
|
|
955
|
+
* Why a person's steer is reported as not consumed — `task.user_steer_undrained`'s `detail.cause`. Closed:
|
|
956
956
|
* · `leg_end` — the leg ended with the steer still owed (this engine mints it, at the leg's settlement);
|
|
957
957
|
* · `crashed` — the engine process ended before the leg settled, and a steer the serving layer had accepted for it was
|
|
958
|
-
*
|
|
958
|
+
* not confirmed consumed before the process died (a live leg gives the serving layer no evidence that a steer reached
|
|
959
|
+
* the model, so such a steer MAY have been consumed). Only the serving layer can count these (the queue was its own), so the serving layer
|
|
959
960
|
* mints this arm, before the next run of the session starts — the one member-level exception to
|
|
960
961
|
* {@link import("./governance-codes.js").NOTICE_MINTER}'s row for the code, which stays `"engine"`.
|
|
961
962
|
*/
|
|
@@ -18,7 +18,8 @@ import type { OnAsk } from "../tool-policy.js";
|
|
|
18
18
|
import { type ArtifactPlane } from "../artifact-host.js";
|
|
19
19
|
import { RosterBuilder } from "../tool-roster.js";
|
|
20
20
|
import type { AgentTool, RunnerDeps } from "../types.js";
|
|
21
|
-
import type {
|
|
21
|
+
import type { ContainmentFace } from "../../tools/fs/index.js";
|
|
22
|
+
import type { PlacedReadDenyMatcher } from "../../tools/fs/read-deny-home.js";
|
|
22
23
|
import { type ArtifactStoreRef } from "./read-root-grant.js";
|
|
23
24
|
import type { Prepared } from "./contracts.js";
|
|
24
25
|
export interface PrepareArtifactInput {
|
|
@@ -46,7 +47,7 @@ export interface PrepareArtifactInput {
|
|
|
46
47
|
* live set: an in-flight read-root grant widens it). */
|
|
47
48
|
additionalRootsCanonical: () => readonly string[];
|
|
48
49
|
/** borrowed-readonly — the read-deny matcher in force for the run, when any. */
|
|
49
|
-
readDenyMatcher:
|
|
50
|
+
readDenyMatcher: PlacedReadDenyMatcher | undefined;
|
|
50
51
|
/** borrowed-readonly — the resolved read face (`"open"` lifts root containment for reads). */
|
|
51
52
|
resolvedReadFace: ContainmentFace | undefined;
|
|
52
53
|
/** borrowed-readonly — the tracked-cwd cell; `current` is read per call as the relative-path base. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type BeforeWriteHook, type CwdRef, type
|
|
1
|
+
import { type BeforeWriteHook, type CwdRef, type PathRuleDenyRow, type ContainmentFace } from "../../tools/fs/index.js";
|
|
2
2
|
import { type ReadFileState } from "../../tools/fs/safety.js";
|
|
3
|
+
import { type PlacedReadDenyMatcher } from "../../tools/fs/read-deny-home.js";
|
|
3
4
|
import { type ShellReadAdmission, type ShellReadCheck } from "../../tools/fs/shell-read-check.js";
|
|
4
5
|
import type { ShellReadBoundary } from "../../tools/fs/shell-read-judge.js";
|
|
5
6
|
import type { RunnerDeps, ShellGateDoctrine, TaskSpec } from "../types.js";
|
|
@@ -254,8 +255,8 @@ export interface PrepareHandsReadFaceResult {
|
|
|
254
255
|
* reader applies the same containment the hands tools do. Undefined ⇔ hands-less. */
|
|
255
256
|
attachmentRootCanonical: string | undefined;
|
|
256
257
|
/** owned — the ONE compiled deny judge per prepare (built-ins ∪ deployment ∪
|
|
257
|
-
* task additions). Undefined ⇔ hands-less (its consumers only exist with hands). */
|
|
258
|
-
readDenyMatcher:
|
|
258
|
+
* task additions), carrying the home views of its placement. Undefined ⇔ hands-less (its consumers only exist with hands). */
|
|
259
|
+
readDenyMatcher: PlacedReadDenyMatcher | undefined;
|
|
259
260
|
/** owned — the shell-seat PROVENANCE set: the shell-running tools whose tier the engine's shell seat (read
|
|
260
261
|
* boundary + shape tier + shellGate floor) is the SOLE POSITIVE source of — no explicit deployment mark on that name.
|
|
261
262
|
* The one seat every consumer reads (risk grading, the gate's tighten attribution, the persisted-rule mandate
|
|
@@ -313,6 +313,7 @@ export async function prepareHandsMount(input) {
|
|
|
313
313
|
tools: shellReadTools,
|
|
314
314
|
seatOf: () => (handsReadOnly ? "readonly" : "full_shell"),
|
|
315
315
|
boundaryOf: () => readonlyBoundary() ?? shellReadBoundary(), rootsCanonical: true,
|
|
316
|
+
homeViews: readDenyPlacement.matcher.homeViews,
|
|
316
317
|
});
|
|
317
318
|
}
|
|
318
319
|
if (!handsReadOnly) {
|
|
@@ -10,7 +10,7 @@ import { type ToolPolicy } from "../tool-policy.js";
|
|
|
10
10
|
import type { RunnerDeps, TaskSpec } from "../types.js";
|
|
11
11
|
import type { CwdRef } from "../../tools/fs/fs-shared.js";
|
|
12
12
|
import { type ContainmentFace } from "../../tools/fs/index.js";
|
|
13
|
-
import type {
|
|
13
|
+
import type { PlacedReadDenyMatcher } from "../../tools/fs/read-deny-home.js";
|
|
14
14
|
import { type ReadFileState } from "../../tools/fs/safety.js";
|
|
15
15
|
import type { Prepared } from "./contracts.js";
|
|
16
16
|
export interface PrepareTurnWiringInput {
|
|
@@ -70,7 +70,7 @@ export interface PrepareTurnWiringInput {
|
|
|
70
70
|
/** borrowed-readonly — the canonical extra read roots (the fence's root set). */
|
|
71
71
|
additionalReadRootsCanonical: ReadonlyArray<string>;
|
|
72
72
|
/** borrowed-readonly — the compiled read-deny judge, or undefined (the reader's withheld arm). */
|
|
73
|
-
readDenyMatcher:
|
|
73
|
+
readDenyMatcher: PlacedReadDenyMatcher | undefined;
|
|
74
74
|
/** borrowed-readonly — the resolved read face, or undefined (the fence's ninth argument). */
|
|
75
75
|
resolvedReadFace: ContainmentFace | undefined;
|
|
76
76
|
/** borrowed-readonly — the tracked-cwd seat, read per call by the kept-tail normalizer (never written here). */
|
|
@@ -2,6 +2,7 @@ import { Type } from "typebox";
|
|
|
2
2
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
3
3
|
import { type FsViolation, type ReadFileState } from "./safety.js";
|
|
4
4
|
import { type DecodedTextFile } from "./encoding.js";
|
|
5
|
+
import { type HomeViewsCarrier } from "./read-deny-home.js";
|
|
5
6
|
import { type ImageDownsampler } from "../../core/mcp.js";
|
|
6
7
|
import { type EngineNotice } from "../../core/types.js";
|
|
7
8
|
/**
|
|
@@ -543,7 +544,7 @@ export declare function enoentMessage(env: ExecutionEnv, key: string, cwd: strin
|
|
|
543
544
|
matchPath(path: string): {
|
|
544
545
|
pattern: string;
|
|
545
546
|
} | null;
|
|
546
|
-
}): Promise<string>;
|
|
547
|
+
} & HomeViewsCarrier): Promise<string>;
|
|
547
548
|
/** Per-task mutable working directory shared by the shell and the path-taking fs tools.
|
|
548
549
|
* Holds the RAW path (never canonicalized): bash `cd` updates `current`, and the fs tools resolve relative
|
|
549
550
|
* paths against it. Containment is still enforced per-op by resolveKey (canonicalize + within), so a `cd`
|
|
@@ -345,7 +345,7 @@ export function applyContextShrinkToReadFileState(state, shrink) {
|
|
|
345
345
|
export async function enoentMessage(env, key, cwd, signal, readDeny) {
|
|
346
346
|
let msg = `File does not exist. Note: your current working directory is ${cwd}.`;
|
|
347
347
|
const sep = Math.max(key.lastIndexOf("/"), key.lastIndexOf("\\"));
|
|
348
|
-
const views = readDeny !== undefined ? homeViewsOf(env, signal) : undefined;
|
|
348
|
+
const views = readDeny !== undefined ? homeViewsOf(env, readDeny, signal) : undefined;
|
|
349
349
|
const denied = async (path, leaf) => {
|
|
350
350
|
if (readDeny === undefined || views === undefined)
|
|
351
351
|
return false;
|
package/dist/tools/fs/index.d.ts
CHANGED
|
@@ -186,17 +186,24 @@ export interface HandsToolkitOptions {
|
|
|
186
186
|
* time (never silently ignored). Deployment seat ONLY, same clamp argument as the tiers key. */
|
|
187
187
|
readDenyBuiltinExclude?: readonly string[];
|
|
188
188
|
/** The spellings of the execution environment's home a home-anchored deny row (`~/…`) is placed under
|
|
189
|
-
* ({@link import("./read-deny-home.js").readDenyHomesOf}: the declared home and
|
|
190
|
-
* strings only — any other shape throws at wiring time, naming the seat. The Runner passes the
|
|
191
|
-
* was compiled with.
|
|
189
|
+
* ({@link import("./read-deny-home.js").readDenyHomesOf}: the declared home and the account home, each with its canonical
|
|
190
|
+
* spelling). Absolute path strings only — any other shape throws at wiring time, naming the seat. The Runner passes the
|
|
191
|
+
* spellings its own judge was compiled with. A list given here is the band's home set as is: the account database is
|
|
192
|
+
* not asked and no home is added (each listed home is anchored at its spelling and, where the environment answers one,
|
|
193
|
+
* its canonical spelling), and the band's home views (a path spelled through a firmlink or a bind mount of a home,
|
|
194
|
+
* judged at the home) read this list and the environment's declared home; an entry that is not the declared home is probed once,
|
|
195
|
+
* under a short bound of its own, and a probe of it that fails costs that entry alone. */
|
|
192
196
|
readDenyHomes?: readonly string[];
|
|
193
197
|
/** Where each home-anchored row's literal head leads beyond the home's spellings
|
|
194
198
|
* ({@link import("./read-deny-home.js").readDenyHomeLinksOf}, keyed by the row's pattern) — the Runner passes the
|
|
195
199
|
* locations its own judge was compiled with, so the band and the gate judge one placement.
|
|
196
200
|
* Unless a mount carries both, the band resolves the placement itself the way the Runner does (the homes it carries
|
|
197
|
-
* are
|
|
198
|
-
*
|
|
199
|
-
* rows held anywhere.
|
|
201
|
+
* are its home set — the account is not asked and no home is added;
|
|
202
|
+
* {@link import("./read-deny-home.js").placeReadDenyAtHome}), and its read tools (Read / Grep / Glob / RepoMap) wait
|
|
203
|
+
* for that placement before they judge anything. A placement that fails leaves the home-anchored rows held anywhere.
|
|
204
|
+
* Carrying both is the engine's own handoff of ONE placement — its home views travel with it — so
|
|
205
|
+
* `readDenyHomes` must then be the very list that placement returned: any other list (a copy) holds the home-anchored
|
|
206
|
+
* rows at every location and is named on `console.warn`. */
|
|
200
207
|
readDenyHomeLinks?: ReadonlyMap<string, readonly string[]>;
|
|
201
208
|
/** The permission-rule rows of the deny table ({@link import("./read-deny.js").PathRuleDenyRow}, `ToolPolicy.pathDenyRules`):
|
|
202
209
|
* the read-face rows join the structured read faces' judge beside the segment rows. The Runner passes the leg's own. */
|
package/dist/tools/fs/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { BASH_READONLY_DEFAULT_ALLOW } from "./bash-readonly-classifier.js";
|
|
|
16
16
|
export { BASH_CLASSIFY_EXTRA_ALLOW, bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
|
|
17
17
|
import { checkedReadDenyHomes, compileReadDeny, compileWriteDeny, readDenyBuiltinConfigOf, readDenyPatternRun } from "./read-deny.js";
|
|
18
18
|
import { withGateTimeout } from "../../core/gate-bounded-await.js";
|
|
19
|
-
import { placeReadDenyAtHome, readDenyPlacedLater } from "./read-deny-home.js";
|
|
19
|
+
import { carryHomeViews, homeViewsAt, placeReadDenyAtHome, placementViewsOf, readDenyPlacedLater, sayPlacementLost } from "./read-deny-home.js";
|
|
20
20
|
import { deploymentFaceClampNotice, resolveContainmentFace } from "./containment-face.js";
|
|
21
21
|
export * from "./containment-face.js";
|
|
22
22
|
import {} from "./fs-shared.js";
|
|
@@ -48,9 +48,17 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
48
48
|
const compileAt = (homes, linked) => compileReadDeny(readDenyAdditions, "HandsToolkitOptions.readDenyPatterns", readDenyBuiltinCfg, readDenyRules, homes, linked);
|
|
49
49
|
const unplaced = compileAt([]);
|
|
50
50
|
const carried = readDenyHomes !== undefined && opts.readDenyHomeLinks !== undefined;
|
|
51
|
+
const carriedViews = carried ? placementViewsOf(opts.readDenyHomes) : undefined;
|
|
52
|
+
if (carried && carriedViews === undefined)
|
|
53
|
+
sayPlacementLost();
|
|
51
54
|
const placesNothing = !unplaced.entries.some((e) => readDenyPatternRun(e.pattern).homeAnchored);
|
|
52
|
-
const late = carried || placesNothing ? undefined : readDenyPlacedLater(unplaced, () => placeReadDenyAtHome(env, compileAt, readDenyHomes));
|
|
53
|
-
const readDeny = late?.matcher ??
|
|
55
|
+
const late = carried || placesNothing ? undefined : readDenyPlacedLater(carryHomeViews(unplaced, homeViewsAt(env, [])), () => placeReadDenyAtHome(env, compileAt, readDenyHomes));
|
|
56
|
+
const readDeny = late?.matcher ??
|
|
57
|
+
(carried
|
|
58
|
+
? carriedViews !== undefined
|
|
59
|
+
? carryHomeViews(compileAt(readDenyHomes, opts.readDenyHomeLinks), carriedViews)
|
|
60
|
+
: carryHomeViews(unplaced, homeViewsAt(env, []))
|
|
61
|
+
: carryHomeViews(unplaced, homeViewsAt(env, readDenyHomes ?? [])));
|
|
54
62
|
const afterPlacement = (tool) => late === undefined
|
|
55
63
|
? tool
|
|
56
64
|
: {
|
|
@@ -61,7 +69,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
61
69
|
},
|
|
62
70
|
};
|
|
63
71
|
const writeDenyCompiled = compileWriteDeny(readDenyRules);
|
|
64
|
-
const writeDeny = writeDenyCompiled.ruleRows.length > 0 ? writeDenyCompiled : undefined;
|
|
72
|
+
const writeDeny = writeDenyCompiled.ruleRows.length > 0 ? carryHomeViews(writeDenyCompiled, readDeny.homeViews) : undefined;
|
|
65
73
|
const readFace = resolveContainmentFace({
|
|
66
74
|
kind: "read",
|
|
67
75
|
depsFace: opts.readFace,
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
2
2
|
import { type PathFamily } from "./safety.js";
|
|
3
3
|
import { type ReadDenyMatcher } from "./read-deny.js";
|
|
4
|
-
/** The
|
|
5
|
-
*
|
|
4
|
+
/** The one bound every probe of an AUXILIARY home shares within one placement (see the module head): long enough for a
|
|
5
|
+
* local directory's canonical spelling, its chain of identities and its rows' heads, short enough that a hung home
|
|
6
|
+
* costs a placement seconds, not the gate's bound. */
|
|
7
|
+
export declare const AUXILIARY_HOME_PROBE_MS = 2000;
|
|
8
|
+
/** {@link declaredReadDenyHomesAnswered}'s anchor list. */
|
|
6
9
|
export declare function declaredReadDenyHomes(env: Pick<ExecutionEnv, "homeDir">): string[];
|
|
7
|
-
/** The declared home and, where the environment's canonical
|
|
8
|
-
* symlink), that spelling too: the faces judge canonical keys beside
|
|
9
|
-
* the spelled home would never meet a canonical key. A home the
|
|
10
|
+
/** The declared home and the account home ({@link declaredReadDenyHomes}) and, where the environment's canonical
|
|
11
|
+
* spelling of one differs (a home reached through a symlink), that spelling too: the faces judge canonical keys beside
|
|
12
|
+
* the spelled views, so a row placed only at the spelled home would never meet a canonical key. A home the
|
|
13
|
+
* environment cannot resolve keeps its spelling — the account home's probe waits at most {@link AUXILIARY_HOME_PROBE_MS}.
|
|
14
|
+
* `[]` — the rows hold their run at every location — where the environment declares no home, where the account lookup
|
|
15
|
+
* gave no answer, or where the declared home's own probe did not answer (ask again later). */
|
|
10
16
|
export declare function readDenyHomesOf(env: Pick<ExecutionEnv, "homeDir" | "canonicalPath">, signal?: AbortSignal): Promise<string[]>;
|
|
11
17
|
/**
|
|
12
18
|
* Where the home-anchored rows of a table LEAD beyond the home's own spellings, keyed by the row's pattern — the
|
|
@@ -15,16 +21,20 @@ export declare function readDenyHomesOf(env: Pick<ExecutionEnv, "homeDir" | "can
|
|
|
15
21
|
* (`canonicalizeTarget`: the longest existing prefix resolved, the rest rejoined); a location that differs from every
|
|
16
22
|
* home's own spelling of the head is where one of the head's segments is a link, and the row is placed there too.
|
|
17
23
|
* · a head that resolves to its own spelling under a home adds nothing (the row is already placed there);
|
|
18
|
-
* · a head that
|
|
19
|
-
*
|
|
24
|
+
* · a head under a spelling that is no directory (a segment of it is a file, a symlink to a file, a device node or a
|
|
25
|
+
* socket — the environment answers `not_directory` for the head) has nothing below it: an answer that adds nothing;
|
|
26
|
+
* · any other head that cannot be resolved (an unanswered or refused probe — a directory the probe may not enter — a
|
|
27
|
+
* probe that throws) adds nothing, and the answer is not WHOLE (`whole: false`): the row stays placed at the home's
|
|
28
|
+
* spellings until the mount places again;
|
|
20
29
|
* · a row whose head is empty (`~/.sema.*`) has no location to resolve and adds nothing;
|
|
21
|
-
* · the heads are resolved together, so one placement waits at most one probe bound
|
|
30
|
+
* · the heads are resolved together, so one placement waits at most one probe bound — or, with `deadline` (an absolute
|
|
31
|
+
* time), each head until then: a head still unresolved at the deadline is a head that did not answer;
|
|
22
32
|
* · read per placement — a link created afterwards is followed from the next placement on.
|
|
23
33
|
*/
|
|
24
34
|
export declare function readDenyHomeLinksOf(env: ExecutionEnv, entries: readonly {
|
|
25
35
|
readonly pattern: string;
|
|
26
36
|
readonly caseSensitive?: boolean;
|
|
27
|
-
}[], homes: readonly string[], signal?: AbortSignal): Promise<{
|
|
37
|
+
}[], homes: readonly string[], signal?: AbortSignal, deadline?: number): Promise<{
|
|
28
38
|
readonly linked: Map<string, string[]>;
|
|
29
39
|
readonly whole: boolean;
|
|
30
40
|
}>;
|
|
@@ -38,15 +48,81 @@ export declare function whereItLeads(env: ExecutionEnv, at: string, family?: Pat
|
|
|
38
48
|
* placed narrower than that base's own canonical spelling. The whole walk waits at most one bounded-await deadline.
|
|
39
49
|
* `undefined` when no prefix answers in time. */
|
|
40
50
|
export declare function whereItsAnsweredPrefixLeads(env: ExecutionEnv, at: string, family?: PathFamily, signal?: AbortSignal): Promise<string | undefined>;
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
51
|
+
/** The environment surface a home view reads. */
|
|
52
|
+
export type HomeViewEnv = Pick<ExecutionEnv, "homeDir" | "canonicalPath" | "fileIdentity">;
|
|
53
|
+
/**
|
|
54
|
+
* The HOME VIEWS of one placement — the one reader every face that judges a deny row by location uses (the structured
|
|
55
|
+
* read and write tools' target judgment, the shell station's establishment, the search walker, the ripgrep leg's output
|
|
56
|
+
* check and its scope choice). A judge carries the source its placement made ({@link HomeViewsCarrier}); a judgment
|
|
57
|
+
* takes one {@link HomeViews} from it ({@link homeViewsOf}).
|
|
58
|
+
*
|
|
59
|
+
* The view of a path is the path itself when it IS a directory on a home's chain (each home's canonical spelling and
|
|
60
|
+
* each of its ancestors, matched by {@link ExecutionEnv.fileIdentity}) — that directory's canonical spelling — and
|
|
61
|
+
* otherwise its parent's view with its own name rejoined. One identity keeps ONE spelling, and a row is placed at a
|
|
62
|
+
* HOME's spelling: a directory that IS a home (its own identity answered) keeps the home's own spelling over an
|
|
63
|
+
* ancestor's (a declared home below a mount of the account home meets the account home's identity at an ancestor); a
|
|
64
|
+
* home whose own identity did not answer has no directory to claim. Homes in order: the declared home, then the
|
|
65
|
+
* auxiliary homes. The homes are the placement's:
|
|
66
|
+
* · the DECLARED home (`env.homeDir`, no lookup) is always among them — whatever the account lookup answered — and its
|
|
67
|
+
* chain is read by the first judgment that asks, kept once every level answered, and asked again by the next
|
|
68
|
+
* judgment otherwise (a home that does not exist yet is read once it does);
|
|
69
|
+
* · each AUXILIARY home's chain is the one its placement read, once ({@link AUXILIARY_HOME_PROBE_MS}); a chain that did
|
|
70
|
+
* not answer stays unanswered for that placement.
|
|
71
|
+
*/
|
|
72
|
+
export interface HomeViewSource {
|
|
73
|
+
/** The home views of one judgment (a tool call, a command's establishment, one search): a view per directory is read
|
|
74
|
+
* once and shared by every path below it. */
|
|
75
|
+
views(signal?: AbortSignal): HomeViews;
|
|
76
|
+
}
|
|
77
|
+
/** A judgment's views. `of` answers the view only where it differs from the path as spelled; `undefined` otherwise, and
|
|
78
|
+
* wherever it cannot be read: no `fileIdentity` seat, no home, a level of the path whose identity fails (the path is
|
|
79
|
+
* judged as spelled, never wider). A level that does not exist (a write's missing tail) takes its parent's view.
|
|
80
|
+
* `leaf: "file"` skips the path's own identity (a file is never a directory on the chain). */
|
|
81
|
+
export interface HomeViews {
|
|
82
|
+
of(path: string, opts?: {
|
|
83
|
+
readonly leaf?: "file";
|
|
84
|
+
}): Promise<string | undefined>;
|
|
85
|
+
}
|
|
86
|
+
/** A judge that carries the home views of the placement its rows were compiled at. */
|
|
87
|
+
export interface HomeViewsCarrier {
|
|
88
|
+
readonly homeViews?: HomeViewSource;
|
|
89
|
+
}
|
|
90
|
+
/** A read deny judge with its placement's home views. */
|
|
91
|
+
export type PlacedReadDenyMatcher = ReadDenyMatcher & {
|
|
92
|
+
readonly homeViews: HomeViewSource;
|
|
93
|
+
};
|
|
94
|
+
/** The views of the placement that returned `homes` — the very list object, as the engine hands it on (the Runner's
|
|
95
|
+
* tool band). `undefined` for any other list: a copy, or a host's own. A mount that was handed the engine's placement
|
|
96
|
+
* and cannot find its views has lost it: an engine defect, which the mount names ({@link sayPlacementLost}) and answers
|
|
97
|
+
* by holding its home-anchored rows at every location — never by reading the homes a second time. */
|
|
98
|
+
export declare function placementViewsOf(homes: readonly string[]): HomeViewSource | undefined;
|
|
99
|
+
/** The line a mount says when the engine handed it a home list that is not its placement's (an engine defect, said every
|
|
100
|
+
* time: no latch). */
|
|
101
|
+
export declare function sayPlacementLost(): void;
|
|
102
|
+
/** The home views over a home list fixed now — a mount that places nothing itself (a toolkit handed its placement, or
|
|
103
|
+
* one whose table has no home-anchored row) and the public shell judgment: the declared home, and every entry of
|
|
104
|
+
* `homes` that is not the declared home as an auxiliary home, read once, at once. */
|
|
105
|
+
export declare function homeViewsAt(env: HomeViewEnv, homes: readonly string[]): HomeViewSource;
|
|
106
|
+
/** The home views of one judgment, off the judge it judges with: the views of the judge's placement. A judge that
|
|
107
|
+
* carries none (one built outside a mount) reads the declared home alone. */
|
|
108
|
+
export declare function homeViewsOf(env: HomeViewEnv, judge: HomeViewsCarrier | undefined, signal?: AbortSignal): HomeViews;
|
|
109
|
+
/** A judge with the home views `source` gives it. */
|
|
110
|
+
export declare function carryHomeViews<T extends object>(judge: T, source: HomeViewSource): T & {
|
|
111
|
+
readonly homeViews: HomeViewSource;
|
|
112
|
+
};
|
|
113
|
+
/** One mount's read-deny judge with its home-anchored rows placed, and the home views of that placement:
|
|
114
|
+
* · the HOME SET — `homes` when the mount carries a list (the account is not asked and no home is added), else the
|
|
115
|
+
* declared home and the account lookup's answer ({@link declaredReadDenyHomes}); every home of the set is anchored at
|
|
116
|
+
* its spelling and, where the environment answered one, its canonical spelling — one rule for both forms;
|
|
117
|
+
* · the DECLARED home's canonical spelling under the tree's gate bound, and its rows' heads ({@link readDenyHomeLinksOf}):
|
|
118
|
+
* a declared home, or a head under it, that did not answer leaves the placement not WHOLE — and an account lookup
|
|
119
|
+
* with no answer, or a declared home that did not answer, places nothing: the rows are held anywhere (`compile([])`)
|
|
120
|
+
* rather than at a spelling that may miss the home's canonical one;
|
|
121
|
+
* · each AUXILIARY home (see the module head) read once under {@link AUXILIARY_HOME_PROBE_MS} — its spellings, its
|
|
122
|
+
* chain, its rows' heads — at the same time; whatever of it did not answer costs that home alone and never `whole`.
|
|
123
|
+
* `compile` is the mount's own compile; the rows' patterns are read off its compile at no home. */
|
|
48
124
|
export declare function placeReadDenyAtHome(env: ExecutionEnv, compile: (homes: readonly string[], linked?: ReadonlyMap<string, readonly string[]>) => ReadDenyMatcher, homes?: readonly string[], signal?: AbortSignal): Promise<{
|
|
49
|
-
readonly matcher:
|
|
125
|
+
readonly matcher: PlacedReadDenyMatcher;
|
|
50
126
|
readonly homes: readonly string[];
|
|
51
127
|
readonly linked: ReadonlyMap<string, readonly string[]>;
|
|
52
128
|
readonly whole: boolean;
|
|
@@ -58,36 +134,13 @@ export declare function placeReadDenyAtHome(env: ExecutionEnv, compile: (homes:
|
|
|
58
134
|
* (either way) and, while no placement has come back WHOLE (a failed one, or one with a head that did not answer),
|
|
59
135
|
* places again first — an unanswered lookup is never kept as the answer. A failed placement leaves the table it
|
|
60
136
|
* replaced (the rows held anywhere, or an earlier placement), never unplaced-and-narrow. The mount holds its read faces
|
|
61
|
-
* until `ready()`.
|
|
137
|
+
* until `ready()`. The judge's home views are the latest settled placement's: a judgment that asks for one waits for the
|
|
138
|
+
* placement in flight (it never starts one).
|
|
62
139
|
*/
|
|
63
|
-
export declare function readDenyPlacedLater(unplaced:
|
|
64
|
-
readonly matcher:
|
|
140
|
+
export declare function readDenyPlacedLater(unplaced: PlacedReadDenyMatcher, place: () => Promise<{
|
|
141
|
+
readonly matcher: PlacedReadDenyMatcher;
|
|
65
142
|
readonly whole: boolean;
|
|
66
143
|
}>): {
|
|
67
|
-
readonly matcher:
|
|
144
|
+
readonly matcher: PlacedReadDenyMatcher;
|
|
68
145
|
readonly ready: () => Promise<void>;
|
|
69
146
|
};
|
|
70
|
-
/** The environment surface a home view reads. */
|
|
71
|
-
export type HomeViewEnv = Pick<ExecutionEnv, "homeDir" | "canonicalPath" | "fileIdentity">;
|
|
72
|
-
/**
|
|
73
|
-
* A path's HOME VIEW — the one reader every face that judges a deny row by location uses (the structured read and
|
|
74
|
-
* write tools' target judgment, the shell station's establishment, the search walker, the ripgrep leg's output check
|
|
75
|
-
* and its scope choice). The view of a path is the path itself when it IS a directory on the home's chain (the
|
|
76
|
-
* canonical home and each of its ancestors, matched by {@link ExecutionEnv.fileIdentity}) — that directory's
|
|
77
|
-
* canonical spelling — and otherwise its parent's view with its own name rejoined. `of` answers the view only where
|
|
78
|
-
* it differs from the path as spelled; `undefined` otherwise, and wherever it cannot be read:
|
|
79
|
-
* · the environment declares no home, no `fileIdentity`, or its home does not resolve ⇒ no view (the spelling form);
|
|
80
|
-
* a home, or a level of its chain, that could not be read is asked again by the next judgment — an unanswered
|
|
81
|
-
* lookup is never kept as the answer;
|
|
82
|
-
* · a level that does not exist (a write's missing tail) has no identity of its own and takes its parent's view;
|
|
83
|
-
* · any other failure at a level stops the view there: the path is judged as spelled, never wider.
|
|
84
|
-
* `leaf: "file"` skips the path's own identity (a file is never a directory on the chain).
|
|
85
|
-
*/
|
|
86
|
-
export interface HomeViews {
|
|
87
|
-
of(path: string, opts?: {
|
|
88
|
-
readonly leaf?: "file";
|
|
89
|
-
}): Promise<string | undefined>;
|
|
90
|
-
}
|
|
91
|
-
/** The home views of one judgment (a tool call, a command's establishment, one search): a view per directory is read
|
|
92
|
-
* once and shared by every path below it. */
|
|
93
|
-
export declare function homeViewsOf(env: HomeViewEnv, signal?: AbortSignal): HomeViews;
|
|
@@ -1,23 +1,106 @@
|
|
|
1
|
+
import { userInfo } from "node:os";
|
|
1
2
|
import { canonicalizeTarget, isAbsolutePathForm, joinForFamily, normalizeAbsPathLexically, pathFamilyOf } from "./safety.js";
|
|
2
3
|
import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, withGateTimeout } from "../../core/gate-bounded-await.js";
|
|
4
|
+
import { thrownText } from "../../core/thrown-value.js";
|
|
3
5
|
import { readDenyPatternRun, readDenyRunHead } from "./read-deny.js";
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
export const AUXILIARY_HOME_PROBE_MS = 2_000;
|
|
7
|
+
const said = new Set();
|
|
8
|
+
function sayOnce(kind, line) {
|
|
9
|
+
if (said.has(kind))
|
|
10
|
+
return;
|
|
11
|
+
said.add(kind);
|
|
12
|
+
try {
|
|
13
|
+
console.warn(line);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
}
|
|
7
17
|
}
|
|
8
|
-
|
|
9
|
-
|
|
18
|
+
function isNoAccountRow(e) {
|
|
19
|
+
try {
|
|
20
|
+
const info = typeof e === "object" && e !== null ? e.info : undefined;
|
|
21
|
+
return typeof info === "object" && info !== null && info.code === "ENOENT";
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
10
26
|
}
|
|
11
|
-
|
|
12
|
-
|
|
27
|
+
function valueText(v) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.stringify(v) ?? String(v);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return typeof v;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function accountHomeOf() {
|
|
36
|
+
try {
|
|
37
|
+
const home = userInfo().homedir;
|
|
38
|
+
if (typeof home === "string" && isAbsolutePathForm(home))
|
|
39
|
+
return { state: "home", home };
|
|
40
|
+
sayOnce("bad_value", `read-deny homes: the account home lookup answered ${valueText(home)}, not an absolute path — home-anchored read-deny rows are placed under the environment's declared home only.`);
|
|
41
|
+
return { state: "none" };
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
if (isNoAccountRow(e))
|
|
45
|
+
return { state: "none" };
|
|
46
|
+
sayOnce("unanswered", `read-deny homes: the account home could not be read (${thrownText(e)}) — home-anchored read-deny rows hold their run at every location until the lookup answers.`);
|
|
47
|
+
return { state: "unanswered" };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function declaredHomeOf(env) {
|
|
51
|
+
const home = env.homeDir;
|
|
52
|
+
return home !== undefined && isAbsolutePathForm(home) ? home : undefined;
|
|
53
|
+
}
|
|
54
|
+
function declaredReadDenyHomesAnswered(env) {
|
|
55
|
+
const home = declaredHomeOf(env);
|
|
13
56
|
if (home === undefined)
|
|
14
57
|
return { homes: [], whole: true };
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
17
|
-
return { homes:
|
|
18
|
-
|
|
58
|
+
const account = accountHomeOf();
|
|
59
|
+
if (account.state === "unanswered")
|
|
60
|
+
return { homes: [], whole: false };
|
|
61
|
+
if (account.state === "none" || normalizeAbsPathLexically(account.home) === normalizeAbsPathLexically(home))
|
|
62
|
+
return { homes: [home], whole: true };
|
|
63
|
+
return { homes: [home, account.home], whole: true };
|
|
64
|
+
}
|
|
65
|
+
export function declaredReadDenyHomes(env) {
|
|
66
|
+
return declaredReadDenyHomesAnswered(env).homes;
|
|
67
|
+
}
|
|
68
|
+
function isDeclared(declared, home) {
|
|
69
|
+
return declared !== undefined && normalizeAbsPathLexically(home) === normalizeAbsPathLexically(declared);
|
|
19
70
|
}
|
|
20
|
-
|
|
71
|
+
async function homeSpellingsOf(env, home, signal) {
|
|
72
|
+
try {
|
|
73
|
+
const canonical = await env.canonicalPath(home, signal);
|
|
74
|
+
if (canonical.ok)
|
|
75
|
+
return { spellings: canonical.value !== home ? [home, canonical.value] : [home], canonical: canonical.value, answered: true };
|
|
76
|
+
return { spellings: [home], answered: canonical.error.code === "not_found" || canonical.error.code === "not_directory" };
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return { spellings: [home], answered: false };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export async function readDenyHomesOf(env, signal) {
|
|
83
|
+
const listed = declaredReadDenyHomesAnswered(env);
|
|
84
|
+
const [declared, ...auxiliary] = listed.homes;
|
|
85
|
+
if (!listed.whole || declared === undefined)
|
|
86
|
+
return [];
|
|
87
|
+
const [own, ...others] = await Promise.all([
|
|
88
|
+
homeSpellingsOf(env, declared, signal),
|
|
89
|
+
...auxiliary.map((home) => until(homeSpellingsOf(env, home, signal), Date.now() + AUXILIARY_HOME_PROBE_MS, signal).then((r) => r ?? { spellings: [home], answered: false })),
|
|
90
|
+
]);
|
|
91
|
+
if (!own.answered)
|
|
92
|
+
return [];
|
|
93
|
+
if (others.some((r) => !r.answered))
|
|
94
|
+
sayAuxiliary("its path probe did not answer");
|
|
95
|
+
return [...new Set([...own.spellings, ...others.flatMap((r) => r.spellings)])];
|
|
96
|
+
}
|
|
97
|
+
async function until(work, deadline, signal) {
|
|
98
|
+
return await withGateTimeout(work, Math.max(0, deadline - Date.now()), signal, "auxiliaryHome").catch(() => undefined);
|
|
99
|
+
}
|
|
100
|
+
function sayAuxiliary(what) {
|
|
101
|
+
sayOnce("auxiliary", `read-deny homes: a home beside the environment's declared home did not answer its probes within ${AUXILIARY_HOME_PROBE_MS} ms (${what}) — its home-anchored read-deny rows sit at its spelling only, and a path through an alias of it is judged as spelled, until the next placement.`);
|
|
102
|
+
}
|
|
103
|
+
export async function readDenyHomeLinksOf(env, entries, homes, signal, deadline) {
|
|
21
104
|
const rows = [];
|
|
22
105
|
const familyOf = new Map();
|
|
23
106
|
for (const { pattern, caseSensitive } of entries) {
|
|
@@ -35,7 +118,8 @@ export async function readDenyHomeLinksOf(env, entries, homes, signal) {
|
|
|
35
118
|
});
|
|
36
119
|
rows.push({ pattern, caseSensitive: caseSensitive === true, spelled });
|
|
37
120
|
}
|
|
38
|
-
const
|
|
121
|
+
const leadOf = async (at, family) => (await whereItLeads(env, at, family, signal)) ?? ((await holdsNothingBelow(env, at, signal)) ? at : undefined);
|
|
122
|
+
const resolved = new Map(await Promise.all([...familyOf].map(async ([at, family]) => [at, deadline === undefined ? await leadOf(at, family) : await until(leadOf(at, family), deadline, signal)])));
|
|
39
123
|
const out = new Map();
|
|
40
124
|
const whole = [...resolved.values()].every((location) => location !== undefined);
|
|
41
125
|
for (const { pattern, caseSensitive, spelled } of rows) {
|
|
@@ -52,6 +136,10 @@ export async function readDenyHomeLinksOf(env, entries, homes, signal) {
|
|
|
52
136
|
}
|
|
53
137
|
return { linked: out, whole };
|
|
54
138
|
}
|
|
139
|
+
async function holdsNothingBelow(env, at, signal) {
|
|
140
|
+
const probe = await withGateTimeout(Promise.resolve().then(() => env.canonicalPath(at, signal)), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "readDenyHead").catch(() => undefined);
|
|
141
|
+
return probe !== undefined && !probe.ok && probe.error.code === "not_directory";
|
|
142
|
+
}
|
|
55
143
|
export async function whereItLeads(env, at, family = pathFamilyOf({ root: at }), signal) {
|
|
56
144
|
try {
|
|
57
145
|
const key = await canonicalizeTarget(env, at, signal, family);
|
|
@@ -77,17 +165,189 @@ export async function whereItsAnsweredPrefixLeads(env, at, family = pathFamilyOf
|
|
|
77
165
|
};
|
|
78
166
|
return await withGateTimeout(walk(), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "ruleRowHead").catch(() => undefined);
|
|
79
167
|
}
|
|
168
|
+
const NO_CHAIN = { ancestors: [], whole: false };
|
|
169
|
+
async function chainAbove(env, identity, canonical, signal, deadline) {
|
|
170
|
+
let own;
|
|
171
|
+
const ancestors = [];
|
|
172
|
+
let whole = true;
|
|
173
|
+
for (let dir = normalizeAbsPathLexically(canonical), first = true;; first = false) {
|
|
174
|
+
const id = deadline === undefined ? await identityOf(env, identity, dir, signal) : await until(identityOf(env, identity, dir, signal), deadline, signal);
|
|
175
|
+
if (id === undefined && deadline !== undefined && Date.now() >= deadline) {
|
|
176
|
+
whole = false;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
if (id?.ok !== true)
|
|
180
|
+
whole = false;
|
|
181
|
+
else if (first)
|
|
182
|
+
own = { dir, id: id.value };
|
|
183
|
+
else
|
|
184
|
+
ancestors.push({ dir, id: id.value });
|
|
185
|
+
const parent = normalizeAbsPathLexically(`${dir}/..`);
|
|
186
|
+
if (parent === dir)
|
|
187
|
+
break;
|
|
188
|
+
dir = parent;
|
|
189
|
+
}
|
|
190
|
+
return { ...(own !== undefined ? { own } : {}), ancestors, whole };
|
|
191
|
+
}
|
|
192
|
+
async function identityOf(env, identity, path, signal) {
|
|
193
|
+
try {
|
|
194
|
+
return await identity.call(env, path, signal);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function readAuxiliaryHome(env, home, opts, signal) {
|
|
201
|
+
const deadline = Date.now() + AUXILIARY_HOME_PROBE_MS;
|
|
202
|
+
const spelled = await until(homeSpellingsOf(env, home, signal), deadline, signal);
|
|
203
|
+
let reading;
|
|
204
|
+
if (spelled?.canonical === undefined) {
|
|
205
|
+
const answered = spelled?.answered === true;
|
|
206
|
+
reading = { place: [home], chain: answered ? { ancestors: [], whole: true } : NO_CHAIN, linked: new Map(), answered };
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const place = spelled.spellings;
|
|
210
|
+
const identity = env.fileIdentity;
|
|
211
|
+
const [chain, heads] = await Promise.all([
|
|
212
|
+
typeof identity === "function" ? chainAbove(env, identity, spelled.canonical, signal, deadline) : Promise.resolve({ ancestors: [], whole: true }),
|
|
213
|
+
opts.entries !== undefined ? readDenyHomeLinksOf(env, opts.entries, place, signal, deadline) : Promise.resolve({ linked: new Map(), whole: true }),
|
|
214
|
+
]);
|
|
215
|
+
reading = { place, chain, linked: heads.linked, answered: chain.whole && heads.whole };
|
|
216
|
+
}
|
|
217
|
+
if (!reading.answered)
|
|
218
|
+
sayAuxiliary("a probe did not answer");
|
|
219
|
+
return reading;
|
|
220
|
+
}
|
|
221
|
+
function homeViewSourceOver(env, auxiliary) {
|
|
222
|
+
const declared = declaredHomeOf(env);
|
|
223
|
+
const identity = env.fileIdentity;
|
|
224
|
+
let declaredChain;
|
|
225
|
+
const readDeclared = () => {
|
|
226
|
+
if (declared === undefined || typeof identity !== "function")
|
|
227
|
+
return Promise.resolve(NO_CHAIN);
|
|
228
|
+
if (declaredChain === undefined) {
|
|
229
|
+
const reading = withGateTimeout((async () => {
|
|
230
|
+
const spelled = await homeSpellingsOf(env, declared);
|
|
231
|
+
return spelled.canonical === undefined ? NO_CHAIN : await chainAbove(env, identity, spelled.canonical);
|
|
232
|
+
})(), DEFAULT_GATE_AWAIT_TIMEOUT_MS, undefined, "homeChain").catch(() => NO_CHAIN);
|
|
233
|
+
declaredChain = reading;
|
|
234
|
+
void reading.then((c) => {
|
|
235
|
+
if (!c.whole && declaredChain === reading)
|
|
236
|
+
declaredChain = undefined;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return declaredChain;
|
|
240
|
+
};
|
|
241
|
+
return {
|
|
242
|
+
views(signal) {
|
|
243
|
+
let index;
|
|
244
|
+
const indexOf = () => (index ??= (async () => {
|
|
245
|
+
const chains = [await readDeclared(), ...(await Promise.all(auxiliary)).map((a) => a.chain)];
|
|
246
|
+
const out = new Map();
|
|
247
|
+
for (const { own } of chains)
|
|
248
|
+
if (own !== undefined && !out.has(own.id))
|
|
249
|
+
out.set(own.id, own.dir);
|
|
250
|
+
for (const { ancestors } of chains)
|
|
251
|
+
for (const { dir, id } of ancestors)
|
|
252
|
+
if (!out.has(id))
|
|
253
|
+
out.set(id, dir);
|
|
254
|
+
return out.size > 0 ? out : undefined;
|
|
255
|
+
})());
|
|
256
|
+
const views = new Map();
|
|
257
|
+
const viewOf = (path, leafIsFile) => {
|
|
258
|
+
const key = `${leafIsFile ? "f" : "d"}:${path}`;
|
|
259
|
+
let v = views.get(key);
|
|
260
|
+
if (v === undefined) {
|
|
261
|
+
v = (async () => {
|
|
262
|
+
const index = await indexOf();
|
|
263
|
+
if (index === undefined || typeof identity !== "function")
|
|
264
|
+
return undefined;
|
|
265
|
+
if (!leafIsFile) {
|
|
266
|
+
const id = await identityOf(env, identity, path, signal);
|
|
267
|
+
if (id === undefined)
|
|
268
|
+
return undefined;
|
|
269
|
+
if (id.ok) {
|
|
270
|
+
const at = index.get(id.value);
|
|
271
|
+
if (at !== undefined)
|
|
272
|
+
return at;
|
|
273
|
+
}
|
|
274
|
+
else if (id.error.code !== "not_found" && id.error.code !== "not_directory")
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
const parent = normalizeAbsPathLexically(`${path}/..`);
|
|
278
|
+
if (parent === path)
|
|
279
|
+
return undefined;
|
|
280
|
+
const up = await viewOf(parent, false);
|
|
281
|
+
return up === undefined ? undefined : joinForFamily(pathFamilyOf({ root: path }), up, path.slice(parent.length).replace(/^[\\/]+/, ""));
|
|
282
|
+
})();
|
|
283
|
+
views.set(key, v);
|
|
284
|
+
}
|
|
285
|
+
return v;
|
|
286
|
+
};
|
|
287
|
+
return {
|
|
288
|
+
async of(path, opts) {
|
|
289
|
+
if (typeof identity !== "function" || (declared === undefined && auxiliary.length === 0) || !isAbsolutePathForm(path))
|
|
290
|
+
return undefined;
|
|
291
|
+
const spelled = normalizeAbsPathLexically(path);
|
|
292
|
+
const view = await viewOf(spelled, opts?.leaf === "file");
|
|
293
|
+
return view === undefined || view === spelled ? undefined : view;
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const PLACEMENT_VIEWS = new WeakMap();
|
|
300
|
+
function placementOf(matcher, homes, linked, whole) {
|
|
301
|
+
const list = Object.freeze([...homes]);
|
|
302
|
+
PLACEMENT_VIEWS.set(list, matcher.homeViews);
|
|
303
|
+
return { matcher, homes: list, linked, whole };
|
|
304
|
+
}
|
|
305
|
+
export function placementViewsOf(homes) {
|
|
306
|
+
return PLACEMENT_VIEWS.get(homes);
|
|
307
|
+
}
|
|
308
|
+
export function sayPlacementLost() {
|
|
309
|
+
try {
|
|
310
|
+
console.warn("read-deny homes: this tool band was handed a home list that is not the placement's own (an engine defect) — its home-anchored read-deny rows hold their run at every location, and only the declared home's views are read.");
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export function homeViewsAt(env, homes) {
|
|
316
|
+
const declared = declaredHomeOf(env);
|
|
317
|
+
const auxiliary = [...new Set(homes.filter((h) => isAbsolutePathForm(h) && !isDeclared(declared, h)))];
|
|
318
|
+
return homeViewSourceOver(env, auxiliary.map((home) => readAuxiliaryHome(env, home, {})));
|
|
319
|
+
}
|
|
320
|
+
export function homeViewsOf(env, judge, signal) {
|
|
321
|
+
return (judge?.homeViews ?? homeViewsAt(env, [])).views(signal);
|
|
322
|
+
}
|
|
323
|
+
export function carryHomeViews(judge, source) {
|
|
324
|
+
return { ...judge, homeViews: source };
|
|
325
|
+
}
|
|
80
326
|
export async function placeReadDenyAtHome(env, compile, homes, signal) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
327
|
+
const entries = compile([]).entries;
|
|
328
|
+
const declared = declaredHomeOf(env);
|
|
329
|
+
const heldAnywhere = (whole) => placementOf(carryHomeViews(compile([]), homeViewSourceOver(env, [])), [], new Map(), whole);
|
|
330
|
+
const listed = homes === undefined ? declaredReadDenyHomesAnswered(env) : { homes, whole: true };
|
|
331
|
+
if (!listed.whole)
|
|
332
|
+
return heldAnywhere(false);
|
|
333
|
+
const own = listed.homes.filter((h) => isDeclared(declared, h));
|
|
334
|
+
let declaredAt = [];
|
|
335
|
+
if (own.length > 0) {
|
|
336
|
+
const ownSpellings = await withGateTimeout(homeSpellingsOf(env, own[0], signal), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "readDenyHomes");
|
|
337
|
+
if (!ownSpellings.answered)
|
|
338
|
+
return heldAnywhere(false);
|
|
339
|
+
declaredAt = [...new Set([...own, ...ownSpellings.spellings])];
|
|
340
|
+
}
|
|
341
|
+
const auxiliary = listed.homes.filter((h) => !isDeclared(declared, h)).map((home) => readAuxiliaryHome(env, home, { entries }, signal));
|
|
342
|
+
const [heads, readings] = await Promise.all([readDenyHomeLinksOf(env, entries, declaredAt, signal), Promise.all(auxiliary)]);
|
|
343
|
+
const at = [...new Set([...declaredAt, ...readings.flatMap((r) => r.place)])];
|
|
344
|
+
const linked = new Map();
|
|
345
|
+
for (const map of [heads.linked, ...readings.map((r) => r.linked)]) {
|
|
346
|
+
for (const [pattern, locations] of map)
|
|
347
|
+
linked.set(pattern, [...new Set([...(linked.get(pattern) ?? []), ...locations])]);
|
|
87
348
|
}
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
return { matcher: linked.size === 0 ? atHomes : compile(at, linked), homes: at, linked, whole };
|
|
349
|
+
const matcher = linked.size === 0 ? compile(at) : compile(at, linked);
|
|
350
|
+
return placementOf(carryHomeViews(matcher, homeViewSourceOver(env, auxiliary)), at, linked, heads.whole);
|
|
91
351
|
}
|
|
92
352
|
export function readDenyPlacedLater(unplaced, place) {
|
|
93
353
|
let current = unplaced;
|
|
@@ -108,6 +368,17 @@ export function readDenyPlacedLater(unplaced, place) {
|
|
|
108
368
|
if (!whole)
|
|
109
369
|
await again();
|
|
110
370
|
};
|
|
371
|
+
const settled = async () => {
|
|
372
|
+
if (pending !== undefined)
|
|
373
|
+
await pending;
|
|
374
|
+
return current;
|
|
375
|
+
};
|
|
376
|
+
const homeViews = {
|
|
377
|
+
views(signal) {
|
|
378
|
+
let views;
|
|
379
|
+
return { of: async (path, opts) => await (await (views ??= settled().then((m) => m.homeViews.views(signal)))).of(path, opts) };
|
|
380
|
+
},
|
|
381
|
+
};
|
|
111
382
|
const matcher = {
|
|
112
383
|
get entries() {
|
|
113
384
|
return current.entries;
|
|
@@ -125,101 +396,7 @@ export function readDenyPlacedLater(unplaced, place) {
|
|
|
125
396
|
},
|
|
126
397
|
unexcludableUnder: (scope) => current.unexcludableUnder?.(scope) ?? false,
|
|
127
398
|
rowAtOrBelow: (path) => current.rowAtOrBelow?.(path) ?? null,
|
|
399
|
+
homeViews,
|
|
128
400
|
};
|
|
129
401
|
return { matcher, ready };
|
|
130
402
|
}
|
|
131
|
-
const HOME_CHAINS = new WeakMap();
|
|
132
|
-
function homeChainOf(env) {
|
|
133
|
-
const identity = env.fileIdentity;
|
|
134
|
-
const home = declaredReadDenyHomes(env)[0];
|
|
135
|
-
if (typeof identity !== "function" || home === undefined)
|
|
136
|
-
return undefined;
|
|
137
|
-
let perEnv = HOME_CHAINS.get(env);
|
|
138
|
-
if (perEnv === undefined)
|
|
139
|
-
HOME_CHAINS.set(env, (perEnv = new Map()));
|
|
140
|
-
const known = perEnv;
|
|
141
|
-
let chain = known.get(home);
|
|
142
|
-
if (chain === undefined) {
|
|
143
|
-
const reading = withGateTimeout((async () => {
|
|
144
|
-
const canonical = await env.canonicalPath(home);
|
|
145
|
-
if (!canonical.ok)
|
|
146
|
-
return { index: undefined, whole: false };
|
|
147
|
-
const out = new Map();
|
|
148
|
-
let whole = true;
|
|
149
|
-
for (let dir = normalizeAbsPathLexically(canonical.value);;) {
|
|
150
|
-
const id = await identityOf(env, identity, dir);
|
|
151
|
-
if (id?.ok === true) {
|
|
152
|
-
if (!out.has(id.value))
|
|
153
|
-
out.set(id.value, dir);
|
|
154
|
-
}
|
|
155
|
-
else
|
|
156
|
-
whole = false;
|
|
157
|
-
const parent = normalizeAbsPathLexically(`${dir}/..`);
|
|
158
|
-
if (parent === dir)
|
|
159
|
-
break;
|
|
160
|
-
dir = parent;
|
|
161
|
-
}
|
|
162
|
-
return { index: out.size > 0 ? out : undefined, whole };
|
|
163
|
-
})(), DEFAULT_GATE_AWAIT_TIMEOUT_MS, undefined, "homeChain").catch(() => ({ index: undefined, whole: false }));
|
|
164
|
-
chain = reading;
|
|
165
|
-
known.set(home, reading);
|
|
166
|
-
const drop = () => {
|
|
167
|
-
if (known.get(home) === reading)
|
|
168
|
-
known.delete(home);
|
|
169
|
-
};
|
|
170
|
-
void reading.then((c) => (c.whole ? undefined : drop()));
|
|
171
|
-
}
|
|
172
|
-
return chain.then((c) => c.index);
|
|
173
|
-
}
|
|
174
|
-
async function identityOf(env, identity, path, signal) {
|
|
175
|
-
try {
|
|
176
|
-
return await identity.call(env, path, signal);
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
return undefined;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
export function homeViewsOf(env, signal) {
|
|
183
|
-
const chain = homeChainOf(env);
|
|
184
|
-
const identity = env.fileIdentity;
|
|
185
|
-
const views = new Map();
|
|
186
|
-
const viewOf = (path, leafIsFile) => {
|
|
187
|
-
const key = `${leafIsFile ? "f" : "d"}:${path}`;
|
|
188
|
-
let v = views.get(key);
|
|
189
|
-
if (v === undefined) {
|
|
190
|
-
v = (async () => {
|
|
191
|
-
const index = await chain;
|
|
192
|
-
if (index === undefined || typeof identity !== "function")
|
|
193
|
-
return undefined;
|
|
194
|
-
if (!leafIsFile) {
|
|
195
|
-
const id = await identityOf(env, identity, path, signal);
|
|
196
|
-
if (id === undefined)
|
|
197
|
-
return undefined;
|
|
198
|
-
if (id.ok) {
|
|
199
|
-
const at = index.get(id.value);
|
|
200
|
-
if (at !== undefined)
|
|
201
|
-
return at;
|
|
202
|
-
}
|
|
203
|
-
else if (id.error.code !== "not_found" && id.error.code !== "not_directory")
|
|
204
|
-
return undefined;
|
|
205
|
-
}
|
|
206
|
-
const parent = normalizeAbsPathLexically(`${path}/..`);
|
|
207
|
-
if (parent === path)
|
|
208
|
-
return undefined;
|
|
209
|
-
const up = await viewOf(parent, false);
|
|
210
|
-
return up === undefined ? undefined : joinForFamily(pathFamilyOf({ root: path }), up, path.slice(parent.length).replace(/^[\\/]+/, ""));
|
|
211
|
-
})();
|
|
212
|
-
views.set(key, v);
|
|
213
|
-
}
|
|
214
|
-
return v;
|
|
215
|
-
};
|
|
216
|
-
return {
|
|
217
|
-
async of(path, opts) {
|
|
218
|
-
if (chain === undefined || !isAbsolutePathForm(path))
|
|
219
|
-
return undefined;
|
|
220
|
-
const spelled = normalizeAbsPathLexically(path);
|
|
221
|
-
const view = await viewOf(spelled, opts?.leaf === "file");
|
|
222
|
-
return view === undefined || view === spelled ? undefined : view;
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
}
|
|
@@ -392,8 +392,8 @@ export declare function checkedReadDenyHomes(homes: unknown, seat?: string): rea
|
|
|
392
392
|
* layer or knob.
|
|
393
393
|
*
|
|
394
394
|
* `homes` — the spellings of the execution environment's home directory a home-anchored entry
|
|
395
|
-
* ({@link READ_DENY_HOME_WORD}) is placed under: the declared home and, where the environment's
|
|
396
|
-
* spelling differs, that one too (the faces judge canonical keys beside the spelled views). Absolute
|
|
395
|
+
* ({@link READ_DENY_HOME_WORD}) is placed under: the declared home, the account home and, where the environment's
|
|
396
|
+
* canonical spelling of one differs, that one too (the faces judge canonical keys beside the spelled views). Absolute
|
|
397
397
|
* spellings only — any other shape refuses loudly, naming `readDenyHomes`. EMPTY = the environment declares no
|
|
398
398
|
* home: a home-anchored entry then matches its run anywhere — a row that cannot be placed holds wider, never narrower.
|
|
399
399
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HomeViewSource } from "./read-deny-home.js";
|
|
1
2
|
import type { AssertAllKeysHandled } from "../../core/ask-origin.js";
|
|
2
3
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
3
4
|
import { protectivePathTargetOf, type ToolCallLike } from "../../core/tool-registry.js";
|
|
@@ -449,6 +450,7 @@ export type RootsSeat = readonly string[] | (() => readonly string[] | undefined
|
|
|
449
450
|
*/
|
|
450
451
|
export declare function resolveKey(env: ExecutionEnv, rootCanonical: string, path: string, signal?: AbortSignal, baseCwd?: string, additionalRoots?: RootsSeat, exactFileReadExemption?: (canonicalKey: string) => boolean, readDeny?: {
|
|
451
452
|
readonly face?: "write";
|
|
453
|
+
readonly homeViews?: HomeViewSource;
|
|
452
454
|
matchTarget(canonicalKey: string, lexicalView?: string, opts?: {
|
|
453
455
|
readonly aliasResolved?: boolean;
|
|
454
456
|
}): {
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -469,7 +469,7 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
|
|
|
469
469
|
const judged = { aliasResolved: canon.aliasResolved === true };
|
|
470
470
|
let hit = readDeny.matchTarget(key, lexicalViewOf(path, baseCwd ?? rootCanonical), judged);
|
|
471
471
|
if (hit === null) {
|
|
472
|
-
const homeView = await homeViewsOf(env, signal).of(key);
|
|
472
|
+
const homeView = await homeViewsOf(env, readDeny, signal).of(key);
|
|
473
473
|
if (homeView !== undefined)
|
|
474
474
|
hit = readDeny.matchTarget(key, homeView, judged);
|
|
475
475
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
2
|
+
import { type HomeViewsCarrier } from "./read-deny-home.js";
|
|
2
3
|
/** The JS scanner's own output ceiling, in bytes of emitted content rows. Pinned to the
|
|
3
4
|
* ripgrep leg's figure ({@link MAX_EXEC_OUTPUT_BYTES}, the exec pipe's rolling-tail bound) so the two
|
|
4
5
|
* legs cannot disagree about how much one search may return; see the collection site in {@link jsGrep}
|
|
@@ -130,7 +131,7 @@ export declare function walk(env: ExecutionEnv, root: string, start: string, ign
|
|
|
130
131
|
matchPath(path: string): {
|
|
131
132
|
pattern: string;
|
|
132
133
|
} | null;
|
|
133
|
-
}): Promise<WalkResult>;
|
|
134
|
+
} & HomeViewsCarrier): Promise<WalkResult>;
|
|
134
135
|
/** The JS-leg withholding disclosure: an empty result must stay distinguishable from
|
|
135
136
|
* searched-and-empty, so this note rides EVERY output shape when something was pruned. Distinct from
|
|
136
137
|
* {@link walkCaveat}: pruning is deliberate withholding, not incompleteness.
|
|
@@ -157,8 +158,8 @@ export interface ReadDenyWithheld {
|
|
|
157
158
|
count?: number;
|
|
158
159
|
patterns?: readonly string[];
|
|
159
160
|
}
|
|
160
|
-
/** The deny judge seam every search leg shares (structural subset of ReadDenyMatcher). */
|
|
161
|
-
export interface ReadDenyJudge {
|
|
161
|
+
/** The deny judge seam every search leg shares (structural subset of ReadDenyMatcher), with its placement's home views. */
|
|
162
|
+
export interface ReadDenyJudge extends HomeViewsCarrier {
|
|
162
163
|
matchPath(path: string): {
|
|
163
164
|
pattern: string;
|
|
164
165
|
} | null;
|
package/dist/tools/fs/search.js
CHANGED
|
@@ -228,7 +228,7 @@ export async function walk(env, root, start, ignore, signal, deny) {
|
|
|
228
228
|
let aborted = false;
|
|
229
229
|
let denyPruned = 0;
|
|
230
230
|
const denyPatterns = [];
|
|
231
|
-
const views = deny !== undefined ? homeViewsOf(env, signal) : undefined;
|
|
231
|
+
const views = deny !== undefined ? homeViewsOf(env, deny, signal) : undefined;
|
|
232
232
|
const denyHit = async (path, leaf) => {
|
|
233
233
|
let hit = deny?.matchPath(path);
|
|
234
234
|
if (!hit && views !== undefined) {
|
|
@@ -1156,7 +1156,7 @@ function rgRecordAbsolute(base) {
|
|
|
1156
1156
|
return (p) => (base === undefined || family === undefined || isAbsoluteForFamily(family, p) ? p : joinForFamily(family, base, p.replace(/^\.[\\/]/, "")));
|
|
1157
1157
|
}
|
|
1158
1158
|
async function rgOutputDenyTripwireAt(env, stdout, mode, deny, opts, signal) {
|
|
1159
|
-
const views = homeViewsOf(env, signal);
|
|
1159
|
+
const views = homeViewsOf(env, deny, signal);
|
|
1160
1160
|
const absolute = rgRecordAbsolute(opts.base);
|
|
1161
1161
|
const placed = new Map();
|
|
1162
1162
|
for (const record of parseRgRecords(stdout, mode, opts.dropIncompleteTail === true)) {
|
|
@@ -1343,7 +1343,7 @@ export async function runGrepDetailed(env, root, p, signal, deny) {
|
|
|
1343
1343
|
return false;
|
|
1344
1344
|
if (deny.unexcludableUnder(scope))
|
|
1345
1345
|
return true;
|
|
1346
|
-
const view = await homeViewsOf(env, signal).of(scope);
|
|
1346
|
+
const view = await homeViewsOf(env, deny, signal).of(scope);
|
|
1347
1347
|
return view !== undefined && deny.unexcludableUnder(view);
|
|
1348
1348
|
};
|
|
1349
1349
|
if (!(await unexcludableUnder()) && (await detectRipgrep(env)))
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
21
21
|
import { violationDetails } from "./safety.js";
|
|
22
22
|
import { type ShellReadPlan } from "./shell-read-plan.js";
|
|
23
|
+
import { type HomeViewSource } from "./read-deny-home.js";
|
|
23
24
|
import { type EstablishedRead, type ShellReadBoundary, type ShellReadDisposition, type ShellReadFinding, type ShellReadSeat } from "./shell-read-judge.js";
|
|
24
25
|
import type { ReadRootGrantCandidate } from "../../core/tool-spec.js";
|
|
25
26
|
import type { ToolCallLike } from "../../core/tool-registry.js";
|
|
@@ -145,6 +146,9 @@ export interface ShellReadStationOptions {
|
|
|
145
146
|
/** `true` ⇔ every root `boundaryOf` returns is already a canonical identity (the Runner's): the station never
|
|
146
147
|
* re-resolves one, so a root swapped for a link after it was declared or granted cannot bring in the link's target. */
|
|
147
148
|
readonly rootsCanonical?: boolean;
|
|
149
|
+
/** The home views of the placement the boundary's deny seats were compiled at (read-deny-home.ts) — the Runner's. Absent,
|
|
150
|
+
* the declared home alone. */
|
|
151
|
+
readonly homeViews?: HomeViewSource;
|
|
148
152
|
}
|
|
149
153
|
/** What a consumer hands {@link judgeShellRead}: the command as written, the environment whose filesystem
|
|
150
154
|
* answers where its words land (symlinks resolved, patterns expanded — a `NodeExecutionEnv` over a real
|
|
@@ -191,6 +195,11 @@ export interface ShellReadJudgment {
|
|
|
191
195
|
* LAXER where only a filesystem sees the escape (an in-root symlink pointing out vouches) and answers a
|
|
192
196
|
* pattern on its spelling alone, so the two diverge in both directions.
|
|
193
197
|
*
|
|
198
|
+
* A read target is also judged at its HOME VIEW (a path spelled through a firmlink or a bind mount of a home, respelled
|
|
199
|
+
* at the home): this judgment is its own placement — the views read the environment's declared home and the account
|
|
200
|
+
* home the account database answers once, for this call (`readDenyHomesOf`'s homes); the account home's probes wait at
|
|
201
|
+
* most a short bound of their own.
|
|
202
|
+
*
|
|
194
203
|
* It carries NO deadline. Bounding an environment-facing wait and substituting a fail-closed verdict is the
|
|
195
204
|
* gate FOLD's policy on the gate's one deadline seat; the station has never held one on either entry, and a
|
|
196
205
|
* second default here would decide the same number twice and turn a slow-but-correct filesystem's true
|
|
@@ -6,6 +6,7 @@ import { NOT_AUTO_ALLOWED, programPositionRefusalSentence, readProgramPosition }
|
|
|
6
6
|
import { bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
|
|
7
7
|
import { planShellReads } from "./shell-read-plan.js";
|
|
8
8
|
import { createShellReadEstablisher, establishShellReadsLexically } from "./shell-read-establish.js";
|
|
9
|
+
import { declaredReadDenyHomes, homeViewsAt } from "./read-deny-home.js";
|
|
9
10
|
import { programArgs, spellsOption } from "./shell-read-write-targets.js";
|
|
10
11
|
import { allFindings, decideShellRead, denyRowAtOrBelow, foldShellReadFindings, judgeEstablishedPlan, readRootGrantCandidateOf, } from "./shell-read-judge.js";
|
|
11
12
|
import { seatOf } from "../../core/tool-name-readings.js";
|
|
@@ -326,7 +327,7 @@ async function runShellReadStation(tool, command, declared, seat, readers, estab
|
|
|
326
327
|
}
|
|
327
328
|
export async function judgeShellRead(input) {
|
|
328
329
|
const readers = input.readers ?? new Set(bashClassifyDefaultAllow());
|
|
329
|
-
const run = await runShellReadStation(input.tool ?? "Bash", input.command, input.boundary, input.seat, readers, createShellReadEstablisher(input.env, input.boundary.roots), input.env, input.signal);
|
|
330
|
+
const run = await runShellReadStation(input.tool ?? "Bash", input.command, input.boundary, input.seat, readers, createShellReadEstablisher(input.env, input.boundary.roots, { homeViews: homeViewsAt(input.env, declaredReadDenyHomes(input.env)) }), input.env, input.signal);
|
|
330
331
|
const offer = normalizeReadRootCandidate(run.verdict.readRootCandidate);
|
|
331
332
|
return {
|
|
332
333
|
plan: run.plan,
|
|
@@ -349,7 +350,7 @@ export function createShellReadStation(opts) {
|
|
|
349
350
|
const key = roots.join("\0");
|
|
350
351
|
let e = establishers.get(key);
|
|
351
352
|
if (e === undefined) {
|
|
352
|
-
e = createShellReadEstablisher(opts.env, roots, { rootsCanonical: opts.rootsCanonical === true });
|
|
353
|
+
e = createShellReadEstablisher(opts.env, roots, { rootsCanonical: opts.rootsCanonical === true, ...(opts.homeViews !== undefined ? { homeViews: opts.homeViews } : {}) });
|
|
353
354
|
establishers.set(key, e);
|
|
354
355
|
}
|
|
355
356
|
return e;
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
41
41
|
import type { ShellReadPlan, ShellReadSegment } from "./shell-read-plan.js";
|
|
42
42
|
import type { EstablishedRead, ShellReadBoundary } from "./shell-read-judge.js";
|
|
43
|
+
import type { HomeViewSource } from "./read-deny-home.js";
|
|
43
44
|
/**
|
|
44
45
|
* How many directories one glob operand (or one traversal root) may enumerate, and how many names it may
|
|
45
46
|
* match, before the leg gives up and answers `unestablishable`.
|
|
@@ -82,7 +83,9 @@ export declare function establishShellReadsLexically(plan: ShellReadPlan, bounda
|
|
|
82
83
|
* The filesystem-resolving establisher for one environment and one root set. `readRoots` are the roots
|
|
83
84
|
* as declared (canonical by contract); they are resolved once more per instance because the comparison is
|
|
84
85
|
* against `realpath` output on both sides — an aliased root would otherwise refuse every read under itself.
|
|
86
|
+
* `homeViews`: the home views of the placement the deny seats were compiled at (read-deny-home.ts); absent, the declared home's.
|
|
85
87
|
*/
|
|
86
88
|
export declare function createShellReadEstablisher(env: ExecutionEnv, readRoots: readonly string[], opts?: {
|
|
87
89
|
readonly rootsCanonical?: boolean;
|
|
90
|
+
readonly homeViews?: HomeViewSource;
|
|
88
91
|
}): ShellReadEstablisher;
|
|
@@ -534,7 +534,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
534
534
|
if (lexicalOnly)
|
|
535
535
|
return establishShellReadsLexically(plan, boundary);
|
|
536
536
|
const roots = await canonicalRoots(signal);
|
|
537
|
-
return await withHomeViews(env, await establishPlan(plan, { ...boundary, roots }, roots, signal), signal);
|
|
537
|
+
return await withHomeViews(env, opts, await establishPlan(plan, { ...boundary, roots }, roots, signal), signal);
|
|
538
538
|
},
|
|
539
539
|
reestablish: async (previous, boundary, signal) => {
|
|
540
540
|
if (lexicalOnly)
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { EstablishedPlan } from "./shell-read-establish.js";
|
|
11
11
|
import type { EstablishedRead } from "./shell-read-judge.js";
|
|
12
|
-
import { type HomeViewEnv } from "./read-deny-home.js";
|
|
12
|
+
import { type HomeViewEnv, type HomeViewsCarrier } from "./read-deny-home.js";
|
|
13
13
|
/** Every read of either establishment whose candidate's settled target set differs between the two. */
|
|
14
14
|
export declare function movedReads(previous: EstablishedPlan, again: EstablishedPlan): EstablishedRead[];
|
|
15
15
|
/**
|
|
16
16
|
* The establishment with every read that landed carrying its HOME VIEW (read-deny-home.ts) among its spellings, nested
|
|
17
17
|
* plans included: the deny judge then places a home-anchored row wherever the target IS below the home, whatever it is
|
|
18
|
-
* spelled through.
|
|
18
|
+
* spelled through. The views are `placed`'s — the home views of the placement the boundary's deny seats were compiled
|
|
19
|
+
* at. A read that did not land (lexical, unestablishable) is left as it is.
|
|
19
20
|
*/
|
|
20
|
-
export declare function withHomeViews(env: HomeViewEnv, established: EstablishedPlan, signal?: AbortSignal): Promise<EstablishedPlan>;
|
|
21
|
+
export declare function withHomeViews(env: HomeViewEnv, placed: HomeViewsCarrier | undefined, established: EstablishedPlan, signal?: AbortSignal): Promise<EstablishedPlan>;
|
|
@@ -38,8 +38,8 @@ function targetsByCandidate(e) {
|
|
|
38
38
|
function sameTargets(a, b) {
|
|
39
39
|
return a.size === b.size && [...a].every((t) => b.has(t));
|
|
40
40
|
}
|
|
41
|
-
export async function withHomeViews(env, established, signal) {
|
|
42
|
-
const views = homeViewsOf(env, signal);
|
|
41
|
+
export async function withHomeViews(env, placed, established, signal) {
|
|
42
|
+
const views = homeViewsOf(env, placed, signal);
|
|
43
43
|
const place = async (p) => {
|
|
44
44
|
const reads = new Map();
|
|
45
45
|
for (const [segment, rs] of p.reads) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repo": "core",
|
|
3
|
+
"version": "7.38.1",
|
|
4
|
+
"from": "f195abb9b3764b7d306d30ac7eee0c9ee82719f8",
|
|
5
|
+
"to": "0ee3363eac0ed4103b2598b72279b80b61f30a94",
|
|
6
|
+
"added": [],
|
|
7
|
+
"removed": [],
|
|
8
|
+
"changed": [],
|
|
9
|
+
"narrowed": [],
|
|
10
|
+
"closedSetMembers": [],
|
|
11
|
+
"closedSetsUnreadable": [],
|
|
12
|
+
"typeKeys": [],
|
|
13
|
+
"typeKeysUnreadableArms": [],
|
|
14
|
+
"typeKeysFlattenedCount": 220,
|
|
15
|
+
"declarationMoves": [],
|
|
16
|
+
"mintMoves": [],
|
|
17
|
+
"criteriaObserves": [
|
|
18
|
+
{
|
|
19
|
+
"kind": "taskEventField",
|
|
20
|
+
"name": "tool_end.errorCode",
|
|
21
|
+
"assertions": [
|
|
22
|
+
"G1.a",
|
|
23
|
+
"G1.b",
|
|
24
|
+
"G1.d",
|
|
25
|
+
"G1.e",
|
|
26
|
+
"G1.f"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"kind": "taskEventField",
|
|
31
|
+
"name": "tool_end.output",
|
|
32
|
+
"assertions": [
|
|
33
|
+
"G1.a",
|
|
34
|
+
"G1.c",
|
|
35
|
+
"G1.d",
|
|
36
|
+
"G1.f"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"kind": "typeMember",
|
|
41
|
+
"name": "ToolCall.id",
|
|
42
|
+
"assertions": [
|
|
43
|
+
"G2.a",
|
|
44
|
+
"G2.b"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"consumersHint": {
|
|
49
|
+
"cli": [],
|
|
50
|
+
"server": [],
|
|
51
|
+
"sdk": [],
|
|
52
|
+
"note": "scanned cli@64a76326 server@61d9a1d1 sdk@925ffec — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
|
|
53
|
+
},
|
|
54
|
+
"breakingCount": 0,
|
|
55
|
+
"breakingItems": []
|
|
56
|
+
}
|