@sema-agent/core 7.33.7 → 7.34.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 (206) hide show
  1. package/CHANGELOG.md +357 -0
  2. package/dist/agents/cross-session-judge.d.ts +16 -7
  3. package/dist/agents/cross-session-judge.js +18 -15
  4. package/dist/agents/peer-session-drain.d.ts +4 -0
  5. package/dist/agents/peer-session-drain.js +5 -2
  6. package/dist/agents/repair-loop.d.ts +1 -1
  7. package/dist/agents/subagent.d.ts +3 -0
  8. package/dist/agents/subagent.js +12 -3
  9. package/dist/brain/anthropic.js +1 -0
  10. package/dist/brain/open-responses.js +1 -0
  11. package/dist/brain/openai.js +1 -0
  12. package/dist/brain/stream-engine.d.ts +3 -0
  13. package/dist/brain/stream-engine.js +2 -1
  14. package/dist/core/a2a.d.ts +3 -1
  15. package/dist/core/a2a.js +1 -1
  16. package/dist/core/ask-origin.d.ts +351 -154
  17. package/dist/core/ask-origin.js +223 -45
  18. package/dist/core/ask-unresolvable-notice.d.ts +5 -2
  19. package/dist/core/ask-unresolvable-notice.js +4 -3
  20. package/dist/core/auto-mode-arming.d.ts +17 -4
  21. package/dist/core/auto-mode-arming.js +31 -6
  22. package/dist/core/auto-mode-defaults.d.ts +16 -7
  23. package/dist/core/auto-mode-defaults.js +2 -1
  24. package/dist/core/auto-mode-directive.d.ts +18 -30
  25. package/dist/core/auto-mode-directive.js +9 -18
  26. package/dist/core/auto-mode-prompt.d.ts +62 -1
  27. package/dist/core/auto-mode-prompt.js +42 -4
  28. package/dist/core/auto-mode-rebuild.d.ts +2 -1
  29. package/dist/core/auto-mode-rebuild.js +1 -0
  30. package/dist/core/auto-mode.d.ts +32 -3
  31. package/dist/core/auto-mode.js +80 -41
  32. package/dist/core/checkpoint-store.d.ts +50 -35
  33. package/dist/core/checkpoint-store.js +58 -3
  34. package/dist/core/child-clamp-seats.js +1 -0
  35. package/dist/core/classifier-unavailable-arm.d.ts +57 -0
  36. package/dist/core/classifier-unavailable-arm.js +17 -0
  37. package/dist/core/effective-path-target.d.ts +12 -0
  38. package/dist/core/effective-path-target.js +18 -7
  39. package/dist/core/engine-notice.d.ts +53 -2
  40. package/dist/core/fs-write-gate-policy.d.ts +5 -1
  41. package/dist/core/fs-write-gate-policy.js +4 -1
  42. package/dist/core/gate-fold.js +17 -1
  43. package/dist/core/gate-lanes.js +111 -46
  44. package/dist/core/gate-outcome.d.ts +40 -7
  45. package/dist/core/gate-outcome.js +26 -4
  46. package/dist/core/governance-codes.d.ts +1 -1
  47. package/dist/core/governance-codes.js +12 -0
  48. package/dist/core/hooks.d.ts +50 -29
  49. package/dist/core/hooks.js +1 -3
  50. package/dist/core/mcp.js +1 -1
  51. package/dist/core/mode-policy.d.ts +182 -0
  52. package/dist/core/mode-policy.js +179 -0
  53. package/dist/core/oracle-isolation.d.ts +3 -3
  54. package/dist/core/permission-rule-model.d.ts +16 -3
  55. package/dist/core/permission-rule-model.js +36 -3
  56. package/dist/core/permission-rule-provider.d.ts +22 -11
  57. package/dist/core/permission-rule-provider.js +17 -7
  58. package/dist/core/permission-rules.d.ts +23 -1
  59. package/dist/core/permission-rules.js +35 -8
  60. package/dist/core/persisted-rule-arms.d.ts +10 -9
  61. package/dist/core/persisted-rule-arms.js +9 -6
  62. package/dist/core/present-plan-tool.d.ts +1 -1
  63. package/dist/core/present-plan-tool.js +3 -3
  64. package/dist/core/read-only-shell.d.ts +19 -19
  65. package/dist/core/read-only-shell.js +38 -12
  66. package/dist/core/retired-keys.d.ts +9 -3
  67. package/dist/core/retired-keys.js +5 -1
  68. package/dist/core/runner/classifier-ladder.d.ts +43 -1
  69. package/dist/core/runner/classifier-ladder.js +29 -0
  70. package/dist/core/runner/contracts.d.ts +37 -35
  71. package/dist/core/runner/denial-limit-arms.d.ts +30 -7
  72. package/dist/core/runner/denial-limit-arms.js +15 -9
  73. package/dist/core/runner/env-capability-census.js +1 -1
  74. package/dist/core/runner/gate-exit.d.ts +30 -4
  75. package/dist/core/runner/gate-exit.js +28 -7
  76. package/dist/core/runner/permission-rule-lanes.d.ts +9 -5
  77. package/dist/core/runner/permission-rule-lanes.js +44 -40
  78. package/dist/core/runner/persisted-path-rows.d.ts +64 -0
  79. package/dist/core/runner/persisted-path-rows.js +89 -0
  80. package/dist/core/runner/prepare-artifact.d.ts +5 -1
  81. package/dist/core/runner/prepare-artifact.js +9 -3
  82. package/dist/core/runner/prepare-ask-lane.d.ts +5 -3
  83. package/dist/core/runner/prepare-ask-lane.js +1 -1
  84. package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
  85. package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
  86. package/dist/core/runner/prepare-gate-stations.d.ts +33 -6
  87. package/dist/core/runner/prepare-gate-stations.js +32 -19
  88. package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
  89. package/dist/core/runner/prepare-hands-readface.js +64 -53
  90. package/dist/core/runner/prepare-inherited-gate.d.ts +22 -22
  91. package/dist/core/runner/prepare-inherited-gate.js +37 -23
  92. package/dist/core/runner/prepare-park-ask.d.ts +2 -2
  93. package/dist/core/runner/prepare-park-ask.js +2 -1
  94. package/dist/core/runner/prepare-policy-chain.d.ts +22 -2
  95. package/dist/core/runner/prepare-policy-chain.js +39 -12
  96. package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
  97. package/dist/core/runner/prepare-protocol-tools.js +13 -3
  98. package/dist/core/runner/prepare-run-refs.d.ts +6 -6
  99. package/dist/core/runner/prepare-run-refs.js +9 -4
  100. package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
  101. package/dist/core/runner/prepare-safety-scan.js +19 -3
  102. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
  103. package/dist/core/runner/prepare-suspend-saga.js +6 -6
  104. package/dist/core/runner/prepare-task.js +51 -22
  105. package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
  106. package/dist/core/runner/prepare-wiring-manifest.js +5 -3
  107. package/dist/core/runner/read-root-grant.d.ts +9 -0
  108. package/dist/core/runner/read-root-grant.js +12 -0
  109. package/dist/core/runner/retired-runner-deps.js +1 -0
  110. package/dist/core/runner/run-identity-wiring.js +1 -1
  111. package/dist/core/runner/run-leg.js +24 -15
  112. package/dist/core/runner/run-notification-lane.js +1 -0
  113. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
  114. package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
  115. package/dist/core/runner/run-turn-boundary.js +1 -1
  116. package/dist/core/runner/runtask.js +3 -0
  117. package/dist/core/runner/stream-mode-verb.d.ts +27 -0
  118. package/dist/core/runner/stream-mode-verb.js +19 -0
  119. package/dist/core/runner/turn-attachments.d.ts +3 -3
  120. package/dist/core/runner/turn-attachments.js +1 -1
  121. package/dist/core/runner/write-protection-station.d.ts +7 -1
  122. package/dist/core/runner/write-protection-station.js +6 -3
  123. package/dist/core/runner-deps.d.ts +31 -34
  124. package/dist/core/runtime-caps.d.ts +12 -3
  125. package/dist/core/safety-axis-vocab.d.ts +2 -2
  126. package/dist/core/safety-axis-vocab.js +1 -1
  127. package/dist/core/secret-env.d.ts +16 -2
  128. package/dist/core/secret-env.js +25 -1
  129. package/dist/core/sensitive-path-policy.d.ts +4 -2
  130. package/dist/core/sensitive-path-policy.js +39 -30
  131. package/dist/core/spec-contract.d.ts +15 -0
  132. package/dist/core/spec-contract.js +19 -10
  133. package/dist/core/task-event.d.ts +1 -1
  134. package/dist/core/task-result.d.ts +1 -1
  135. package/dist/core/task-spec.d.ts +41 -89
  136. package/dist/core/task-stream.d.ts +16 -0
  137. package/dist/core/terminal-cause.js +1 -0
  138. package/dist/core/tighten-task-spec.d.ts +2 -2
  139. package/dist/core/tighten-task-spec.js +12 -14
  140. package/dist/core/tool-catalog-entries.js +4 -3
  141. package/dist/core/tool-policy.d.ts +54 -25
  142. package/dist/core/tool-policy.js +43 -2
  143. package/dist/core/tool-registry.d.ts +2 -0
  144. package/dist/core/tool-registry.js +3 -0
  145. package/dist/core/tool-spec.d.ts +8 -9
  146. package/dist/core/trace.d.ts +7 -6
  147. package/dist/core/types.d.ts +2 -1
  148. package/dist/core/wiring-manifest.d.ts +31 -5
  149. package/dist/core/wiring-manifest.js +28 -1
  150. package/dist/core/write-protect.d.ts +11 -1
  151. package/dist/core/write-protect.js +26 -15
  152. package/dist/engine/execution-env/kill-tree.js +4 -1
  153. package/dist/engine/execution-env/node-execution-env.d.ts +3 -0
  154. package/dist/engine/execution-env/node-execution-env.js +34 -35
  155. package/dist/engine/harness/types.d.ts +6 -7
  156. package/dist/engine/llm/types.d.ts +2 -4
  157. package/dist/engine/loop/types.d.ts +7 -0
  158. package/dist/index.d.ts +15 -5
  159. package/dist/index.js +12 -3
  160. package/dist/orchestration/run-spec.d.ts +4 -0
  161. package/dist/orchestration/run-spec.js +4 -3
  162. package/dist/orchestration/workflow-governance.js +1 -1
  163. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  164. package/dist/orchestration/workflow-size-guideline.js +2 -2
  165. package/dist/stores/file/fs-atomic.js +2 -1
  166. package/dist/tools/artifact/artifact-tool.d.ts +17 -0
  167. package/dist/tools/artifact/artifact-tool.js +16 -1
  168. package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
  169. package/dist/tools/fs/bash-readonly-classifier.js +88 -0
  170. package/dist/tools/fs/containment-face.d.ts +9 -3
  171. package/dist/tools/fs/containment-face.js +2 -0
  172. package/dist/tools/fs/file-read-check.d.ts +49 -0
  173. package/dist/tools/fs/file-read-check.js +71 -0
  174. package/dist/tools/fs/fs-bash.d.ts +3 -3
  175. package/dist/tools/fs/fs-read.d.ts +1 -0
  176. package/dist/tools/fs/fs-search-tools.d.ts +2 -2
  177. package/dist/tools/fs/fs-search-tools.js +4 -4
  178. package/dist/tools/fs/fs-shared.js +20 -6
  179. package/dist/tools/fs/fs-write.d.ts +4 -3
  180. package/dist/tools/fs/fs-write.js +9 -9
  181. package/dist/tools/fs/index.d.ts +39 -3
  182. package/dist/tools/fs/index.js +38 -15
  183. package/dist/tools/fs/read-deny-home.d.ts +93 -0
  184. package/dist/tools/fs/read-deny-home.js +225 -0
  185. package/dist/tools/fs/read-deny.d.ts +176 -34
  186. package/dist/tools/fs/read-deny.js +268 -61
  187. package/dist/tools/fs/safety.d.ts +6 -1
  188. package/dist/tools/fs/safety.js +18 -5
  189. package/dist/tools/fs/search.d.ts +3 -0
  190. package/dist/tools/fs/search.js +47 -9
  191. package/dist/tools/fs/shell-read-check.d.ts +42 -11
  192. package/dist/tools/fs/shell-read-check.js +188 -13
  193. package/dist/tools/fs/shell-read-establish.d.ts +7 -3
  194. package/dist/tools/fs/shell-read-establish.js +134 -21
  195. package/dist/tools/fs/shell-read-identity.d.ts +7 -0
  196. package/dist/tools/fs/shell-read-identity.js +21 -0
  197. package/dist/tools/fs/shell-read-judge.d.ts +92 -32
  198. package/dist/tools/fs/shell-read-judge.js +43 -18
  199. package/dist/tools/fs/shell-read-plan.d.ts +4 -0
  200. package/dist/tools/fs/shell-read-plan.js +26 -4
  201. package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
  202. package/dist/tools/fs/shell-read-write-targets.js +173 -0
  203. package/package.json +3 -1
  204. package/test/export-surface.snapshot.json +103 -11
  205. package/wire-changes/core@7.33.8.json +165 -0
  206. package/wire-changes/core@7.34.0.json +2621 -0
@@ -22,19 +22,22 @@ import { violationDetails } from "./safety.js";
22
22
  import { type ShellReadPlan } from "./shell-read-plan.js";
23
23
  import { type EstablishedRead, type ShellReadBoundary, type ShellReadDisposition, type ShellReadFinding, type ShellReadSeat } from "./shell-read-judge.js";
24
24
  import type { ReadRootGrantCandidate } from "../../core/tool-spec.js";
25
- /** The ONE mint of the containment card — every arm that refuses or asks on a containment finding (a refusal, a
26
- * mandated card's own card, a station that could not answer, the pre-spawn re-establishment) spells it here. */
25
+ /** The ONE mint of the containment card — the targets a finding established OUTSIDE the roots (an out-of-root read, a
26
+ * target that resolves elsewhere by the spawn). */
27
27
  declare function containmentDetails(paths: readonly string[]): {
28
28
  readonly type: "readonly_out_of_root";
29
29
  readonly code: "readonly_out_of_root";
30
30
  readonly paths: readonly string[];
31
31
  };
32
- /** The structured details a refusal carries — the same two wire discriminants the shell tools' own
33
- * refusals carried before the station existed, so a consumer counting either code counts one thing: the deny
34
- * card is the read faces' own (`violationDetails`, safety.ts), the containment card is minted above. */
35
- export type ShellReadRefusalDetails = Extract<NonNullable<ReturnType<typeof violationDetails>>, {
36
- code: "read_path_denied";
37
- }> | ReturnType<typeof containmentDetails>;
32
+ /** The ONE mint of the card of a finding that established nothing: where the command reads could not be established
33
+ * (an unreadable or re-entered program text, a program named from its input, a station that could not answer). */
34
+ declare function unestablishedDetails(): {
35
+ readonly type: "read_unestablished";
36
+ readonly code: "read_unestablished";
37
+ };
38
+ /** The structured details a refusal or a mandated card carries: the deny card is the read faces' own (`violationDetails`,
39
+ * safety.ts), the containment and the unestablished cards are minted above. */
40
+ export type ShellReadRefusalDetails = NonNullable<ReturnType<typeof violationDetails>> | ReturnType<typeof containmentDetails> | ReturnType<typeof unestablishedDetails>;
38
41
  /** What the station answers for ONE call. */
39
42
  export interface ShellReadVerdict {
40
43
  readonly disposition: ShellReadDisposition;
@@ -43,12 +46,40 @@ export interface ShellReadVerdict {
43
46
  * sentence). `mandated` / `ordinary`: the ACCOUNT of the finding, for the approval card's disclosure seat
44
47
  * (never the ask's own message — that is the auto-mode classifier's input). Absent for `vouch`. */
45
48
  readonly sentence?: string;
46
- /** `refuse`: the refusal's structured details. `mandated` on an `outside_roots` / `unestablishable` finding: the
47
- * same containment card, so an ask a person refuses or nobody can answer still carries WHAT was found on
48
- * its `tool_end` (`readonly_out_of_root`), as the tool's own refusal of that finding did. */
49
+ /** `refuse`: the refusal's structured details. `mandated` on an `outside_roots` / `unestablishable` finding (and
50
+ * `ordinary` on the file tools' out-of-root read): the finding's card, so an ask a person refuses or nobody can answer
51
+ * still carries WHAT was found on its `tool_end` — the targets outside the roots (`readonly_out_of_root`, or the file
52
+ * tools' `path_not_in_root`), or that nothing could be established (`read_unestablished`). */
49
53
  readonly details?: ShellReadRefusalDetails;
50
54
  /** `mandated` on an `outside_roots` finding: the directory whose grant would clear this ask. */
51
55
  readonly readRootCandidate?: ReadRootGrantCandidate;
56
+ /** A FACT beside the disposition, for the mode that answers a file command itself: every write target this command
57
+ * names was established inside the boundary's WRITE roots at every seat the program writes it (the entry, where it
58
+ * lands, or both — `WriteSeat`, shell-read-write-targets.ts), off the write-protection table and off every deployment
59
+ * write guard (`ShellReadBoundary.writeProtectionJudge` / `writeTargetGuard`), no removal (`rm` / `rmdir`) targets a
60
+ * write root or a directory containing one, every program the command runs is in its text (no `sed -f` file, no
61
+ * re-entered shell, no unreadable text), and the line's order keeps each segment's establishment true
62
+ * ({@link lineOrderPlaced}). Absent ⇒ not established — the mode does not answer; the station's own table is unchanged. */
63
+ readonly writesPlaced?: true;
64
+ /** A FACT beside the disposition: a write target this command names lands on the write-protection table — judged by
65
+ * the table's own judge, as a write tool's target is — so the command's ask carries that table's class, whichever
66
+ * mode reads it. Absent ⇒ no target the station established is covered (or no judge is mounted). */
67
+ readonly writeProtected?: true;
68
+ /** A FACT beside `writesPlaced`'s absence: the command is one of the mode's file commands whose writes the station would
69
+ * have placed but for an OPTION WORD on a `cp` / `mv` segment ({@link UNPLACED_OPTIONS}: any word starting with `-` — the
70
+ * reference implementation's rule, "with flags requires manual approval to ensure path safety"). Names the first such
71
+ * program, so the approval card and the headless refusal can say WHY the mode did not answer. Absent ⇒ not that reason
72
+ * (or the writes were placed). */
73
+ readonly unplacedOptions?: {
74
+ readonly program: "cp" | "mv";
75
+ };
76
+ /** A FACT beside the disposition: some read of the command could not be established (an `unestablishable` or `declined`
77
+ * finding anywhere in it, whichever finding won). A mode that releases the call releases it past the read-deny table. */
78
+ readonly unestablished?: true;
79
+ /** The station's admission of this call's judged target, activated only by the caller that APPLIES this verdict (the
80
+ * gate's fold, the resumed leg's belt): a verdict nobody received — a wait that elapsed, a judgment that threw —
81
+ * admits nothing, whenever it completes. */
82
+ readonly admit?: () => void;
52
83
  }
53
84
  /** The station seat one tool call is judged on (the `rejudge` seat is asked for by the caller). */
54
85
  export type ShellReadStationSeat = Exclude<ShellReadSeat, "rejudge">;
@@ -1,17 +1,182 @@
1
1
  import { errorResult } from "../../core/tools.js";
2
2
  import { BASH_READONLY_CONFINEMENT_NOTE } from "./fs-shared.js";
3
- import { violationDetails } from "./safety.js";
3
+ import { isAbsoluteForFamily, normalizeAbsPathLexically, pathFamilyOf, violationDetails, withinAnyRoot } from "./safety.js";
4
4
  import { fitMiddle, READ_DENY_SENTENCE_PART_MAX, readDenySentence } from "./read-deny.js";
5
5
  import { NOT_AUTO_ALLOWED, programPositionRefusalSentence, readProgramPosition } from "./bash-program-position.js";
6
6
  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
- import { allFindings, decideShellRead, foldShellReadFindings, judgeEstablishedPlan, readRootGrantCandidateOf, } from "./shell-read-judge.js";
9
+ import { programArgs, spellsOption } from "./shell-read-write-targets.js";
10
+ import { allFindings, decideShellRead, denyRowAtOrBelow, foldShellReadFindings, judgeEstablishedPlan, readRootGrantCandidateOf, } from "./shell-read-judge.js";
10
11
  import { normalizeReadRootCandidate } from "../../core/read-root-candidate.js";
11
12
  import { argvOf, lexShellText } from "../../shell/lex.js";
12
13
  function containmentDetails(paths) {
13
14
  return { type: "readonly_out_of_root", code: "readonly_out_of_root", paths };
14
15
  }
16
+ function unestablishedDetails() {
17
+ return { type: "read_unestablished", code: "read_unestablished" };
18
+ }
19
+ function findingDetails(finding, paths) {
20
+ return finding === "unestablishable" || finding === "declined" ? unestablishedDetails() : containmentDetails(paths);
21
+ }
22
+ const REMOVAL_COMMANDS = new Set(["rm", "rmdir"]);
23
+ const WRITES_BENEATH = {
24
+ cp: () => true,
25
+ mv: () => true,
26
+ rm: (s) => s.argv.some((w) => spellsOption(w.folded, "recursive", "[rR]")),
27
+ };
28
+ const isPattern = (f) => f.read.candidate.expands !== false && f.read.candidate.expands !== "unresolvable";
29
+ const RESHAPES_TREE = new Set(["mv"]);
30
+ const anyOptionWord = (words) => words.some((w) => w.startsWith("-"));
31
+ const UNPLACED_OPTIONS = {
32
+ rmdir: (ws) => ws.some((w) => spellsOption(w, "parents", "p")),
33
+ sed: (ws) => ws.some((w) => spellsOption(w, "file", "f")),
34
+ cp: anyOptionWord,
35
+ mv: anyOptionWord,
36
+ };
37
+ function runOrderOf(judged) {
38
+ const runs = [];
39
+ const plans = [];
40
+ let concurrent = false;
41
+ const walk = (j) => {
42
+ plans.push(j);
43
+ for (const s of j.segments) {
44
+ s.nested.forEach(walk);
45
+ runs.push(s);
46
+ if (j.plan.shape.backgrounded || s.segment.connector === "|")
47
+ concurrent = true;
48
+ }
49
+ };
50
+ walk(judged);
51
+ return { runs, plans, concurrent };
52
+ }
53
+ function lineOrderPlaced(runs, concurrent) {
54
+ const reshapes = (s) => RESHAPES_TREE.has(s.segment.verb?.basename ?? "");
55
+ const writes = (s) => s.findings.some((f) => f.read.candidate.face === "write");
56
+ const stepsUp = (s) => s.findings.some((f) => f.read.candidate.spelled.split(/[\\/]/).includes(".."));
57
+ return runs.every((later, j) => runs.every((earlier, i) => i === j || (i > j && !concurrent) || (!reshapes(earlier) && !(writes(earlier) && stepsUp(later)))));
58
+ }
59
+ function writesPlacedOf(judged, boundary) {
60
+ const writeRoots = boundary.writeRoots;
61
+ if (writeRoots === undefined || writeRoots.length === 0)
62
+ return false;
63
+ const { runs, plans, concurrent } = runOrderOf(judged);
64
+ const placed = (s) => {
65
+ const verb = s.segment.verb?.basename ?? "";
66
+ if (s.segment.reentry === "shell" || s.segment.unreadProgramText !== undefined)
67
+ return false;
68
+ if (UNPLACED_OPTIONS[verb]?.(programArgs(s.segment).map((w) => w.spelled)) === true)
69
+ return false;
70
+ const beneath = WRITES_BENEATH[verb]?.(s.segment) === true;
71
+ for (const f of s.findings) {
72
+ if (f.read.candidate.face !== "write")
73
+ continue;
74
+ const target = f.read.target;
75
+ if (f.read.established === "unestablishable" || target === undefined || isPattern(f) || !withinAnyRoot(writeRoots, target))
76
+ return false;
77
+ if (beneath && denyRowAtOrBelow(f.read, boundary, !REMOVAL_COMMANDS.has(verb)))
78
+ return false;
79
+ if (REMOVAL_COMMANDS.has(verb) && writeRoots.some((root) => withinAnyRoot([target], root)))
80
+ return false;
81
+ }
82
+ return true;
83
+ };
84
+ return plans.every((p) => p.plan.unreadable === undefined) && runs.every(placed) && lineOrderPlaced(runs, concurrent);
85
+ }
86
+ function unplacedOptionsOf(judged) {
87
+ for (const s of judged.segments) {
88
+ const nested = s.nested.map(unplacedOptionsOf).find((u) => u !== undefined);
89
+ if (nested !== undefined)
90
+ return nested;
91
+ const verb = s.segment.verb?.basename;
92
+ if ((verb === "cp" || verb === "mv") && anyOptionWord(programArgs(s.segment).map((w) => w.spelled)))
93
+ return { program: verb };
94
+ }
95
+ return undefined;
96
+ }
97
+ async function writeGuardsOf(judged, boundary, placed, env, signal) {
98
+ const protection = boundary.writeProtectionJudge;
99
+ const guard = placed ? boundary.writeTargetGuard : undefined;
100
+ if (protection === undefined && guard === undefined)
101
+ return { unplaced: false, writeProtected: false };
102
+ const { runs, concurrent } = runOrderOf(judged);
103
+ const asKey = new Map();
104
+ const asSpelling = new Map();
105
+ const landingKeys = new Set();
106
+ const writtenBy = runs.map(() => []);
107
+ const add = (into, view, child, at) => {
108
+ if (!isAbsoluteForFamily(pathFamilyOf({ root: view }), view))
109
+ return;
110
+ const seen = into.get(view);
111
+ into.set(view, seen === undefined ? { child, at } : { child: seen.child && child, at: Math.max(seen.at, at) });
112
+ };
113
+ runs.forEach((s, at) => {
114
+ for (const f of s.findings) {
115
+ const written = f.read.written;
116
+ if (written === undefined || f.read.established === "unestablishable" || f.read.target === undefined)
117
+ continue;
118
+ const child = written.child === true;
119
+ const key = f.read.established === "canonical" || f.read.established === "expansion";
120
+ add(key ? asKey : asSpelling, f.read.target, child, at);
121
+ if (key && written.seat === "landing")
122
+ landingKeys.add(f.read.target);
123
+ if (written.seat === "landing")
124
+ for (const spelling of f.read.spellings)
125
+ add(asSpelling, spelling, child, at);
126
+ writtenBy[at].push(f.read.target, ...f.read.spellings);
127
+ }
128
+ });
129
+ const probes = new Map();
130
+ const probe = (p) => {
131
+ let r = probes.get(p);
132
+ if (r === undefined) {
133
+ r = env.canonicalPath(p, signal).catch(() => undefined);
134
+ probes.set(p, r);
135
+ }
136
+ return r;
137
+ };
138
+ const parentOf = (view) => normalizeAbsPathLexically(`${view}/..`);
139
+ const writable = async (view, v) => {
140
+ const at = await probe(view);
141
+ if (at === undefined || at.ok)
142
+ return true;
143
+ if (at.error.code === "not_directory")
144
+ return false;
145
+ if (!v.child || at.error.code !== "not_found")
146
+ return true;
147
+ const dest = await probe(parentOf(view));
148
+ return dest === undefined || dest.ok || (dest.error.code !== "not_found" && dest.error.code !== "not_directory");
149
+ };
150
+ const changedBefore = (view, at) => {
151
+ const fold = (p) => p.normalize("NFC").toLowerCase();
152
+ const dir = fold(parentOf(view));
153
+ return writtenBy.some((paths, i) => i !== at && (i < at || concurrent) && paths.some((w) => withinAnyRoot([fold(w)], dir) || withinAnyRoot([dir], fold(w))));
154
+ };
155
+ const answer = async (ask) => {
156
+ try {
157
+ return (await ask()) === true;
158
+ }
159
+ catch {
160
+ return undefined;
161
+ }
162
+ };
163
+ const asked = [...[...asKey].map(([view, v]) => [view, v, true]), ...[...asSpelling].filter(([view]) => !landingKeys.has(view)).map(([view, v]) => [view, v, false])];
164
+ let unplaced = false;
165
+ let writeProtected = false;
166
+ for (const [view, v, key] of asked) {
167
+ if (!(await writable(view, v)) && !changedBefore(view, v.at))
168
+ continue;
169
+ const covered = protection === undefined ? false : await answer(() => protection(view, { ...(signal !== undefined ? { signal } : {}), ...(key ? { canonical: true } : {}) }));
170
+ const guarded = guard === undefined ? false : await answer(() => guard(view, signal));
171
+ if (covered === true)
172
+ writeProtected = true;
173
+ if (covered !== false || guarded !== false)
174
+ unplaced = true;
175
+ if (writeProtected)
176
+ break;
177
+ }
178
+ return { unplaced, writeProtected };
179
+ }
15
180
  const ADMISSION_MAX = 1024;
16
181
  function shellCommandOf(args) {
17
182
  const command = args?.command;
@@ -76,14 +241,17 @@ function refusalOf(tool, fold, roots, paths) {
76
241
  const w = fold.witness;
77
242
  if (fold.finding === "deny_hit" && w?.deny !== undefined) {
78
243
  const target = w.read.target ?? w.deny.matchedView;
79
- const core = readDenySentence({ target, pattern: w.deny.pattern, matchedView: w.deny.matchedView });
80
- const sentence = `Error (${tool}): ${core} ${DENY_REFUSAL_NOTE}`;
81
- return { sentence, details: violationDetails({ code: "read_path_denied", message: sentence, target, pattern: w.deny.pattern }) };
244
+ const write = w.deny.face === "write";
245
+ const core = readDenySentence({ target, pattern: w.deny.pattern, matchedView: w.deny.matchedView, ...(write ? { face: "write" } : {}) });
246
+ const sentence = `Error (${tool}): ${core} ${write ? WRITE_DENY_REFUSAL_NOTE : DENY_REFUSAL_NOTE}`;
247
+ return { sentence, ...(write ? {} : { details: violationDetails({ code: "read_path_denied", message: sentence, target, pattern: w.deny.pattern }) }) };
82
248
  }
83
249
  const account = findingAccount(fold, roots);
84
250
  const sentence = `Error (${tool}): ${account}. ${BASH_READONLY_CONFINEMENT_NOTE}`;
85
- return { sentence, details: containmentDetails(paths) };
251
+ return { sentence, details: findingDetails(fold.finding, paths) };
86
252
  }
253
+ const WRITE_DENY_REFUSAL_NOTE = "The boundary judges this command's write targets — its output redirections and the operands of mkdir, touch, rm, " +
254
+ "rmdir, mv, cp, tee and an in-place sed — against the write deny list; a denied write is refused here rather than escalated.";
87
255
  const DENY_REFUSAL_NOTE = "The read boundary judges this command's written spelling and, where a filesystem can be consulted, the " +
88
256
  "paths it would actually read; a deny-listed read is refused here rather than escalated.";
89
257
  function verdictOf(tool, seat, fold, judged, boundary) {
@@ -91,17 +259,19 @@ function verdictOf(tool, seat, fold, judged, boundary) {
91
259
  if (disposition === "vouch")
92
260
  return { disposition, finding: fold.finding };
93
261
  const paths = outsidePathsOf(judged);
262
+ const unestablished = fold.unestablished === true ? { unestablished: true } : {};
94
263
  if (disposition === "refuse") {
95
264
  const r = refusalOf(tool, fold, boundary.roots, paths);
96
- return { disposition, finding: fold.finding, sentence: r.sentence, details: r.details };
265
+ return { disposition, finding: fold.finding, sentence: r.sentence, ...(r.details !== undefined ? { details: r.details } : {}) };
97
266
  }
98
267
  const grant = fold.finding === "outside_roots" ? readRootGrantCandidateOf(judged, boundary) : undefined;
99
268
  const containment = fold.finding === "outside_roots" || fold.finding === "unestablishable";
100
269
  return {
101
270
  disposition,
102
271
  finding: fold.finding,
272
+ ...unestablished,
103
273
  sentence: findingAccount(fold, boundary.roots),
104
- ...(disposition === "mandated" && containment ? { details: containmentDetails(paths) } : {}),
274
+ ...(disposition === "mandated" && containment ? { details: findingDetails(fold.finding, paths) } : {}),
105
275
  ...(grant !== undefined ? { readRootCandidate: { dir: grant.dir, ...(grant.covers !== undefined ? { covers: grant.covers } : {}), clearsThisAsk: true } } : {}),
106
276
  };
107
277
  }
@@ -137,17 +307,22 @@ async function applyAlsoInside(judged, boundary) {
137
307
  });
138
308
  return rewrite(judged);
139
309
  }
140
- async function runShellReadStation(tool, command, declared, seat, readers, establisher, signal) {
310
+ async function runShellReadStation(tool, command, declared, seat, readers, establisher, env, signal) {
141
311
  const plan = planShellReads(command, declarationsOf(declared, readers));
142
312
  const established = await establisher.establish(plan, declared, signal);
143
313
  const boundary = { ...declared, roots: established.roots };
144
314
  const judged = await applyAlsoInside(judgePlan(plan, established, boundary), boundary);
145
- const verdict = verdictOf(tool, seat, foldShellReadFindings(judged), judged, boundary);
315
+ const judgedVerdict = verdictOf(tool, seat, foldShellReadFindings(judged), judged, boundary);
316
+ const unplaced = unplacedOptionsOf(judged);
317
+ const placed = writesPlacedOf(judged, boundary);
318
+ const guarded = judgedVerdict.disposition === "refuse" ? { unplaced: false, writeProtected: false } : await writeGuardsOf(judged, boundary, placed, env, signal);
319
+ const facts = guarded.writeProtected ? { ...judgedVerdict, writeProtected: true } : judgedVerdict;
320
+ const verdict = placed && !guarded.unplaced ? { ...facts, writesPlaced: true } : unplaced !== undefined ? { ...facts, unplacedOptions: unplaced } : facts;
146
321
  return { plan, established, judged, boundary, verdict };
147
322
  }
148
323
  export async function judgeShellRead(input) {
149
324
  const readers = input.readers ?? new Set(bashClassifyDefaultAllow());
150
- const run = await runShellReadStation(input.tool ?? "Bash", input.command, input.boundary, input.seat, readers, createShellReadEstablisher(input.env, input.boundary.roots), input.signal);
325
+ const run = await runShellReadStation(input.tool ?? "Bash", input.command, input.boundary, input.seat, readers, createShellReadEstablisher(input.env, input.boundary.roots), input.env, input.signal);
151
326
  const offer = normalizeReadRootCandidate(run.verdict.readRootCandidate);
152
327
  return {
153
328
  plan: run.plan,
@@ -196,7 +371,7 @@ export function createShellReadStation(opts) {
196
371
  return null;
197
372
  const seat = callOpts.seat ?? opts.seatOf(toolName);
198
373
  return (async () => {
199
- const run = await runShellReadStation(toolName, command, boundary, seat, readers, establisherFor(boundary.roots), signal);
374
+ const run = await runShellReadStation(toolName, command, boundary, seat, readers, establisherFor(boundary.roots), opts.env, signal);
200
375
  if (run.verdict.disposition !== "refuse" && callOpts.toolCallId !== undefined)
201
376
  remember(callOpts.toolCallId, { toolName, boundary: run.boundary, established: run.established, command });
202
377
  return run.verdict;
@@ -207,7 +382,7 @@ export function createShellReadStation(opts) {
207
382
  const fold = { finding: "unestablishable" };
208
383
  const disposition = decideShellRead("unestablishable", seat);
209
384
  const account = `${cause}, so where this command reads could not be established ${NOT_AUTO_ALLOWED}`;
210
- const details = containmentDetails([]);
385
+ const details = unestablishedDetails();
211
386
  if (disposition === "refuse")
212
387
  return { disposition, finding: "unestablishable", sentence: `Error (${toolName}): ${account}. ${BASH_READONLY_CONFINEMENT_NOTE}`, details };
213
388
  return { disposition, finding: fold.finding, sentence: account, ...(disposition === "mandated" ? { details } : {}) };
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * ESTABLISHMENT — where a shell command's read candidates LAND once the filesystem has answered, as one
3
3
  * value the gate's shell read station holds per execution environment. The plan (`shell-read-plan.ts`)
4
- * decides WHICH words a command may read; this module decides WHERE each of them lands: the canonical
4
+ * decides WHICH words a command may read, `shell-read-write-targets.ts` which words it writes and at which seat;
5
+ * this module decides WHERE each of them lands: the canonical
5
6
  * target of a spelling (symlinks resolved, a `..` applied the way the kernel applies it), the expansion of
6
7
  * a pattern (shell pathname semantics over `env.listDir`), the members of a traversal root (a bounded
7
8
  * recursive enumeration, dot files included, symlinked directories not followed), and the fail-closed arm
@@ -11,7 +12,10 @@
11
12
  * Properties every consumer relies on:
12
13
  * 1. A NON-EXISTENT target is not a refusal — `cat nope.txt` keeps the shell's own "No such file". The
13
14
  * deepest EXISTING ancestor is canonicalized and the missing tail rejoined, so a symlinked PARENT still
14
- * cannot carry a target out.
15
+ * cannot carry a target out. For a WRITE target a component the resolver cannot find but the filesystem
16
+ * still holds (a link whose target does not exist) is not absent: a write through it creates the link's
17
+ * target, so the target is established where the link lands (read one hop at a time, bounded) — or not at
18
+ * all when the environment cannot read the link.
15
19
  * 2. An unanswerable filesystem manufactures no refusal on its own: a chain no ancestor answered degrades
16
20
  * to the lexical spelling on an environment that declares no authority for its resolver, and is
17
21
  * `unestablishable` on one that does (`canonicalPathIsAuthoritative`).
@@ -35,7 +39,7 @@
35
39
  */
36
40
  import type { ExecutionEnv } from "../../internal/harness-types.js";
37
41
  import type { ShellReadPlan, ShellReadSegment } from "./shell-read-plan.js";
38
- import { type EstablishedRead, type ShellReadBoundary } from "./shell-read-judge.js";
42
+ import type { EstablishedRead, ShellReadBoundary } from "./shell-read-judge.js";
39
43
  /**
40
44
  * How many directories one glob operand (or one traversal root) may enumerate, and how many names it may
41
45
  * match, before the leg gives up and answers `unestablishable`.
@@ -1,11 +1,12 @@
1
- import { normalizeAbsPathLexically, pathFamilyOf } from "./safety.js";
1
+ import { isAbsoluteForFamily, normalizeAbsPathLexically, pathFamilyOf } from "./safety.js";
2
2
  import { boundaryIsTheSandbox, canonicalPathIsAuthoritative } from "../../core/remote-env.js";
3
3
  import { resolveOperandSpellings } from "./bash-readonly-classifier.js";
4
- import { denyFloorShadowsOf } from "./shell-read-judge.js";
5
- import { movedReads } from "./shell-read-identity.js";
4
+ import { judgedCandidatesOf, sedBackupNames } from "./shell-read-write-targets.js";
5
+ import { movedReads, withHomeViews } from "./shell-read-identity.js";
6
6
  const GLOB_METACHAR_RE = /[*?[]/;
7
7
  export const MAX_GLOB_DIRS = 256;
8
8
  export const MAX_GLOB_MATCHES = 8192;
9
+ const WRITE_LINK_HOPS = 32;
9
10
  function compileGlobSegment(pattern) {
10
11
  if (pattern.includes("\\"))
11
12
  return undefined;
@@ -131,15 +132,25 @@ function lexicalReadsOf(candidate, boundary) {
131
132
  }
132
133
  return out;
133
134
  }
134
- function judgedCandidatesOf(segment) {
135
- const reads = segment.reads.filter((c) => c.provenance !== "write_redirect_target");
136
- return [...reads, ...denyFloorShadowsOf(reads)];
135
+ function unfollowedBackupReads(j, boundary, cause) {
136
+ 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
+ return [{ candidate: j.candidate, established: "unestablishable", spellings: [...new Set(spellings)], cause }];
138
+ }
139
+ function writtenAt(read, j, seat) {
140
+ if (read.candidate.face !== "write" || !(j.written ?? j.seats ?? ["landing"]).includes(seat))
141
+ return read;
142
+ return { ...read, written: { seat, ...(j.child === true ? { child: true } : {}) } };
137
143
  }
138
144
  export function establishShellReadsLexically(plan, boundary) {
139
145
  const reads = new Map();
140
146
  const nested = new Map();
141
147
  for (const segment of plan.segments) {
142
- reads.set(segment, judgedCandidatesOf(segment).flatMap((c) => lexicalReadsOf(c, boundary)));
148
+ reads.set(segment, judgedCandidatesOf(segment).flatMap((j) => {
149
+ if (j.followed !== undefined)
150
+ return unfollowedBackupReads({ ...j, followed: j.followed }, boundary, `"${j.followed.operand.raw}" names its backup through a link chain only the filesystem can follow`);
151
+ const seat = (j.written ?? j.seats ?? ["landing"]).includes("landing") ? "landing" : "entry";
152
+ return lexicalReadsOf(j.candidate, boundary).map((r) => writtenAt(r, j, seat));
153
+ }));
143
154
  for (const n of segment.nested)
144
155
  nested.set(n, establishShellReadsLexically(n, boundary));
145
156
  }
@@ -164,12 +175,23 @@ export function createShellReadEstablisher(env, readRoots, opts) {
164
175
  })();
165
176
  return await rootsOnce;
166
177
  };
167
- const resolveWithoutParentSteps = async (p, signal) => {
178
+ const resolveWithoutParentSteps = async (p, signal, writeTarget = false, linkHops = WRITE_LINK_HOPS) => {
168
179
  const parts = p.split(SEP);
169
180
  const tail = [];
170
181
  let allAbsent = true;
171
182
  while (parts.length > 0) {
172
- const canon = await env.canonicalPath(parts.join("/") || "/", signal);
183
+ const at = parts.join("/") || "/";
184
+ const canon = await env.canonicalPath(at, signal);
185
+ if (!canon.ok && canon.error.code === "not_found" && writeTarget && allAbsent) {
186
+ const entry = await env.fileInfo(at, signal);
187
+ const link = entry.ok && entry.value.kind === "symlink" && linkHops > 0 ? await env.readLink?.(at, signal) : undefined;
188
+ if (link?.ok === true) {
189
+ const landing = isAbsoluteForFamily(family, link.value) ? link.value : joinUnder(parts.slice(0, -1).join("/") || "/", link.value);
190
+ return await resolve([landing, ...[...tail].reverse()].join("/"), signal, true, linkHops - 1);
191
+ }
192
+ if (entry.ok || entry.error.code !== "not_found")
193
+ allAbsent = false;
194
+ }
173
195
  if (canon.ok) {
174
196
  if (tail.length === 0)
175
197
  return { value: canon.value, exact: true };
@@ -182,20 +204,20 @@ export function createShellReadEstablisher(env, readRoots, opts) {
182
204
  return undefined;
183
205
  };
184
206
  const parentOf = (canonical) => normalizeAbsPathLexically(`${canonical}/..`);
185
- const resolve = async (p, signal) => {
207
+ const resolve = async (p, signal, writeTarget = false, linkHops = WRITE_LINK_HOPS) => {
186
208
  const parts = p.split(SEP);
187
209
  const first = parts.findIndex((seg) => seg === "..");
188
210
  if (first < 0)
189
- return await resolveWithoutParentSteps(p, signal);
211
+ return await resolveWithoutParentSteps(p, signal, writeTarget, linkHops);
190
212
  const headEnd = parts.slice(0, first).reduce((n, seg) => n + seg.length + 1, 0);
191
- let head = await resolveWithoutParentSteps(p.slice(0, headEnd) || "/", signal);
213
+ let head = await resolveWithoutParentSteps(p.slice(0, headEnd) || "/", signal, writeTarget, linkHops);
192
214
  if (head === undefined)
193
215
  return undefined;
194
216
  let pending = [];
195
217
  const flush = async () => {
196
218
  if (pending.length === 0)
197
219
  return true;
198
- const next = await resolveWithoutParentSteps([head.value.replace(TRAILING_SEP, ""), ...pending].join("/"), signal);
220
+ const next = await resolveWithoutParentSteps([head.value.replace(TRAILING_SEP, ""), ...pending].join("/"), signal, writeTarget, linkHops);
199
221
  pending = [];
200
222
  if (next === undefined)
201
223
  return false;
@@ -342,8 +364,18 @@ export function createShellReadEstablisher(env, readRoots, opts) {
342
364
  const members = found.map(({ path, canonical, symlink }) => ({ path, ...(canonical !== undefined ? { canonical } : {}), node: (symlink && canonical !== undefined && listedDirs.has(canonical) ? "directory" : "file") }));
343
365
  return { ...(root !== undefined ? { root } : {}), members };
344
366
  };
345
- const canonicalReadOf = async (candidate, spelling, spellings, established, origin, canonicalKnown, signal) => {
346
- const real = canonicalKnown !== undefined ? { value: canonicalKnown, exact: true } : await resolve(spelling, signal);
367
+ const entryOf = async (spelling, signal) => {
368
+ const parts = spelling.split(SEP);
369
+ const leaf = parts.pop() ?? "";
370
+ if (leaf === "" || leaf === "." || leaf === ".." || parts.length === 0)
371
+ return await resolve(spelling, signal, true);
372
+ const parent = await resolve(parts.join("/") || "/", signal, true);
373
+ if (parent === undefined)
374
+ return undefined;
375
+ return { value: joinUnder(parent.value, leaf), exact: parent.exact, ...(parent.missing !== undefined ? { missing: parent.missing } : {}) };
376
+ };
377
+ const canonicalReadOf = async (candidate, spelling, spellings, established, origin, canonicalKnown, signal, seat = "landing") => {
378
+ const real = canonicalKnown !== undefined ? { value: canonicalKnown, exact: true } : seat === "entry" ? await entryOf(spelling, signal) : await resolve(spelling, signal, candidate.face === "write");
347
379
  if (real === undefined) {
348
380
  if (resolverIsAuthoritative)
349
381
  return { candidate, established: "unestablishable", spellings, ...(origin !== undefined ? { origin } : {}), cause: `"${spelling}" cannot be resolved to verify where it reads` };
@@ -354,10 +386,88 @@ export function createShellReadEstablisher(env, readRoots, opts) {
354
386
  }
355
387
  return { candidate, established, target: real.value, spellings, ...(origin !== undefined ? { origin } : {}) };
356
388
  };
389
+ const followedName = async (spelled, base, homeDir, signal) => {
390
+ let name = spelled;
391
+ for (let hop = 0; hop <= WRITE_LINK_HOPS; hop++) {
392
+ const at = resolveOperandSpellings(base === "" ? undefined : base, name, homeDir);
393
+ if (at === undefined)
394
+ return undefined;
395
+ const walked = at.spelled ?? at.folded;
396
+ if (TRAILING_SEP.test(walked))
397
+ return name;
398
+ const parts = walked.split(SEP);
399
+ const leaf = parts.pop() ?? "";
400
+ if (leaf === "" || leaf === "." || leaf === "..")
401
+ return name;
402
+ const parent = await resolve(parts.join("/") || "/", signal);
403
+ if (parent === undefined)
404
+ return undefined;
405
+ if (!parent.exact)
406
+ return parent.missing === true ? name : undefined;
407
+ const entry = await env.fileInfo(joinUnder(parent.value, leaf), signal);
408
+ if (!entry.ok)
409
+ return entry.error.code === "not_found" || entry.error.code === "not_directory" ? name : undefined;
410
+ if (entry.value.kind !== "symlink")
411
+ return name;
412
+ const link = await env.readLink?.(joinUnder(parent.value, leaf), signal);
413
+ if (link?.ok !== true)
414
+ return undefined;
415
+ const dir = name.slice(0, name.search(/[^/]*$/));
416
+ name = isAbsoluteForFamily(family, link.value) ? link.value : `${dir}${link.value}`;
417
+ }
418
+ return undefined;
419
+ };
420
+ const followedBackupReads = async (j, boundary, signal) => {
421
+ const { operand, suffix } = j.followed;
422
+ const bases = basesFor(operand, boundary);
423
+ const unfollowed = (cause) => unfollowedBackupReads(j, boundary, cause);
424
+ if (bases === "unmodelled")
425
+ return unfollowed(`"${operand.raw}" is relative to a working directory this command's text does not settle`);
426
+ const out = [];
427
+ for (const base of bases) {
428
+ const name = await followedName(operand.spelled, base, boundary.homeDir, signal);
429
+ if (name === undefined) {
430
+ out.push(...unfollowed(`"${operand.raw}" names its backup through a link chain that could not be read`));
431
+ continue;
432
+ }
433
+ for (const backup of sedBackupNames(name, suffix)) {
434
+ const at = resolveOperandSpellings(base === "" ? undefined : base, backup, boundary.homeDir);
435
+ if (at === undefined) {
436
+ out.push(...unfollowed(`"${backup}" cannot be placed against "${base}"`));
437
+ continue;
438
+ }
439
+ const spellings = at.spelled !== undefined ? [at.folded, at.spelled] : [at.folded];
440
+ for (const seat of j.seats ?? ["landing"])
441
+ out.push(writtenAt(await canonicalReadOf(j.candidate, at.spelled ?? at.folded, spellings, "canonical", undefined, undefined, signal, seat), j, seat));
442
+ }
443
+ }
444
+ return out;
445
+ };
357
446
  const establishSegment = async (segment, boundary, roots, signal) => {
358
447
  const out = [];
359
- for (const candidate of judgedCandidatesOf(segment)) {
448
+ for (const judged of judgedCandidatesOf(segment)) {
449
+ if (judged.followed !== undefined) {
450
+ out.push(...(await followedBackupReads({ ...judged, followed: judged.followed }, boundary, signal)));
451
+ continue;
452
+ }
453
+ const candidate = judged.candidate;
360
454
  const traversal = candidate.provenance === "traversal_root";
455
+ const resolvesItself = TRAILING_SEP.test(candidate.spelled) || [".", ".."].includes(candidate.spelled.split(SEP).pop() ?? "");
456
+ const landsToo = (ss) => (resolvesItself && ss.length > 0 && !ss.includes("landing") ? [...ss, "landing"] : ss);
457
+ const seats = judged.seats === undefined ? ["landing"] : landsToo(judged.seats);
458
+ const writtenSeats = { ...judged, written: judged.written === undefined ? seats : landsToo(judged.written) };
459
+ const atSeats = async (spelling, spellings, established, origin, canonicalKnown) => {
460
+ const reads = [];
461
+ for (const seat of seats) {
462
+ const read = writtenAt(await canonicalReadOf(candidate, spelling, spellings, established, origin, canonicalKnown, signal, seat), writtenSeats, seat);
463
+ const same = reads.findIndex((r) => r.established === read.established && r.target === read.target);
464
+ if (same < 0)
465
+ reads.push(read);
466
+ else if (reads[same].written === undefined && read.written !== undefined)
467
+ reads[same] = read;
468
+ }
469
+ return reads;
470
+ };
361
471
  const land = async (read, spellings, folded) => {
362
472
  if (!traversal || (read.established !== "canonical" && read.established !== "expansion") || read.target === undefined) {
363
473
  out.push(read);
@@ -371,7 +481,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
371
481
  }
372
482
  out.push({ ...read, ...(walkedTree.root !== undefined ? { node: walkedTree.root } : {}) });
373
483
  for (const m of walkedTree.members)
374
- out.push({ ...(await canonicalReadOf(candidate, m.path, [m.path], "expansion", folded, m.canonical, signal)), node: m.node });
484
+ out.push({ ...(await canonicalReadOf(candidate, m.path, [m.path], "expansion", folded, m.canonical, signal)), node: m.node, walked: true });
375
485
  };
376
486
  for (const lexical of lexicalReadsOf(candidate, boundary)) {
377
487
  if (lexical.established !== "lexical") {
@@ -393,14 +503,17 @@ export function createShellReadEstablisher(env, readRoots, opts) {
393
503
  if (expanded !== undefined && expanded.paths.length > 0) {
394
504
  matched = true;
395
505
  for (const t of expanded.paths)
396
- await land(await canonicalReadOf(candidate, t, [t, folded], "expansion", folded, expanded.canonical.get(t), signal), spellings, folded);
506
+ for (const read of await atSeats(t, [t, folded], "expansion", folded, expanded.canonical.get(t)))
507
+ await land(read, spellings, folded);
397
508
  }
398
509
  }
399
510
  if (!matched)
400
- await land(await canonicalReadOf(candidate, walked, spellings, "canonical", undefined, undefined, signal), spellings, folded);
511
+ for (const read of await atSeats(walked, spellings, "canonical", undefined, undefined))
512
+ await land(read, spellings, folded);
401
513
  continue;
402
514
  }
403
- await land(await canonicalReadOf(candidate, walked, spellings, "canonical", undefined, undefined, signal), spellings, folded);
515
+ for (const read of await atSeats(walked, spellings, "canonical", undefined, undefined))
516
+ await land(read, spellings, folded);
404
517
  }
405
518
  }
406
519
  return out;
@@ -421,7 +534,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
421
534
  if (lexicalOnly)
422
535
  return establishShellReadsLexically(plan, boundary);
423
536
  const roots = await canonicalRoots(signal);
424
- return await establishPlan(plan, { ...boundary, roots }, roots, signal);
537
+ return await withHomeViews(env, await establishPlan(plan, { ...boundary, roots }, roots, signal), signal);
425
538
  },
426
539
  reestablish: async (previous, boundary, signal) => {
427
540
  if (lexicalOnly)
@@ -9,5 +9,12 @@
9
9
  */
10
10
  import type { EstablishedPlan } from "./shell-read-establish.js";
11
11
  import type { EstablishedRead } from "./shell-read-judge.js";
12
+ import { type HomeViewEnv } from "./read-deny-home.js";
12
13
  /** Every read of either establishment whose candidate's settled target set differs between the two. */
13
14
  export declare function movedReads(previous: EstablishedPlan, again: EstablishedPlan): EstablishedRead[];
15
+ /**
16
+ * The establishment with every read that landed carrying its HOME VIEW (read-deny-home.ts) among its spellings, nested
17
+ * plans included: the deny judge then places a home-anchored row wherever the target IS below the home, whatever it is
18
+ * spelled through. A read that did not land (lexical, unestablishable) is left as it is.
19
+ */
20
+ export declare function withHomeViews(env: HomeViewEnv, established: EstablishedPlan, signal?: AbortSignal): Promise<EstablishedPlan>;