@sema-agent/core 7.49.0 → 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 +85 -0
- package/dist/core/arg-summary.d.ts +4 -4
- package/dist/core/arg-summary.js +106 -42
- package/dist/core/credential-vocabulary.js +19 -2
- package/dist/core/engine-notice.d.ts +11 -4
- package/dist/core/gate-fold.js +2 -2
- package/dist/core/hooks.d.ts +17 -0
- 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/run-harness-handlers.js +2 -2
- package/dist/core/runner/tool-turn-repair-seat.d.ts +28 -4
- package/dist/core/runner/tool-turn-repair-seat.js +36 -13
- package/dist/core/runner-deps.d.ts +2 -2
- package/dist/core/shell-wrapper-projection.js +1 -1
- 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-program-position.d.ts +2 -1
- package/dist/tools/fs/bash-program-position.js +15 -4
- 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/program-read-model.d.ts +7 -2
- package/dist/tools/fs/program-read-model.js +21 -1
- package/dist/tools/fs/program-read-model.rows.js +11 -10
- 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 +43 -14
- 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 -15
- package/dist/tools/fs/shell-read-judge.js +30 -10
- 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
|
@@ -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>;
|
|
@@ -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(
|
|
42
|
-
const views = homeViewsOf(
|
|
41
|
+
export async function withHomeViews(door, placed, established, signal) {
|
|
42
|
+
const views = homeViewsOf(door.fs, placed, signal);
|
|
43
43
|
const place = async (p) => {
|
|
44
44
|
const reads = new Map();
|
|
45
45
|
for (const [segment, rs] of p.reads) {
|
|
@@ -56,5 +56,7 @@ export async function withHomeViews(env, placed, established, signal) {
|
|
|
56
56
|
nested.set(n, await place(np));
|
|
57
57
|
return { ...p, reads, nested };
|
|
58
58
|
};
|
|
59
|
-
|
|
59
|
+
const withViews = await place(established);
|
|
60
|
+
door.adopt(views.unread.map((p) => ({ op: "fileIdentity", detail: `"${p}" cannot be identified against the home directories` })));
|
|
61
|
+
return withViews;
|
|
60
62
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* judge is `withinAnyRoot` over the roots the boundary declares — a WRITE target's over the write roots, at the
|
|
12
12
|
* seat it is written, under the write face in force ({@link judgeWrite}).
|
|
13
13
|
* · `foldShellReadFindings` folds a command's findings — every segment, every nested plan — to ONE word by
|
|
14
|
-
* the priority `identity_changed > deny_hit > outside_roots > unestablishable > declined > none`.
|
|
14
|
+
* the priority `identity_changed > deny_hit > unanswered > outside_roots > unestablishable > declined > none`.
|
|
15
15
|
* · `SHELL_READ_DECISION` is the total table (finding × seat → disposition) that is the ONLY source of
|
|
16
16
|
* "refuse / mandated ask / ordinary ask / vouch" for a shell read. A seat is a FACE word, not a tool name
|
|
17
17
|
* and not a permission mode: which shell face the call runs on (the full shell, the read-only shell) or
|
|
@@ -22,11 +22,12 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import type { BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
|
|
24
24
|
import type { ReadCandidate, ShellReadPlan, ShellReadSegment } from "./shell-read-plan.js";
|
|
25
|
+
import type { Incomplete, ObservedFilesystem } from "./shell-read-environment.js";
|
|
25
26
|
import type { WriteSeat } from "./shell-read-write-targets.js";
|
|
26
27
|
import { type ContainmentFace } from "./containment-face.js";
|
|
27
28
|
import type { AssertAllKeysHandled } from "../../core/ask-origin.js";
|
|
28
29
|
/** WHAT the judge found about a command — a closed set, registered in `docs/CLOSED-SETS.md`. */
|
|
29
|
-
export declare const SHELL_READ_FINDINGS: readonly ["deny_hit", "outside_roots", "unestablishable", "declined", "identity_changed", "none"];
|
|
30
|
+
export declare const SHELL_READ_FINDINGS: readonly ["deny_hit", "outside_roots", "unestablishable", "declined", "identity_changed", "unanswered", "none"];
|
|
30
31
|
export type ShellReadFinding = (typeof SHELL_READ_FINDINGS)[number];
|
|
31
32
|
/** WHERE a read is judged — the seats the decision table is keyed on, one per FACE: `full_shell` the
|
|
32
33
|
* writable shell (`Bash`, `Monitor`) under every permission mode and every shell-gate floor; `readonly`
|
|
@@ -38,9 +39,10 @@ export type ShellReadSeat = (typeof SHELL_READ_SEATS)[number];
|
|
|
38
39
|
export declare const SHELL_READ_FINDING_RANK: {
|
|
39
40
|
readonly identity_changed: 6;
|
|
40
41
|
readonly deny_hit: 5;
|
|
41
|
-
readonly
|
|
42
|
-
readonly
|
|
43
|
-
readonly
|
|
42
|
+
readonly unanswered: 4;
|
|
43
|
+
readonly outside_roots: 3;
|
|
44
|
+
readonly unestablishable: 2;
|
|
45
|
+
readonly declined: 1;
|
|
44
46
|
readonly none: 0;
|
|
45
47
|
};
|
|
46
48
|
/** Compile-time fence: `never` while every finding has a rank. */
|
|
@@ -50,6 +52,14 @@ export type RankTableCoversEveryFinding = AssertAllKeysHandled<Exclude<ShellRead
|
|
|
50
52
|
* the auto-mode classifier, a stored allow rule) but no answerless engine table may; `vouch` = the station
|
|
51
53
|
* has no question. */
|
|
52
54
|
export type ShellReadDisposition = "refuse" | "mandated" | "ordinary" | "vouch";
|
|
55
|
+
/** One cell of the decision table: a disposition, or — where the READ FACE the question was raised under decides it — the two
|
|
56
|
+
* readings, `narrowed` (the `roots` face: the fence the deployment narrowed, and the engine default) and `open`. */
|
|
57
|
+
export type ShellReadCell = ShellReadDisposition | {
|
|
58
|
+
readonly narrowed: ShellReadDisposition;
|
|
59
|
+
readonly open: ShellReadDisposition;
|
|
60
|
+
};
|
|
61
|
+
/** A cell, read for a question raised under a narrowed read face (`true`) or an open one (`false`). */
|
|
62
|
+
export declare function dispositionOfCell(cell: ShellReadCell, narrowedReadFace: boolean): ShellReadDisposition;
|
|
53
63
|
/**
|
|
54
64
|
* THE decision table — total over findings × seats; no cell is n/a. Row by row:
|
|
55
65
|
* · `deny_hit` — refused on every seat, for a spelling the station can establish: a deny row is a floor
|
|
@@ -58,8 +68,14 @@ export type ShellReadDisposition = "refuse" | "mandated" | "ordinary" | "vouch";
|
|
|
58
68
|
* · `outside_roots` — asked where a person can be asked (the full shell), refused where none can (the
|
|
59
69
|
* read-only face has no approval channel; a rewrite that leaves the roots after approval was seen by
|
|
60
70
|
* nobody).
|
|
61
|
-
* · `unestablishable` — the
|
|
62
|
-
*
|
|
71
|
+
* · `unestablishable` — the floor could not say where the command reads, so it is not vouched anywhere: asked
|
|
72
|
+
* where a person can be asked (the full shell), refused where none can be asked. The full shell's cell is read by the read face
|
|
73
|
+
* the question was raised under: on a NARROWED (`roots`) face — the engine default — the deployment asked to be asked, so the
|
|
74
|
+
* ask is MANDATED and no automatic lane answers; on the OPEN face (where the station asks only because a deny list has
|
|
75
|
+
* rows), and for a written target alone (a narrowed WRITE face fences what is written; it is no request to be asked about
|
|
76
|
+
* every word the floor cannot place), it is the ORDINARY ask — the reference implementation's plain permission ask (CC
|
|
77
|
+
* 2.1.296 `sR`, cli296.js @13116941, reason `other`), which a whole-tool allow (`$pn` @18717070), bypass (`$pn` @18716980)
|
|
78
|
+
* and the auto-mode classifier release and a prefix allow does not.
|
|
63
79
|
* · `declined` — the one deliberate fail-open of this table: the scan could not read the program bash
|
|
64
80
|
* will run, or the operands arrive on a stream. On the full shell it is an ORDINARY ask — an answerer
|
|
65
81
|
* may settle it, and an out-of-root read hidden inside such a command is then not asked again. The
|
|
@@ -67,6 +83,15 @@ export type ShellReadDisposition = "refuse" | "mandated" | "ordinary" | "vouch";
|
|
|
67
83
|
* n/a.
|
|
68
84
|
* · `identity_changed` — a candidate established at approval time resolves elsewhere at spawn time:
|
|
69
85
|
* refused everywhere (it is only ever produced on the `rejudge` seat).
|
|
86
|
+
* · `unanswered` — the judge did not FINISH: it threw or timed out, the lookup it stands on was absent, or — recorded at the
|
|
87
|
+
* one door to the environment ({@link import("./shell-read-environment.js").Incomplete}) — an environment lookup gave no
|
|
88
|
+
* answer (an error that is no conclusive "not there"), the station stopped at its own bound (more directories or names than
|
|
89
|
+
* it enumerates, a link chain past its hop bound), or a home view could not be read. Any one makes the whole command
|
|
90
|
+
* `unanswered`, whatever the other words found. Not a fact about the command but the state of the station, so it is not
|
|
91
|
+
* the `unestablishable` row's question (a text the station cannot place — a word the shell rewrites, a lost working
|
|
92
|
+
* directory, a pattern its matcher does not model). Absence is not
|
|
93
|
+
* "nothing there": the full shell asks it MANDATED on every read face — a standing allow was given on the premise that the
|
|
94
|
+
* judge works, and a broken judge is not cleared by it — and the read-only face refuses.
|
|
70
95
|
* · `none` — vouched: the station has no question (the full shell's shape tier may still ask).
|
|
71
96
|
*/
|
|
72
97
|
export declare const SHELL_READ_DECISION: {
|
|
@@ -81,7 +106,10 @@ export declare const SHELL_READ_DECISION: {
|
|
|
81
106
|
readonly rejudge: "refuse";
|
|
82
107
|
};
|
|
83
108
|
readonly unestablishable: {
|
|
84
|
-
readonly full_shell:
|
|
109
|
+
readonly full_shell: {
|
|
110
|
+
readonly narrowed: "mandated";
|
|
111
|
+
readonly open: "ordinary";
|
|
112
|
+
};
|
|
85
113
|
readonly readonly: "refuse";
|
|
86
114
|
readonly rejudge: "refuse";
|
|
87
115
|
};
|
|
@@ -95,6 +123,11 @@ export declare const SHELL_READ_DECISION: {
|
|
|
95
123
|
readonly readonly: "refuse";
|
|
96
124
|
readonly rejudge: "refuse";
|
|
97
125
|
};
|
|
126
|
+
readonly unanswered: {
|
|
127
|
+
readonly full_shell: "mandated";
|
|
128
|
+
readonly readonly: "refuse";
|
|
129
|
+
readonly rejudge: "refuse";
|
|
130
|
+
};
|
|
98
131
|
readonly none: {
|
|
99
132
|
readonly full_shell: "vouch";
|
|
100
133
|
readonly readonly: "vouch";
|
|
@@ -109,7 +142,15 @@ export type DecisionTableCoversEverySeat = AssertAllKeysHandled<Exclude<ShellRea
|
|
|
109
142
|
/** The disposition of one command-level finding on one seat — the table, read. A seat word outside
|
|
110
143
|
* {@link SHELL_READ_SEATS} (an untyped caller, or one still passing a retired word) is refused loudly: an
|
|
111
144
|
* absent disposition would read as "no verdict", which no caller can act on safely. */
|
|
112
|
-
export declare function decideShellRead(finding: ShellReadFinding, seat: ShellReadSeat): ShellReadDisposition;
|
|
145
|
+
export declare function decideShellRead(finding: ShellReadFinding, seat: ShellReadSeat, context?: ShellReadCellContext): ShellReadDisposition;
|
|
146
|
+
/** What the table is read for beyond (finding, seat): the two facts of a judged command a cell can depend on. */
|
|
147
|
+
export interface ShellReadCellContext {
|
|
148
|
+
/** Every containment finding is an output redirection outside the write roots ({@link SHELL_REDIRECT_DECISION}). Absent ⇒ no. */
|
|
149
|
+
readonly redirectsOnly?: boolean;
|
|
150
|
+
/** The question was raised under a NARROWED (`roots`) read face — a read candidate or the text's own structure while the read
|
|
151
|
+
* face runs the roots containment; `false` for the open face and for a written target alone. Absent ⇒ narrowed, the engine default. */
|
|
152
|
+
readonly narrowedReadFace?: boolean;
|
|
153
|
+
}
|
|
113
154
|
/**
|
|
114
155
|
* The boundary a shell read is judged against: the declared roots (canonical), the working directory and
|
|
115
156
|
* home the plan's bases start from, the deny seat (the SAME `denyMatch` closure every read face mounts),
|
|
@@ -140,8 +181,11 @@ export interface ShellReadBoundary extends BashReadonlyRootBoundary {
|
|
|
140
181
|
* the path is a key the station established (judged as a key, never as a spelling). `"protected"`: the table covers it —
|
|
141
182
|
* the target is not placed and the verdict says so ({@link import("./shell-read-check.js").ShellReadVerdict.writeProtected});
|
|
142
183
|
* `"guarded"`: a deployment row covers it — the target is not placed; `undefined`: nothing covers it. Read by the
|
|
143
|
-
* station's write facts only. A judgment that throws fails the station's answer (the caller's fail-closed arm).
|
|
144
|
-
|
|
184
|
+
* station's write facts only. A judgment that throws fails the station's answer (the caller's fail-closed arm). Every lookup
|
|
185
|
+
* it makes goes through `fs` — the command's door — and through no environment of its own: the station's one decision about
|
|
186
|
+
* what a silence means reads the answers the judgment got as well as the ones it did not. */
|
|
187
|
+
readonly writeTargetJudge?: (target: string, opts: {
|
|
188
|
+
readonly fs: ObservedFilesystem;
|
|
145
189
|
readonly signal?: AbortSignal;
|
|
146
190
|
readonly canonical?: true;
|
|
147
191
|
}) => Promise<"protected" | "guarded" | undefined>;
|
|
@@ -174,7 +218,8 @@ export interface EstablishedRead {
|
|
|
174
218
|
/** The candidate's spelling WAS placed and the environment could not resolve where it leads (the whole chain unanswered by
|
|
175
219
|
* a resolver that declares itself authoritative, or a component it could not follow) — the reading's
|
|
176
220
|
* `target_unresolved` fact, as opposed to a text the station cannot read (a rewritten word, an unsettled working
|
|
177
|
-
* directory). Present only on an `unestablishable` read.
|
|
221
|
+
* directory). Present only on an `unestablishable` read. (That the environment gave no answer is the establishment's
|
|
222
|
+
* `incomplete`, which makes the command `unanswered` whether or not a candidate carries this mark.) */
|
|
178
223
|
readonly unresolved?: true;
|
|
179
224
|
/** WHERE the command's program writes this target — present only on a write target the program itself writes, at the
|
|
180
225
|
* seat this read was established at (`landing`: through the path's links; `entry`: the directory entry, a link there
|
|
@@ -186,7 +231,8 @@ export interface EstablishedRead {
|
|
|
186
231
|
};
|
|
187
232
|
}
|
|
188
233
|
/** What the judge found about ONE candidate. `kind` is one of the four candidate-level findings
|
|
189
|
-
* (`declined` is a segment fact, `identity_changed` a re-establishment fact
|
|
234
|
+
* (`declined` is a segment fact, `identity_changed` a re-establishment fact, `unanswered` a fact of the whole establishment —
|
|
235
|
+
* none is a candidate's). */
|
|
190
236
|
export interface ReadFinding {
|
|
191
237
|
readonly read: EstablishedRead;
|
|
192
238
|
readonly kind: Extract<ShellReadFinding, "deny_hit" | "outside_roots" | "unestablishable" | "none">;
|
|
@@ -252,6 +298,8 @@ export interface JudgedPlan {
|
|
|
252
298
|
readonly guards: boolean;
|
|
253
299
|
/** {@link writeFaceGuards} of the same boundary — what a WRITE target's finding folds by. */
|
|
254
300
|
readonly writeGuards: boolean;
|
|
301
|
+
/** What the command could not finish (the station's one door, decided after the write guards asked); on the top plan only. */
|
|
302
|
+
readonly incomplete?: readonly Incomplete[];
|
|
255
303
|
}
|
|
256
304
|
/**
|
|
257
305
|
* Does the station GUARD anything for this boundary — a closed face (roots: containment is what it guards; an absent
|
|
@@ -278,12 +326,18 @@ export interface ShellReadFold {
|
|
|
278
326
|
readonly finding: ShellReadFinding;
|
|
279
327
|
readonly witness?: ReadFinding;
|
|
280
328
|
readonly declined?: ShellReadSegment;
|
|
281
|
-
/**
|
|
329
|
+
/** What the judge did not finish, for an `unanswered` finding: the first thing the establishment recorded ({@link Incomplete.detail}). */
|
|
330
|
+
readonly cause?: string;
|
|
331
|
+
/** Some read of the text could not be established (an `unestablishable`, `unanswered` or `declined` finding anywhere, winning or not). */
|
|
282
332
|
readonly unestablished?: true;
|
|
333
|
+
/** Some `unestablishable` finding anywhere is about the READ face — a read candidate, or the text's own structure (a rewritten
|
|
334
|
+
* word, a re-entered shell text) — rather than a written target alone. The cell of the row reads it with the read face in force
|
|
335
|
+
* ({@link decideShellRead}). */
|
|
336
|
+
readonly unplacedRead?: true;
|
|
283
337
|
}
|
|
284
338
|
/**
|
|
285
339
|
* Fold a judged plan to ONE finding by {@link SHELL_READ_FINDING_RANK}, over every segment and every nested
|
|
286
|
-
* plan. Where the station guards nothing an `unestablishable` or `declined` finding is no finding: there is nothing to
|
|
340
|
+
* plan. Where the station guards nothing an `unestablishable`, `unanswered` or `declined` finding is no finding: there is nothing to
|
|
287
341
|
* confirm it against. Which guard is asked is the face the finding is about — a read candidate's by the read face's
|
|
288
342
|
* ({@link JudgedPlan.guards}), a write target's by the write face's ({@link JudgedPlan.writeGuards}), and a finding about
|
|
289
343
|
* the text's own structure (no candidate stands in for it) by either ({@link anyFaceGuards}). Two structural rules ride the fold:
|
|
@@ -2,32 +2,43 @@ import { hasWin32AliasedComponent, normalizeAbsPathLexically, withinAnyRoot } fr
|
|
|
2
2
|
import { globInWord } from "./bash-readonly-classifier.js";
|
|
3
3
|
import { readOfText } from "./shell-read-word.js";
|
|
4
4
|
import { skipsRootsContainment } from "./containment-face.js";
|
|
5
|
-
export const SHELL_READ_FINDINGS = Object.freeze(["deny_hit", "outside_roots", "unestablishable", "declined", "identity_changed", "none"]);
|
|
5
|
+
export const SHELL_READ_FINDINGS = Object.freeze(["deny_hit", "outside_roots", "unestablishable", "declined", "identity_changed", "unanswered", "none"]);
|
|
6
6
|
export const SHELL_READ_SEATS = Object.freeze(["full_shell", "readonly", "rejudge"]);
|
|
7
7
|
export const SHELL_READ_FINDING_RANK = {
|
|
8
8
|
identity_changed: 6,
|
|
9
9
|
deny_hit: 5,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
unanswered: 4,
|
|
11
|
+
outside_roots: 3,
|
|
12
|
+
unestablishable: 2,
|
|
13
|
+
declined: 1,
|
|
13
14
|
none: 0,
|
|
14
15
|
};
|
|
16
|
+
export function dispositionOfCell(cell, narrowedReadFace) {
|
|
17
|
+
return typeof cell === "string" ? cell : narrowedReadFace ? cell.narrowed : cell.open;
|
|
18
|
+
}
|
|
15
19
|
export const SHELL_READ_DECISION = {
|
|
16
20
|
deny_hit: { full_shell: "refuse", readonly: "refuse", rejudge: "refuse" },
|
|
17
21
|
outside_roots: { full_shell: "mandated", readonly: "refuse", rejudge: "refuse" },
|
|
18
|
-
unestablishable: { full_shell: "mandated", readonly: "refuse", rejudge: "refuse" },
|
|
22
|
+
unestablishable: { full_shell: { narrowed: "mandated", open: "ordinary" }, readonly: "refuse", rejudge: "refuse" },
|
|
19
23
|
declined: { full_shell: "ordinary", readonly: "refuse", rejudge: "refuse" },
|
|
20
24
|
identity_changed: { full_shell: "refuse", readonly: "refuse", rejudge: "refuse" },
|
|
25
|
+
unanswered: { full_shell: "mandated", readonly: "refuse", rejudge: "refuse" },
|
|
21
26
|
none: { full_shell: "vouch", readonly: "vouch", rejudge: "vouch" },
|
|
22
27
|
};
|
|
23
|
-
export function decideShellRead(finding, seat) {
|
|
28
|
+
export function decideShellRead(finding, seat, context = {}) {
|
|
29
|
+
const redirectCell = context.redirectsOnly === true && Object.hasOwn(SHELL_REDIRECT_DECISION, finding) ? SHELL_REDIRECT_DECISION[finding]?.[seat] : undefined;
|
|
30
|
+
if (redirectCell !== undefined)
|
|
31
|
+
return redirectCell;
|
|
24
32
|
const row = Object.hasOwn(SHELL_READ_DECISION, finding) ? SHELL_READ_DECISION[finding] : undefined;
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
33
|
+
const cell = row !== undefined && Object.hasOwn(row, seat) ? row[seat] : undefined;
|
|
34
|
+
if (cell === undefined) {
|
|
27
35
|
throw new Error(`shell read decision: no cell for finding ${JSON.stringify(finding)} on seat ${JSON.stringify(seat)} — the seats are ${SHELL_READ_SEATS.map((w) => JSON.stringify(w)).join(", ")} (a seat names the face the command runs on)`);
|
|
28
36
|
}
|
|
29
|
-
return
|
|
37
|
+
return dispositionOfCell(cell, context.narrowedReadFace !== false);
|
|
30
38
|
}
|
|
39
|
+
const SHELL_REDIRECT_DECISION = {
|
|
40
|
+
outside_roots: { full_shell: "ordinary" },
|
|
41
|
+
};
|
|
31
42
|
export function judgeRead(read, boundary) {
|
|
32
43
|
const writeTarget = read.candidate.face === "write";
|
|
33
44
|
const deny = writeTarget ? boundary.writeDenyMatch : boundary.denyMatch;
|
|
@@ -103,12 +114,15 @@ export function anyFaceGuards(boundary) {
|
|
|
103
114
|
export function foldShellReadFindings(judged) {
|
|
104
115
|
let best = { finding: "none" };
|
|
105
116
|
let unestablished = false;
|
|
117
|
+
let unplacedRead = false;
|
|
106
118
|
const raise = (next) => {
|
|
107
119
|
if (next.finding === "unestablishable" || next.finding === "declined") {
|
|
108
120
|
const face = next.witness?.read.candidate.face;
|
|
109
121
|
if (!(next.witness === undefined ? judged.guards || judged.writeGuards : face === "write" ? judged.writeGuards : judged.guards))
|
|
110
122
|
return;
|
|
111
123
|
unestablished = true;
|
|
124
|
+
if (next.finding === "unestablishable" && face !== "write")
|
|
125
|
+
unplacedRead = true;
|
|
112
126
|
}
|
|
113
127
|
const rank = SHELL_READ_FINDING_RANK[next.finding] - SHELL_READ_FINDING_RANK[best.finding];
|
|
114
128
|
if (rank > 0 || (rank === 0 && next.witness?.read.candidate.face === "write" && best.witness?.read.candidate.face !== "write"))
|
|
@@ -141,7 +155,12 @@ export function foldShellReadFindings(judged) {
|
|
|
141
155
|
}
|
|
142
156
|
};
|
|
143
157
|
walk(judged, false);
|
|
144
|
-
|
|
158
|
+
const unfinished = judged.incomplete?.[0];
|
|
159
|
+
if (unfinished !== undefined && (judged.guards || judged.writeGuards) && SHELL_READ_FINDING_RANK.unanswered > SHELL_READ_FINDING_RANK[best.finding]) {
|
|
160
|
+
unestablished = true;
|
|
161
|
+
best = { finding: "unanswered", cause: unfinished.detail, ...(best.finding === "unestablishable" ? { ...(best.witness !== undefined ? { witness: best.witness } : {}), ...(best.declined !== undefined ? { declined: best.declined } : {}) } : {}) };
|
|
162
|
+
}
|
|
163
|
+
return { ...best, ...(unestablished ? { unestablished: true } : {}), ...(unplacedRead ? { unplacedRead: true } : {}) };
|
|
145
164
|
}
|
|
146
165
|
export function judgeEstablishedPlan(plan, established, boundary, nestedOf) {
|
|
147
166
|
return {
|
|
@@ -237,5 +256,6 @@ export function rejudge(judged, boundary) {
|
|
|
237
256
|
})),
|
|
238
257
|
guards: stationGuards(boundary),
|
|
239
258
|
writeGuards: writeFaceGuards(boundary),
|
|
259
|
+
...(judged.incomplete !== undefined ? { incomplete: judged.incomplete } : {}),
|
|
240
260
|
};
|
|
241
261
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repo": "core",
|
|
3
|
+
"version": "7.49.1",
|
|
4
|
+
"from": "a58f6fef9c81bf031cc3dc1a12a8c5bc72d35193",
|
|
5
|
+
"to": "57363d52a115a659d902a81e8aad865ed98e2419",
|
|
6
|
+
"added": [],
|
|
7
|
+
"removed": [],
|
|
8
|
+
"changed": [],
|
|
9
|
+
"narrowed": [],
|
|
10
|
+
"closedSetMembers": [
|
|
11
|
+
{
|
|
12
|
+
"set": "OPTION_MODES",
|
|
13
|
+
"at": [
|
|
14
|
+
"src/tools/fs/program-read-model.ts:38"
|
|
15
|
+
],
|
|
16
|
+
"newSet": false,
|
|
17
|
+
"added": [
|
|
18
|
+
"queries"
|
|
19
|
+
],
|
|
20
|
+
"removed": [],
|
|
21
|
+
"breaking": false,
|
|
22
|
+
"criteriaCovered": false
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"set": "ToolTurnOutcome",
|
|
26
|
+
"at": [
|
|
27
|
+
"src/core/runner/tool-turn-repair-seat.ts:50"
|
|
28
|
+
],
|
|
29
|
+
"newSet": true,
|
|
30
|
+
"added": [
|
|
31
|
+
"reissued",
|
|
32
|
+
"terminal"
|
|
33
|
+
],
|
|
34
|
+
"removed": [],
|
|
35
|
+
"breaking": false,
|
|
36
|
+
"criteriaCovered": false
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"closedSetsUnreadable": [],
|
|
40
|
+
"typeKeys": [],
|
|
41
|
+
"typeKeysUnreadableArms": [],
|
|
42
|
+
"typeKeysFlattenedCount": 223,
|
|
43
|
+
"signatures": [],
|
|
44
|
+
"declarationMoves": [],
|
|
45
|
+
"mintMoves": [],
|
|
46
|
+
"criteriaObserves": [],
|
|
47
|
+
"consumersHint": {
|
|
48
|
+
"cli": [
|
|
49
|
+
"OPTION_MODES"
|
|
50
|
+
],
|
|
51
|
+
"server": [],
|
|
52
|
+
"sdk": [],
|
|
53
|
+
"note": "scanned cli@82bb8c2d server@a51468d9 sdk@006ce24 — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
|
|
54
|
+
},
|
|
55
|
+
"breakingCount": 0,
|
|
56
|
+
"breakingItems": []
|
|
57
|
+
}
|