@sema-agent/core 7.44.0 → 7.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/CHANGELOG.md +418 -1
  2. package/dist/agents/extra-tool-names.d.ts +65 -0
  3. package/dist/agents/extra-tool-names.js +124 -0
  4. package/dist/agents/subagent.d.ts +17 -5
  5. package/dist/agents/subagent.js +65 -38
  6. package/dist/agents/tool-filter.d.ts +82 -5
  7. package/dist/agents/tool-filter.js +134 -4
  8. package/dist/core/agent-config-dir.d.ts +20 -0
  9. package/dist/core/agent-config-dir.js +49 -0
  10. package/dist/core/agent-definition.d.ts +2 -2
  11. package/dist/core/allow-entry.d.ts +30 -4
  12. package/dist/core/allow-entry.js +32 -5
  13. package/dist/core/ask-origin.d.ts +23 -4
  14. package/dist/core/ask-origin.js +11 -5
  15. package/dist/core/checkpoint-store.d.ts +41 -4
  16. package/dist/core/decision-receipt.js +1 -1
  17. package/dist/core/effective-path-target.d.ts +143 -10
  18. package/dist/core/effective-path-target.js +113 -9
  19. package/dist/core/engine-notice.d.ts +25 -3
  20. package/dist/core/engine-notice.js +12 -6
  21. package/dist/core/gate-fold.js +3 -0
  22. package/dist/core/gate-lanes.js +11 -3
  23. package/dist/core/gate-outcome.d.ts +5 -0
  24. package/dist/core/gate-outcome.js +2 -0
  25. package/dist/core/governance-codes.d.ts +3 -1
  26. package/dist/core/governance-codes.js +3 -0
  27. package/dist/core/hooks.d.ts +5 -0
  28. package/dist/core/memory-engine/content-origin.d.ts +0 -34
  29. package/dist/core/memory-engine/content-origin.js +9 -4
  30. package/dist/core/memory-engine/layout.d.ts +5 -1
  31. package/dist/core/memory-engine/layout.js +1 -0
  32. package/dist/core/path-rule-binding.d.ts +65 -0
  33. package/dist/core/path-rule-binding.js +99 -0
  34. package/dist/core/permission-rule-model.d.ts +47 -18
  35. package/dist/core/permission-rule-model.js +38 -42
  36. package/dist/core/permission-rule-provider.d.ts +7 -0
  37. package/dist/core/permission-rule-provider.js +10 -3
  38. package/dist/core/permission-rule-store.d.ts +8 -0
  39. package/dist/core/permission-rule-store.js +6 -2
  40. package/dist/core/permission-rule-sync.js +2 -2
  41. package/dist/core/permission-rules.d.ts +22 -36
  42. package/dist/core/permission-rules.js +50 -40
  43. package/dist/core/real-approval.d.ts +9 -6
  44. package/dist/core/real-approval.js +5 -5
  45. package/dist/core/runner/child-tree.d.ts +110 -0
  46. package/dist/core/runner/child-tree.js +107 -0
  47. package/dist/core/runner/construction-doors.d.ts +5 -0
  48. package/dist/core/runner/construction-doors.js +6 -0
  49. package/dist/core/runner/contracts.d.ts +54 -2
  50. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  51. package/dist/core/runner/permission-rule-lanes.js +18 -12
  52. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  53. package/dist/core/runner/persisted-path-rows.js +31 -12
  54. package/dist/core/runner/prepare-ask-lane.d.ts +6 -0
  55. package/dist/core/runner/prepare-ask-lane.js +4 -2
  56. package/dist/core/runner/prepare-config-doors.js +3 -1
  57. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  58. package/dist/core/runner/prepare-execution-env.js +11 -3
  59. package/dist/core/runner/prepare-gate-stations.d.ts +6 -0
  60. package/dist/core/runner/prepare-gate-stations.js +3 -1
  61. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  62. package/dist/core/runner/prepare-hands-readface.js +6 -6
  63. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  64. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  65. package/dist/core/runner/prepare-listings.d.ts +4 -4
  66. package/dist/core/runner/prepare-listings.js +2 -22
  67. package/dist/core/runner/prepare-park-ask.d.ts +5 -0
  68. package/dist/core/runner/prepare-park-ask.js +6 -4
  69. package/dist/core/runner/prepare-policy-chain.d.ts +16 -2
  70. package/dist/core/runner/prepare-policy-chain.js +14 -5
  71. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  72. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  73. package/dist/core/runner/prepare-task.js +41 -18
  74. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  75. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  76. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  77. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  78. package/dist/core/runner/resume-apply.js +17 -3
  79. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  80. package/dist/core/runner/resume-policy-outcome.js +2 -1
  81. package/dist/core/runner/run-leg.js +5 -1
  82. package/dist/core/runner/run-notification-lane.js +2 -2
  83. package/dist/core/runner/runtask.d.ts +2 -0
  84. package/dist/core/runner/runtask.js +4 -2
  85. package/dist/core/runner/stream-steer-verb.js +2 -2
  86. package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
  87. package/dist/core/runner/sub-agent-deny-lists.js +121 -0
  88. package/dist/core/runner/synthetic-tools.d.ts +1 -1
  89. package/dist/core/runner/synthetic-tools.js +3 -2
  90. package/dist/core/runner/write-protection-station.d.ts +3 -2
  91. package/dist/core/runner/write-protection-station.js +1 -0
  92. package/dist/core/runner-deps.d.ts +14 -0
  93. package/dist/core/sensitive-path-policy.d.ts +8 -0
  94. package/dist/core/sensitive-path-policy.js +18 -2
  95. package/dist/core/session-policy-store.d.ts +6 -2
  96. package/dist/core/session-policy-store.js +2 -1
  97. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  98. package/dist/core/shell-wrapper-projection.js +56 -0
  99. package/dist/core/shell-wrapper-table.d.ts +144 -34
  100. package/dist/core/shell-wrapper-table.js +40 -20
  101. package/dist/core/skill-grant.d.ts +10 -2
  102. package/dist/core/skill-grant.js +20 -4
  103. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  104. package/dist/core/synthetic-read-verdict.js +3 -1
  105. package/dist/core/task-notification.d.ts +12 -1
  106. package/dist/core/task-notification.js +13 -1
  107. package/dist/core/task-spec.d.ts +5 -3
  108. package/dist/core/tool-catalog-entries.d.ts +21 -0
  109. package/dist/core/tool-catalog-entries.js +9 -0
  110. package/dist/core/tool-policy.d.ts +45 -12
  111. package/dist/core/tool-policy.js +18 -20
  112. package/dist/core/tool-spec.d.ts +23 -4
  113. package/dist/core/types.d.ts +1 -1
  114. package/dist/core/types.js +1 -1
  115. package/dist/core/write-protect.d.ts +4 -0
  116. package/dist/core/write-protect.js +30 -9
  117. package/dist/index.d.ts +10 -6
  118. package/dist/index.js +9 -5
  119. package/dist/orchestration/run-spec.js +8 -7
  120. package/dist/orchestration/workflow.js +3 -2
  121. package/dist/shell/lex.d.ts +7 -0
  122. package/dist/shell/lex.js +17 -0
  123. package/dist/tools/fs/bash-program-position.d.ts +8 -87
  124. package/dist/tools/fs/bash-program-position.js +33 -69
  125. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  126. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  127. package/dist/tools/fs/index.d.ts +3 -2
  128. package/dist/tools/fs/program-read-model.d.ts +694 -0
  129. package/dist/tools/fs/program-read-model.js +1252 -0
  130. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  131. package/dist/tools/fs/program-read-model.rows.js +613 -0
  132. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  133. package/dist/tools/fs/read-deny-home.js +62 -24
  134. package/dist/tools/fs/read-deny.d.ts +54 -14
  135. package/dist/tools/fs/read-deny.js +104 -50
  136. package/dist/tools/fs/removal-targets.d.ts +83 -0
  137. package/dist/tools/fs/removal-targets.js +236 -0
  138. package/dist/tools/fs/safety.d.ts +3 -0
  139. package/dist/tools/fs/safety.js +10 -3
  140. package/dist/tools/fs/shell-read-argv.d.ts +85 -0
  141. package/dist/tools/fs/shell-read-argv.js +386 -0
  142. package/dist/tools/fs/shell-read-bindings.d.ts +273 -0
  143. package/dist/tools/fs/shell-read-bindings.js +746 -0
  144. package/dist/tools/fs/shell-read-check.js +23 -15
  145. package/dist/tools/fs/shell-read-establish.js +79 -5
  146. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  147. package/dist/tools/fs/shell-read-judge.js +9 -1
  148. package/dist/tools/fs/shell-read-plan.d.ts +31 -7
  149. package/dist/tools/fs/shell-read-plan.js +299 -393
  150. package/dist/tools/fs/shell-read-word.d.ts +29 -26
  151. package/dist/tools/fs/shell-read-word.js +30 -38
  152. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -37
  153. package/dist/tools/fs/shell-read-write-targets.js +83 -136
  154. package/dist/tools/fs/shell-reentry-text.d.ts +38 -6
  155. package/dist/tools/fs/shell-reentry-text.js +90 -53
  156. package/package.json +1 -1
  157. package/test/export-surface.snapshot.json +49 -3
  158. package/wire-changes/core@7.45.0.json +1084 -0
  159. package/wire-changes/core@7.46.0.json +1522 -0
@@ -0,0 +1,236 @@
1
+ import { planShellReads } from "./shell-read-plan.js";
2
+ import { literalTextOf } from "../../shell/lex.js";
3
+ import { createShellReadEstablisher, establishShellReadsLexically } from "./shell-read-establish.js";
4
+ import { isAbsolutePathForm, isWinFormPath, normalizeAbsPathLexically } from "./safety.js";
5
+ import { reentryText } from "./shell-reentry-text.js";
6
+ import { bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
7
+ import { programArgs } from "./shell-read-write-targets.js";
8
+ import { implementationsOf, programRow, runsShellText, selectedOptions, writeReadings } from "./program-read-model.js";
9
+ import { inlineUntrusted } from "../../core/untrusted-text.js";
10
+ export const SHELL_REMOVAL_COMMANDS = Object.freeze(["rm", "rmdir"]);
11
+ const REMOVAL_COMMAND_SET = new Set(SHELL_REMOVAL_COMMANDS);
12
+ function removalKey(path) {
13
+ let k = normalizeAbsPathLexically(path.normalize("NFC")).replace(/\\/g, "/").toLowerCase();
14
+ k = k.replace(/(?:\/+\*+)+\/*$/, "");
15
+ const drive = driveOf(path, k);
16
+ let rest = k.slice(drive.length);
17
+ if (rest === "")
18
+ rest = "/";
19
+ if (rest.length > 1)
20
+ rest = rest.replace(/\/+$/, "");
21
+ return drive + rest.replace(/^\/private\/(etc|var|tmp|home)(?=\/|$)/, "/$1");
22
+ }
23
+ function driveOf(path, key) {
24
+ return isWinFormPath(path) && key.length >= 2 && key[1] === ":" ? key.slice(0, 2) : "";
25
+ }
26
+ export function dangerousRemovalOf(path, seat) {
27
+ if (!isAbsolutePathForm(path))
28
+ return undefined;
29
+ const k = removalKey(path);
30
+ const rest = k.slice(driveOf(path, k).length);
31
+ if (rest === "/" || /^\/\/[^/]+\/[^/]+$/.test(k))
32
+ return "filesystem_root";
33
+ if (seat.homes.some((h) => isAbsolutePathForm(h) && removalKey(h) === k))
34
+ return "home";
35
+ const prefix = k.endsWith("/") ? k : `${k}/`;
36
+ if (seat.workspace.some((w) => isAbsolutePathForm(w) && (removalKey(w) === k || removalKey(w).startsWith(prefix))))
37
+ return "workspace";
38
+ if (/^\/[^/]+$/.test(rest))
39
+ return "top_level";
40
+ return undefined;
41
+ }
42
+ function tildeNamesAHome(spelled) {
43
+ const m = /^~([^/]*)(\/.*)?$/.exec(spelled);
44
+ if (m === null)
45
+ return false;
46
+ const key = removalKey(`/~home${m[2] ?? ""}`);
47
+ return key === "/~home" || !key.startsWith("/~home/");
48
+ }
49
+ function removalKindOf(read, seat) {
50
+ const floors = read.established === "unestablishable" && read.candidate.bases === "unmodelled" ? read.spellings : [];
51
+ for (const view of [read.target, read.origin, ...floors]) {
52
+ const kind = view === undefined ? undefined : dangerousRemovalOf(view, seat);
53
+ if (kind !== undefined)
54
+ return kind;
55
+ }
56
+ return read.established === "unestablishable" && tildeNamesAHome(read.candidate.spelled) ? "home" : undefined;
57
+ }
58
+ const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
59
+ const NAME_BINDERS = new Set(["unset", "local", "declare", "typeset", "readonly", "export", "read"]);
60
+ function bindingsOf(plan) {
61
+ const out = new Map();
62
+ const bind = (name, value) => {
63
+ out.set(name, [...(out.get(name) ?? []), value]);
64
+ };
65
+ for (const segment of plan.segments) {
66
+ for (const w of segment.argv)
67
+ if (w.assignment !== undefined)
68
+ bind(w.assignment.name, w.assignment.settlesWholeName ? literalTextOf(w.assignment.value.pieces) : undefined);
69
+ const [head, name] = segment.argv;
70
+ if ((head?.folded === "for" || head?.folded === "select") && name !== undefined && NAME.test(name.folded)) {
71
+ const at = segment.argv.findIndex((w, i) => i > 1 && w.folded === "in");
72
+ const words = at < 0 ? [undefined] : segment.argv.slice(at + 1).map((w) => literalTextOf(w.pieces));
73
+ for (const v of words.length > 0 ? words : [undefined])
74
+ bind(name.folded, v);
75
+ }
76
+ if (head !== undefined && NAME_BINDERS.has(head.folded))
77
+ for (const w of segment.argv.slice(1))
78
+ if (w.assignment === undefined && NAME.test(w.folded))
79
+ bind(w.folded, undefined);
80
+ }
81
+ return out;
82
+ }
83
+ const BOUND_SPELLINGS_MAX = 64;
84
+ function boundSpellingsOf(word, bindings) {
85
+ const named = word.pieces.filter((q) => q.literal === undefined);
86
+ if (named.length === 0 || !named.some((q) => q.expansion?.kind === "parameter" && q.expansion.name !== undefined && bindings.has(q.expansion.name)))
87
+ return undefined;
88
+ let spellings = [""];
89
+ for (const q of word.pieces) {
90
+ if (q.literal !== undefined) {
91
+ spellings = spellings.map((sp) => sp + q.literal);
92
+ continue;
93
+ }
94
+ const name = q.expansion?.kind === "parameter" && (q.expansion.body === undefined || q.expansion.body === q.expansion.name) ? q.expansion.name : undefined;
95
+ const values = name !== undefined ? bindings.get(name) : undefined;
96
+ if (name === undefined || values === undefined)
97
+ return { unreadable: q.expansion?.name ?? word.raw };
98
+ if (values.some((v) => v === undefined || (q.quotedBy === undefined && /\s/.test(v))))
99
+ return { unreadable: name };
100
+ spellings = spellings.flatMap((sp) => values.map((v) => sp + v));
101
+ if (spellings.length > BOUND_SPELLINGS_MAX)
102
+ return { unreadable: name };
103
+ }
104
+ return { spellings };
105
+ }
106
+ function recursiveForce(row, args) {
107
+ return implementationsOf(row).some((view) => {
108
+ const selected = selectedOptions(view, args, "one-implementation");
109
+ return selected.some((o) => o.selects === "traversal") && selected.some((o) => o.mode === "forced");
110
+ });
111
+ }
112
+ export function removalFindingsOf(established, seat) {
113
+ const out = [];
114
+ const bindings = bindingsOf(established.plan);
115
+ for (const segment of established.plan.segments) {
116
+ const program = segment.verb?.basename;
117
+ const row = program !== undefined && REMOVAL_COMMAND_SET.has(program) ? programRow(program) : undefined;
118
+ if (program !== undefined && row !== undefined) {
119
+ const args = programArgs(segment);
120
+ const spelled = args.map((w) => w.spelled);
121
+ const removed = new Set(writeReadings(row, spelled).flatMap((r) => r.written.map((w) => w.at)));
122
+ const options = new Set(args.filter((_, at) => !removed.has(at)).map((w) => w.raw));
123
+ for (const read of established.reads.get(segment) ?? []) {
124
+ if (read.candidate.face !== "write" || options.has(read.candidate.raw))
125
+ continue;
126
+ const kind = removalKindOf(read, seat);
127
+ if (kind !== undefined) {
128
+ out.push({ guard: "dangerous_delete", kind, program, operand: read.candidate.raw, target: read.target ?? read.candidate.floorSpelled ?? read.candidate.spelled });
129
+ continue;
130
+ }
131
+ if (read.established !== "unestablishable")
132
+ continue;
133
+ const word = segment.argv.find((w) => w.raw === read.candidate.raw);
134
+ const bound = word === undefined ? undefined : boundSpellingsOf(word, bindings);
135
+ if (bound === undefined)
136
+ continue;
137
+ if ("unreadable" in bound) {
138
+ if (recursiveForce(row, spelled))
139
+ out.push({ guard: "unverifiable_delete", program, operand: read.candidate.raw, name: bound.unreadable });
140
+ continue;
141
+ }
142
+ const bases = segment.bases === "unmodelled" ? (read.candidate.floorBases ?? []) : segment.bases;
143
+ for (const spelled of bound.spellings) {
144
+ const views = isAbsolutePathForm(spelled) ? [spelled] : bases.map((base) => `${base}/${spelled}`);
145
+ for (const view of views) {
146
+ const hit = dangerousRemovalOf(view, seat);
147
+ if (hit === undefined)
148
+ continue;
149
+ out.push({ guard: "dangerous_delete", kind: hit, program, operand: read.candidate.raw, target: normalizeAbsPathLexically(view) });
150
+ break;
151
+ }
152
+ }
153
+ }
154
+ }
155
+ const script = runtimeScriptOf(segment);
156
+ if (script !== undefined && program !== undefined)
157
+ out.push({ guard: "dangerous_delete", kind: "runtime_script", program, operand: script, target: script });
158
+ for (const nested of segment.nested) {
159
+ const sub = established.nested.get(nested);
160
+ if (sub !== undefined)
161
+ out.push(...removalFindingsOf(sub, seat));
162
+ }
163
+ }
164
+ return out;
165
+ }
166
+ function runtimeScriptOf(segment) {
167
+ const program = segment.verb;
168
+ if (segment.reentry !== "shell" || program === undefined || !runsShellText(program.basename))
169
+ return undefined;
170
+ const at = segment.argv.findIndex((w) => w.folded === program.name);
171
+ const MARK = "\u0000";
172
+ const words = at < 0 ? [] : segment.argv.slice(at + 1).map((w) => literalTextOf(w.pieces) ?? `${MARK}${w.raw}`);
173
+ const texts = [reentryText(program.basename, words), ...(segment.stdin ?? []).map((s) => reentryText(program.basename, words, s.literal ?? `${MARK}${s.from}`))];
174
+ const text = texts.find((t) => t?.includes(MARK) === true);
175
+ return text === undefined ? undefined : text.split(MARK).join("");
176
+ }
177
+ export function planRunsRemoval(plan) {
178
+ return plan.segments.some((s) => (s.verb !== undefined && REMOVAL_COMMAND_SET.has(s.verb.basename)) || runtimeScriptOf(s) !== undefined || s.nested.some(planRunsRemoval));
179
+ }
180
+ const DANGEROUS_REMOVAL_SENTENCE = {
181
+ filesystem_root: (p, t) => `\`${p}\` would remove the filesystem root (${t})`,
182
+ home: (p, t) => `\`${p}\` would remove a home directory (${t})`,
183
+ workspace: (p, t) => `\`${p}\` would remove a workspace directory — the task root, the working directory or an additional working directory — or one of its ancestors (${t})`,
184
+ top_level: (p, t) => `\`${p}\` would remove a top-level system directory (${t})`,
185
+ runtime_script: (p, t) => `\`${p}\` runs a script built only when the command runs (${t}), and it may remove the filesystem root, a home directory or the task's directories before anyone can read it`,
186
+ };
187
+ function verdictOf(findings) {
188
+ for (const f of findings)
189
+ if (f.guard === "dangerous_delete")
190
+ return { guard: f.guard, text: DANGEROUS_REMOVAL_SENTENCE[f.kind](f.program, inlineUntrusted(f.target, 200)) };
191
+ for (const f of findings)
192
+ if (f.guard === "unverifiable_delete")
193
+ return { guard: f.guard, text: `\`${f.program}\` recursive+force target ${inlineUntrusted(f.operand, 200)} takes ${f.name}'s value, which this command sets in a form the guard cannot read (an expansion, an unset or bare declaration, a \`read\`, a loop over unread words) — the deleted path cannot be verified from this command` };
194
+ return undefined;
195
+ }
196
+ const REMOVAL_RESOLVE_TIMEOUT_MS = 10_000;
197
+ export function removalReadingOf(command, req, seat) {
198
+ const homeDir = seat?.env?.homeDir;
199
+ const cwd = req.cwd ?? req.root ?? "";
200
+ const roots = req.root !== undefined ? [req.root] : cwd !== "" ? [cwd] : [];
201
+ const boundary = { roots, ...(cwd !== "" ? { cwd } : {}), ...(homeDir !== undefined ? { homeDir } : {}) };
202
+ const plan = planShellReads(command, { readers: new Set(bashClassifyDefaultAllow()), cwd, roots, ...(homeDir !== undefined ? { homeDir } : {}) });
203
+ if (!planRunsRemoval(plan))
204
+ return undefined;
205
+ return {
206
+ async judge(signal) {
207
+ const env = seat?.env;
208
+ const resolved = env !== undefined && homeDir !== undefined ? await resolvedOrUndefined(env.canonicalPath(homeDir, signal), signal) : undefined;
209
+ const removalSeat = {
210
+ homes: [...(homeDir !== undefined ? [homeDir] : []), ...(resolved !== undefined && resolved.ok ? [resolved.value] : [])],
211
+ workspace: [...(req.root !== undefined ? [req.root] : []), ...(req.cwd !== undefined ? [req.cwd] : []), ...(seat?.additionalRoots?.() ?? [])],
212
+ };
213
+ const lexical = removalFindingsOf(establishShellReadsLexically(plan, boundary), removalSeat);
214
+ if (lexical.some((f) => f.guard === "dangerous_delete") || env === undefined || roots.length === 0)
215
+ return verdictOf(lexical);
216
+ const established = await resolvedOrUndefined(createShellReadEstablisher(env, roots, { rootsCanonical: true }).establish(plan, boundary, signal), signal);
217
+ return verdictOf(established === undefined ? lexical : [...removalFindingsOf(established, removalSeat), ...lexical]);
218
+ },
219
+ };
220
+ }
221
+ async function resolvedOrUndefined(leg, signal) {
222
+ let timer;
223
+ try {
224
+ return await Promise.race([
225
+ leg.catch(() => undefined),
226
+ new Promise((resolve) => {
227
+ timer = setTimeout(() => resolve(undefined), REMOVAL_RESOLVE_TIMEOUT_MS);
228
+ signal?.addEventListener("abort", () => resolve(undefined), { once: true });
229
+ }),
230
+ ]);
231
+ }
232
+ finally {
233
+ if (timer !== undefined)
234
+ clearTimeout(timer);
235
+ }
236
+ }
@@ -129,6 +129,9 @@ export interface FsViolation {
129
129
  * lands it outside every POSIX/drive root, i.e. a location the working directory does not contain.
130
130
  */
131
131
  export declare function isAbsolutePathForm(p: string): boolean;
132
+ /** Does this absolute spelling name the opening process's STANDARD INPUT (`/dev/stdin`, `/dev/fd/0`, `/proc/<pid>/fd/0`),
133
+ * folded lexically first (`/dev/./stdin`, `/dev//fd/0`)? */
134
+ export declare function namesStandardInput(path: string): boolean;
132
135
  /**
133
136
  * Windows-FAMILY spelling of a path (extracted here so {@link canonicalizeTarget} judges
134
137
  * the UNC family by the same rule): drive-rooted (`C:\…`/`C:/…`), UNC-backslash (`\\host\share`), or a
@@ -3,6 +3,7 @@ import { readDenySentence } from "./read-deny.js";
3
3
  import { homeViewsOf } from "./read-deny-home.js";
4
4
  import { sliceHeadSafe } from "../../core/surrogate-safe-slice.js";
5
5
  import { pathTargetValue, protectivePathTargetOf } from "../../core/tool-registry.js";
6
+ import { readPathViews } from "../../core/effective-path-target.js";
6
7
  import { withGateTimeout, DEFAULT_GATE_AWAIT_TIMEOUT_MS } from "../../core/gate-bounded-await.js";
7
8
  import { thrownText } from "../../core/thrown-value.js";
8
9
  export function fileArgPath(args) {
@@ -41,6 +42,10 @@ const BLOCKED_DEVICE_PATHS = new Set([
41
42
  function isProcStdioFd(key) {
42
43
  return key.startsWith("/proc/") && (key.endsWith("/fd/0") || key.endsWith("/fd/1") || key.endsWith("/fd/2"));
43
44
  }
45
+ export function namesStandardInput(path) {
46
+ const k = path.startsWith("/") ? normalizeAbsPathLexically(path) : "";
47
+ return k === "/dev/stdin" || k === "/dev/fd/0" || (k.startsWith("/proc/") && k.endsWith("/fd/0"));
48
+ }
44
49
  const PROC_SENSITIVE_SUFFIXES = ["/environ", "/cmdline", "/auxv", "/maps", "/mem", "/stat"];
45
50
  function isProcSensitiveFile(key) {
46
51
  return key.startsWith("/proc/") && PROC_SENSITIVE_SUFFIXES.some((suf) => key.endsWith(suf));
@@ -457,7 +462,8 @@ function within(rootCanonical, p) {
457
462
  return pN.identity.startsWith(rootN.identity.endsWith(SEP) ? rootN.identity : rootN.identity + SEP);
458
463
  }
459
464
  export async function resolveKey(env, rootCanonical, path, signal, baseCwd, additionalRoots, exactFileReadExemption, readDeny, containmentFace) {
460
- const canon = await canonicalizeTarget(env, path, signal, pathFamilyOf({ root: rootCanonical }), baseCwd);
465
+ const views = await readPathViews(env, path, baseCwd ?? rootCanonical, { root: rootCanonical, ...(baseCwd !== undefined ? { cwd: baseCwd } : {}) }, signal, { hops: readDeny !== undefined });
466
+ const canon = views.canonical ?? (await canonicalizeTarget(env, path, signal, pathFamilyOf({ root: rootCanonical }), baseCwd));
461
467
  if (!canon.ok)
462
468
  return { ok: false, violation: { code: "invalid", message: canon.message } };
463
469
  const key = canon.key;
@@ -466,8 +472,9 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
466
472
  return { ok: false, violation: { code: "invalid", message: `path "${path}" is a blocked device/special file (reading it can hang the process); refused.` } };
467
473
  }
468
474
  if (readDeny !== undefined) {
469
- const judged = { aliasResolved: canon.aliasResolved === true };
470
- let hit = readDeny.matchTarget(key, lexicalViewOf(path, baseCwd ?? rootCanonical), judged);
475
+ const judged = { aliasResolved: canon.aliasResolved === true, hops: views.hops };
476
+ const lexical = views.lexical?.kind === "path" ? spellingOfIdentity(views.lexical.identity, pathFamilyOf({ root: rootCanonical })) : undefined;
477
+ let hit = readDeny.matchTarget(key, lexical, judged);
471
478
  if (hit === null) {
472
479
  const homeView = await homeViewsOf(env, readDeny, signal).of(key);
473
480
  if (homeView !== undefined)
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The candidates ONE program's words hand the read station — the program read model's rows read word by word: a
3
+ * listed reader's operands and option-value slots, an unlisted program's words (its own row read where it has one,
4
+ * read the speculative way where it has none), a subcommand's words by the subcommand's row, a module's program text,
5
+ * an assignment word's value. `shell-read-plan.ts` threads the segments and their bases; this module is what one
6
+ * segment's argv offers.
7
+ */
8
+ import { type LexedWord } from "../../shell/lex.js";
9
+ import { type Read } from "./shell-read-word.js";
10
+ import { type BindingTable, type BoundValue } from "./shell-read-bindings.js";
11
+ import { subcommandOf, type ArgRole, type ProgramRow } from "./program-read-model.js";
12
+ import type { ReadCandidate, ReadCandidateProvenance } from "./shell-read-plan.js";
13
+ /** A candidate before the threading fills its bases. `alternatives`: the other spellings its word may expand to (a name
14
+ * bound to several values) — one candidate each once the segment's reads are read. */
15
+ export type Bare = Omit<ReadCandidate, "bases"> & {
16
+ readonly alternatives?: readonly string[];
17
+ readonly floors?: readonly BoundValue[];
18
+ };
19
+ /** What reading a segment's words found beside its candidates. */
20
+ export interface WordContext {
21
+ /** The names at this segment. */
22
+ readonly table: BindingTable;
23
+ /** Shell text the arithmetic evaluator runs (a substitution in a subscript it evaluates): the segment re-enters it. */
24
+ readonly reentered: string[];
25
+ /** Program text the station does not read — inline text in a language that may open files, or a program read from
26
+ * standard input: why the segment's reads are not established. */
27
+ programText?: string;
28
+ /** Some program text was found in a slot (whether or not it may open files). */
29
+ programTextSeen: boolean;
30
+ }
31
+ /** The subcommand whose row says how ITS words name paths (operands or a traversal of its own), or `undefined`: a
32
+ * subcommand whose row states only its grammar — its message slots, read by {@link messageWords} — leaves its words to
33
+ * the program's own reading. */
34
+ export declare function pathSubcommandOf(row: ProgramRow | undefined, args: readonly string[]): ReturnType<typeof subcommandOf>;
35
+ /** The sentence of inline program text the station does not read. */
36
+ export declare const programTextSentence: (language: string) => string;
37
+ /** One candidate without its bases (filled by the threading pass): the word's spelling — or `spelled`, a caller's
38
+ * reading of PART of it (an option payload, a `key=value` value) — with the word's own expansion facts. A word whose
39
+ * names may hold several values offers each as its whole spelling; a PART of such a word is not followed through them
40
+ * (it is unresolvable). */
41
+ export declare function bare(r: Read, provenance: ReadCandidateProvenance, spelled?: string): Bare;
42
+ /** The reads one value of a declared role hands over: a candidate per file it names, or — a value the program expands
43
+ * past the listed bound — the word itself, unresolvable. A mount source is a read the daemon makes of the whole tree
44
+ * under it (a read and a traversal root); an object path
45
+ * `<rev>:<path>` offers the word as a path and the `<path>` it spells to the deny judge; program text is never a path
46
+ * ({@link programTextReads}). `alsoOperand`: the walk read the option past the first operand, where the program may
47
+ * have stopped reading options — a path role is still offered (more candidates), but no program text is read there:
48
+ * the word is the operands' (the script's own argument), as the program's delivery reading says. */
49
+ export declare function roleReads(r: Read, provenance: ReadCandidateProvenance, value: string, role: ArgRole, globs: "expanded" | "literal", ctx: WordContext, alsoOperand?: boolean): Bare[];
50
+ /**
51
+ * The candidates a `NAME=value` word hands the judge, both facts about its VALUE (the word is a path to nobody):
52
+ * what path it NAMES, and what the text cannot SETTLE — placed as an operand is wherever an environment value
53
+ * names ONE path (a leading tilde against the declared home; a `:` joins a second). Where the text runs nothing able
54
+ * to open a file, {@link planShellReads} drops them: the value names a path to nobody.
55
+ *
56
+ * Bash applies its assignment-word expansion to such a word even when it is an ARGUMENT to a simple command, so
57
+ * the program receives `NAME=<expanded value>`, and a program that reads its value as a path (`dd if=…`, a build
58
+ * or tool variable, a launcher's environment word) opens what the value names. Two facts of the lexer's
59
+ * {@link AssignmentFact} are read: a tilde the shell EXPANDS in the value (at its start and after each `:`, where
60
+ * unquoted — `value.expansions`) leaves the path unsettled, and the append form (`NAME+=value`) names `<whatever
61
+ * NAME held>` + value, so the whole value is not in the text — but the text it appends IS, and an unset or empty
62
+ * NAME makes it the whole value, so it is a candidate too. A value that is a substitution is the nested plan
63
+ * its body already is, never a candidate of its own text (its placeholder is not a path).
64
+ */
65
+ export declare function pushAssignment(reads: Bare[], w: LexedWord, provenance: ReadCandidateProvenance): void;
66
+ /** Every word of a segment whose program's grammar this parser does not model, from `from` onward: option
67
+ * payloads and path-shaped option words included, the launcher chain's own words excluded, a program declared
68
+ * to open no path operand handing only the environment words in front of it. A program with NO row is read the
69
+ * speculative way: every word is still handed over, but the filesystem decides it
70
+ * ({@link ReadCandidate.speculative}). */
71
+ export declare function unlistedWords(reads: Bare[], toks: readonly Read[], from: number, launcherIndices: ReadonlySet<number>, provenance: ReadCandidateProvenance, ctx: WordContext, program?: {
72
+ readonly index: number;
73
+ readonly basename: string;
74
+ }): void;
75
+ /** The positional operands and declared option-value slots of a listed reader — the program read model's argv walk,
76
+ * one candidate per word the program may open, plus the traversal roots of a recursive form. A subcommand's words
77
+ * are read by its own row; the words a module takes as its program text are program text. */
78
+ export declare function listedVerbCandidates(name: string, argReads: readonly Read[], reads: Bare[], ctx: WordContext): void;
79
+ /** The subscript of a variable-name word (`a[…]`), as the arithmetic text that is evaluated; the empty text for a plain
80
+ * name. */
81
+ export declare function subscriptOnly(word: string): string;
82
+ /** An assignment word's arithmetic: its own subscript (`a[$(…)]=1`, quoted or not — not for a builtin that takes the
83
+ * name `asWritten`), and its value where the name carries the integer attribute (`declare -i n; n='a[$(…)]'`, bash's
84
+ * own `OPTIND` / `RANDOM` / …). */
85
+ export declare function assignmentArithmetic(w: LexedWord, integer: boolean, ctx: WordContext, asWritten?: boolean): void;