@sema-agent/core 7.49.1 → 7.49.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 +46 -0
- package/dist/core/arg-summary.js +23 -5
- package/dist/core/credential-vocabulary.js +19 -2
- package/dist/core/gate-fold.js +2 -2
- package/dist/core/runner/prepare-gate-stations.js +2 -2
- package/dist/core/runner/prepare-lsp.d.ts +4 -4
- package/dist/core/runner/prepare-lsp.js +2 -2
- package/dist/core/runner/prepare-safety-scan.js +10 -6
- package/dist/core/runner-deps.d.ts +2 -2
- package/dist/core/task-spec.d.ts +2 -2
- package/dist/core/tool-catalog-entries.js +1 -1
- package/dist/core/tool-conformance.js +2 -1
- package/dist/core/tool-roster.d.ts +11 -6
- package/dist/core/tool-roster.js +8 -4
- package/dist/core/write-protect.d.ts +3 -1
- package/dist/core/write-protect.js +4 -5
- package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -2
- package/dist/tools/fs/bash-readonly-classifier.js +5 -3
- package/dist/tools/fs/read-deny-home.d.ts +5 -1
- package/dist/tools/fs/read-deny-home.js +40 -19
- package/dist/tools/fs/shell-read-check.d.ts +1 -1
- package/dist/tools/fs/shell-read-check.js +29 -15
- package/dist/tools/fs/shell-read-environment.d.ts +74 -0
- package/dist/tools/fs/shell-read-environment.js +71 -0
- package/dist/tools/fs/shell-read-establish.d.ts +8 -4
- package/dist/tools/fs/shell-read-establish.js +34 -36
- package/dist/tools/fs/shell-read-identity.d.ts +3 -2
- package/dist/tools/fs/shell-read-identity.js +5 -3
- package/dist/tools/fs/shell-read-judge.d.ts +69 -18
- package/dist/tools/fs/shell-read-judge.js +27 -14
- package/package.json +1 -1
- package/wire-changes/core@7.49.1.json +57 -0
- package/wire-changes/core@7.49.2.json +138 -0
|
@@ -199,13 +199,18 @@ export async function whereItsAnsweredPrefixLeads(env, at, family = pathFamilyOf
|
|
|
199
199
|
};
|
|
200
200
|
return await withGateTimeout(walk(), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "ruleRowHead").catch(() => undefined);
|
|
201
201
|
}
|
|
202
|
-
const NO_CHAIN = { ancestors: [], whole: false };
|
|
202
|
+
const NO_CHAIN = { ancestors: [], whole: false, unread: [] };
|
|
203
|
+
const unreadChain = (at) => ({ ancestors: [], whole: false, unread: [at] });
|
|
204
|
+
const identityUnread = (id) => id === undefined || (!id.ok && id.error.code !== "not_found" && id.error.code !== "not_directory" && id.error.code !== "not_supported");
|
|
203
205
|
async function chainAbove(env, identity, canonical, signal, deadline) {
|
|
204
206
|
let own;
|
|
205
207
|
const ancestors = [];
|
|
208
|
+
const unread = [];
|
|
206
209
|
let whole = true;
|
|
207
210
|
for (let dir = normalizeAbsPathLexically(canonical), first = true;; first = false) {
|
|
208
211
|
const id = deadline === undefined ? await identityOf(env, identity, dir, signal) : await until(identityOf(env, identity, dir, signal), deadline, signal);
|
|
212
|
+
if (identityUnread(id))
|
|
213
|
+
unread.push(dir);
|
|
209
214
|
if (id === undefined && deadline !== undefined && Date.now() >= deadline) {
|
|
210
215
|
whole = false;
|
|
211
216
|
break;
|
|
@@ -221,7 +226,7 @@ async function chainAbove(env, identity, canonical, signal, deadline) {
|
|
|
221
226
|
break;
|
|
222
227
|
dir = parent;
|
|
223
228
|
}
|
|
224
|
-
return { ...(own !== undefined ? { own } : {}), ancestors, whole };
|
|
229
|
+
return { ...(own !== undefined ? { own } : {}), ancestors, whole, unread };
|
|
225
230
|
}
|
|
226
231
|
async function identityOf(env, identity, path, signal) {
|
|
227
232
|
try {
|
|
@@ -238,13 +243,13 @@ async function readAuxiliaryHome(env, home, opts, signal) {
|
|
|
238
243
|
if (spelled?.canonical === undefined) {
|
|
239
244
|
const answered = spelled?.answered === true;
|
|
240
245
|
const linked = opts.configRoot === true && opts.entries !== undefined ? configRootSpelledLinks(opts.entries, [home]) : new Map();
|
|
241
|
-
reading = { place: [home], chain: answered ? { ancestors: [], whole: true } :
|
|
246
|
+
reading = { place: [home], chain: answered ? { ancestors: [], whole: true, unread: [] } : unreadChain(home), linked, answered };
|
|
242
247
|
}
|
|
243
248
|
else {
|
|
244
249
|
const place = spelled.spellings;
|
|
245
250
|
const identity = env.fileIdentity;
|
|
246
251
|
const [chain, heads] = await Promise.all([
|
|
247
|
-
typeof identity === "function" ? chainAbove(env, identity, spelled.canonical, signal, deadline) : Promise.resolve({ ancestors: [], whole: true }),
|
|
252
|
+
typeof identity === "function" ? chainAbove(env, identity, spelled.canonical, signal, deadline) : Promise.resolve({ ancestors: [], whole: true, unread: [] }),
|
|
248
253
|
opts.entries === undefined
|
|
249
254
|
? Promise.resolve({ linked: new Map(), whole: true })
|
|
250
255
|
: readDenyLinksOf(env, opts.entries, opts.configRoot === true ? { at: "unanchored", roots: place } : { at: "home", homes: place }, signal, deadline),
|
|
@@ -257,16 +262,16 @@ async function readAuxiliaryHome(env, home, opts, signal) {
|
|
|
257
262
|
}
|
|
258
263
|
function homeViewSourceOver(env, auxiliary) {
|
|
259
264
|
const declared = declaredHomeOf(env);
|
|
260
|
-
const identity = env.fileIdentity;
|
|
261
265
|
let declaredChain;
|
|
262
|
-
const readDeclared = () => {
|
|
266
|
+
const readDeclared = (at) => {
|
|
267
|
+
const identity = at.fileIdentity;
|
|
263
268
|
if (declared === undefined || typeof identity !== "function")
|
|
264
269
|
return Promise.resolve(NO_CHAIN);
|
|
265
270
|
if (declaredChain === undefined) {
|
|
266
271
|
const reading = withGateTimeout((async () => {
|
|
267
|
-
const spelled = await homeSpellingsOf(
|
|
268
|
-
return spelled.canonical === undefined ? NO_CHAIN : await chainAbove(
|
|
269
|
-
})(), DEFAULT_GATE_AWAIT_TIMEOUT_MS, undefined, "homeChain").catch(() =>
|
|
272
|
+
const spelled = await homeSpellingsOf(at, declared);
|
|
273
|
+
return spelled.canonical === undefined ? (spelled.answered ? NO_CHAIN : unreadChain(declared)) : await chainAbove(at, identity, spelled.canonical);
|
|
274
|
+
})(), DEFAULT_GATE_AWAIT_TIMEOUT_MS, undefined, "homeChain").catch(() => unreadChain(declared));
|
|
270
275
|
declaredChain = reading;
|
|
271
276
|
void reading.then((c) => {
|
|
272
277
|
if (!c.whole && declaredChain === reading)
|
|
@@ -276,10 +281,16 @@ function homeViewSourceOver(env, auxiliary) {
|
|
|
276
281
|
return declaredChain;
|
|
277
282
|
};
|
|
278
283
|
return {
|
|
279
|
-
views(signal) {
|
|
284
|
+
views(signal, judgmentEnv) {
|
|
285
|
+
const at = judgmentEnv ?? env;
|
|
286
|
+
const identity = at.fileIdentity;
|
|
280
287
|
let index;
|
|
288
|
+
const unread = [];
|
|
281
289
|
const indexOf = () => (index ??= (async () => {
|
|
282
|
-
const chains = [await readDeclared(), ...(await Promise.all(auxiliary)).map((a) => a.chain)];
|
|
290
|
+
const chains = [await readDeclared(at), ...(await Promise.all(auxiliary)).map((a) => a.chain)];
|
|
291
|
+
for (const at of chains.flatMap((c) => c.unread))
|
|
292
|
+
if (!unread.includes(at))
|
|
293
|
+
unread.push(at);
|
|
283
294
|
const out = new Map();
|
|
284
295
|
for (const { own } of chains)
|
|
285
296
|
if (own !== undefined && !out.has(own.id))
|
|
@@ -300,15 +311,15 @@ function homeViewSourceOver(env, auxiliary) {
|
|
|
300
311
|
if (index === undefined || typeof identity !== "function")
|
|
301
312
|
return undefined;
|
|
302
313
|
if (!leafIsFile) {
|
|
303
|
-
const id = await identityOf(
|
|
304
|
-
if (id
|
|
305
|
-
return
|
|
306
|
-
if (id
|
|
314
|
+
const id = await identityOf(at, identity, path, signal);
|
|
315
|
+
if (identityUnread(id))
|
|
316
|
+
return void unread.push(path);
|
|
317
|
+
if (id?.ok === true) {
|
|
307
318
|
const at = index.get(id.value);
|
|
308
319
|
if (at !== undefined)
|
|
309
320
|
return at;
|
|
310
321
|
}
|
|
311
|
-
else if (id
|
|
322
|
+
else if (id?.error.code === "not_supported")
|
|
312
323
|
return undefined;
|
|
313
324
|
}
|
|
314
325
|
const parent = normalizeAbsPathLexically(`${path}/..`);
|
|
@@ -329,6 +340,7 @@ function homeViewSourceOver(env, auxiliary) {
|
|
|
329
340
|
const view = await viewOf(spelled, opts?.leaf === "file");
|
|
330
341
|
return view === undefined || view === spelled ? undefined : view;
|
|
331
342
|
},
|
|
343
|
+
unread,
|
|
332
344
|
};
|
|
333
345
|
},
|
|
334
346
|
};
|
|
@@ -357,7 +369,7 @@ export function homeViewsAt(env, homes) {
|
|
|
357
369
|
return homeViewSourceOver(env, readAuxiliaryHomes(env, homes, {}));
|
|
358
370
|
}
|
|
359
371
|
export function homeViewsOf(env, judge, signal) {
|
|
360
|
-
return (judge?.homeViews ?? homeViewsAt(env, [])).views(signal);
|
|
372
|
+
return (judge?.homeViews ?? homeViewsAt(env, [])).views(signal, env);
|
|
361
373
|
}
|
|
362
374
|
export function carryHomeViews(judge, source) {
|
|
363
375
|
return { ...judge, homeViews: source };
|
|
@@ -421,9 +433,18 @@ export function readDenyPlacedLater(unplaced, place) {
|
|
|
421
433
|
return current;
|
|
422
434
|
};
|
|
423
435
|
const homeViews = {
|
|
424
|
-
views(signal) {
|
|
436
|
+
views(signal, judgmentEnv) {
|
|
425
437
|
let views;
|
|
426
|
-
|
|
438
|
+
const unread = [];
|
|
439
|
+
return {
|
|
440
|
+
of: async (path, opts) => {
|
|
441
|
+
const inner = await (views ??= settled().then((m) => m.homeViews.views(signal, judgmentEnv)));
|
|
442
|
+
const view = await inner.of(path, opts);
|
|
443
|
+
unread.splice(0, unread.length, ...inner.unread);
|
|
444
|
+
return view;
|
|
445
|
+
},
|
|
446
|
+
unread,
|
|
447
|
+
};
|
|
427
448
|
},
|
|
428
449
|
};
|
|
429
450
|
const matcher = {
|
|
@@ -225,7 +225,7 @@ export interface ShellReadJudgment {
|
|
|
225
225
|
* gate FOLD's policy on the gate's one deadline seat; the station has never held one on either entry, and a
|
|
226
226
|
* second default here would decide the same number twice and turn a slow-but-correct filesystem's true
|
|
227
227
|
* answer into a fail-closed one unasked. A consumer wanting that posture races its own deadline and reads
|
|
228
|
-
* the row the
|
|
228
|
+
* the row the gate's fallback reads: `SHELL_READ_DECISION.unanswered[seat]`.
|
|
229
229
|
*/
|
|
230
230
|
export declare function judgeShellRead(input: ShellReadJudgeInput): Promise<ShellReadJudgment>;
|
|
231
231
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { observeEnvironment } from "./shell-read-environment.js";
|
|
1
2
|
import { errorResult } from "../../core/tools.js";
|
|
2
3
|
import { BASH_READONLY_CONFINEMENT_NOTE } from "./fs-shared.js";
|
|
3
4
|
import { isAbsoluteForFamily, normalizeAbsPathLexically, pathFamilyOf, violationDetails, withinAnyRoot } from "./safety.js";
|
|
@@ -9,7 +10,7 @@ import { createShellReadEstablisher, establishShellReadsLexically } from "./shel
|
|
|
9
10
|
import { declaredReadDenyHomes, homeViewsAt } from "./read-deny-home.js";
|
|
10
11
|
import { programArgs } from "./shell-read-write-targets.js";
|
|
11
12
|
import { programRow, selectedOptions } from "./program-read-model.js";
|
|
12
|
-
import { allFindings, anyFaceGuards, decideShellRead,
|
|
13
|
+
import { allFindings, anyFaceGuards, decideShellRead, denyRowAtOrBelow, foldShellReadFindings, judgeEstablishedPlan, readRootGrantCandidateOf, stationGuards, writeFaceGuards, } from "./shell-read-judge.js";
|
|
13
14
|
import { seatOf } from "../../core/tool-name-readings.js";
|
|
14
15
|
import { normalizeReadRootCandidate } from "../../core/read-root-candidate.js";
|
|
15
16
|
import { argvOf, lexShellText } from "../../shell/lex.js";
|
|
@@ -21,7 +22,7 @@ function unestablishedDetails() {
|
|
|
21
22
|
return { type: "read_unestablished", code: "read_unestablished" };
|
|
22
23
|
}
|
|
23
24
|
function findingDetails(finding, paths) {
|
|
24
|
-
return finding === "unestablishable" || finding === "declined" ? unestablishedDetails() : containmentDetails(paths);
|
|
25
|
+
return finding === "unestablishable" || finding === "declined" || finding === "unanswered" ? unestablishedDetails() : containmentDetails(paths);
|
|
25
26
|
}
|
|
26
27
|
const REMOVAL_COMMANDS = new Set(["rm", "rmdir"]);
|
|
27
28
|
const WRITES_BENEATH = {
|
|
@@ -155,7 +156,7 @@ async function writeGuardsOf(judged, boundary, env, signal) {
|
|
|
155
156
|
const dir = fold(parentOf(view));
|
|
156
157
|
return writtenBy.some((paths, i) => i !== at && (i < at || concurrent) && paths.some((w) => withinAnyRoot([fold(w)], dir) || withinAnyRoot([dir], fold(w))));
|
|
157
158
|
};
|
|
158
|
-
const answer = async (view, key) => (await judge(view, { ...(signal !== undefined ? { signal } : {}), ...(key ? { canonical: true } : {}) })) ?? "clear";
|
|
159
|
+
const answer = async (view, key) => (await judge(view, { fs: env, ...(signal !== undefined ? { signal } : {}), ...(key ? { canonical: true } : {}) })) ?? "clear";
|
|
159
160
|
const asked = [...[...asKey].map(([view, v]) => [view, v, true]), ...[...asSpelling].filter(([view]) => !landingKeys.has(view)).map(([view, v]) => [view, v, false])];
|
|
160
161
|
let unplaced = false;
|
|
161
162
|
let writeProtected = false;
|
|
@@ -207,12 +208,15 @@ function findingAccount(fold, boundary) {
|
|
|
207
208
|
case "outside_roots":
|
|
208
209
|
return `${writes ? "writes" : "reads"} outside the allowed directories for this session: "${fitMiddle(w?.read.target ?? "?", ACCOUNT_PART_MAX)}" is not under ${allowed} ${NOT_AUTO_ALLOWED}`;
|
|
209
210
|
case "unestablishable":
|
|
211
|
+
case "unanswered":
|
|
210
212
|
if (writes && boundary.writeRoots === undefined)
|
|
211
213
|
return `this command writes "${fitMiddle(w.read.target ?? w.read.candidate.spelled, ACCOUNT_PART_MAX)}", and this session declares no directories it may write ${NOT_AUTO_ALLOWED}`;
|
|
212
214
|
if (writes)
|
|
213
215
|
return `where this command writes could not be established${w.read.cause !== undefined ? `: ${fitCause(w.read.cause)}` : ""}, so it cannot be confirmed to write only inside the allowed directories for this session (${allowed}) ${NOT_AUTO_ALLOWED}`;
|
|
214
216
|
if (w?.read.cause !== undefined)
|
|
215
217
|
return `where this command reads could not be established: ${fitCause(w.read.cause)}, so it cannot be confirmed ${boundary.face === "open" ? "not to read a path the deny list names" : `to read only inside the allowed directories for this session (${allowed})`} ${NOT_AUTO_ALLOWED}`;
|
|
218
|
+
if (fold.cause !== undefined)
|
|
219
|
+
return `where this command reads could not be established: ${fitCause(fold.cause)}, so it cannot be confirmed ${boundary.face === "open" ? "not to read a path the deny list names" : `to read only inside the allowed directories for this session (${allowed})`} ${NOT_AUTO_ALLOWED}`;
|
|
216
220
|
if (fold.declined?.baseMove !== undefined)
|
|
217
221
|
return `"${fitMiddle(fold.declined.text.trim(), ACCOUNT_PART_MAX)}" moves the working directory somewhere this command's text does not settle ${NOT_AUTO_ALLOWED}`;
|
|
218
222
|
if (fold.declined?.unreadProgramText !== undefined)
|
|
@@ -276,7 +280,7 @@ const DENY_REFUSAL_NOTE = "The read boundary judges the paths this command names
|
|
|
276
280
|
"own pattern) reaches into the tree a recursive read walks.";
|
|
277
281
|
function verdictOf(tool, seat, fold, judged, boundary) {
|
|
278
282
|
const redirectsOnly = fold.finding === "outside_roots" && fold.unestablished !== true && outsideFindingsAreRedirections(judged);
|
|
279
|
-
const disposition =
|
|
283
|
+
const disposition = decideShellRead(fold.finding, seat, { redirectsOnly, narrowedReadFace: fold.unplacedRead === true && boundary.face !== "open" });
|
|
280
284
|
if (disposition === "vouch")
|
|
281
285
|
return { disposition, finding: fold.finding };
|
|
282
286
|
const paths = outsidePathsOf(judged);
|
|
@@ -287,14 +291,14 @@ function verdictOf(tool, seat, fold, judged, boundary) {
|
|
|
287
291
|
return { disposition, finding: fold.finding, ...writes, sentence: r.sentence, ...(r.details !== undefined ? { details: r.details } : {}) };
|
|
288
292
|
}
|
|
289
293
|
const grant = fold.finding === "outside_roots" ? readRootGrantCandidateOf(judged, boundary) : undefined;
|
|
290
|
-
const containment = fold.finding === "outside_roots" || fold.finding === "unestablishable";
|
|
294
|
+
const containment = fold.finding === "outside_roots" || fold.finding === "unestablishable" || fold.finding === "unanswered";
|
|
291
295
|
return {
|
|
292
296
|
disposition,
|
|
293
297
|
finding: fold.finding,
|
|
294
298
|
...writes,
|
|
295
299
|
...unestablished,
|
|
296
300
|
sentence: findingAccount(fold, boundary),
|
|
297
|
-
...(disposition === "mandated" && containment ? { details: findingDetails(fold.finding, paths) } : {}),
|
|
301
|
+
...((disposition === "mandated" && containment) || (disposition === "ordinary" && fold.finding === "unestablishable") ? { details: findingDetails(fold.finding, paths) } : {}),
|
|
298
302
|
...(grant !== undefined ? { readRootCandidate: { dir: grant.dir, ...(grant.covers !== undefined ? { covers: grant.covers } : {}), clearsThisAsk: true } } : {}),
|
|
299
303
|
};
|
|
300
304
|
}
|
|
@@ -325,6 +329,7 @@ async function applyAlsoInside(judged, boundary) {
|
|
|
325
329
|
plan: j.plan,
|
|
326
330
|
guards: j.guards,
|
|
327
331
|
writeGuards: j.writeGuards,
|
|
332
|
+
...(j.incomplete !== undefined ? { incomplete: j.incomplete } : {}),
|
|
328
333
|
segments: j.segments.map((s) => ({
|
|
329
334
|
segment: s.segment,
|
|
330
335
|
findings: s.findings.map((f) => (outsideRead(f) && rescued.has(f.read.target) ? { ...f, kind: "none", containment: "inside" } : f)),
|
|
@@ -335,13 +340,23 @@ async function applyAlsoInside(judged, boundary) {
|
|
|
335
340
|
}
|
|
336
341
|
async function runShellReadStation(tool, command, declared, seat, readers, establisher, env, signal) {
|
|
337
342
|
const plan = planShellReads(command, declarationsOf(declared, readers, env.cdPath));
|
|
338
|
-
const
|
|
343
|
+
const door = observeEnvironment(env);
|
|
344
|
+
const established = await establisher.establish(plan, declared, signal, door);
|
|
339
345
|
const boundary = { ...declared, roots: established.roots };
|
|
340
|
-
const
|
|
341
|
-
const
|
|
346
|
+
const onPlan = await applyAlsoInside(judgePlan(plan, established, boundary), boundary);
|
|
347
|
+
const judgedNow = () => {
|
|
348
|
+
const unfinished = door.settled();
|
|
349
|
+
return unfinished.length > 0 ? { ...onPlan, incomplete: unfinished } : onPlan;
|
|
350
|
+
};
|
|
351
|
+
let judged = judgedNow();
|
|
352
|
+
let judgedVerdict = verdictOf(tool, seat, foldShellReadFindings(judged), judged, boundary);
|
|
353
|
+
const probesFrom = door.mark();
|
|
354
|
+
const guarded = judgedVerdict.disposition === "refuse" ? { unplaced: false, writeProtected: false } : await writeGuardsOf(judged, boundary, door.fs, signal);
|
|
355
|
+
door.silence(probesFrom);
|
|
356
|
+
judged = judgedNow();
|
|
357
|
+
judgedVerdict = verdictOf(tool, seat, foldShellReadFindings(judged), judged, boundary);
|
|
342
358
|
const unplaced = unplacedOptionsOf(judged);
|
|
343
|
-
const placed = writesPlacedOf(judged, boundary);
|
|
344
|
-
const guarded = judgedVerdict.disposition === "refuse" ? { unplaced: false, writeProtected: false } : await writeGuardsOf(judged, boundary, env, signal);
|
|
359
|
+
const placed = judgedVerdict.finding !== "unanswered" && writesPlacedOf(judged, boundary);
|
|
345
360
|
const unresolved = judgedVerdict.disposition !== "refuse" && runOrderOf(judged).runs.some((s) => s.findings.some((f) => f.read.written !== undefined && f.read.unresolved === true));
|
|
346
361
|
const facts = { ...judgedVerdict, ...(guarded.writeProtected ? { writeProtected: true } : {}), ...(unresolved ? { targetUnresolved: true } : {}) };
|
|
347
362
|
const verdict = placed && !guarded.unplaced ? { ...facts, writesPlaced: true } : unplaced !== undefined ? { ...facts, unplacedOptions: unplaced } : facts;
|
|
@@ -415,13 +430,12 @@ export function createShellReadStation(opts) {
|
|
|
415
430
|
const boundary = opts.boundaryOf(member, undefined);
|
|
416
431
|
if (boundary === undefined || !(anyFaceGuards(boundary) || boundary.writeTargetJudge !== undefined))
|
|
417
432
|
return { disposition: "vouch", finding: "none" };
|
|
418
|
-
const
|
|
419
|
-
const disposition = decideShellRead("unestablishable", seat);
|
|
433
|
+
const disposition = decideShellRead("unanswered", seat);
|
|
420
434
|
const account = `${cause}, so where this command reads could not be established ${NOT_AUTO_ALLOWED}`;
|
|
421
435
|
const details = unestablishedDetails();
|
|
422
436
|
if (disposition === "refuse")
|
|
423
|
-
return { disposition, finding: "
|
|
424
|
-
return { disposition, finding:
|
|
437
|
+
return { disposition, finding: "unanswered", sentence: `Error (${toolName}): ${account}. ${BASH_READONLY_CONFINEMENT_NOTE}`, details };
|
|
438
|
+
return { disposition, finding: "unanswered", sentence: account, details };
|
|
425
439
|
},
|
|
426
440
|
};
|
|
427
441
|
const admit = async (toolName, toolCallId, command, signal) => {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE door between the shell read station's establisher and the execution environment's filesystem.
|
|
3
|
+
*
|
|
4
|
+
* The establisher answers "where does each word of this command land" by asking the environment (a resolution, a
|
|
5
|
+
* directory listing, a stat, a link read). An environment that does not give an answer — an error that is not a
|
|
6
|
+
* conclusive "not there", a lookup that came back inconclusive — and a bound the station itself stops at are the same
|
|
7
|
+
* fact for the verdict: the judge did not finish checking the command against the deny rows, so it cannot vouch for it. That
|
|
8
|
+
* fact is recorded HERE, at the door, once per judgment, not by each producer that happens to remember to mark it: the
|
|
9
|
+
* establisher and the write guards are handed an {@link ObservedFilesystem}, never the environment, so a lookup they make
|
|
10
|
+
* cannot escape the record.
|
|
11
|
+
*
|
|
12
|
+
* What is NOT a failure: a conclusive absence (`not_found`, `not_directory` — the answer is "nothing there") and an adapter
|
|
13
|
+
* that declares no filesystem surface (`not_supported` — it keeps the lexical reading). Everything else is.
|
|
14
|
+
*
|
|
15
|
+
* ONE decision, taken once per command: a resolver that did not declare its answers authoritative and answered NOTHING AT ALL,
|
|
16
|
+
* in the whole command, is read as an absence (the lexical reading every such adapter has had). It is a statement about the
|
|
17
|
+
* command, not about a word or a station: the walks and probes that found the resolver silent are marked
|
|
18
|
+
* ({@link ObservedEnvironment.silence}), and {@link ObservedEnvironment.settled} — read by whoever made the door, after EVERY
|
|
19
|
+
* lookup the command makes (the establishment, the write guards) has asked — takes the marked records back only when no lookup
|
|
20
|
+
* was ever answered. A resolver that answered once has shown it can; a silence after that is a failure, wherever it falls. A
|
|
21
|
+
* resolver that declared its answers authoritative is never read as an absence.
|
|
22
|
+
*/
|
|
23
|
+
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
24
|
+
/** One thing the station could not finish: an environment lookup that gave no answer, or a bound the station stopped at. */
|
|
25
|
+
export interface Incomplete {
|
|
26
|
+
/** The lookup (`canonicalPath`, `listDir`, `fileInfo`, `readLink`, `fileIdentity`) or `bound`. */
|
|
27
|
+
readonly op: string;
|
|
28
|
+
/** The sentence the card discloses ("… cannot be resolved (permission_denied)"). */
|
|
29
|
+
readonly detail: string;
|
|
30
|
+
}
|
|
31
|
+
/** Only {@link observeEnvironment} mints the brand: a bare environment is not assignable where a door is asked for. */
|
|
32
|
+
declare const OBSERVED: unique symbol;
|
|
33
|
+
/** The environment as the station's paths may use it: every path lookup the engine's resolvers make (`canonicalPath`,
|
|
34
|
+
* `listDir`, `fileInfo`, `readLink`, `exists`, `absolutePath`, `canonicalChain`, `fileIdentity`) goes through the door and is
|
|
35
|
+
* recorded when it gives no answer; everything else is the environment's own. It IS an `ExecutionEnv` to the resolvers
|
|
36
|
+
* (`canonicalizeTarget`, the home views, a deployment's guarded-row provider) — so none of them has to be narrowed — and
|
|
37
|
+
* nominal to the station: the establisher, the write guards, the write judgment and the home views are typed over it, so
|
|
38
|
+
* none of them can be handed the bare environment, nor a captured one. */
|
|
39
|
+
export type ObservedFilesystem = ExecutionEnv & {
|
|
40
|
+
readonly [OBSERVED]: true;
|
|
41
|
+
};
|
|
42
|
+
/** The observed door for one command. */
|
|
43
|
+
export interface ObservedEnvironment {
|
|
44
|
+
readonly fs: ObservedFilesystem;
|
|
45
|
+
/** Whether the environment DECLARES its `canonicalPath` the authority on where a path lands: its silence is then a failure
|
|
46
|
+
* that nothing takes back ({@link silence} does nothing). */
|
|
47
|
+
readonly authoritative: boolean;
|
|
48
|
+
/** What the door recorded, in the order it happened, as the command stands on it NOW — the one decision about what a
|
|
49
|
+
* silence means, taken on everything asked so far; call it after the last lookup of the command. */
|
|
50
|
+
settled(): Incomplete[];
|
|
51
|
+
/** The station stopped at its own bound (more directories or names than it enumerates, a link chain past its hop bound). */
|
|
52
|
+
bound(detail: string): void;
|
|
53
|
+
/** Records that came from outside the door and stand as they are: the declared roots' own lookup (`answered`: it was
|
|
54
|
+
* answered, which counts as the resolver's answer) and the home views a read could not be placed against. */
|
|
55
|
+
adopt(records: readonly Incomplete[], answered?: boolean): void;
|
|
56
|
+
/** Whether the resolver answered a path question (a path, or a conclusive "nothing there") at least once. */
|
|
57
|
+
answered(): boolean;
|
|
58
|
+
/** Where a walk's (or a probe's) records start: pass it to {@link silence} once the lookups are done. */
|
|
59
|
+
mark(): number;
|
|
60
|
+
/** The lookups since `from` found the resolver silent: their records are taken back by {@link settled} only when the
|
|
61
|
+
* resolver never answered anything in the command (and never where it is {@link authoritative}). */
|
|
62
|
+
silence(from: number): void;
|
|
63
|
+
}
|
|
64
|
+
/** The declared roots, canonicalized ONCE per establisher (through the same door), with what that lookup could not finish —
|
|
65
|
+
* merged into every establishment. `known`: the caller already holds them as canonical identities and they are used verbatim
|
|
66
|
+
* (re-resolving one that was since swapped for a link would substitute the link's target for the root that was approved). */
|
|
67
|
+
export declare function canonicalRootsOnce(env: ExecutionEnv, readRoots: readonly string[], known: boolean): (signal?: AbortSignal) => Promise<{
|
|
68
|
+
readonly roots: readonly string[];
|
|
69
|
+
readonly incomplete: readonly Incomplete[];
|
|
70
|
+
readonly answered: boolean;
|
|
71
|
+
}>;
|
|
72
|
+
/** Wrap `env` so every lookup that fails inconclusively is recorded; the result a caller sees is the environment's own. */
|
|
73
|
+
export declare function observeEnvironment(env: ExecutionEnv): ObservedEnvironment;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { canonicalPathIsAuthoritative } from "../../core/remote-env.js";
|
|
2
|
+
const OBSERVED = Symbol("observedFilesystem");
|
|
3
|
+
const CONCLUSIVE = new Set(["not_found", "not_directory", "not_supported"]);
|
|
4
|
+
const ABSENCES = new Set(["not_found", "not_directory"]);
|
|
5
|
+
export function canonicalRootsOnce(env, readRoots, known) {
|
|
6
|
+
let once = known ? Promise.resolve({ roots: readRoots, incomplete: [], answered: false }) : undefined;
|
|
7
|
+
return (signal) => (once ??= (async () => {
|
|
8
|
+
const seen = observeEnvironment(env);
|
|
9
|
+
const roots = [];
|
|
10
|
+
for (const r of readRoots) {
|
|
11
|
+
const c = await seen.fs.canonicalPath(r, signal);
|
|
12
|
+
roots.push(c.ok ? c.value : r);
|
|
13
|
+
}
|
|
14
|
+
const result = { roots, incomplete: seen.settled(), answered: seen.answered() };
|
|
15
|
+
if (result.incomplete.length > 0)
|
|
16
|
+
once = undefined;
|
|
17
|
+
return result;
|
|
18
|
+
})());
|
|
19
|
+
}
|
|
20
|
+
export function observeEnvironment(env) {
|
|
21
|
+
const authoritative = canonicalPathIsAuthoritative(env);
|
|
22
|
+
const records = [];
|
|
23
|
+
let answers = 0;
|
|
24
|
+
const note = (op, path, what, code) => void records.push({ op, detail: `"${path}" ${what} (${code})`, silent: false });
|
|
25
|
+
const watch = (op, path, what, ask) => new Promise((resolve) => resolve(ask())).then((r) => {
|
|
26
|
+
if (op === "canonicalPath" && (r.ok || ABSENCES.has(r.error.code)))
|
|
27
|
+
answers += 1;
|
|
28
|
+
if (!r.ok && !CONCLUSIVE.has(r.error.code))
|
|
29
|
+
note(op, path, what, r.error.code);
|
|
30
|
+
return r;
|
|
31
|
+
}, (e) => {
|
|
32
|
+
note(op, path, what, e instanceof Error ? e.name : "threw");
|
|
33
|
+
throw e;
|
|
34
|
+
});
|
|
35
|
+
const look = (op, what, member) => member === undefined ? {} : { [op]: (path, signal) => watch(op, path, what, () => member.call(env, path, signal)) };
|
|
36
|
+
const overlay = {
|
|
37
|
+
...look("canonicalPath", "cannot be resolved", env.canonicalPath),
|
|
38
|
+
...look("listDir", "cannot be enumerated", env.listDir),
|
|
39
|
+
...look("fileInfo", "cannot be inspected", env.fileInfo),
|
|
40
|
+
...look("readLink", "cannot be read as a link", env.readLink),
|
|
41
|
+
...look("exists", "cannot be checked", env.exists),
|
|
42
|
+
...look("absolutePath", "cannot be made absolute", env.absolutePath),
|
|
43
|
+
...look("canonicalChain", "its link chain cannot be read", env.canonicalChain),
|
|
44
|
+
...look("fileIdentity", "cannot be identified", env.fileIdentity),
|
|
45
|
+
[OBSERVED]: true,
|
|
46
|
+
};
|
|
47
|
+
const fs = new Proxy(env, {
|
|
48
|
+
get: (target, prop) => {
|
|
49
|
+
if (Object.prototype.hasOwnProperty.call(overlay, prop))
|
|
50
|
+
return overlay[prop];
|
|
51
|
+
const value = Reflect.get(target, prop, target);
|
|
52
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
53
|
+
},
|
|
54
|
+
has: (target, prop) => Object.prototype.hasOwnProperty.call(overlay, prop) || Reflect.has(target, prop),
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
fs,
|
|
58
|
+
authoritative,
|
|
59
|
+
settled: () => records.filter((r) => answers > 0 || !r.silent).map(({ op, detail }) => ({ op, detail })),
|
|
60
|
+
bound: (detail) => void records.push({ op: "bound", detail, silent: false }),
|
|
61
|
+
adopt: (adopted, answered = false) => {
|
|
62
|
+
for (const r of adopted)
|
|
63
|
+
records.push({ ...r, silent: false });
|
|
64
|
+
if (answered)
|
|
65
|
+
answers += 1;
|
|
66
|
+
},
|
|
67
|
+
mark: () => records.length,
|
|
68
|
+
silence: (from) => (authoritative ? undefined : void records.slice(from).forEach((r) => void (r.silent = true))),
|
|
69
|
+
answered: () => answers > 0,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* target, so the target is established where the link lands (read one hop at a time, bounded) — or not at
|
|
18
18
|
* all when the environment cannot read the link.
|
|
19
19
|
* 2. An unanswerable filesystem manufactures no refusal on its own: a chain no ancestor answered degrades
|
|
20
|
-
* to the lexical spelling on an environment that declares no authority for its resolver
|
|
21
|
-
* `unestablishable` on one that does (`canonicalPathIsAuthoritative`).
|
|
20
|
+
* to the lexical spelling on an environment that declares no authority for its resolver AND answered nothing
|
|
21
|
+
* in the whole command, and is `unestablishable` on one that does (`canonicalPathIsAuthoritative`).
|
|
22
22
|
* 3. An ISOLATED environment stays lexical, disclosed rather than silently degraded: the sandbox is the
|
|
23
23
|
* containment there and `canonicalPath` is an RPC into a filesystem that is not this host's
|
|
24
24
|
* (`boundaryIsTheSandbox`). The split is the DECLARED isolation capability, never the remote interface's
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
* call.
|
|
39
39
|
*/
|
|
40
40
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
41
|
+
import { type ObservedEnvironment } from "./shell-read-environment.js";
|
|
41
42
|
import type { ShellReadPlan, ShellReadSegment } from "./shell-read-plan.js";
|
|
42
43
|
import type { EstablishedRead, ShellReadBoundary } from "./shell-read-judge.js";
|
|
43
44
|
import type { HomeViewSource } from "./read-deny-home.js";
|
|
@@ -64,8 +65,10 @@ export interface EstablishedPlan {
|
|
|
64
65
|
export interface ShellReadEstablisher {
|
|
65
66
|
/** Whether this environment is judged lexically (an isolated sandbox — property 3). */
|
|
66
67
|
readonly lexicalOnly: boolean;
|
|
67
|
-
/** Establish every read candidate of `plan` (nested plans included) against `boundary`'s roots.
|
|
68
|
-
|
|
68
|
+
/** Establish every read candidate of `plan` (nested plans included) against `boundary`'s roots. Every lookup goes through
|
|
69
|
+
* `door` (a fresh one when the caller does not keep it): what the establishment could not finish is recorded THERE, and the
|
|
70
|
+
* caller that made the door reads it once, after everything else the command asks of the environment (the write guards) has asked. */
|
|
71
|
+
establish(plan: ShellReadPlan, boundary: ShellReadBoundary, signal?: AbortSignal, door?: ObservedEnvironment): Promise<EstablishedPlan>;
|
|
69
72
|
/** Establish the same plan again and report every candidate whose canonical target moved — the pre-spawn
|
|
70
73
|
* identity re-check. Candidates that were lexical or unestablishable are not compared: nothing was
|
|
71
74
|
* established to move. */
|
|
@@ -84,6 +87,7 @@ export declare function establishShellReadsLexically(plan: ShellReadPlan, bounda
|
|
|
84
87
|
* as declared (canonical by contract); they are resolved once more per instance because the comparison is
|
|
85
88
|
* against `realpath` output on both sides — an aliased root would otherwise refuse every read under itself.
|
|
86
89
|
* `homeViews`: the home views of the placement the deny seats were compiled at (read-deny-home.ts); absent, the declared home's.
|
|
90
|
+
* Each establishment asks the environment through ONE observed door ({@link observeEnvironment}), the caller's when it keeps one.
|
|
87
91
|
*/
|
|
88
92
|
export declare function createShellReadEstablisher(env: ExecutionEnv, readRoots: readonly string[], opts?: {
|
|
89
93
|
readonly rootsCanonical?: boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { canonicalRootsOnce, observeEnvironment } from "./shell-read-environment.js";
|
|
1
2
|
import { anchorSpelling, normalizeAbsPathLexically, pathFamilyOf } from "./safety.js";
|
|
2
|
-
import { boundaryIsTheSandbox
|
|
3
|
+
import { boundaryIsTheSandbox } from "../../core/remote-env.js";
|
|
3
4
|
import { resolveOperandSpellings } from "./bash-readonly-classifier.js";
|
|
4
5
|
import { judgedCandidatesOf, sedBackupNames } from "./shell-read-write-targets.js";
|
|
5
6
|
import { movedReads, withHomeViews } from "./shell-read-identity.js";
|
|
@@ -196,33 +197,44 @@ export function establishShellReadsLexically(plan, boundary) {
|
|
|
196
197
|
}
|
|
197
198
|
export function createShellReadEstablisher(env, readRoots, opts) {
|
|
198
199
|
const lexicalOnly = boundaryIsTheSandbox(env);
|
|
199
|
-
const
|
|
200
|
+
const canonicalRoots = canonicalRootsOnce(env, readRoots, opts?.rootsCanonical === true);
|
|
201
|
+
const legOver = (seen) => establishLegOver(seen, readRoots);
|
|
202
|
+
return {
|
|
203
|
+
lexicalOnly,
|
|
204
|
+
establish: async (plan, boundary, signal, door = observeEnvironment(env)) => {
|
|
205
|
+
if (lexicalOnly)
|
|
206
|
+
return establishShellReadsLexically(plan, boundary);
|
|
207
|
+
const { roots, incomplete, answered } = await canonicalRoots(signal);
|
|
208
|
+
door.adopt(incomplete, answered);
|
|
209
|
+
return await withHomeViews(door, opts, await legOver(door)(plan, { ...boundary, roots }, roots, signal), signal);
|
|
210
|
+
},
|
|
211
|
+
reestablish: async (previous, boundary, signal) => {
|
|
212
|
+
if (lexicalOnly)
|
|
213
|
+
return { changed: [] };
|
|
214
|
+
const again = await legOver(observeEnvironment(env))(previous.plan, { ...boundary, roots: previous.roots }, previous.roots, signal);
|
|
215
|
+
return { changed: movedReads(previous, again) };
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function establishLegOver(seen, readRoots) {
|
|
220
|
+
const env = seen.fs;
|
|
221
|
+
const resolverIsAuthoritative = seen.authoritative;
|
|
222
|
+
const bounded = (detail) => (seen.bound(detail), { unverifiable: detail });
|
|
200
223
|
const family = pathFamilyOf({ root: readRoots[0] ?? "/" });
|
|
201
224
|
const SEP = family === "posix" ? /\// : /[\\/]/;
|
|
202
225
|
const TRAILING_SEP = family === "posix" ? /\/+$/ : /[\\/]+$/;
|
|
203
226
|
const joinUnder = (dir, name) => `${dir.replace(TRAILING_SEP, "")}/${name}`;
|
|
204
|
-
let rootsOnce = opts?.rootsCanonical === true ? Promise.resolve(readRoots) : undefined;
|
|
205
|
-
const canonicalRoots = async (signal) => {
|
|
206
|
-
rootsOnce ??= (async () => {
|
|
207
|
-
const out = [];
|
|
208
|
-
for (const r of readRoots) {
|
|
209
|
-
const c = await env.canonicalPath(r, signal);
|
|
210
|
-
out.push(c.ok ? c.value : r);
|
|
211
|
-
}
|
|
212
|
-
return out;
|
|
213
|
-
})();
|
|
214
|
-
return await rootsOnce;
|
|
215
|
-
};
|
|
216
227
|
const resolveWithoutParentSteps = async (p, signal, writeTarget = false, linkHops = WRITE_LINK_HOPS) => {
|
|
217
228
|
const parts = p.split(SEP);
|
|
218
229
|
const tail = [];
|
|
219
230
|
let allAbsent = true;
|
|
231
|
+
const began = seen.mark();
|
|
220
232
|
while (parts.length > 0) {
|
|
221
233
|
const at = parts.join("/") || "/";
|
|
222
234
|
const canon = await env.canonicalPath(at, signal);
|
|
223
235
|
if (!canon.ok && canon.error.code === "not_found" && writeTarget && allAbsent) {
|
|
224
236
|
const entry = await env.fileInfo(at, signal);
|
|
225
|
-
const link = entry.ok && entry.value.kind === "symlink"
|
|
237
|
+
const link = entry.ok && entry.value.kind === "symlink" ? (linkHops > 0 ? await env.readLink?.(at, signal) : void bounded(`"${at}" is a link chain of more than ${WRITE_LINK_HOPS} links, too many to follow to verify where it lands`)) : undefined;
|
|
226
238
|
if (link?.ok === true) {
|
|
227
239
|
const landing = anchorSpelling(family, parts.slice(0, -1).join("/") || "/", link.value);
|
|
228
240
|
return await resolve([landing, ...[...tail].reverse()].join("/"), signal, true, linkHops - 1);
|
|
@@ -239,7 +251,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
239
251
|
allAbsent = false;
|
|
240
252
|
tail.push(parts.pop());
|
|
241
253
|
}
|
|
242
|
-
return
|
|
254
|
+
return void seen.silence(began);
|
|
243
255
|
};
|
|
244
256
|
const parentOf = (canonical) => normalizeAbsPathLexically(`${canonical}/..`);
|
|
245
257
|
const resolve = async (p, signal, writeTarget = false, linkHops = WRITE_LINK_HOPS) => {
|
|
@@ -318,7 +330,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
318
330
|
const next = [];
|
|
319
331
|
for (const dir of frontier) {
|
|
320
332
|
if (++dirsUsed > MAX_GLOB_DIRS)
|
|
321
|
-
return
|
|
333
|
+
return bounded(`"${p}" expands over more than ${MAX_GLOB_DIRS} directories, too many to enumerate to verify`);
|
|
322
334
|
const canonDir = await resolve(dir, signal);
|
|
323
335
|
if (canonDir !== undefined && canonDir.missing === true)
|
|
324
336
|
continue;
|
|
@@ -340,7 +352,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
340
352
|
if (!globSegmentMatches(re, seg, entry.name))
|
|
341
353
|
continue;
|
|
342
354
|
if (++matches > MAX_GLOB_MATCHES)
|
|
343
|
-
return
|
|
355
|
+
return bounded(`"${p}" expands to more than ${MAX_GLOB_MATCHES} names, too many to enumerate to verify`);
|
|
344
356
|
const candidate = joinUnder(dir, entry.name);
|
|
345
357
|
if (canonDirHere !== undefined && entry.kind !== "symlink")
|
|
346
358
|
canonicalOfMatch.set(candidate, joinUnder(canonDirHere, entry.name));
|
|
@@ -363,7 +375,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
363
375
|
while (stack.length > 0) {
|
|
364
376
|
const { dir, spell } = stack.pop();
|
|
365
377
|
if (++dirsUsed > MAX_GLOB_DIRS)
|
|
366
|
-
return
|
|
378
|
+
return bounded(`"${rootCanonical}" holds more than ${MAX_GLOB_DIRS} directories, too many to enumerate to verify what a recursive read would open`);
|
|
367
379
|
const listed = await env.listDir(dir, signal);
|
|
368
380
|
if (!listed.ok) {
|
|
369
381
|
if (dir === rootCanonical && listed.error.code === "not_directory")
|
|
@@ -377,7 +389,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
377
389
|
root = "directory";
|
|
378
390
|
for (const entry of listed.value) {
|
|
379
391
|
if (found.length >= MAX_GLOB_MATCHES)
|
|
380
|
-
return
|
|
392
|
+
return bounded(`"${rootCanonical}" holds more than ${MAX_GLOB_MATCHES} entries, too many to enumerate to verify what a recursive read would open`);
|
|
381
393
|
const path = joinUnder(spell, entry.name);
|
|
382
394
|
const canonicalPath = joinUnder(dir, entry.name);
|
|
383
395
|
if (entry.kind === "symlink") {
|
|
@@ -453,7 +465,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
453
465
|
const dir = name.slice(0, name.search(/[^/]*$/));
|
|
454
466
|
name = dir === "" ? link.value : anchorSpelling(family, dir, link.value);
|
|
455
467
|
}
|
|
456
|
-
return
|
|
468
|
+
return void bounded(`"${spelled}" is a link chain of more than ${WRITE_LINK_HOPS} links, too many to follow to verify where its backup is written`);
|
|
457
469
|
};
|
|
458
470
|
const followedBackupReads = async (j, boundary, signal) => {
|
|
459
471
|
const { operand, suffix } = j.followed;
|
|
@@ -601,19 +613,5 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
601
613
|
}
|
|
602
614
|
return { plan, reads, nested, roots };
|
|
603
615
|
};
|
|
604
|
-
return
|
|
605
|
-
lexicalOnly,
|
|
606
|
-
establish: async (plan, boundary, signal) => {
|
|
607
|
-
if (lexicalOnly)
|
|
608
|
-
return establishShellReadsLexically(plan, boundary);
|
|
609
|
-
const roots = await canonicalRoots(signal);
|
|
610
|
-
return await withHomeViews(env, opts, await establishPlan(plan, { ...boundary, roots }, roots, signal), signal);
|
|
611
|
-
},
|
|
612
|
-
reestablish: async (previous, boundary, signal) => {
|
|
613
|
-
if (lexicalOnly)
|
|
614
|
-
return { changed: [] };
|
|
615
|
-
const again = await establishPlan(previous.plan, { ...boundary, roots: previous.roots }, previous.roots, signal);
|
|
616
|
-
return { changed: movedReads(previous, again) };
|
|
617
|
-
},
|
|
618
|
-
};
|
|
616
|
+
return establishPlan;
|
|
619
617
|
}
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* matched, or onto a home shadow's target, left the pool unchanged and the call was admitted unjudged.
|
|
9
9
|
*/
|
|
10
10
|
import type { EstablishedPlan } from "./shell-read-establish.js";
|
|
11
|
+
import type { ObservedEnvironment } from "./shell-read-environment.js";
|
|
11
12
|
import type { EstablishedRead } from "./shell-read-judge.js";
|
|
12
|
-
import { type
|
|
13
|
+
import { type HomeViewsCarrier } from "./read-deny-home.js";
|
|
13
14
|
/** Every read of either establishment whose candidate's settled target set differs between the two. */
|
|
14
15
|
export declare function movedReads(previous: EstablishedPlan, again: EstablishedPlan): EstablishedRead[];
|
|
15
16
|
/**
|
|
@@ -18,4 +19,4 @@ export declare function movedReads(previous: EstablishedPlan, again: Established
|
|
|
18
19
|
* spelled through. The views are `placed`'s — the home views of the placement the boundary's deny seats were compiled
|
|
19
20
|
* at. A read that did not land (lexical, unestablishable) is left as it is.
|
|
20
21
|
*/
|
|
21
|
-
export declare function withHomeViews(
|
|
22
|
+
export declare function withHomeViews(door: Pick<ObservedEnvironment, "fs" | "adopt">, placed: HomeViewsCarrier | undefined, established: EstablishedPlan, signal?: AbortSignal): Promise<EstablishedPlan>;
|