@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
|
@@ -31,6 +31,7 @@ const OBJECT_PATH = { kind: "object-path" };
|
|
|
31
31
|
const text = (language) => ({ kind: "program-text", language });
|
|
32
32
|
const file = (language) => ({ kind: "program-file", language });
|
|
33
33
|
const SWITCH = { arity: 0 };
|
|
34
|
+
const QUERY = { arity: 0, mode: "queries" };
|
|
34
35
|
const val = (value, extra = {}) => ({ arity: 1, ...(value !== undefined ? { value } : {}), ...extra });
|
|
35
36
|
const optional = (value) => ({ arity: "attached", ...(value !== undefined ? { value } : {}) });
|
|
36
37
|
const switches = (letters, extra = {}) => Object.fromEntries([...letters].map((l) => [l, { arity: 0, ...extra }]));
|
|
@@ -380,10 +381,10 @@ const launchers = [
|
|
|
380
381
|
values: { "-u": LABEL, "--unset": LABEL, "-C": WORKDIR, "--chdir": WORKDIR, "-a": LABEL, "--argv0": LABEL, "-P": PROGRAM_SEARCH },
|
|
381
382
|
optional: { "--default-signal": LABEL, "--ignore-signal": LABEL },
|
|
382
383
|
shellText: ["-S", "--split-string"],
|
|
383
|
-
}), { lacks: ["-P"] }, { lacks: ["-a", "--ignore-environment", "--debug", "--list-signal-handling", "--help", "--version", "--unset", "--chdir", "--argv0", "--default-signal", "--ignore-signal", "--split-string"] }), launcher: { assignmentOperands: true, bareDash: true }, witness: BINARY },
|
|
384
|
-
launcher("command", {}, grammar({ p: SWITCH })),
|
|
385
|
-
launcher("builtin"),
|
|
386
|
-
launcher("exec", {}, launcherGrammar({ switches: ["-c", "-l"], values: { "-a": LABEL } })),
|
|
384
|
+
}, { stepsDashDash: true }), { lacks: ["-P"] }, { lacks: ["-a", "--ignore-environment", "--debug", "--list-signal-handling", "--help", "--version", "--unset", "--chdir", "--argv0", "--default-signal", "--ignore-signal", "--split-string"] }), launcher: { assignmentOperands: true, bareDash: true }, witness: BINARY },
|
|
385
|
+
launcher("command", {}, grammar({ p: SWITCH, v: QUERY, V: QUERY }, {}, { stepsDashDash: true })),
|
|
386
|
+
launcher("builtin", {}, grammar({}, {}, { stepsDashDash: true })),
|
|
387
|
+
launcher("exec", {}, launcherGrammar({ switches: ["-c", "-l"], values: { "-a": LABEL } }, { stepsDashDash: true })),
|
|
387
388
|
launcher("noglob", {}, undefined, { witness: { declared: "a zsh precommand modifier" } }),
|
|
388
389
|
launcher("nocorrect", {}, undefined, { witness: { declared: "a zsh precommand modifier" } }),
|
|
389
390
|
{ key: "xargs", grammars: implementations(launcherGrammar({
|
|
@@ -406,8 +407,8 @@ const launchers = [
|
|
|
406
407
|
{ key: "runuser", grammars: [launcherGrammar({ ...SU_OPTIONS, values: { ...SU_OPTIONS.values, "-u": LABEL, "--user": LABEL } }, { afterFirstOperand: "options", stepsDashDash: true })], launcher: { shellText: "user", programWith: ["-u", "--user"], asUser: { home: "target", loginWith: ["-l", "--login"] } }, witness: PRIVILEGED },
|
|
407
408
|
launcher("chroot", { requiredOperands: 1, whenNoProgram: "shell" }, undefined, { witness: PRIVILEGED }),
|
|
408
409
|
launcher("setpriv", {}, undefined, { witness: PRIVILEGED }),
|
|
409
|
-
launcher("nohup", {}, grammar({}, BANNERS)),
|
|
410
|
-
{ key: "nice", grammars: implementations(grammar({ n: val(COUNT) }, { adjustment: val(COUNT), ...BANNERS }), {}, { lacks: ["--adjustment", "--help", "--version"] }), launcher: { negativeIntegerIsOwnArgument: true }, witness: BINARY },
|
|
410
|
+
launcher("nohup", {}, grammar({}, BANNERS, { stepsDashDash: true })),
|
|
411
|
+
{ key: "nice", grammars: implementations(grammar({ n: val(COUNT) }, { adjustment: val(COUNT), ...BANNERS }, { stepsDashDash: true }), {}, { lacks: ["--adjustment", "--help", "--version"] }), launcher: { negativeIntegerIsOwnArgument: true }, witness: BINARY },
|
|
411
412
|
launcher("ionice", {}, launcherGrammar({
|
|
412
413
|
switches: ["-t", "--ignore", "-h", "--help", "-V", "--version"],
|
|
413
414
|
banners: ["-h", "-V"],
|
|
@@ -418,8 +419,8 @@ const launchers = [
|
|
|
418
419
|
values: { "-p": COUNT, "--pid": COUNT, "-T": COUNT, "-P": COUNT, "-D": COUNT, "--sched-runtime": COUNT, "--sched-period": COUNT, "--sched-deadline": COUNT },
|
|
419
420
|
}), { witness: LINUX_NAMESPACE }),
|
|
420
421
|
launcher("taskset", { requiredOperands: 1, requiredOperandSpelling: /^(?:0x)?[\da-f]+$|^[\d,:-]+$/i }, launcherGrammar({ switches: ["-a", "-p", "-c", "--all-tasks", "--pid", "--cpu-list", "--help", "--version"] }), { witness: LINUX_NAMESPACE }),
|
|
421
|
-
{ key: "stdbuf", grammars: implementations(grammar({ i: val(LABEL), o: val(LABEL), e: val(LABEL) }, { input: val(LABEL), output: val(LABEL), error: val(LABEL), ...BANNERS }), {}, { lacks: ["--input", "--output", "--error", "--help", "--version"] }), launcher: {}, witness: BINARY },
|
|
422
|
-
launcher("timeout", { requiredOperands: 1, requiredOperandSpelling: DURATION_SPELLING }, grammar({ v: SWITCH, k: val(COUNT), s: val(LABEL) }, { foreground: SWITCH, "preserve-status": SWITCH, verbose: SWITCH, "kill-after": val(COUNT), signal: val(LABEL), ...BANNERS })),
|
|
422
|
+
{ key: "stdbuf", grammars: implementations(grammar({ i: val(LABEL), o: val(LABEL), e: val(LABEL) }, { input: val(LABEL), output: val(LABEL), error: val(LABEL), ...BANNERS }, { stepsDashDash: true }), {}, { lacks: ["--input", "--output", "--error", "--help", "--version"] }), launcher: {}, witness: BINARY },
|
|
423
|
+
launcher("timeout", { requiredOperands: 1, requiredOperandSpelling: DURATION_SPELLING }, grammar({ v: SWITCH, k: val(COUNT), s: val(LABEL) }, { foreground: SWITCH, "preserve-status": SWITCH, verbose: SWITCH, "kill-after": val(COUNT), signal: val(LABEL), ...BANNERS }, { stepsDashDash: true })),
|
|
423
424
|
{ key: "time", grammars: implementations(grammar({ p: SWITCH, a: SWITCH, v: SWITCH, q: SWITCH, f: val(LABEL), o: val(WRITE) }, { portability: SWITCH, append: SWITCH, verbose: SWITCH, quiet: SWITCH, help: SWITCH, version: SWITCH, format: val(LABEL), output: val(WRITE) }, { stepsDashDash: true }), {}, { lacks: ["-v", "-q", "-f", "--portability", "--append", "--verbose", "--quiet", "--help", "--version", "--format", "--output"] }), launcher: {}, witness: BINARY },
|
|
424
425
|
{ key: "watch", grammars: [
|
|
425
426
|
launcherGrammar({
|
|
@@ -430,12 +431,12 @@ const launchers = [
|
|
|
430
431
|
}, { afterFirstOperand: "operands", stepsDashDash: true }),
|
|
431
432
|
launcherGrammar({ switches: ["-d", "-t"], values: { "-n": COUNT } }, { afterFirstOperand: "operands", stepsDashDash: true }),
|
|
432
433
|
], launcher: { shellText: "joined", programWith: ["-x", "--exec"] }, witness: SHELL_TEXT_LAUNCHER },
|
|
433
|
-
launcher("setsid", {}, launcherGrammar({ switches: ["-c", "-f", "-w", "--ctty", "--fork", "--wait", "--help", "--version"] })),
|
|
434
|
+
launcher("setsid", {}, launcherGrammar({ switches: ["-c", "-f", "-w", "--ctty", "--fork", "--wait", "--help", "--version"] }, { stepsDashDash: true })),
|
|
434
435
|
launcher("flock", { requiredOperands: 1 }, launcherGrammar({
|
|
435
436
|
switches: ["-s", "-x", "-u", "-n", "-o", "-F", "--shared", "--exclusive", "--unlock", "--nonblock", "--nb", "--close", "--no-fork", "--verbose", "--help", "--version"],
|
|
436
437
|
values: { "-w": COUNT, "-E": COUNT, "--timeout": COUNT, "--wait": COUNT, "--conflict-exit-code": COUNT },
|
|
437
438
|
shellText: ["-c", "--command"],
|
|
438
|
-
}), { operands: { rest: CREATED } }),
|
|
439
|
+
}, { stepsDashDash: true }), { operands: { rest: CREATED } }),
|
|
439
440
|
launcher("unshare", { whenNoProgram: "shell" }, launcherGrammar({
|
|
440
441
|
switches: ["-m", "-u", "-i", "-n", "-p", "-U", "-C", "-T", "-f", "-r", "-c", "--mount", "--uts", "--ipc", "--net", "--pid", "--user", "--cgroup", "--time", "--fork", "--map-root-user", "--map-current-user", "--kill-child", "--mount-proc", "--keep-caps", "--help", "--version"],
|
|
441
442
|
values: { "-R": ROOTDIR, "-w": WORKDIR, "-S": LABEL, "-G": LABEL, "--setuid": LABEL, "--setgid": LABEL, "--root": ROOTDIR, "--wd": WORKDIR, "--propagation": LABEL, "--setgroups": LABEL, "--monotonic": COUNT, "--boottime": COUNT, "--map-user": LABEL, "--map-group": LABEL },
|
|
@@ -93,7 +93,7 @@ export type HomeViewEnv = Pick<ExecutionEnv, "homeDir" | "canonicalPath" | "file
|
|
|
93
93
|
export interface HomeViewSource {
|
|
94
94
|
/** The home views of one judgment (a tool call, a command's establishment, one search): a view per directory is read
|
|
95
95
|
* once and shared by every path below it. */
|
|
96
|
-
views(signal?: AbortSignal): HomeViews;
|
|
96
|
+
views(signal?: AbortSignal, env?: HomeViewEnv): HomeViews;
|
|
97
97
|
}
|
|
98
98
|
/** A judgment's views. `of` answers the view only where it differs from the path as spelled; `undefined` otherwise, and
|
|
99
99
|
* wherever it cannot be read: no `fileIdentity` seat, no home, a level of the path whose identity fails (the path is
|
|
@@ -103,6 +103,10 @@ export interface HomeViews {
|
|
|
103
103
|
of(path: string, opts?: {
|
|
104
104
|
readonly leaf?: "file";
|
|
105
105
|
}): Promise<string | undefined>;
|
|
106
|
+
/** The paths whose identity lookup gave no answer in this judgment (a throw, a timeout, an error that is no conclusive "not
|
|
107
|
+
* there"): for them `of` answered `undefined` for want of a reading, not because there is no view — a consumer that
|
|
108
|
+
* must not vouch past an unread view (the shell station) reads this after its lookups. Live: grows as `of` is asked. */
|
|
109
|
+
readonly unread: readonly string[];
|
|
106
110
|
}
|
|
107
111
|
/** A judge that carries the home views of the placement its rows were compiled at. */
|
|
108
112
|
export interface HomeViewsCarrier {
|
|
@@ -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";
|
|
@@ -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)
|
|
@@ -241,6 +245,20 @@ function outsidePathsOf(judged) {
|
|
|
241
245
|
}
|
|
242
246
|
return out;
|
|
243
247
|
}
|
|
248
|
+
function outsideFindingsAreRedirections(judged) {
|
|
249
|
+
const walk = (plan) => plan.segments.every((s) => {
|
|
250
|
+
const redirected = new Set(s.segment.redirects.flatMap((r) => (r.direction === "out" && r.target !== undefined ? [r.target.raw] : [])));
|
|
251
|
+
const slotWrites = new Set(s.segment.reads.filter((c) => c.provenance === "write_redirect_target").map((c) => c.raw));
|
|
252
|
+
for (const f of s.findings) {
|
|
253
|
+
if (f.kind !== "outside_roots")
|
|
254
|
+
continue;
|
|
255
|
+
if (f.read.written === undefined || f.read.candidate.provenance !== "write_redirect_target" || !redirected.has(f.read.candidate.raw) || slotWrites.has(f.read.candidate.raw))
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return s.nested.every(walk);
|
|
259
|
+
});
|
|
260
|
+
return walk(judged);
|
|
261
|
+
}
|
|
244
262
|
function refusalOf(tool, fold, boundary, paths) {
|
|
245
263
|
const w = fold.witness;
|
|
246
264
|
if (fold.finding === "deny_hit" && w?.deny !== undefined) {
|
|
@@ -261,7 +279,8 @@ const DENY_REFUSAL_NOTE = "The read boundary judges the paths this command names
|
|
|
261
279
|
"a file the command reaches without naming it is judged only where a segment deny row (built in, or the deployment's " +
|
|
262
280
|
"own pattern) reaches into the tree a recursive read walks.";
|
|
263
281
|
function verdictOf(tool, seat, fold, judged, boundary) {
|
|
264
|
-
const
|
|
282
|
+
const redirectsOnly = fold.finding === "outside_roots" && fold.unestablished !== true && outsideFindingsAreRedirections(judged);
|
|
283
|
+
const disposition = decideShellRead(fold.finding, seat, { redirectsOnly, narrowedReadFace: fold.unplacedRead === true && boundary.face !== "open" });
|
|
265
284
|
if (disposition === "vouch")
|
|
266
285
|
return { disposition, finding: fold.finding };
|
|
267
286
|
const paths = outsidePathsOf(judged);
|
|
@@ -272,14 +291,14 @@ function verdictOf(tool, seat, fold, judged, boundary) {
|
|
|
272
291
|
return { disposition, finding: fold.finding, ...writes, sentence: r.sentence, ...(r.details !== undefined ? { details: r.details } : {}) };
|
|
273
292
|
}
|
|
274
293
|
const grant = fold.finding === "outside_roots" ? readRootGrantCandidateOf(judged, boundary) : undefined;
|
|
275
|
-
const containment = fold.finding === "outside_roots" || fold.finding === "unestablishable";
|
|
294
|
+
const containment = fold.finding === "outside_roots" || fold.finding === "unestablishable" || fold.finding === "unanswered";
|
|
276
295
|
return {
|
|
277
296
|
disposition,
|
|
278
297
|
finding: fold.finding,
|
|
279
298
|
...writes,
|
|
280
299
|
...unestablished,
|
|
281
300
|
sentence: findingAccount(fold, boundary),
|
|
282
|
-
...(disposition === "mandated" && containment ? { details: findingDetails(fold.finding, paths) } : {}),
|
|
301
|
+
...((disposition === "mandated" && containment) || (disposition === "ordinary" && fold.finding === "unestablishable") ? { details: findingDetails(fold.finding, paths) } : {}),
|
|
283
302
|
...(grant !== undefined ? { readRootCandidate: { dir: grant.dir, ...(grant.covers !== undefined ? { covers: grant.covers } : {}), clearsThisAsk: true } } : {}),
|
|
284
303
|
};
|
|
285
304
|
}
|
|
@@ -310,6 +329,7 @@ async function applyAlsoInside(judged, boundary) {
|
|
|
310
329
|
plan: j.plan,
|
|
311
330
|
guards: j.guards,
|
|
312
331
|
writeGuards: j.writeGuards,
|
|
332
|
+
...(j.incomplete !== undefined ? { incomplete: j.incomplete } : {}),
|
|
313
333
|
segments: j.segments.map((s) => ({
|
|
314
334
|
segment: s.segment,
|
|
315
335
|
findings: s.findings.map((f) => (outsideRead(f) && rescued.has(f.read.target) ? { ...f, kind: "none", containment: "inside" } : f)),
|
|
@@ -320,13 +340,23 @@ async function applyAlsoInside(judged, boundary) {
|
|
|
320
340
|
}
|
|
321
341
|
async function runShellReadStation(tool, command, declared, seat, readers, establisher, env, signal) {
|
|
322
342
|
const plan = planShellReads(command, declarationsOf(declared, readers, env.cdPath));
|
|
323
|
-
const
|
|
343
|
+
const door = observeEnvironment(env);
|
|
344
|
+
const established = await establisher.establish(plan, declared, signal, door);
|
|
324
345
|
const boundary = { ...declared, roots: established.roots };
|
|
325
|
-
const
|
|
326
|
-
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);
|
|
327
358
|
const unplaced = unplacedOptionsOf(judged);
|
|
328
|
-
const placed = writesPlacedOf(judged, boundary);
|
|
329
|
-
const guarded = judgedVerdict.disposition === "refuse" ? { unplaced: false, writeProtected: false } : await writeGuardsOf(judged, boundary, env, signal);
|
|
359
|
+
const placed = judgedVerdict.finding !== "unanswered" && writesPlacedOf(judged, boundary);
|
|
330
360
|
const unresolved = judgedVerdict.disposition !== "refuse" && runOrderOf(judged).runs.some((s) => s.findings.some((f) => f.read.written !== undefined && f.read.unresolved === true));
|
|
331
361
|
const facts = { ...judgedVerdict, ...(guarded.writeProtected ? { writeProtected: true } : {}), ...(unresolved ? { targetUnresolved: true } : {}) };
|
|
332
362
|
const verdict = placed && !guarded.unplaced ? { ...facts, writesPlaced: true } : unplaced !== undefined ? { ...facts, unplacedOptions: unplaced } : facts;
|
|
@@ -400,13 +430,12 @@ export function createShellReadStation(opts) {
|
|
|
400
430
|
const boundary = opts.boundaryOf(member, undefined);
|
|
401
431
|
if (boundary === undefined || !(anyFaceGuards(boundary) || boundary.writeTargetJudge !== undefined))
|
|
402
432
|
return { disposition: "vouch", finding: "none" };
|
|
403
|
-
const
|
|
404
|
-
const disposition = decideShellRead("unestablishable", seat);
|
|
433
|
+
const disposition = decideShellRead("unanswered", seat);
|
|
405
434
|
const account = `${cause}, so where this command reads could not be established ${NOT_AUTO_ALLOWED}`;
|
|
406
435
|
const details = unestablishedDetails();
|
|
407
436
|
if (disposition === "refuse")
|
|
408
|
-
return { disposition, finding: "
|
|
409
|
-
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 };
|
|
410
439
|
},
|
|
411
440
|
};
|
|
412
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;
|