@sema-agent/core 7.45.0 → 7.47.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.
Files changed (139) hide show
  1. package/CHANGELOG.md +298 -1
  2. package/dist/agents/extra-tool-names.d.ts +65 -0
  3. package/dist/agents/extra-tool-names.js +124 -0
  4. package/dist/agents/subagent.d.ts +17 -5
  5. package/dist/agents/subagent.js +33 -25
  6. package/dist/agents/tool-filter.d.ts +17 -1
  7. package/dist/agents/tool-filter.js +7 -0
  8. package/dist/core/agent-config-dir.d.ts +20 -0
  9. package/dist/core/agent-config-dir.js +49 -0
  10. package/dist/core/allow-entry.d.ts +6 -4
  11. package/dist/core/allow-entry.js +6 -5
  12. package/dist/core/ask-origin.d.ts +35 -12
  13. package/dist/core/ask-origin.js +24 -7
  14. package/dist/core/checkpoint-store.d.ts +39 -2
  15. package/dist/core/effective-path-target.d.ts +136 -4
  16. package/dist/core/effective-path-target.js +110 -2
  17. package/dist/core/engine-notice.d.ts +12 -4
  18. package/dist/core/gate-fold.js +5 -2
  19. package/dist/core/gate-lanes.js +11 -4
  20. package/dist/core/gate-outcome.d.ts +4 -2
  21. package/dist/core/gate-outcome.js +2 -2
  22. package/dist/core/governance-codes.d.ts +3 -1
  23. package/dist/core/governance-codes.js +3 -0
  24. package/dist/core/hooks.d.ts +3 -0
  25. package/dist/core/path-rule-binding.d.ts +65 -0
  26. package/dist/core/path-rule-binding.js +99 -0
  27. package/dist/core/permission-rule-model.d.ts +59 -24
  28. package/dist/core/permission-rule-model.js +48 -44
  29. package/dist/core/permission-rule-provider.d.ts +7 -0
  30. package/dist/core/permission-rule-provider.js +10 -3
  31. package/dist/core/permission-rule-store.d.ts +8 -0
  32. package/dist/core/permission-rule-store.js +6 -2
  33. package/dist/core/permission-rule-sync.js +2 -2
  34. package/dist/core/permission-rules.d.ts +26 -45
  35. package/dist/core/permission-rules.js +77 -57
  36. package/dist/core/runner/child-tree.d.ts +97 -0
  37. package/dist/core/runner/child-tree.js +104 -0
  38. package/dist/core/runner/construction-doors.d.ts +5 -0
  39. package/dist/core/runner/construction-doors.js +6 -0
  40. package/dist/core/runner/contracts.d.ts +46 -0
  41. package/dist/core/runner/gate-exit.d.ts +4 -1
  42. package/dist/core/runner/gate-exit.js +5 -1
  43. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  44. package/dist/core/runner/permission-rule-lanes.js +18 -12
  45. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  46. package/dist/core/runner/persisted-path-rows.js +31 -12
  47. package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
  48. package/dist/core/runner/prepare-ask-lane.js +4 -2
  49. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  50. package/dist/core/runner/prepare-execution-env.js +10 -3
  51. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  52. package/dist/core/runner/prepare-gate-stations.js +2 -1
  53. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  54. package/dist/core/runner/prepare-hands-readface.js +11 -11
  55. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  56. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  57. package/dist/core/runner/prepare-park-ask.d.ts +3 -0
  58. package/dist/core/runner/prepare-park-ask.js +5 -3
  59. package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
  60. package/dist/core/runner/prepare-policy-chain.js +13 -5
  61. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  62. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  63. package/dist/core/runner/prepare-task.js +37 -18
  64. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  65. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  66. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  67. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  68. package/dist/core/runner/resume-apply.js +17 -3
  69. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  70. package/dist/core/runner/run-leg.js +5 -1
  71. package/dist/core/runner/runtask.js +2 -2
  72. package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
  73. package/dist/core/runner/write-protection-station.d.ts +3 -2
  74. package/dist/core/runner/write-protection-station.js +1 -0
  75. package/dist/core/runner-deps.d.ts +14 -0
  76. package/dist/core/sensitive-path-policy.d.ts +8 -0
  77. package/dist/core/sensitive-path-policy.js +18 -2
  78. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  79. package/dist/core/shell-wrapper-projection.js +56 -0
  80. package/dist/core/shell-wrapper-table.d.ts +144 -34
  81. package/dist/core/shell-wrapper-table.js +40 -20
  82. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  83. package/dist/core/synthetic-read-verdict.js +3 -1
  84. package/dist/core/tool-catalog-entries.d.ts +21 -0
  85. package/dist/core/tool-catalog-entries.js +12 -2
  86. package/dist/core/tool-face.d.ts +10 -1
  87. package/dist/core/tool-name-readings.d.ts +8 -0
  88. package/dist/core/tool-name-readings.js +2 -0
  89. package/dist/core/tool-policy.d.ts +40 -17
  90. package/dist/core/tool-policy.js +11 -9
  91. package/dist/core/tool-registry.d.ts +9 -0
  92. package/dist/core/tool-registry.js +10 -0
  93. package/dist/core/tool-spec.d.ts +3 -2
  94. package/dist/core/write-protect.d.ts +14 -3
  95. package/dist/core/write-protect.js +41 -10
  96. package/dist/index.d.ts +6 -4
  97. package/dist/index.js +5 -3
  98. package/dist/orchestration/run-spec.js +8 -7
  99. package/dist/orchestration/workflow.js +1 -1
  100. package/dist/shell/lex.d.ts +22 -0
  101. package/dist/shell/lex.js +75 -13
  102. package/dist/tools/fs/bash-program-position.d.ts +14 -90
  103. package/dist/tools/fs/bash-program-position.js +41 -74
  104. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  105. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  106. package/dist/tools/fs/index.d.ts +3 -2
  107. package/dist/tools/fs/program-read-model.d.ts +711 -0
  108. package/dist/tools/fs/program-read-model.js +1259 -0
  109. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  110. package/dist/tools/fs/program-read-model.rows.js +624 -0
  111. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  112. package/dist/tools/fs/read-deny-home.js +62 -24
  113. package/dist/tools/fs/read-deny.d.ts +54 -14
  114. package/dist/tools/fs/read-deny.js +104 -50
  115. package/dist/tools/fs/removal-targets.d.ts +7 -6
  116. package/dist/tools/fs/removal-targets.js +34 -24
  117. package/dist/tools/fs/safety.d.ts +3 -0
  118. package/dist/tools/fs/safety.js +10 -3
  119. package/dist/tools/fs/shell-read-argv.d.ts +86 -0
  120. package/dist/tools/fs/shell-read-argv.js +388 -0
  121. package/dist/tools/fs/shell-read-bindings.d.ts +300 -0
  122. package/dist/tools/fs/shell-read-bindings.js +755 -0
  123. package/dist/tools/fs/shell-read-check.js +23 -15
  124. package/dist/tools/fs/shell-read-establish.js +82 -8
  125. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  126. package/dist/tools/fs/shell-read-judge.js +9 -1
  127. package/dist/tools/fs/shell-read-plan.d.ts +33 -8
  128. package/dist/tools/fs/shell-read-plan.js +309 -407
  129. package/dist/tools/fs/shell-read-word.d.ts +60 -26
  130. package/dist/tools/fs/shell-read-word.js +47 -37
  131. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
  132. package/dist/tools/fs/shell-read-write-targets.js +85 -137
  133. package/dist/tools/fs/shell-reentry-text.d.ts +136 -6
  134. package/dist/tools/fs/shell-reentry-text.js +203 -53
  135. package/dist/tools/monitor.d.ts +3 -3
  136. package/package.json +1 -1
  137. package/test/export-surface.snapshot.json +25 -5
  138. package/wire-changes/core@7.46.0.json +1522 -0
  139. package/wire-changes/core@7.47.0.json +412 -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, spellsOption } from "./shell-read-write-targets.js";
11
- import { allFindings, decideShellRead, denyRowAtOrBelow, foldShellReadFindings, judgeEstablishedPlan, readRootGrantCandidateOf, } from "./shell-read-judge.js";
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.argv.some((w) => spellsOption(w.folded, "recursive", "[rR]")),
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((w) => spellsOption(w, "parents", "p")),
35
- sed: (ws) => ws.some((w) => spellsOption(w, "file", "f")),
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's written spelling and, where a filesystem can be consulted, the " +
261
- "paths it would actually read; a deny-listed read is refused here rather than escalated.";
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 seat = opts.seatOf(memberOf(call) ?? toolName);
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 establishSegment = async (segment, boundary, roots, signal) => {
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
- for (const judged of judgedCandidatesOf(segment)) {
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;
@@ -483,7 +557,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
483
557
  for (const m of walkedTree.members)
484
558
  out.push({ ...(await canonicalReadOf(candidate, m.path, [m.path], "expansion", folded, m.canonical, signal)), node: m.node, walked: true });
485
559
  };
486
- for (const lexical of lexicalReadsOf(candidate, boundary)) {
560
+ for (const [i, lexical] of lexicalReadsOf(candidate, boundary).entries()) {
487
561
  if (lexical.established !== "lexical") {
488
562
  out.push(lexical);
489
563
  continue;
@@ -493,7 +567,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
493
567
  const walked = spellings.find((s) => s !== folded) ?? folded;
494
568
  if (candidate.expands === "glob") {
495
569
  let matched = false;
496
- for (const p of spellings) {
570
+ for (const p of candidate.pattern === undefined ? spellings : lexicalReadsOf({ ...candidate, spelled: candidate.pattern }, boundary)[i]?.spellings ?? spellings) {
497
571
  const expanded = await expandGlobCandidate(p, roots, signal);
498
572
  if (expanded !== undefined && "unverifiable" in expanded) {
499
573
  out.push({ candidate, established: "unestablishable", spellings, origin: folded, cause: expanded.unverifiable });
@@ -507,7 +581,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
507
581
  await land(read, spellings, folded);
508
582
  }
509
583
  }
510
- if (!matched)
584
+ if (!matched || candidate.pattern !== undefined)
511
585
  for (const read of await atSeats(walked, spellings, "canonical", undefined, undefined))
512
586
  await land(read, spellings, folded);
513
587
  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. Two structural rules ride the fold:
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 verb tables of
14
- * `bash-readonly-classifier.ts`, read here through their exported readers, never copied);
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 {
@@ -46,8 +49,9 @@ export interface ShellReadDeclarations {
46
49
  readonly face?: "open" | "roots";
47
50
  }
48
51
  export interface ReadCandidate {
49
- /** The folded word (quotes removed, a literal leading tilde spelled `./~`). */
52
+ /** The folded word (quotes removed, a literal leading tilde spelled `./~`) — a pattern's name where nothing matches. */
50
53
  readonly spelled: string;
54
+ /** The spelling a pattern's names are matched by where the matcher widens a class (`["!"a]`, `[[:alpha:]]`: any one character). */ readonly pattern?: string;
51
55
  /** The raw spelling, quotes retained — where "is this metacharacter quoted" is still answerable. */
52
56
  readonly raw: string;
53
57
  readonly provenance: ReadCandidateProvenance;
@@ -71,6 +75,12 @@ export interface ReadCandidate {
71
75
  /** A WRITE target of the command (a `>`-family redirection, a `sed` `w` file, the operands of a file-writing command):
72
76
  * established like a read and judged against the deny table's WRITE face only — never for containment, never a read. */
73
77
  readonly face?: "write";
78
+ /** A reading the station takes where no row says the program opens the word — a word of a program with no row, a
79
+ * role only another implementation reads — so the filesystem decides it: `containment` = the spelling always meets
80
+ * the deny rows and is a containment candidate only where it exists; `whole` = a whole word (`NAME=value`, an option
81
+ * word) that is a candidate at all only where it exists as a file. Where the filesystem confirms it is not there (and
82
+ * nothing in the text writes or creates it), it is no read; a lexical establishment keeps it whole. */
83
+ readonly speculative?: "containment" | "whole";
74
84
  }
75
85
  /** How the shell reached one segment — what the base-candidate walk needs from the connectors. */
76
86
  export interface SegmentReach {
@@ -107,6 +117,9 @@ export interface ShellReadSegment {
107
117
  readonly declines: readonly ShellScanDecline[];
108
118
  readonly reads: readonly ReadCandidate[];
109
119
  readonly redirects: readonly ShellReadRedirect[];
120
+ /** A shell re-entry's standard input: each text that may arrive there. The shell runs it where its argv names no script
121
+ * (`reentryText`); a text the command spells is then a nested plan. */
122
+ readonly stdin?: readonly StdinText[];
110
123
  readonly baseMove?: {
111
124
  readonly target: ReadCandidate | "home" | "previous" | "unmodelled";
112
125
  };
@@ -129,6 +142,18 @@ export interface ShellReadPlan {
129
142
  readonly strayRedirection: boolean;
130
143
  };
131
144
  }
132
- /** Plan every read candidate of one command (the contract is this module's header). `arithmetic` = the text is an
133
- * `$((…))` body, which bash evaluates and never globs (no extended-glob floor). */
134
- export declare function planShellReads(command: string, decl: ShellReadDeclarations, depth?: number, homeRebound?: boolean, arithmetic?: boolean): ShellReadPlan;
145
+ /** Where a nested text starts its names: a substitution body inherits the table of the segment it sits in (a subshell
146
+ * holds every variable); a text the shell runs later or elsewhere (a code body, `eval`'s text, a child shell's `-c`)
147
+ * starts from the text's own start — every name unknown, `HOME` the declared home unless the enclosing text binds it
148
+ * anywhere. `boundAnywhere` answers for the enclosing texts. */
149
+ interface PlanStart {
150
+ readonly initial: NameReader;
151
+ readonly boundAnywhere: (name: string) => boolean;
152
+ /** `HOME` may already hold something other than the declared home where the nested text starts. */
153
+ readonly homeTouched: boolean;
154
+ /** The attributes and name references in force where the nested text starts. */
155
+ readonly inherited: BindingTable["inherited"];
156
+ }
157
+ /** Plan every read candidate of one command (the contract is this module's header). */
158
+ export declare function planShellReads(command: string, decl: ShellReadDeclarations, depth?: number, start?: PlanStart): ShellReadPlan;
159
+ export {};