@sema-agent/core 7.33.8 → 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 (197) hide show
  1. package/CHANGELOG.md +339 -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/sensitive-path-policy.d.ts +4 -2
  128. package/dist/core/sensitive-path-policy.js +39 -30
  129. package/dist/core/task-event.d.ts +1 -1
  130. package/dist/core/task-result.d.ts +1 -1
  131. package/dist/core/task-spec.d.ts +41 -89
  132. package/dist/core/task-stream.d.ts +16 -0
  133. package/dist/core/terminal-cause.js +1 -0
  134. package/dist/core/tighten-task-spec.d.ts +2 -2
  135. package/dist/core/tighten-task-spec.js +12 -14
  136. package/dist/core/tool-catalog-entries.js +4 -3
  137. package/dist/core/tool-policy.d.ts +54 -25
  138. package/dist/core/tool-policy.js +43 -2
  139. package/dist/core/tool-registry.d.ts +2 -0
  140. package/dist/core/tool-registry.js +3 -0
  141. package/dist/core/tool-spec.d.ts +8 -9
  142. package/dist/core/trace.d.ts +7 -6
  143. package/dist/core/types.d.ts +2 -1
  144. package/dist/core/wiring-manifest.d.ts +31 -5
  145. package/dist/core/wiring-manifest.js +28 -1
  146. package/dist/core/write-protect.d.ts +11 -1
  147. package/dist/core/write-protect.js +26 -15
  148. package/dist/engine/execution-env/node-execution-env.d.ts +3 -0
  149. package/dist/engine/execution-env/node-execution-env.js +11 -1
  150. package/dist/engine/harness/types.d.ts +6 -7
  151. package/dist/engine/llm/types.d.ts +2 -4
  152. package/dist/engine/loop/types.d.ts +7 -0
  153. package/dist/index.d.ts +15 -5
  154. package/dist/index.js +12 -3
  155. package/dist/orchestration/workflow-governance.js +1 -1
  156. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  157. package/dist/orchestration/workflow-size-guideline.js +2 -2
  158. package/dist/tools/artifact/artifact-tool.d.ts +17 -0
  159. package/dist/tools/artifact/artifact-tool.js +16 -1
  160. package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
  161. package/dist/tools/fs/bash-readonly-classifier.js +88 -0
  162. package/dist/tools/fs/containment-face.d.ts +9 -3
  163. package/dist/tools/fs/containment-face.js +2 -0
  164. package/dist/tools/fs/file-read-check.d.ts +49 -0
  165. package/dist/tools/fs/file-read-check.js +71 -0
  166. package/dist/tools/fs/fs-bash.d.ts +3 -3
  167. package/dist/tools/fs/fs-read.d.ts +1 -0
  168. package/dist/tools/fs/fs-search-tools.d.ts +2 -2
  169. package/dist/tools/fs/fs-search-tools.js +4 -4
  170. package/dist/tools/fs/fs-shared.js +20 -6
  171. package/dist/tools/fs/fs-write.d.ts +4 -3
  172. package/dist/tools/fs/fs-write.js +9 -9
  173. package/dist/tools/fs/index.d.ts +39 -3
  174. package/dist/tools/fs/index.js +38 -15
  175. package/dist/tools/fs/read-deny-home.d.ts +93 -0
  176. package/dist/tools/fs/read-deny-home.js +225 -0
  177. package/dist/tools/fs/read-deny.d.ts +176 -34
  178. package/dist/tools/fs/read-deny.js +268 -61
  179. package/dist/tools/fs/safety.d.ts +6 -1
  180. package/dist/tools/fs/safety.js +18 -5
  181. package/dist/tools/fs/search.d.ts +3 -0
  182. package/dist/tools/fs/search.js +47 -9
  183. package/dist/tools/fs/shell-read-check.d.ts +42 -11
  184. package/dist/tools/fs/shell-read-check.js +188 -13
  185. package/dist/tools/fs/shell-read-establish.d.ts +7 -3
  186. package/dist/tools/fs/shell-read-establish.js +134 -21
  187. package/dist/tools/fs/shell-read-identity.d.ts +7 -0
  188. package/dist/tools/fs/shell-read-identity.js +21 -0
  189. package/dist/tools/fs/shell-read-judge.d.ts +92 -32
  190. package/dist/tools/fs/shell-read-judge.js +43 -18
  191. package/dist/tools/fs/shell-read-plan.d.ts +4 -0
  192. package/dist/tools/fs/shell-read-plan.js +26 -4
  193. package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
  194. package/dist/tools/fs/shell-read-write-targets.js +173 -0
  195. package/package.json +1 -1
  196. package/test/export-surface.snapshot.json +103 -11
  197. package/wire-changes/core@7.34.0.json +2621 -0
@@ -1,3 +1,4 @@
1
+ import { homeViewsOf } from "./read-deny-home.js";
1
2
  export function movedReads(previous, again) {
2
3
  const before = targetsByCandidate(previous);
3
4
  const after = targetsByCandidate(again);
@@ -37,3 +38,23 @@ function targetsByCandidate(e) {
37
38
  function sameTargets(a, b) {
38
39
  return a.size === b.size && [...a].every((t) => b.has(t));
39
40
  }
41
+ export async function withHomeViews(env, established, signal) {
42
+ const views = homeViewsOf(env, signal);
43
+ const place = async (p) => {
44
+ const reads = new Map();
45
+ for (const [segment, rs] of p.reads) {
46
+ const out = [];
47
+ for (const r of rs) {
48
+ const landed = r.target !== undefined && (r.established === "canonical" || r.established === "expansion");
49
+ const view = landed ? await views.of(r.target, r.node === "file" ? { leaf: "file" } : undefined) : undefined;
50
+ out.push(view === undefined || r.spellings.includes(view) ? r : { ...r, spellings: [...r.spellings, view] });
51
+ }
52
+ reads.set(segment, out);
53
+ }
54
+ const nested = new Map();
55
+ for (const [n, np] of p.nested)
56
+ nested.set(n, await place(np));
57
+ return { ...p, reads, nested };
58
+ };
59
+ return await place(established);
60
+ }
@@ -12,22 +12,25 @@
12
12
  * · `foldShellReadFindings` folds a command's findings — every segment, every nested plan — to ONE word by
13
13
  * the priority `identity_changed > deny_hit > outside_roots > unestablishable > declined > none`.
14
14
  * · `SHELL_READ_DECISION` is the total table (finding × seat → disposition) that is the ONLY source of
15
- * "refuse / mandated ask / ordinary ask / vouch" for a shell read. A seat is a TRUST POLICY word, not a
16
- * tool name: the four seats differ on one row (`declined`) and nowhere else.
15
+ * "refuse / mandated ask / ordinary ask / vouch" for a shell read. A seat is a FACE word, not a tool name
16
+ * and not a permission mode: which shell face the call runs on (the full shell, the read-only shell) or
17
+ * which moment judges it (the re-judge after approval). The permission mode is not read here — what a
18
+ * mode does with the ask this table mints is decided once, downstream, where every ask is arbitrated.
17
19
  *
18
20
  * The floor this module is (`shell.read_boundary.friction_floor`) is stated on {@link judgeRead}.
19
21
  */
20
22
  import type { BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
21
23
  import type { ReadCandidate, ShellReadPlan, ShellReadSegment } from "./shell-read-plan.js";
24
+ import type { WriteSeat } from "./shell-read-write-targets.js";
22
25
  import type { AssertAllKeysHandled } from "../../core/ask-origin.js";
23
26
  /** WHAT the judge found about a command — a closed set, registered in `docs/CLOSED-SETS.md`. */
24
27
  export declare const SHELL_READ_FINDINGS: readonly ["deny_hit", "outside_roots", "unestablishable", "declined", "identity_changed", "none"];
25
28
  export type ShellReadFinding = (typeof SHELL_READ_FINDINGS)[number];
26
- /** WHO is asking — the trust-policy seats the decision table is keyed on. `bash_off` covers the `off` and
27
- * `always` doctrines (and a bypass mode); `bash_classify` the `classify` doctrine; `readonly` the read-only
28
- * shell face (`handsReadOnly`); `rejudge` the post-approval re-judgment of rewritten bytes and the
29
- * pre-spawn identity re-check. */
30
- export declare const SHELL_READ_SEATS: readonly ["bash_off", "bash_classify", "readonly", "rejudge"];
29
+ /** WHERE a read is judged — the seats the decision table is keyed on, one per FACE: `full_shell` the
30
+ * writable shell (`Bash`, `Monitor`) under every permission mode and every shell-gate floor; `readonly`
31
+ * the read-only shell face (`handsReadOnly`); `rejudge` the post-approval re-judgment of rewritten bytes
32
+ * and the pre-spawn identity re-check. */
33
+ export declare const SHELL_READ_SEATS: readonly ["full_shell", "readonly", "rejudge"];
31
34
  export type ShellReadSeat = (typeof SHELL_READ_SEATS)[number];
32
35
  /** The fold priority: a command's finding is the highest-ranked finding of any candidate in it. */
33
36
  export declare const SHELL_READ_FINDING_RANK: {
@@ -47,56 +50,51 @@ export type RankTableCoversEveryFinding = AssertAllKeysHandled<Exclude<ShellRead
47
50
  export type ShellReadDisposition = "refuse" | "mandated" | "ordinary" | "vouch";
48
51
  /**
49
52
  * THE decision table — total over findings × seats; no cell is n/a. Row by row:
50
- * · `deny_hit` — refused on every seat: a deny row is a floor no root grant and no approval moves.
51
- * · `outside_roots` — asked where a person can be asked (the two full-shell seats), refused where none
52
- * can (the read-only face has no approval channel; a rewrite that leaves the roots after approval was
53
- * seen by nobody).
53
+ * · `deny_hit` — refused on every seat, for a spelling the station can establish: a deny row is a floor
54
+ * no root grant and no approval moves. A spelling the station cannot establish is not a deny hit (it is
55
+ * the `unestablishable` row) — the floor holds what it can place, and says so rather than guessing.
56
+ * · `outside_roots` — asked where a person can be asked (the full shell), refused where none can (the
57
+ * read-only face has no approval channel; a rewrite that leaves the roots after approval was seen by
58
+ * nobody).
54
59
  * · `unestablishable` — the same shape: the floor could not say where the command reads, so it is not
55
60
  * vouched anywhere; a person may still answer it on the full shell.
56
- * · `declined` — the one row the seats disagree on, and the one deliberate fail-open of this table: the
57
- * scan could not read the program bash will run or the operands arrive on a stream. The `off` doctrine
58
- * mounts nothing else that could ask, so it mandates; the `classify` doctrine composes a shape classifier
59
- * beside this station and the ruled trade is an ORDINARY ask there — an answerer may settle it, and an
60
- * out-of-root read hidden inside such a command is then not asked again. The read-only face refuses:
61
- * its allowlist is a deployment knob, so this cell is reachable and must not be n/a.
61
+ * · `declined` — the one deliberate fail-open of this table: the scan could not read the program bash
62
+ * will run, or the operands arrive on a stream. On the full shell it is an ORDINARY ask — an answerer
63
+ * may settle it, and an out-of-root read hidden inside such a command is then not asked again. The
64
+ * read-only face refuses: its allowlist is a deployment knob, so this cell is reachable and must not be
65
+ * n/a.
62
66
  * · `identity_changed` — a candidate established at approval time resolves elsewhere at spawn time:
63
67
  * refused everywhere (it is only ever produced on the `rejudge` seat).
64
- * · `none` — vouched: the station has no question (the classify seat's shape classifier may still ask).
68
+ * · `none` — vouched: the station has no question (the full shell's shape tier may still ask).
65
69
  */
66
70
  export declare const SHELL_READ_DECISION: {
67
71
  readonly deny_hit: {
68
- readonly bash_off: "refuse";
69
- readonly bash_classify: "refuse";
72
+ readonly full_shell: "refuse";
70
73
  readonly readonly: "refuse";
71
74
  readonly rejudge: "refuse";
72
75
  };
73
76
  readonly outside_roots: {
74
- readonly bash_off: "mandated";
75
- readonly bash_classify: "mandated";
77
+ readonly full_shell: "mandated";
76
78
  readonly readonly: "refuse";
77
79
  readonly rejudge: "refuse";
78
80
  };
79
81
  readonly unestablishable: {
80
- readonly bash_off: "mandated";
81
- readonly bash_classify: "mandated";
82
+ readonly full_shell: "mandated";
82
83
  readonly readonly: "refuse";
83
84
  readonly rejudge: "refuse";
84
85
  };
85
86
  readonly declined: {
86
- readonly bash_off: "mandated";
87
- readonly bash_classify: "ordinary";
87
+ readonly full_shell: "ordinary";
88
88
  readonly readonly: "refuse";
89
89
  readonly rejudge: "refuse";
90
90
  };
91
91
  readonly identity_changed: {
92
- readonly bash_off: "refuse";
93
- readonly bash_classify: "refuse";
92
+ readonly full_shell: "refuse";
94
93
  readonly readonly: "refuse";
95
94
  readonly rejudge: "refuse";
96
95
  };
97
96
  readonly none: {
98
- readonly bash_off: "vouch";
99
- readonly bash_classify: "vouch";
97
+ readonly full_shell: "vouch";
100
98
  readonly readonly: "vouch";
101
99
  readonly rejudge: "vouch";
102
100
  };
@@ -106,7 +104,9 @@ export type DecisionTableCoversEveryFinding = AssertAllKeysHandled<Exclude<Shell
106
104
  /** Compile-time fence: `never` while every seat has a cell on the `none` row (the `satisfies` above holds
107
105
  * every other row to the same shape). */
108
106
  export type DecisionTableCoversEverySeat = AssertAllKeysHandled<Exclude<ShellReadSeat, keyof (typeof SHELL_READ_DECISION)["none"]>>;
109
- /** The disposition of one command-level finding on one seat — the table, read. */
107
+ /** The disposition of one command-level finding on one seat — the table, read. A seat word outside
108
+ * {@link SHELL_READ_SEATS} (an untyped caller, or one still passing a retired word) is refused loudly: an
109
+ * absent disposition would read as "no verdict", which no caller can act on safely. */
110
110
  export declare function decideShellRead(finding: ShellReadFinding, seat: ShellReadSeat): ShellReadDisposition;
111
111
  /**
112
112
  * The boundary a shell read is judged against: the declared roots (canonical), the working directory and
@@ -116,6 +116,27 @@ export declare function decideShellRead(finding: ShellReadFinding, seat: ShellRe
116
116
  */
117
117
  export interface ShellReadBoundary extends BashReadonlyRootBoundary {
118
118
  readonly alsoInside?: (canonicalTarget: string) => Promise<boolean>;
119
+ /** The deny table's WRITE face (`compileWriteDeny`, read-deny.ts): judges a command's write targets (candidates marked
120
+ * `face: "write"`) and returns the matched row's rule, or null. PRESENT-IFF there is something to judge. */
121
+ readonly writeDenyMatch?: (resolvedPath: string) => string | null;
122
+ /** The read table's rows AT or BELOW a path (`readDenySeat`'s `denyAtOrBelow`, read-deny.ts): read by the station's
123
+ * `writesPlaced` fact only, through {@link denyRowAtOrBelow}. */
124
+ readonly denyAtOrBelow?: (resolvedPath: string) => string | null;
125
+ /** The WRITE roots (canonical) — where a write may land without a question under a mode that accepts file commands
126
+ * (the task root and every additional working directory; never a read-only grant). Read for the station's
127
+ * `writesPlaced` fact only; the read judgment never reads it. Absent ⇒ no write is placed. */
128
+ readonly writeRoots?: readonly string[];
129
+ /** The write-protection TABLE's judge — the one a write tool's target meets (its spelling, its real target and every hop
130
+ * of its symlink chain): `true` ⇔ a row covers this ABSOLUTE write target; `canonical` ⇔ the path is a key the station
131
+ * established (judged as a key, never as a spelling). Read by the station's write facts only: a covered target is not
132
+ * placed, and the verdict says it was found ({@link import("./shell-read-check.js").ShellReadVerdict.writeProtected}). */
133
+ readonly writeProtectionJudge?: (target: string, opts?: {
134
+ readonly signal?: AbortSignal;
135
+ readonly canonical?: true;
136
+ }) => Promise<boolean>;
137
+ /** The deployment's write-target guard (`ToolPolicy.writeTargetGuard`, every policy of the leg's chain): `true` ⇔ a
138
+ * policy guards this ABSOLUTE write target. Read by the station's `writesPlaced` fact only: a guarded target is not placed. */
139
+ readonly writeTargetGuard?: (target: string, signal?: AbortSignal) => Promise<boolean>;
119
140
  }
120
141
  /** How far one candidate got before it was judged. `lexical` = the folded spelling is the judged target
121
142
  * (an isolated sandbox, or a resolver that declares no authority and did not answer); `canonical` = the
@@ -136,18 +157,31 @@ export interface EstablishedRead {
136
157
  /** What the environment said the target IS, where a read asked — only a traversal asks (it lists the root and
137
158
  * every member it walks; a link's answer is its target's). Absent = nothing asked; a plain read never lists. */
138
159
  readonly node?: "directory" | "file";
160
+ /** A MEMBER a traversal enumerated below its root — never a spelling of the command. The deny judge reads it
161
+ * ({@link import("./read-deny.js").ReadDenyMatcher.matchPath} `walked`): the segment rows still judge it, a
162
+ * rule-DSL row does not. Absent on the root itself, on a pattern's own matches and on every spelled operand. */
163
+ readonly walked?: true;
139
164
  /** Why the candidate could not be established. */
140
165
  readonly cause?: string;
166
+ /** WHERE the command's program writes this target — present only on a write target the program itself writes, at the
167
+ * seat this read was established at (`landing`: through the path's links; `entry`: the directory entry, a link there
168
+ * not followed). Absent on a read, and on a write-face target the program only names (a copy's source). `child`: the
169
+ * candidate's own mark ({@link import("./shell-read-write-targets.js").JudgedCandidate.child}), carried to the read. */
170
+ readonly written?: {
171
+ readonly seat: WriteSeat;
172
+ readonly child?: true;
173
+ };
141
174
  }
142
175
  /** What the judge found about ONE candidate. `kind` is one of the four candidate-level findings
143
176
  * (`declined` is a segment fact, `identity_changed` a re-establishment fact — neither is a candidate's). */
144
177
  export interface ReadFinding {
145
178
  readonly read: EstablishedRead;
146
179
  readonly kind: Extract<ShellReadFinding, "deny_hit" | "outside_roots" | "unestablishable" | "none">;
147
- /** The deny row that matched and the view it matched on. */
180
+ /** The deny row that matched, the view it matched on, and — for a write target — the face (`write`) that refused. */
148
181
  readonly deny?: {
149
182
  readonly pattern: string;
150
183
  readonly matchedView: string;
184
+ readonly face?: "write";
151
185
  };
152
186
  readonly containment: "inside" | "outside" | "open" | "unknown";
153
187
  }
@@ -173,6 +207,17 @@ export interface ReadFinding {
173
207
  * neither is a containment candidate — both still meet the deny judge, on every spelling.
174
208
  */
175
209
  export declare function judgeRead(read: EstablishedRead, boundary: ShellReadBoundary): ReadFinding;
210
+ /**
211
+ * Does a deny row lie AT or BELOW this write target, on any view the judge reads? Then a command that moves, copies or
212
+ * removes the target changes what the row covers, and a mode must not answer it unasked. WHICH tables answer is what the
213
+ * command does to the content below the target: a move or a copy carries it somewhere else (`mv .docker d` leaves
214
+ * `d/config.json` outside `.docker/config.json`; `mv <home> x` takes every home-anchored row's file along), so the READ
215
+ * table's rows answer too (`carriesContent`); a removal carries nothing anywhere — it is a write and nothing else, and
216
+ * only the write table answers it (the reference implementation's path check reads the read rows for a copy's or a
217
+ * move's sources and for sed, never for `rm` / `rmdir`). The read table answers per row (`denyAtOrBelow`); the write
218
+ * table's rows are rule rows, whose reach below a path is not enumerable, so any of them answers.
219
+ */
220
+ export declare function denyRowAtOrBelow(read: EstablishedRead, boundary: ShellReadBoundary, carriesContent: boolean): boolean;
176
221
  /**
177
222
  * The deny floor of every home word the plan does not place: each candidate carrying a `floorSpelled` (the word
178
223
  * spelled with the DECLARED home) gets a SHADOW candidate at that spelling, established exactly as a read is — the
@@ -197,6 +242,8 @@ export interface ShellReadFold {
197
242
  readonly finding: ShellReadFinding;
198
243
  readonly witness?: ReadFinding;
199
244
  readonly declined?: ShellReadSegment;
245
+ /** Some read of the text could not be established (an `unestablishable` or `declined` finding anywhere, winning or not). */
246
+ readonly unestablished?: true;
200
247
  }
201
248
  /**
202
249
  * Fold a judged plan to ONE finding by {@link SHELL_READ_FINDING_RANK}, over every segment and every nested
@@ -215,6 +262,9 @@ export declare function foldShellReadFindings(judged: JudgedPlan): ShellReadFold
215
262
  export declare function judgeEstablishedPlan(plan: ShellReadPlan, established: ReadonlyMap<ShellReadSegment, readonly EstablishedRead[]>, boundary: ShellReadBoundary, nestedOf: (plan: ShellReadPlan) => JudgedPlan): JudgedPlan;
216
263
  /** Every finding of a judged plan, flattened (segments and nested plans alike). */
217
264
  export declare function allFindings(judged: JudgedPlan): ReadFinding[];
265
+ /** One lexical step up, family-aware. A family ROOT is its own parent — the fixpoint the climb below ends
266
+ * on, and the one spelling this file can recognise as "the whole filesystem" without I/O. */
267
+ export declare function parentDirLexically(p: string): string;
218
268
  /**
219
269
  * The deepest directory containing both, or `undefined` when they share none (two path families).
220
270
  *
@@ -238,5 +288,15 @@ export declare function readRootGrantCandidateOf(judged: JudgedPlan, boundary: S
238
288
  readonly dir: string;
239
289
  readonly covers?: "exact";
240
290
  } | undefined;
291
+ /**
292
+ * The grant a read-root card offers for the directory `candidate` it would add. A directory that would hand out more
293
+ * than was read is not offered: a family root grants the whole filesystem, and one CONTAINING the declared home or a
294
+ * root grants the workspace. ONE fallback for both rather than no offer at all — the single path that WAS read
295
+ * (`single`), named VERBATIM (`covers: "exact"`); with several paths read, no offer.
296
+ */
297
+ export declare function grantOfferFor(candidate: string, single: string | undefined, bounds: Pick<ShellReadBoundary, "roots" | "homeDir">): {
298
+ dir: string;
299
+ covers?: "exact";
300
+ } | undefined;
241
301
  /** The same judged plan under another boundary — the established reads are kept, only the judgment reruns. */
242
302
  export declare function rejudge(judged: JudgedPlan, boundary: ShellReadBoundary): JudgedPlan;
@@ -2,7 +2,7 @@ import { hasWin32AliasedComponent, normalizeAbsPathLexically, withinAnyRoot } fr
2
2
  import { globInWord } from "./bash-readonly-classifier.js";
3
3
  import { readOfText } from "./shell-read-word.js";
4
4
  export const SHELL_READ_FINDINGS = ["deny_hit", "outside_roots", "unestablishable", "declined", "identity_changed", "none"];
5
- export const SHELL_READ_SEATS = ["bash_off", "bash_classify", "readonly", "rejudge"];
5
+ export const SHELL_READ_SEATS = ["full_shell", "readonly", "rejudge"];
6
6
  export const SHELL_READ_FINDING_RANK = {
7
7
  identity_changed: 6,
8
8
  deny_hit: 5,
@@ -12,27 +12,36 @@ export const SHELL_READ_FINDING_RANK = {
12
12
  none: 0,
13
13
  };
14
14
  export const SHELL_READ_DECISION = {
15
- deny_hit: { bash_off: "refuse", bash_classify: "refuse", readonly: "refuse", rejudge: "refuse" },
16
- outside_roots: { bash_off: "mandated", bash_classify: "mandated", readonly: "refuse", rejudge: "refuse" },
17
- unestablishable: { bash_off: "mandated", bash_classify: "mandated", readonly: "refuse", rejudge: "refuse" },
18
- declined: { bash_off: "mandated", bash_classify: "ordinary", readonly: "refuse", rejudge: "refuse" },
19
- identity_changed: { bash_off: "refuse", bash_classify: "refuse", readonly: "refuse", rejudge: "refuse" },
20
- none: { bash_off: "vouch", bash_classify: "vouch", readonly: "vouch", rejudge: "vouch" },
15
+ deny_hit: { full_shell: "refuse", readonly: "refuse", rejudge: "refuse" },
16
+ outside_roots: { full_shell: "mandated", readonly: "refuse", rejudge: "refuse" },
17
+ unestablishable: { full_shell: "mandated", readonly: "refuse", rejudge: "refuse" },
18
+ declined: { full_shell: "ordinary", readonly: "refuse", rejudge: "refuse" },
19
+ identity_changed: { full_shell: "refuse", readonly: "refuse", rejudge: "refuse" },
20
+ none: { full_shell: "vouch", readonly: "vouch", rejudge: "vouch" },
21
21
  };
22
22
  export function decideShellRead(finding, seat) {
23
- return SHELL_READ_DECISION[finding][seat];
23
+ const row = Object.hasOwn(SHELL_READ_DECISION, finding) ? SHELL_READ_DECISION[finding] : undefined;
24
+ const disposition = row !== undefined && Object.hasOwn(row, seat) ? row[seat] : undefined;
25
+ if (disposition === undefined) {
26
+ throw new Error(`shell read decision: no cell for finding ${JSON.stringify(finding)} on seat ${JSON.stringify(seat)} — the seats are ${SHELL_READ_SEATS.map((w) => JSON.stringify(w)).join(", ")} (a seat names the face the command runs on)`);
27
+ }
28
+ return disposition;
24
29
  }
25
30
  export function judgeRead(read, boundary) {
26
- const deny = boundary.denyMatch;
31
+ const writeTarget = read.candidate.face === "write";
32
+ const deny = writeTarget ? boundary.writeDenyMatch : boundary.denyMatch;
27
33
  if (deny !== undefined) {
34
+ const walked = read.walked === true ? { walked: true } : undefined;
28
35
  const views = [...(read.target !== undefined ? [read.target] : []), ...read.spellings];
29
36
  for (const view of views) {
30
37
  const folded = normalizeAbsPathLexically(view);
31
- const pattern = deny(folded);
38
+ const pattern = writeTarget ? boundary.writeDenyMatch(folded) : boundary.denyMatch(folded, walked);
32
39
  if (pattern !== null)
33
- return { read, kind: "deny_hit", deny: { pattern, matchedView: folded }, containment: containmentOf(read, boundary) };
40
+ return { read, kind: "deny_hit", deny: { pattern, matchedView: folded, ...(writeTarget ? { face: "write" } : {}) }, containment: containmentOf(read, boundary) };
34
41
  }
35
42
  }
43
+ if (writeTarget)
44
+ return { read, kind: "none", containment: containmentOf(read, boundary) };
36
45
  const containment = containmentOf(read, boundary);
37
46
  if (read.candidate.denyOnly === true)
38
47
  return { read, kind: "none", containment };
@@ -43,8 +52,18 @@ export function judgeRead(read, boundary) {
43
52
  return { read, kind: "unestablishable", containment };
44
53
  return { read, kind: containment === "outside" ? "outside_roots" : "none", containment };
45
54
  }
55
+ export function denyRowAtOrBelow(read, boundary, carriesContent) {
56
+ if (boundary.writeDenyMatch !== undefined)
57
+ return true;
58
+ if (!carriesContent)
59
+ return false;
60
+ const atOrBelow = boundary.denyAtOrBelow;
61
+ if (atOrBelow === undefined)
62
+ return false;
63
+ return [...(read.target !== undefined ? [read.target] : []), ...read.spellings].some((view) => atOrBelow(normalizeAbsPathLexically(view)) !== null);
64
+ }
46
65
  export function denyFloorShadowsOf(candidates) {
47
- return candidates.flatMap((c) => (c.floorSpelled === undefined ? [] : [{ spelled: c.floorSpelled, raw: c.raw, provenance: c.provenance, expands: globInWord(readOfText(c.raw).word) ? "glob" : false, bases: [], denyOnly: true }]));
66
+ return candidates.flatMap((c) => (c.floorSpelled === undefined ? [] : [{ spelled: c.floorSpelled, raw: c.raw, provenance: c.provenance, expands: globInWord(readOfText(c.raw).word) ? "glob" : false, bases: [], denyOnly: true, ...(c.face !== undefined ? { face: c.face } : {}) }]));
48
67
  }
49
68
  function containmentOf(read, boundary) {
50
69
  if (boundary.face === "open")
@@ -55,7 +74,10 @@ function containmentOf(read, boundary) {
55
74
  }
56
75
  export function foldShellReadFindings(judged) {
57
76
  let best = { finding: "none" };
77
+ let unestablished = false;
58
78
  const raise = (next) => {
79
+ if (next.finding === "unestablishable" || next.finding === "declined")
80
+ unestablished = true;
59
81
  if (SHELL_READ_FINDING_RANK[next.finding] > SHELL_READ_FINDING_RANK[best.finding])
60
82
  best = next;
61
83
  };
@@ -86,7 +108,7 @@ export function foldShellReadFindings(judged) {
86
108
  }
87
109
  };
88
110
  walk(judged, false);
89
- return best;
111
+ return unestablished ? { ...best, unestablished: true } : best;
90
112
  }
91
113
  export function judgeEstablishedPlan(plan, established, boundary, nestedOf) {
92
114
  return {
@@ -110,7 +132,7 @@ export function allFindings(judged) {
110
132
  walk(judged);
111
133
  return out;
112
134
  }
113
- function parentDirLexically(p) {
135
+ export function parentDirLexically(p) {
114
136
  return normalizeAbsPathLexically(`${p}/..`);
115
137
  }
116
138
  export function deepestCommonDir(a, b) {
@@ -157,16 +179,19 @@ export function readRootGrantCandidateOf(judged, boundary) {
157
179
  }
158
180
  if (candidate === undefined)
159
181
  return undefined;
160
- const tooWide = parentDirLexically(candidate) === candidate ||
161
- (boundary.homeDir !== undefined && withinAnyRoot([candidate], boundary.homeDir)) ||
162
- boundary.roots.some((root) => withinAnyRoot([candidate], root));
163
- const proposed = tooWide ? (paths.length === 1 ? { dir: paths[0], covers: "exact" } : undefined) : { dir: candidate };
182
+ const proposed = grantOfferFor(candidate, paths.length === 1 ? paths[0] : undefined, boundary);
164
183
  if (proposed === undefined)
165
184
  return undefined;
166
185
  const widened = { ...boundary, roots: [...boundary.roots, proposed.dir] };
167
186
  const replay = rejudge(judged, widened);
168
187
  return foldShellReadFindings(replay).finding === "none" ? proposed : undefined;
169
188
  }
189
+ export function grantOfferFor(candidate, single, bounds) {
190
+ const tooWide = parentDirLexically(candidate) === candidate ||
191
+ (bounds.homeDir !== undefined && withinAnyRoot([candidate], bounds.homeDir)) ||
192
+ bounds.roots.some((root) => withinAnyRoot([candidate], root));
193
+ return tooWide ? (single !== undefined ? { dir: single, covers: "exact" } : undefined) : { dir: candidate };
194
+ }
170
195
  export function rejudge(judged, boundary) {
171
196
  return {
172
197
  plan: judged.plan,
@@ -13,6 +13,7 @@
13
13
  * · a LISTED reader hands its positional operands and the declared option-value slots (the verb tables of
14
14
  * `bash-readonly-classifier.ts`, read here through their exported readers, never copied);
15
15
  * · an UNLISTED program hands every word of its argv (a program declared to open no path operand hands none);
16
+ * · an ASSIGNMENT's value is a candidate only in a text that runs a program able to open a file;
16
17
  * · a LAUNCHER chain is peeled to its program and the chain's own words are never candidates; a launcher
17
18
  * option the row cannot read is recorded as a decline and every word after it is handed over;
18
19
  * · a SHELL re-entry (`sh -c S`, `eval S`, a launcher handing text to a shell, `env -S S`, a builtin's code-body
@@ -67,6 +68,9 @@ export interface ReadCandidate {
67
68
  readonly floorSpelled?: string;
68
69
  /** A deny-floor shadow of a `floorSpelled` spelling, established as a read and judged for deny rows only. */
69
70
  readonly denyOnly?: true;
71
+ /** A WRITE target of the command (a `>`-family redirection, a `sed` `w` file, the operands of a file-writing command):
72
+ * established like a read and judged against the deny table's WRITE face only — never for containment, never a read. */
73
+ readonly face?: "write";
70
74
  }
71
75
  /** How the shell reached one segment — what the base-candidate walk needs from the connectors. */
72
76
  export interface SegmentReach {
@@ -1,7 +1,7 @@
1
1
  import { lexShell, PARSE_FAILURE_SENTENCE, substitutionsInHeredocBody, literalTildeSpelling, } from "../../shell/lex.js";
2
2
  import { compoundArrayElements, dropDefinitionHeads, expandsOf, readOfText, readsOf, rewritten, spelledWithHome, isArithmeticBody, unreadableSentenceOf } from "./shell-read-word.js";
3
3
  import { LAUNCHER_TABLE, SHELL_BLOCK_WORDS, programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, } from "./bash-program-position.js";
4
- import { ASSIGNMENT_VALUE_NOT_PATHS, INTERPRETER_INLINE_PROGRAMS, NO_PATH_OPERAND_COMMANDS, POLL_LOOP_MAX_BEATS, RECURSIVE_READ_FORMS, SHELL_REENTRY_PROGRAM_RE, UNMODELLED_BASE_MOVERS, argvProgramOf, homeMayBeRebound, attachedOptionPayloads, resolveOperandLexically, inlineProgramClause, interpreterKey, isPathShapedToken, readVerbOptionSlot, sedScriptSlotFiles, segmentSelectsRecursiveRead, slotValueCandidates, suppliesFirstOperandSlot, verbFirstOperandSlot, verbOptionsArePositionIndependent, } from "./bash-readonly-classifier.js";
4
+ import { ASSIGNMENT_VALUE_NOT_PATHS, DECLARATION_BUILTINS, INERT_BUILTINS, INTERPRETER_INLINE_PROGRAMS, NO_PATH_OPERAND_COMMANDS, NO_PATH_OPERAND_EVALUATORS, POLL_LOOP_MAX_BEATS, RECURSIVE_READ_FORMS, SHELL_REENTRY_PROGRAM_RE, UNMODELLED_BASE_MOVERS, argvProgramOf, homeMayBeRebound, attachedOptionPayloads, resolveOperandLexically, inlineProgramClause, interpreterKey, isPathShapedToken, messageSlotWords, readVerbOptionSlot, sedScriptSlotFiles, segmentSelectsRecursiveRead, slotValueCandidates, suppliesFirstOperandSlot, verbFirstOperandSlot, verbOptionsArePositionIndependent, } from "./bash-readonly-classifier.js";
5
5
  import { isAbsolutePathForm } from "./safety.js";
6
6
  import { codeBodies, reentryText } from "./shell-reentry-text.js";
7
7
  export const READ_CANDIDATE_PROVENANCES = [
@@ -29,7 +29,9 @@ export function planShellReads(command, decl, depth = 0, homeRebound = false, ar
29
29
  const unreadable = unreadableSentenceOf(lexed, command, !arithmetic);
30
30
  const nestedPlan = (text, arithmetic = false) => planShellReads(text, decl, depth + 1, home === undefined, arithmetic);
31
31
  const projected = lexed.segments.map((segment) => projectSegment(segment));
32
- const facts = projected.map((p, si) => segmentFacts(lexed.segments[si], p, decl, home, nestedPlan));
32
+ const planned = projected.map((p, si) => segmentFacts(lexed.segments[si], p, decl, home, nestedPlan));
33
+ const inert = depth === 0 && unreadable === undefined && planned.every(segmentOpensNothing);
34
+ const facts = inert ? planned.map((f) => ({ ...f, reads: f.reads.filter((c) => c.provenance !== "env_assignment") })) : planned;
33
35
  shape.grouped = projected.some((p) => p.grouped);
34
36
  shape.backgrounded = lexed.segments.some((s) => s.connector === "&") || facts.some((f) => f.backgrounds === true);
35
37
  shape.strayRedirection = projected.some((p) => p.argv.length === 0 && p.redirects.length > 0);
@@ -165,6 +167,14 @@ export function planShellReads(command, decl, depth = 0, homeRebound = false, ar
165
167
  }
166
168
  return { segments, shape, ...(unreadable !== undefined ? { unreadable } : {}) };
167
169
  }
170
+ function segmentOpensNothing(f) {
171
+ if (f.empty)
172
+ return true;
173
+ const program = f.verb;
174
+ const at = program === undefined ? -1 : f.argv.findIndex((w) => w.folded === program.name);
175
+ const inertProgram = program === undefined || (program.name === program.basename && INERT_BUILTINS.has(program.basename) && !(DECLARATION_BUILTINS.has(program.basename) && f.argv.slice(at + 1).some((w) => /^[-+]/.test(w.folded))));
176
+ return inertProgram && f.argv.every((w) => !/[[\]$`]/.test(w.raw)) && f.nested.length === 0 && f.reentry === undefined && f.declines.length === 0 && f.launcherChain.length === 0 && f.redirects.every((r) => r.direction !== "in") && f.reads.every((c) => c.provenance === "env_assignment");
177
+ }
168
178
  function projectSegment(segment) {
169
179
  const tokens = [...segment.tokens];
170
180
  let grouped = tokens.some((t) => t.kind === "group");
@@ -389,6 +399,17 @@ function segmentFactsRead(segment, p, decl, home, nestedPlan) {
389
399
  else if (listed) {
390
400
  listedVerbCandidates(basename, argReads, reads);
391
401
  }
402
+ else if (DECLARATION_BUILTINS.has(basename) && program.name === basename) {
403
+ for (const r of argReads) {
404
+ const a = r.word.assignment;
405
+ if (a !== undefined && (r.expands !== "unresolvable" || a.value.expansions.length > 0))
406
+ pushAssignment(reads, r.word, "env_assignment");
407
+ else if (r.expands === "unresolvable") {
408
+ reads.push(bare(r, "unlisted_word"));
409
+ pushAssignment(reads, r.word, "unlisted_word");
410
+ }
411
+ }
412
+ }
392
413
  else {
393
414
  unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", program);
394
415
  }
@@ -518,6 +539,7 @@ function unlistedWords(reads, toks, from, launcherIndices, provenance, program)
518
539
  const argv = program !== undefined ? { program: program.basename, from: program.index } : argvProgramOf(toks.map((r) => r.word));
519
540
  const argvStart = argv?.from ?? toks.length;
520
541
  const slotVerb = argv?.program;
542
+ const message = slotVerb === undefined || argv === undefined ? new Set() : messageSlotWords(slotVerb, toks, argvStart);
521
543
  const declaredNotPaths = ASSIGNMENT_VALUE_NOT_PATHS.has(slotVerb ?? "");
522
544
  const noPathOperands = slotVerb !== undefined && NO_PATH_OPERAND_COMMANDS.has(slotVerb);
523
545
  const permutes = slotVerb === undefined || !verbOptionsArePositionIndependent(slotVerb);
@@ -525,7 +547,7 @@ function unlistedWords(reads, toks, from, launcherIndices, provenance, program)
525
547
  for (let j = from; j < toks.length; j++) {
526
548
  if (launcherIndices.has(j))
527
549
  continue;
528
- if (noPathOperands && j > argvStart)
550
+ if (noPathOperands && j > argvStart && !NO_PATH_OPERAND_EVALUATORS.has(slotVerb))
529
551
  break;
530
552
  if (toks[j].expands !== "unresolvable")
531
553
  continue;
@@ -541,7 +563,7 @@ function unlistedWords(reads, toks, from, launcherIndices, provenance, program)
541
563
  let endOfOptions = false;
542
564
  let sawArgvOperand = false;
543
565
  for (let j = from; j < toks.length; j++) {
544
- if (launcherIndices.has(j) || expanding.has(j))
566
+ if (launcherIndices.has(j) || expanding.has(j) || message.has(j))
545
567
  continue;
546
568
  const r = toks[j];
547
569
  const word = r.folded;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * WRITE TARGETS — which words of a shell segment a program writes, under which names, and at which seat, read off the
3
+ * plan's text alone (no filesystem): the `>`-family redirections and a `sed` script's `w` files, the operands of the
4
+ * write commands ({@link SHELL_WRITE_COMMANDS}), a copy's or a move's destination children (GNU's argument grammar), an
5
+ * in-place `sed`'s backups, and the seat each is written at ({@link WriteSeat}: the directory entry, or where the path
6
+ * lands). The establisher (`shell-read-establish.ts`) places what this module names; the judge reads the result on the
7
+ * deny table's write face; the station's `writesPlaced` fact reads the option readers exported here.
8
+ */
9
+ import type { ReadCandidate, ShellReadSegment } from "./shell-read-plan.js";
10
+ /**
11
+ * The commands whose operands are WRITE targets — the reference implementation's path-checked commands whose operation
12
+ * is a write or a create (`mkdir` / `touch` create; `rm` / `rmdir` / `mv` / `cp` / `tee` write; `sed` writes when it
13
+ * edits in place). Every operand counts (a `cp` source is judged as a write too, as the reference judges it): the write
14
+ * face only ever REFUSES, so a word read as a target it is not costs a refusal of a command that names a denied path.
15
+ */
16
+ export declare const SHELL_WRITE_COMMANDS: readonly ["mkdir", "touch", "rm", "rmdir", "mv", "cp", "tee", "sed"];
17
+ /**
18
+ * Does an option WORD spell this option — the long name or any abbreviation of it GNU getopt accepts (`--in-p`, `--fi`,
19
+ * `--par`: an unambiguous prefix, `=value` allowed), or the short letter alone or inside a bundle (`-i`, `-ni`, `-i.bak`)?
20
+ * Read in the tightening direction: a prefix that another long option shares still counts (the program would refuse it).
21
+ */
22
+ export declare function spellsOption(word: string, long: string, short: string): boolean;
23
+ /** A word of a program's argv: its raw spelling (the plan's candidates are keyed by it) and its folded spelling. */
24
+ export interface ArgWord {
25
+ readonly raw: string;
26
+ readonly spelled: string;
27
+ }
28
+ /** The words a segment hands its program — the argv after the program word (a launcher's own words are not the program's). */
29
+ export declare function programArgs(segment: ShellReadSegment): ArgWord[];
30
+ /** What a `sed` invocation's option words say about writing, under either grammar ({@link SED_GRAMMARS}): whether it edits
31
+ * in place (`-i` / `-I` / `--in-place`), the backup suffixes it is given (their values; an empty value keeps no backup),
32
+ * and whether GNU `--follow-symlinks` is on. */
33
+ export interface SedOptions {
34
+ readonly inPlace: boolean;
35
+ readonly suffixes: readonly string[];
36
+ readonly followsLinks: boolean;
37
+ }
38
+ export declare function sedOptionsOf(segment: ShellReadSegment): SedOptions;
39
+ /**
40
+ * The BACKUP names an in-place `sed` gives the file it knows as `name` — both readings a suffix has: GNU sed replaces every
41
+ * `*` in the suffix with the name (`bak/*` on `d/f` is `bak/d/f`, `/elsewhere/*` is `/elsewhere/d/f`) and appends a suffix
42
+ * without one (`d/f.bak`); the BSD and BusyBox seds append the suffix literally (`d/f` + `bak/*`). A name resolves against
43
+ * the working directory like the operand itself, so a suffix carrying a separator places the backup wherever it points.
44
+ */
45
+ export declare function sedBackupNames(name: string, suffix: string): string[];
46
+ /**
47
+ * WHERE a write reaches a path it names. `landing` — the path followed through every link to where it lands: the program
48
+ * opens the path and writes through it. `entry` — the directory entry itself, the canonical parent joined with the last
49
+ * name: the program renames over the entry, unlinks it or replaces it, and a link there is not followed (a name ending in
50
+ * a separator, `.` or `..` resolves itself, so it lands).
51
+ */
52
+ export type WriteSeat = "landing" | "entry";
53
+ /** One candidate handed to the judge. `seats`: where a write target is established ({@link WriteSeat}; absent ⇒ it lands —
54
+ * every read does). `written`: the seats among `seats` the program itself WRITES the target at (absent ⇒ all of them;
55
+ * `[]` ⇒ none — a copy's source, judged on the write face as a write path and read by the program); the write guards are
56
+ * asked about these alone. `child`: a copy's or a move's destination child, written only where the destination is a
57
+ * directory when the program runs. `followed`: a GNU sed `--follow-symlinks` backup — its names are {@link sedBackupNames}
58
+ * of the operand's FOLLOWED name, which only the filesystem answers (the candidate's own spelling is the operand's as
59
+ * written, the identity the pre-spawn re-check compares). */
60
+ export interface JudgedCandidate {
61
+ readonly candidate: ReadCandidate;
62
+ readonly seats?: readonly WriteSeat[];
63
+ readonly written?: readonly WriteSeat[];
64
+ readonly child?: true;
65
+ readonly followed?: {
66
+ readonly operand: ReadCandidate;
67
+ readonly suffix: string;
68
+ };
69
+ }
70
+ /** Every candidate a segment hands the judge: its reads minus the write targets, plus its `cd` target (the plan already
71
+ * lists it among the reads), plus its WRITE targets for the write face ({@link writeTargetsOf}), plus the deny-floor
72
+ * shadow of every unplaced home word ({@link denyFloorShadowsOf}) on both faces. */
73
+ export declare function judgedCandidatesOf(segment: ShellReadSegment): readonly JudgedCandidate[];