@sema-agent/core 7.44.0 → 7.46.0
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 +418 -1
- package/dist/agents/extra-tool-names.d.ts +65 -0
- package/dist/agents/extra-tool-names.js +124 -0
- package/dist/agents/subagent.d.ts +17 -5
- package/dist/agents/subagent.js +65 -38
- package/dist/agents/tool-filter.d.ts +82 -5
- package/dist/agents/tool-filter.js +134 -4
- package/dist/core/agent-config-dir.d.ts +20 -0
- package/dist/core/agent-config-dir.js +49 -0
- package/dist/core/agent-definition.d.ts +2 -2
- package/dist/core/allow-entry.d.ts +30 -4
- package/dist/core/allow-entry.js +32 -5
- package/dist/core/ask-origin.d.ts +23 -4
- package/dist/core/ask-origin.js +11 -5
- package/dist/core/checkpoint-store.d.ts +41 -4
- package/dist/core/decision-receipt.js +1 -1
- package/dist/core/effective-path-target.d.ts +143 -10
- package/dist/core/effective-path-target.js +113 -9
- package/dist/core/engine-notice.d.ts +25 -3
- package/dist/core/engine-notice.js +12 -6
- package/dist/core/gate-fold.js +3 -0
- package/dist/core/gate-lanes.js +11 -3
- package/dist/core/gate-outcome.d.ts +5 -0
- package/dist/core/gate-outcome.js +2 -0
- package/dist/core/governance-codes.d.ts +3 -1
- package/dist/core/governance-codes.js +3 -0
- package/dist/core/hooks.d.ts +5 -0
- package/dist/core/memory-engine/content-origin.d.ts +0 -34
- package/dist/core/memory-engine/content-origin.js +9 -4
- package/dist/core/memory-engine/layout.d.ts +5 -1
- package/dist/core/memory-engine/layout.js +1 -0
- package/dist/core/path-rule-binding.d.ts +65 -0
- package/dist/core/path-rule-binding.js +99 -0
- package/dist/core/permission-rule-model.d.ts +47 -18
- package/dist/core/permission-rule-model.js +38 -42
- package/dist/core/permission-rule-provider.d.ts +7 -0
- package/dist/core/permission-rule-provider.js +10 -3
- package/dist/core/permission-rule-store.d.ts +8 -0
- package/dist/core/permission-rule-store.js +6 -2
- package/dist/core/permission-rule-sync.js +2 -2
- package/dist/core/permission-rules.d.ts +22 -36
- package/dist/core/permission-rules.js +50 -40
- package/dist/core/real-approval.d.ts +9 -6
- package/dist/core/real-approval.js +5 -5
- package/dist/core/runner/child-tree.d.ts +110 -0
- package/dist/core/runner/child-tree.js +107 -0
- package/dist/core/runner/construction-doors.d.ts +5 -0
- package/dist/core/runner/construction-doors.js +6 -0
- package/dist/core/runner/contracts.d.ts +54 -2
- package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
- package/dist/core/runner/permission-rule-lanes.js +18 -12
- package/dist/core/runner/persisted-path-rows.d.ts +31 -7
- package/dist/core/runner/persisted-path-rows.js +31 -12
- package/dist/core/runner/prepare-ask-lane.d.ts +6 -0
- package/dist/core/runner/prepare-ask-lane.js +4 -2
- package/dist/core/runner/prepare-config-doors.js +3 -1
- package/dist/core/runner/prepare-execution-env.d.ts +5 -2
- package/dist/core/runner/prepare-execution-env.js +11 -3
- package/dist/core/runner/prepare-gate-stations.d.ts +6 -0
- package/dist/core/runner/prepare-gate-stations.js +3 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
- package/dist/core/runner/prepare-hands-readface.js +6 -6
- package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
- package/dist/core/runner/prepare-inherited-gate.js +8 -2
- package/dist/core/runner/prepare-listings.d.ts +4 -4
- package/dist/core/runner/prepare-listings.js +2 -22
- package/dist/core/runner/prepare-park-ask.d.ts +5 -0
- package/dist/core/runner/prepare-park-ask.js +6 -4
- package/dist/core/runner/prepare-policy-chain.d.ts +16 -2
- package/dist/core/runner/prepare-policy-chain.js +14 -5
- package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
- package/dist/core/runner/prepare-suspend-saga.js +7 -1
- package/dist/core/runner/prepare-task.js +41 -18
- package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
- package/dist/core/runner/prepare-turn-wiring.js +2 -1
- package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
- package/dist/core/runner/prepare-workspace-restore.js +1 -0
- package/dist/core/runner/resume-apply.js +17 -3
- package/dist/core/runner/resume-checkpoint-screen.js +13 -0
- package/dist/core/runner/resume-policy-outcome.js +2 -1
- package/dist/core/runner/run-leg.js +5 -1
- package/dist/core/runner/run-notification-lane.js +2 -2
- package/dist/core/runner/runtask.d.ts +2 -0
- package/dist/core/runner/runtask.js +4 -2
- package/dist/core/runner/stream-steer-verb.js +2 -2
- package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
- package/dist/core/runner/sub-agent-deny-lists.js +121 -0
- package/dist/core/runner/synthetic-tools.d.ts +1 -1
- package/dist/core/runner/synthetic-tools.js +3 -2
- package/dist/core/runner/write-protection-station.d.ts +3 -2
- package/dist/core/runner/write-protection-station.js +1 -0
- package/dist/core/runner-deps.d.ts +14 -0
- package/dist/core/sensitive-path-policy.d.ts +8 -0
- package/dist/core/sensitive-path-policy.js +18 -2
- package/dist/core/session-policy-store.d.ts +6 -2
- package/dist/core/session-policy-store.js +2 -1
- package/dist/core/shell-wrapper-projection.d.ts +11 -0
- package/dist/core/shell-wrapper-projection.js +56 -0
- package/dist/core/shell-wrapper-table.d.ts +144 -34
- package/dist/core/shell-wrapper-table.js +40 -20
- package/dist/core/skill-grant.d.ts +10 -2
- package/dist/core/skill-grant.js +20 -4
- package/dist/core/synthetic-read-verdict.d.ts +3 -0
- package/dist/core/synthetic-read-verdict.js +3 -1
- package/dist/core/task-notification.d.ts +12 -1
- package/dist/core/task-notification.js +13 -1
- package/dist/core/task-spec.d.ts +5 -3
- package/dist/core/tool-catalog-entries.d.ts +21 -0
- package/dist/core/tool-catalog-entries.js +9 -0
- package/dist/core/tool-policy.d.ts +45 -12
- package/dist/core/tool-policy.js +18 -20
- package/dist/core/tool-spec.d.ts +23 -4
- package/dist/core/types.d.ts +1 -1
- package/dist/core/types.js +1 -1
- package/dist/core/write-protect.d.ts +4 -0
- package/dist/core/write-protect.js +30 -9
- package/dist/index.d.ts +10 -6
- package/dist/index.js +9 -5
- package/dist/orchestration/run-spec.js +8 -7
- package/dist/orchestration/workflow.js +3 -2
- package/dist/shell/lex.d.ts +7 -0
- package/dist/shell/lex.js +17 -0
- package/dist/tools/fs/bash-program-position.d.ts +8 -87
- package/dist/tools/fs/bash-program-position.js +33 -69
- package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
- package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
- package/dist/tools/fs/index.d.ts +3 -2
- package/dist/tools/fs/program-read-model.d.ts +694 -0
- package/dist/tools/fs/program-read-model.js +1252 -0
- package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
- package/dist/tools/fs/program-read-model.rows.js +613 -0
- package/dist/tools/fs/read-deny-home.d.ts +19 -1
- package/dist/tools/fs/read-deny-home.js +62 -24
- package/dist/tools/fs/read-deny.d.ts +54 -14
- package/dist/tools/fs/read-deny.js +104 -50
- package/dist/tools/fs/removal-targets.d.ts +83 -0
- package/dist/tools/fs/removal-targets.js +236 -0
- package/dist/tools/fs/safety.d.ts +3 -0
- package/dist/tools/fs/safety.js +10 -3
- package/dist/tools/fs/shell-read-argv.d.ts +85 -0
- package/dist/tools/fs/shell-read-argv.js +386 -0
- package/dist/tools/fs/shell-read-bindings.d.ts +273 -0
- package/dist/tools/fs/shell-read-bindings.js +746 -0
- package/dist/tools/fs/shell-read-check.js +23 -15
- package/dist/tools/fs/shell-read-establish.js +79 -5
- package/dist/tools/fs/shell-read-judge.d.ts +11 -1
- package/dist/tools/fs/shell-read-judge.js +9 -1
- package/dist/tools/fs/shell-read-plan.d.ts +31 -7
- package/dist/tools/fs/shell-read-plan.js +299 -393
- package/dist/tools/fs/shell-read-word.d.ts +29 -26
- package/dist/tools/fs/shell-read-word.js +30 -38
- package/dist/tools/fs/shell-read-write-targets.d.ts +15 -37
- package/dist/tools/fs/shell-read-write-targets.js +83 -136
- package/dist/tools/fs/shell-reentry-text.d.ts +38 -6
- package/dist/tools/fs/shell-reentry-text.js +90 -53
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +49 -3
- package/wire-changes/core@7.45.0.json +1084 -0
- package/wire-changes/core@7.46.0.json +1522 -0
|
@@ -7,8 +7,9 @@ import { bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
|
|
|
7
7
|
import { planShellReads } from "./shell-read-plan.js";
|
|
8
8
|
import { createShellReadEstablisher, establishShellReadsLexically } from "./shell-read-establish.js";
|
|
9
9
|
import { declaredReadDenyHomes, homeViewsAt } from "./read-deny-home.js";
|
|
10
|
-
import { programArgs
|
|
11
|
-
import {
|
|
10
|
+
import { programArgs } from "./shell-read-write-targets.js";
|
|
11
|
+
import { programRow, selectedOptions } from "./program-read-model.js";
|
|
12
|
+
import { allFindings, decideShellRead, denyRowAtOrBelow, foldShellReadFindings, judgeEstablishedPlan, readRootGrantCandidateOf, stationGuards, } from "./shell-read-judge.js";
|
|
12
13
|
import { seatOf } from "../../core/tool-name-readings.js";
|
|
13
14
|
import { normalizeReadRootCandidate } from "../../core/read-root-candidate.js";
|
|
14
15
|
import { argvOf, lexShellText } from "../../shell/lex.js";
|
|
@@ -25,14 +26,14 @@ const REMOVAL_COMMANDS = new Set(["rm", "rmdir"]);
|
|
|
25
26
|
const WRITES_BENEATH = {
|
|
26
27
|
cp: () => true,
|
|
27
28
|
mv: () => true,
|
|
28
|
-
rm: (s) => s.
|
|
29
|
+
rm: (s) => selectedOptions(programRow("rm"), programArgs(s).map((w) => w.spelled)).some((o) => o.selects === "traversal"),
|
|
29
30
|
};
|
|
30
31
|
const isPattern = (f) => f.read.candidate.expands !== false && f.read.candidate.expands !== "unresolvable";
|
|
31
32
|
const RESHAPES_TREE = new Set(["mv"]);
|
|
32
33
|
const anyOptionWord = (words) => words.some((w) => w.startsWith("-"));
|
|
33
34
|
const UNPLACED_OPTIONS = {
|
|
34
|
-
rmdir: (ws) => ws.some((
|
|
35
|
-
sed: (ws) => ws.some((
|
|
35
|
+
rmdir: (ws) => selectedOptions(programRow("rmdir"), ws).some((o) => o.effect === "writes"),
|
|
36
|
+
sed: (ws) => selectedOptions(programRow("sed"), ws).some((o) => o.value?.kind === "program-file"),
|
|
36
37
|
cp: anyOptionWord,
|
|
37
38
|
mv: anyOptionWord,
|
|
38
39
|
};
|
|
@@ -205,7 +206,7 @@ const CAUSE_SPELLING_MAX = 64;
|
|
|
205
206
|
function fitCause(cause) {
|
|
206
207
|
return cause.replace(/^"([^"]*)"/, (_m, spelling) => `"${fitMiddle(spelling, CAUSE_SPELLING_MAX)}"`);
|
|
207
208
|
}
|
|
208
|
-
function findingAccount(fold, roots) {
|
|
209
|
+
function findingAccount(fold, roots, face) {
|
|
209
210
|
const allowed = fitMiddle(roots.length > 0 ? roots.join(", ") : "(none)", ACCOUNT_PART_MAX);
|
|
210
211
|
const w = fold.witness;
|
|
211
212
|
switch (fold.finding) {
|
|
@@ -213,7 +214,7 @@ function findingAccount(fold, roots) {
|
|
|
213
214
|
return `reads outside the allowed directories for this session: "${fitMiddle(w?.read.target ?? "?", ACCOUNT_PART_MAX)}" is not under ${allowed} ${NOT_AUTO_ALLOWED}`;
|
|
214
215
|
case "unestablishable":
|
|
215
216
|
if (w?.read.cause !== undefined)
|
|
216
|
-
return `where this command reads could not be established: ${fitCause(w.read.cause)}, so it cannot be confirmed to read only inside the allowed directories for this session (${allowed}) ${NOT_AUTO_ALLOWED}`;
|
|
217
|
+
return `where this command reads could not be established: ${fitCause(w.read.cause)}, so it cannot be confirmed ${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}`;
|
|
217
218
|
if (fold.declined?.baseMove !== undefined)
|
|
218
219
|
return `"${fitMiddle(fold.declined.text.trim(), ACCOUNT_PART_MAX)}" moves the working directory somewhere this command's text does not settle ${NOT_AUTO_ALLOWED}`;
|
|
219
220
|
if (fold.declined?.unreadProgramText !== undefined)
|
|
@@ -242,7 +243,7 @@ function outsidePathsOf(judged) {
|
|
|
242
243
|
}
|
|
243
244
|
return out;
|
|
244
245
|
}
|
|
245
|
-
function refusalOf(tool, fold, roots, paths) {
|
|
246
|
+
function refusalOf(tool, fold, roots, paths, face) {
|
|
246
247
|
const w = fold.witness;
|
|
247
248
|
if (fold.finding === "deny_hit" && w?.deny !== undefined) {
|
|
248
249
|
const target = w.read.target ?? w.deny.matchedView;
|
|
@@ -251,14 +252,16 @@ function refusalOf(tool, fold, roots, paths) {
|
|
|
251
252
|
const sentence = `Error (${tool}): ${core} ${write ? WRITE_DENY_REFUSAL_NOTE : DENY_REFUSAL_NOTE}`;
|
|
252
253
|
return { sentence, ...(write ? {} : { details: violationDetails({ code: "read_path_denied", message: sentence, target, pattern: w.deny.pattern }) }) };
|
|
253
254
|
}
|
|
254
|
-
const account = findingAccount(fold, roots);
|
|
255
|
+
const account = findingAccount(fold, roots, face);
|
|
255
256
|
const sentence = `Error (${tool}): ${account}. ${BASH_READONLY_CONFINEMENT_NOTE}`;
|
|
256
257
|
return { sentence, details: findingDetails(fold.finding, paths) };
|
|
257
258
|
}
|
|
258
259
|
const WRITE_DENY_REFUSAL_NOTE = "The boundary judges this command's write targets — its output redirections and the operands of mkdir, touch, rm, " +
|
|
259
260
|
"rmdir, mv, cp, tee and an in-place sed — against the write deny list; a denied write is refused here rather than escalated.";
|
|
260
|
-
const DENY_REFUSAL_NOTE = "The read boundary judges this command
|
|
261
|
-
"
|
|
261
|
+
const DENY_REFUSAL_NOTE = "The read boundary judges the paths this command names — as written and, where a filesystem can be consulted, where " +
|
|
262
|
+
"they resolve and what a pattern among them matches — and refuses a deny-listed one here rather than escalating it; " +
|
|
263
|
+
"a file the command reaches without naming it is judged only where a segment deny row (built in, or the deployment's " +
|
|
264
|
+
"own pattern) reaches into the tree a recursive read walks.";
|
|
262
265
|
function verdictOf(tool, seat, fold, judged, boundary) {
|
|
263
266
|
const disposition = decideShellRead(fold.finding, seat);
|
|
264
267
|
if (disposition === "vouch")
|
|
@@ -266,7 +269,7 @@ function verdictOf(tool, seat, fold, judged, boundary) {
|
|
|
266
269
|
const paths = outsidePathsOf(judged);
|
|
267
270
|
const unestablished = fold.unestablished === true ? { unestablished: true } : {};
|
|
268
271
|
if (disposition === "refuse") {
|
|
269
|
-
const r = refusalOf(tool, fold, boundary.roots, paths);
|
|
272
|
+
const r = refusalOf(tool, fold, boundary.roots, paths, boundary.face);
|
|
270
273
|
return { disposition, finding: fold.finding, sentence: r.sentence, ...(r.details !== undefined ? { details: r.details } : {}) };
|
|
271
274
|
}
|
|
272
275
|
const grant = fold.finding === "outside_roots" ? readRootGrantCandidateOf(judged, boundary) : undefined;
|
|
@@ -275,7 +278,7 @@ function verdictOf(tool, seat, fold, judged, boundary) {
|
|
|
275
278
|
disposition,
|
|
276
279
|
finding: fold.finding,
|
|
277
280
|
...unestablished,
|
|
278
|
-
sentence: findingAccount(fold, boundary.roots),
|
|
281
|
+
sentence: findingAccount(fold, boundary.roots, boundary.face),
|
|
279
282
|
...(disposition === "mandated" && containment ? { details: findingDetails(fold.finding, paths) } : {}),
|
|
280
283
|
...(grant !== undefined ? { readRootCandidate: { dir: grant.dir, ...(grant.covers !== undefined ? { covers: grant.covers } : {}), clearsThisAsk: true } } : {}),
|
|
281
284
|
};
|
|
@@ -286,7 +289,7 @@ function declarationsOf(boundary, readers) {
|
|
|
286
289
|
function judgePlan(plan, established, boundary) {
|
|
287
290
|
return judgeEstablishedPlan(plan, established.reads, boundary, (n) => {
|
|
288
291
|
const nested = established.nested.get(n);
|
|
289
|
-
return nested === undefined ? { plan: n, segments: [] } : judgePlan(n, nested, boundary);
|
|
292
|
+
return nested === undefined ? { plan: n, segments: [], guards: stationGuards(boundary) } : judgePlan(n, nested, boundary);
|
|
290
293
|
});
|
|
291
294
|
}
|
|
292
295
|
async function applyAlsoInside(judged, boundary) {
|
|
@@ -304,6 +307,7 @@ async function applyAlsoInside(judged, boundary) {
|
|
|
304
307
|
return judged;
|
|
305
308
|
const rewrite = (j) => ({
|
|
306
309
|
plan: j.plan,
|
|
310
|
+
guards: j.guards,
|
|
307
311
|
segments: j.segments.map((s) => ({
|
|
308
312
|
segment: s.segment,
|
|
309
313
|
findings: s.findings.map((f) => (f.kind === "outside_roots" && f.read.target !== undefined && rescued.has(f.read.target) ? { ...f, kind: "none", containment: "inside" } : f)),
|
|
@@ -386,7 +390,11 @@ export function createShellReadStation(opts) {
|
|
|
386
390
|
},
|
|
387
391
|
unanswered(call, cause) {
|
|
388
392
|
const toolName = call.toolName;
|
|
389
|
-
const
|
|
393
|
+
const member = memberOf(call) ?? toolName;
|
|
394
|
+
const seat = opts.seatOf(member);
|
|
395
|
+
const boundary = opts.boundaryOf(member, undefined);
|
|
396
|
+
if (boundary === undefined || !(stationGuards(boundary) || boundary.writeDenyMatch !== undefined))
|
|
397
|
+
return { disposition: "vouch", finding: "none" };
|
|
390
398
|
const fold = { finding: "unestablishable" };
|
|
391
399
|
const disposition = decideShellRead("unestablishable", seat);
|
|
392
400
|
const account = `${cause}, so where this command reads could not be established ${NOT_AUTO_ALLOWED}`;
|
|
@@ -132,6 +132,45 @@ function lexicalReadsOf(candidate, boundary) {
|
|
|
132
132
|
}
|
|
133
133
|
return out;
|
|
134
134
|
}
|
|
135
|
+
function writtenByText(plan, boundary) {
|
|
136
|
+
const targets = new Set();
|
|
137
|
+
const speculativeSeen = new Set();
|
|
138
|
+
let unplaced = false;
|
|
139
|
+
const walk = (p) => {
|
|
140
|
+
for (const s of p.segments) {
|
|
141
|
+
for (const c of s.reads) {
|
|
142
|
+
if (c.speculative === undefined || c.expands !== false || c.bases === "unmodelled")
|
|
143
|
+
continue;
|
|
144
|
+
for (const r of lexicalReadsOf(c, boundary)) {
|
|
145
|
+
if (r.target === undefined)
|
|
146
|
+
continue;
|
|
147
|
+
const t = normalizeAbsPathLexically(r.target);
|
|
148
|
+
if (speculativeSeen.has(t))
|
|
149
|
+
targets.add(t);
|
|
150
|
+
speculativeSeen.add(t);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const j of judgedCandidatesOf(s)) {
|
|
154
|
+
if (j.candidate.face !== "write" || j.onlyIfExists !== undefined)
|
|
155
|
+
continue;
|
|
156
|
+
if (j.candidate.expands !== false || j.candidate.bases === "unmodelled") {
|
|
157
|
+
unplaced = true;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
for (const r of lexicalReadsOf(j.candidate, boundary)) {
|
|
161
|
+
if (r.target === undefined)
|
|
162
|
+
unplaced = true;
|
|
163
|
+
else
|
|
164
|
+
targets.add(normalizeAbsPathLexically(r.target));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const n of s.nested)
|
|
168
|
+
walk(n);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
walk(plan);
|
|
172
|
+
return { targets, unplaced };
|
|
173
|
+
}
|
|
135
174
|
function unfollowedBackupReads(j, boundary, cause) {
|
|
136
175
|
const spellings = sedBackupNames(j.followed.operand.spelled, j.followed.suffix).flatMap((name) => lexicalReadsOf({ ...j.candidate, spelled: name }, boundary).flatMap((r) => [...(r.target !== undefined ? [r.target] : []), ...r.spellings]));
|
|
137
176
|
return [{ candidate: j.candidate, established: "unestablishable", spellings: [...new Set(spellings)], cause }];
|
|
@@ -443,9 +482,44 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
443
482
|
}
|
|
444
483
|
return out;
|
|
445
484
|
};
|
|
446
|
-
const
|
|
485
|
+
const confirmedMissing = async (candidate, boundary, signal) => {
|
|
486
|
+
if (candidate.expands !== false)
|
|
487
|
+
return false;
|
|
488
|
+
const reads = lexicalReadsOf(candidate, boundary);
|
|
489
|
+
if (reads.length === 0)
|
|
490
|
+
return false;
|
|
491
|
+
for (const r of reads) {
|
|
492
|
+
if (r.established !== "lexical" || r.target === undefined)
|
|
493
|
+
return false;
|
|
494
|
+
const real = await resolve(r.target, signal);
|
|
495
|
+
if (real === undefined || real.exact || real.missing !== true)
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
return true;
|
|
499
|
+
};
|
|
500
|
+
const establishSegment = async (segment, boundary, roots, written, signal) => {
|
|
447
501
|
const out = [];
|
|
448
|
-
|
|
502
|
+
const missing = new Map();
|
|
503
|
+
for (const listed of judgedCandidatesOf(segment)) {
|
|
504
|
+
let judged = listed;
|
|
505
|
+
const speculative = judged.candidate.speculative;
|
|
506
|
+
if (speculative !== undefined && !written.unplaced) {
|
|
507
|
+
const c = judged.candidate;
|
|
508
|
+
if (!missing.has(c))
|
|
509
|
+
missing.set(c, (await confirmedMissing(c, boundary, signal)) && !lexicalReadsOf(c, boundary).some((r) => r.target !== undefined && written.targets.has(normalizeAbsPathLexically(r.target))));
|
|
510
|
+
if (missing.get(c) === true) {
|
|
511
|
+
if (speculative === "whole")
|
|
512
|
+
continue;
|
|
513
|
+
judged = { ...judged, candidate: { ...c, denyOnly: true } };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const decides = judged.onlyIfExists;
|
|
517
|
+
if (decides !== undefined) {
|
|
518
|
+
if (!missing.has(decides))
|
|
519
|
+
missing.set(decides, await confirmedMissing(decides, boundary, signal));
|
|
520
|
+
if (missing.get(decides) === true)
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
449
523
|
if (judged.followed !== undefined) {
|
|
450
524
|
out.push(...(await followedBackupReads({ ...judged, followed: judged.followed }, boundary, signal)));
|
|
451
525
|
continue;
|
|
@@ -518,13 +592,13 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
518
592
|
}
|
|
519
593
|
return out;
|
|
520
594
|
};
|
|
521
|
-
const establishPlan = async (plan, boundary, roots, signal) => {
|
|
595
|
+
const establishPlan = async (plan, boundary, roots, signal, written = writtenByText(plan, boundary)) => {
|
|
522
596
|
const reads = new Map();
|
|
523
597
|
const nested = new Map();
|
|
524
598
|
for (const segment of plan.segments) {
|
|
525
|
-
reads.set(segment, await establishSegment(segment, boundary, roots, signal));
|
|
599
|
+
reads.set(segment, await establishSegment(segment, boundary, roots, written, signal));
|
|
526
600
|
for (const n of segment.nested)
|
|
527
|
-
nested.set(n, await establishPlan(n, boundary, roots, signal));
|
|
601
|
+
nested.set(n, await establishPlan(n, boundary, roots, signal, written));
|
|
528
602
|
}
|
|
529
603
|
return { plan, reads, nested, roots };
|
|
530
604
|
};
|
|
@@ -235,7 +235,16 @@ export interface JudgedSegment {
|
|
|
235
235
|
export interface JudgedPlan {
|
|
236
236
|
readonly plan: ShellReadPlan;
|
|
237
237
|
readonly segments: readonly JudgedSegment[];
|
|
238
|
+
/** {@link stationGuards} of the boundary the plan was judged against. */
|
|
239
|
+
readonly guards: boolean;
|
|
238
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Does the station GUARD anything for this boundary — a closed face (roots: containment is what it guards; an absent
|
|
243
|
+
* face reads as roots, as {@link containmentOf} does), or a deny table with rows (`denyMatch` is present exactly then)?
|
|
244
|
+
* Where it guards nothing (an open face, no deny row), a read it could not establish has nothing to be confirmed
|
|
245
|
+
* against: no finding. The deny judgment itself is untouched (with no row there is nothing to hit).
|
|
246
|
+
*/
|
|
247
|
+
export declare function stationGuards(boundary: Pick<ShellReadBoundary, "face" | "denyMatch">): boolean;
|
|
239
248
|
/** The command-level fold and the finding that produced it. `witness` is absent for `none`; for `declined`
|
|
240
249
|
* the segment that declined stands in for a candidate. */
|
|
241
250
|
export interface ShellReadFold {
|
|
@@ -247,7 +256,8 @@ export interface ShellReadFold {
|
|
|
247
256
|
}
|
|
248
257
|
/**
|
|
249
258
|
* Fold a judged plan to ONE finding by {@link SHELL_READ_FINDING_RANK}, over every segment and every nested
|
|
250
|
-
* plan.
|
|
259
|
+
* plan. Where the station guards nothing ({@link JudgedPlan.guards}) an `unestablishable` or `declined` finding is no
|
|
260
|
+
* finding: there is nothing to confirm the read against. Two structural rules ride the fold:
|
|
251
261
|
* · a plan the lexer refused (`unreadable`) is `unestablishable`, unconditionally — a quote-blind re-read
|
|
252
262
|
* yields more words, never zero, so an unreadable text is never "no candidates";
|
|
253
263
|
* · a SHELL re-entry segment (`sh -c S`, `eval S`, `env -S 'S'`, a launcher handing text to a shell) is at
|
|
@@ -72,12 +72,18 @@ function containmentOf(read, boundary) {
|
|
|
72
72
|
return "unknown";
|
|
73
73
|
return withinAnyRoot(boundary.roots, read.target) ? "inside" : "outside";
|
|
74
74
|
}
|
|
75
|
+
export function stationGuards(boundary) {
|
|
76
|
+
return boundary.face !== "open" || boundary.denyMatch !== undefined;
|
|
77
|
+
}
|
|
75
78
|
export function foldShellReadFindings(judged) {
|
|
76
79
|
let best = { finding: "none" };
|
|
77
80
|
let unestablished = false;
|
|
78
81
|
const raise = (next) => {
|
|
79
|
-
if (next.finding === "unestablishable" || next.finding === "declined")
|
|
82
|
+
if (next.finding === "unestablishable" || next.finding === "declined") {
|
|
83
|
+
if (!judged.guards)
|
|
84
|
+
return;
|
|
80
85
|
unestablished = true;
|
|
86
|
+
}
|
|
81
87
|
if (SHELL_READ_FINDING_RANK[next.finding] > SHELL_READ_FINDING_RANK[best.finding])
|
|
82
88
|
best = next;
|
|
83
89
|
};
|
|
@@ -118,6 +124,7 @@ export function judgeEstablishedPlan(plan, established, boundary, nestedOf) {
|
|
|
118
124
|
findings: (established.get(segment) ?? []).map((r) => judgeRead(r, boundary)),
|
|
119
125
|
nested: segment.nested.map(nestedOf),
|
|
120
126
|
})),
|
|
127
|
+
guards: stationGuards(boundary),
|
|
121
128
|
};
|
|
122
129
|
}
|
|
123
130
|
export function allFindings(judged) {
|
|
@@ -200,5 +207,6 @@ export function rejudge(judged, boundary) {
|
|
|
200
207
|
findings: s.findings.map((f) => judgeRead(f.read, boundary)),
|
|
201
208
|
nested: s.nested.map((n) => rejudge(n, boundary)),
|
|
202
209
|
})),
|
|
210
|
+
guards: stationGuards(boundary),
|
|
203
211
|
};
|
|
204
212
|
}
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* (`shell-read-establish.ts`) and judgment are downstream; nothing here decides allow/ask/deny.
|
|
11
11
|
*
|
|
12
12
|
* Contract, in one sentence per rule:
|
|
13
|
-
* · a LISTED reader hands its positional operands and the declared option-value slots (the
|
|
14
|
-
* `
|
|
13
|
+
* · a LISTED reader hands its positional operands and the declared option-value slots (the program read model's
|
|
14
|
+
* rows, `program-read-model.ts`, walked by its one argv reader — this module only maps what it read to candidates);
|
|
15
15
|
* · an UNLISTED program hands every word of its argv (a program declared to open no path operand hands none);
|
|
16
16
|
* · an ASSIGNMENT's value is a candidate only in a text that runs a program able to open a file;
|
|
17
17
|
* · a LAUNCHER chain is peeled to its program and the chain's own words are never candidates; a launcher
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* an argv re-entry (`xargs`) is a decline unless the target opens nothing it is handed;
|
|
22
22
|
* · an INTERPRETER's inline program is a shape fact (`reentry:"interpreter"`), never parsed, never a finding;
|
|
23
23
|
* · `<` targets are read candidates, `>`-family targets are write targets (tagged, not read), here-documents
|
|
24
|
-
* and here-strings are data (an UNQUOTED here-document body's substitutions are nested plans)
|
|
24
|
+
* and here-strings are data (an UNQUOTED here-document body's substitutions are nested plans) — except to a shell
|
|
25
|
+
* whose argv names no script: what arrives on its standard input is its script, a nested plan where the text spells it;
|
|
25
26
|
* · `$(…)`, backticks and `<(…)` bodies are nested plans; a word carrying one cannot be resolved from the text;
|
|
26
27
|
* · `cd` moves the base the LATER segments resolve against, as a candidate SET (`&&` proves a move, nothing
|
|
27
28
|
* else does); `pushd`/`popd` lose the base; a loop body with a base move is replayed per LITERAL beat and
|
|
@@ -29,9 +30,11 @@
|
|
|
29
30
|
* Every ambiguity resolves toward MORE candidates, never fewer.
|
|
30
31
|
*/
|
|
31
32
|
import { type LenientConnector, type LexedRedirect, type LexedWord } from "../../shell/lex.js";
|
|
33
|
+
import { type BindingTable, type NameReader } from "./shell-read-bindings.js";
|
|
32
34
|
import { type ShellScanDecline } from "./bash-program-position.js";
|
|
35
|
+
import { type StdinText } from "./shell-reentry-text.js";
|
|
33
36
|
/** WHY a word is a read candidate — a closed set, registered in `docs/CLOSED-SETS.md`. */
|
|
34
|
-
export declare const READ_CANDIDATE_PROVENANCES: readonly ["operand", "option_value", "stdin_redirect", "write_redirect_target", "env_assignment", "unlisted_word", "traversal_root", "cd_target", "program_path", "script_slot_file"];
|
|
37
|
+
export declare const READ_CANDIDATE_PROVENANCES: readonly ["operand", "option_value", "stdin_redirect", "write_redirect_target", "env_assignment", "unlisted_word", "traversal_root", "cd_target", "program_path", "script_slot_file", "mount_source", "object_path"];
|
|
35
38
|
export type ReadCandidateProvenance = (typeof READ_CANDIDATE_PROVENANCES)[number];
|
|
36
39
|
/** What the caller declares about the deployment: the listed readers and the boundary the bases start from. */
|
|
37
40
|
export interface ShellReadDeclarations {
|
|
@@ -71,6 +74,12 @@ export interface ReadCandidate {
|
|
|
71
74
|
/** A WRITE target of the command (a `>`-family redirection, a `sed` `w` file, the operands of a file-writing command):
|
|
72
75
|
* established like a read and judged against the deny table's WRITE face only — never for containment, never a read. */
|
|
73
76
|
readonly face?: "write";
|
|
77
|
+
/** A reading the station takes where no row says the program opens the word — a word of a program with no row, a
|
|
78
|
+
* role only another implementation reads — so the filesystem decides it: `containment` = the spelling always meets
|
|
79
|
+
* the deny rows and is a containment candidate only where it exists; `whole` = a whole word (`NAME=value`, an option
|
|
80
|
+
* word) that is a candidate at all only where it exists as a file. Where the filesystem confirms it is not there (and
|
|
81
|
+
* nothing in the text writes or creates it), it is no read; a lexical establishment keeps it whole. */
|
|
82
|
+
readonly speculative?: "containment" | "whole";
|
|
74
83
|
}
|
|
75
84
|
/** How the shell reached one segment — what the base-candidate walk needs from the connectors. */
|
|
76
85
|
export interface SegmentReach {
|
|
@@ -107,6 +116,9 @@ export interface ShellReadSegment {
|
|
|
107
116
|
readonly declines: readonly ShellScanDecline[];
|
|
108
117
|
readonly reads: readonly ReadCandidate[];
|
|
109
118
|
readonly redirects: readonly ShellReadRedirect[];
|
|
119
|
+
/** A shell re-entry's standard input: each text that may arrive there. The shell runs it where its argv names no script
|
|
120
|
+
* (`reentryText`); a text the command spells is then a nested plan. */
|
|
121
|
+
readonly stdin?: readonly StdinText[];
|
|
110
122
|
readonly baseMove?: {
|
|
111
123
|
readonly target: ReadCandidate | "home" | "previous" | "unmodelled";
|
|
112
124
|
};
|
|
@@ -129,6 +141,18 @@ export interface ShellReadPlan {
|
|
|
129
141
|
readonly strayRedirection: boolean;
|
|
130
142
|
};
|
|
131
143
|
}
|
|
132
|
-
/**
|
|
133
|
-
*
|
|
134
|
-
|
|
144
|
+
/** Where a nested text starts its names: a substitution body inherits the table of the segment it sits in (a subshell
|
|
145
|
+
* holds every variable); a text the shell runs later or elsewhere (a code body, `eval`'s text, a child shell's `-c`)
|
|
146
|
+
* starts from the text's own start — every name unknown, `HOME` the declared home unless the enclosing text binds it
|
|
147
|
+
* anywhere. `boundAnywhere` answers for the enclosing texts. */
|
|
148
|
+
interface PlanStart {
|
|
149
|
+
readonly initial: NameReader;
|
|
150
|
+
readonly boundAnywhere: (name: string) => boolean;
|
|
151
|
+
/** `HOME` may already hold something other than the declared home where the nested text starts. */
|
|
152
|
+
readonly homeTouched: boolean;
|
|
153
|
+
/** The attributes and name references in force where the nested text starts. */
|
|
154
|
+
readonly inherited: BindingTable["inherited"];
|
|
155
|
+
}
|
|
156
|
+
/** Plan every read candidate of one command (the contract is this module's header). */
|
|
157
|
+
export declare function planShellReads(command: string, decl: ShellReadDeclarations, depth?: number, start?: PlanStart): ShellReadPlan;
|
|
158
|
+
export {};
|