@sema-agent/core 7.38.0 → 7.38.2
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 +51 -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/core/store-contracts/read-back-shape-contract.d.ts +5 -0
- package/dist/stores/file/fs-atomic.d.ts +23 -10
- package/dist/stores/file/fs-atomic.js +51 -25
- 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/wire-changes/core@7.38.2.json +26 -0
|
@@ -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
|
*
|