@sema-agent/core 7.45.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 (127) hide show
  1. package/CHANGELOG.md +246 -1
  2. package/dist/agents/extra-tool-names.d.ts +65 -0
  3. package/dist/agents/extra-tool-names.js +124 -0
  4. package/dist/agents/subagent.d.ts +17 -5
  5. package/dist/agents/subagent.js +33 -25
  6. package/dist/agents/tool-filter.d.ts +17 -1
  7. package/dist/agents/tool-filter.js +7 -0
  8. package/dist/core/agent-config-dir.d.ts +20 -0
  9. package/dist/core/agent-config-dir.js +49 -0
  10. package/dist/core/allow-entry.d.ts +5 -3
  11. package/dist/core/allow-entry.js +3 -1
  12. package/dist/core/ask-origin.js +1 -1
  13. package/dist/core/checkpoint-store.d.ts +39 -2
  14. package/dist/core/effective-path-target.d.ts +136 -4
  15. package/dist/core/effective-path-target.js +110 -2
  16. package/dist/core/engine-notice.d.ts +5 -0
  17. package/dist/core/gate-fold.js +3 -0
  18. package/dist/core/gate-lanes.js +9 -3
  19. package/dist/core/governance-codes.d.ts +3 -1
  20. package/dist/core/governance-codes.js +3 -0
  21. package/dist/core/hooks.d.ts +3 -0
  22. package/dist/core/path-rule-binding.d.ts +65 -0
  23. package/dist/core/path-rule-binding.js +99 -0
  24. package/dist/core/permission-rule-model.d.ts +47 -18
  25. package/dist/core/permission-rule-model.js +38 -42
  26. package/dist/core/permission-rule-provider.d.ts +7 -0
  27. package/dist/core/permission-rule-provider.js +10 -3
  28. package/dist/core/permission-rule-store.d.ts +8 -0
  29. package/dist/core/permission-rule-store.js +6 -2
  30. package/dist/core/permission-rule-sync.js +2 -2
  31. package/dist/core/permission-rules.d.ts +22 -40
  32. package/dist/core/permission-rules.js +50 -43
  33. package/dist/core/runner/child-tree.d.ts +97 -0
  34. package/dist/core/runner/child-tree.js +104 -0
  35. package/dist/core/runner/construction-doors.d.ts +5 -0
  36. package/dist/core/runner/construction-doors.js +6 -0
  37. package/dist/core/runner/contracts.d.ts +46 -0
  38. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  39. package/dist/core/runner/permission-rule-lanes.js +18 -12
  40. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  41. package/dist/core/runner/persisted-path-rows.js +31 -12
  42. package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
  43. package/dist/core/runner/prepare-ask-lane.js +4 -2
  44. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  45. package/dist/core/runner/prepare-execution-env.js +10 -3
  46. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  47. package/dist/core/runner/prepare-gate-stations.js +2 -1
  48. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  49. package/dist/core/runner/prepare-hands-readface.js +6 -6
  50. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  51. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  52. package/dist/core/runner/prepare-park-ask.d.ts +3 -0
  53. package/dist/core/runner/prepare-park-ask.js +5 -3
  54. package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
  55. package/dist/core/runner/prepare-policy-chain.js +11 -4
  56. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  57. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  58. package/dist/core/runner/prepare-task.js +37 -18
  59. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  60. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  61. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  62. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  63. package/dist/core/runner/resume-apply.js +17 -3
  64. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  65. package/dist/core/runner/run-leg.js +5 -1
  66. package/dist/core/runner/runtask.js +2 -2
  67. package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
  68. package/dist/core/runner/write-protection-station.d.ts +3 -2
  69. package/dist/core/runner/write-protection-station.js +1 -0
  70. package/dist/core/runner-deps.d.ts +14 -0
  71. package/dist/core/sensitive-path-policy.d.ts +8 -0
  72. package/dist/core/sensitive-path-policy.js +18 -2
  73. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  74. package/dist/core/shell-wrapper-projection.js +56 -0
  75. package/dist/core/shell-wrapper-table.d.ts +144 -34
  76. package/dist/core/shell-wrapper-table.js +40 -20
  77. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  78. package/dist/core/synthetic-read-verdict.js +3 -1
  79. package/dist/core/tool-catalog-entries.d.ts +21 -0
  80. package/dist/core/tool-catalog-entries.js +9 -0
  81. package/dist/core/tool-policy.d.ts +30 -8
  82. package/dist/core/tool-policy.js +9 -7
  83. package/dist/core/tool-spec.d.ts +3 -2
  84. package/dist/core/write-protect.d.ts +4 -0
  85. package/dist/core/write-protect.js +30 -9
  86. package/dist/index.d.ts +5 -3
  87. package/dist/index.js +4 -2
  88. package/dist/orchestration/run-spec.js +8 -7
  89. package/dist/orchestration/workflow.js +1 -1
  90. package/dist/shell/lex.d.ts +7 -0
  91. package/dist/shell/lex.js +17 -0
  92. package/dist/tools/fs/bash-program-position.d.ts +8 -87
  93. package/dist/tools/fs/bash-program-position.js +33 -69
  94. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  95. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  96. package/dist/tools/fs/index.d.ts +3 -2
  97. package/dist/tools/fs/program-read-model.d.ts +694 -0
  98. package/dist/tools/fs/program-read-model.js +1252 -0
  99. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  100. package/dist/tools/fs/program-read-model.rows.js +613 -0
  101. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  102. package/dist/tools/fs/read-deny-home.js +62 -24
  103. package/dist/tools/fs/read-deny.d.ts +54 -14
  104. package/dist/tools/fs/read-deny.js +104 -50
  105. package/dist/tools/fs/removal-targets.d.ts +7 -6
  106. package/dist/tools/fs/removal-targets.js +22 -22
  107. package/dist/tools/fs/safety.d.ts +3 -0
  108. package/dist/tools/fs/safety.js +10 -3
  109. package/dist/tools/fs/shell-read-argv.d.ts +85 -0
  110. package/dist/tools/fs/shell-read-argv.js +386 -0
  111. package/dist/tools/fs/shell-read-bindings.d.ts +273 -0
  112. package/dist/tools/fs/shell-read-bindings.js +746 -0
  113. package/dist/tools/fs/shell-read-check.js +23 -15
  114. package/dist/tools/fs/shell-read-establish.js +79 -5
  115. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  116. package/dist/tools/fs/shell-read-judge.js +9 -1
  117. package/dist/tools/fs/shell-read-plan.d.ts +31 -7
  118. package/dist/tools/fs/shell-read-plan.js +299 -393
  119. package/dist/tools/fs/shell-read-word.d.ts +29 -26
  120. package/dist/tools/fs/shell-read-word.js +30 -38
  121. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
  122. package/dist/tools/fs/shell-read-write-targets.js +83 -136
  123. package/dist/tools/fs/shell-reentry-text.d.ts +38 -6
  124. package/dist/tools/fs/shell-reentry-text.js +90 -53
  125. package/package.json +1 -1
  126. package/test/export-surface.snapshot.json +17 -3
  127. package/wire-changes/core@7.46.0.json +1522 -0
@@ -38,6 +38,24 @@ export declare function readDenyHomeLinksOf(env: ExecutionEnv, entries: readonly
38
38
  readonly linked: Map<string, string[]>;
39
39
  readonly whole: boolean;
40
40
  }>;
41
+ /**
42
+ * Where the rows that open with the configuration directory's name (`~/.sema/…`, {@link agentConfigDirTail}) lead when
43
+ * the host declares the directory's root (`RunnerDeps.agentConfigDir`) — the same `linked` answer
44
+ * {@link readDenyHomeLinksOf} gives, for the same rows: the row's literal head below the directory name is spelled under
45
+ * each spelling of the root and resolved the way the read faces mint their keys. A row is not anchored at the root by
46
+ * its own run (it is anchored at the homes), so every location here — the spelling itself and where it leads — is one the
47
+ * row is placed at in addition; the home spelling stays placed. The row's name in every refusal is unchanged.
48
+ * · a row whose run does not open with the directory's name (`~/.sema.*`, the external tools' files) adds nothing;
49
+ * · a row whose head ends at the directory name (`~/.sema/*.json`, `~/.sema`) is placed at the root itself, the rest of
50
+ * its run matched below it.
51
+ */
52
+ export declare function readDenyConfigRootLinksOf(env: ExecutionEnv, entries: readonly {
53
+ readonly pattern: string;
54
+ readonly caseSensitive?: boolean;
55
+ }[], roots: readonly string[], signal?: AbortSignal, deadline?: number): Promise<{
56
+ readonly linked: Map<string, string[]>;
57
+ readonly whole: boolean;
58
+ }>;
41
59
  /** Where an absolute location LEADS in one environment — resolved the way the read and write faces mint their keys
42
60
  * (`canonicalizeTarget`: the longest existing prefix resolved, the rest rejoined). `undefined` when the environment
43
61
  * does not answer (a refused or failed probe, one that throws). The one resolution a deny row's placement reads: a
@@ -122,7 +140,7 @@ export declare function carryHomeViews<T extends object>(judge: T, source: HomeV
122
140
  * · each AUXILIARY home (see the module head) read once under {@link AUXILIARY_HOME_PROBE_MS} — its spellings, its
123
141
  * chain, its rows' heads — at the same time; whatever of it did not answer costs that home alone and never `whole`.
124
142
  * `compile` is the mount's own compile; the rows' patterns are read off its compile at no home. */
125
- export declare function placeReadDenyAtHome(env: ExecutionEnv, compile: (homes: readonly string[], linked?: ReadonlyMap<string, readonly string[]>) => ReadDenyMatcher, homes?: readonly string[], signal?: AbortSignal): Promise<{
143
+ export declare function placeReadDenyAtHome(env: ExecutionEnv, compile: (homes: readonly string[], linked?: ReadonlyMap<string, readonly string[]>) => ReadDenyMatcher, homes?: readonly string[], signal?: AbortSignal, configRoot?: string): Promise<{
126
144
  readonly matcher: PlacedReadDenyMatcher;
127
145
  readonly homes: readonly string[];
128
146
  readonly linked: ReadonlyMap<string, readonly string[]>;
@@ -2,6 +2,7 @@ import { userInfo } from "node:os";
2
2
  import { canonicalizeTarget, isAbsolutePathForm, joinForFamily, normalizeAbsPathLexically, pathFamilyOf } from "./safety.js";
3
3
  import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, withGateTimeout } from "../../core/gate-bounded-await.js";
4
4
  import { thrownText } from "../../core/thrown-value.js";
5
+ import { agentConfigDirTail } from "../../core/agent-config-dir.js";
5
6
  import { readDenyPatternRun, readDenyRunHead } from "./read-deny.js";
6
7
  export const AUXILIARY_HOME_PROBE_MS = 2_000;
7
8
  const said = new Set();
@@ -97,12 +98,15 @@ export async function readDenyHomesOf(env, signal) {
97
98
  async function until(work, deadline, signal) {
98
99
  return await withGateTimeout(work, Math.max(0, deadline - Date.now()), signal, "auxiliaryHome").catch(() => undefined);
99
100
  }
100
- function sayAuxiliary(what) {
101
+ function sayAuxiliary(what, subject = "auxiliary") {
102
+ if (subject === "config_root") {
103
+ sayOnce("config_root", `read-deny homes: the declared agent configuration root did not answer its probes within ${AUXILIARY_HOME_PROBE_MS} ms (${what}) — the rows that name the configuration directory sit at its spelling only, and a path through an alias of it is judged as spelled, until the next placement.`);
104
+ return;
105
+ }
101
106
  sayOnce("auxiliary", `read-deny homes: a home beside the environment's declared home did not answer its probes within ${AUXILIARY_HOME_PROBE_MS} ms (${what}) — its home-anchored read-deny rows sit at its spelling only, and a path through an alias of it is judged as spelled, until the next placement.`);
102
107
  }
103
108
  export async function readDenyHomeLinksOf(env, entries, homes, signal, deadline) {
104
109
  const rows = [];
105
- const familyOf = new Map();
106
110
  for (const { pattern, caseSensitive } of entries) {
107
111
  const { homeAnchored, run } = readDenyPatternRun(pattern);
108
112
  if (!homeAnchored)
@@ -110,26 +114,51 @@ export async function readDenyHomeLinksOf(env, entries, homes, signal, deadline)
110
114
  const head = run.split("/").filter(Boolean).slice(0, readDenyRunHead(run)).join("/");
111
115
  if (head.length === 0)
112
116
  continue;
113
- const spelled = homes.map((home) => {
114
- const family = pathFamilyOf({ root: home });
115
- const at = normalizeAbsPathLexically(joinForFamily(family, home, head));
116
- familyOf.set(at, family);
117
- return at;
118
- });
119
- rows.push({ pattern, caseSensitive: caseSensitive === true, spelled });
117
+ const spelled = homes.map((home) => normalizeAbsPathLexically(joinForFamily(pathFamilyOf({ root: home }), home, head)));
118
+ rows.push({ pattern, caseSensitive: caseSensitive === true, spelled, anchored: spelled });
120
119
  }
120
+ return await linkedLocationsOf(env, rows, signal, deadline);
121
+ }
122
+ export async function readDenyConfigRootLinksOf(env, entries, roots, signal, deadline) {
123
+ return await linkedLocationsOf(env, configRootRowsOf(entries, roots), signal, deadline);
124
+ }
125
+ function configRootRowsOf(entries, roots) {
126
+ const rows = [];
127
+ for (const { pattern, caseSensitive } of entries) {
128
+ const { homeAnchored, run } = readDenyPatternRun(pattern);
129
+ const tail = homeAnchored ? agentConfigDirTail(run, caseSensitive === true) : undefined;
130
+ if (tail === undefined)
131
+ continue;
132
+ const head = tail.split("/").filter(Boolean).slice(0, readDenyRunHead(tail)).join("/");
133
+ const spelled = roots.map((root) => normalizeAbsPathLexically(head.length === 0 ? root : joinForFamily(pathFamilyOf({ root }), root, head)));
134
+ rows.push({ pattern, caseSensitive: caseSensitive === true, spelled, anchored: [] });
135
+ }
136
+ return rows;
137
+ }
138
+ function configRootSpelledLinks(entries, roots) {
139
+ const out = new Map();
140
+ for (const { pattern, spelled } of configRootRowsOf(entries, roots))
141
+ out.set(pattern, [...new Set(spelled)]);
142
+ return out;
143
+ }
144
+ async function linkedLocationsOf(env, rows, signal, deadline) {
145
+ const familyOf = new Map();
146
+ for (const { spelled } of rows)
147
+ for (const at of spelled)
148
+ familyOf.set(at, pathFamilyOf({ root: at }));
121
149
  const leadOf = async (at, family) => (await whereItLeads(env, at, family, signal)) ?? ((await holdsNothingBelow(env, at, signal)) ? at : undefined);
122
150
  const resolved = new Map(await Promise.all([...familyOf].map(async ([at, family]) => [at, deadline === undefined ? await leadOf(at, family) : await until(leadOf(at, family), deadline, signal)])));
123
151
  const out = new Map();
124
152
  const whole = [...resolved.values()].every((location) => location !== undefined);
125
- for (const { pattern, caseSensitive, spelled } of rows) {
153
+ for (const { pattern, caseSensitive, spelled, anchored } of rows) {
126
154
  const fold = (p) => (caseSensitive ? p : p.replace(/[A-Z]/g, (c) => c.toLowerCase()));
127
- const own = new Set(spelled.map(fold));
155
+ const own = new Set(anchored.map(fold));
128
156
  const locations = [];
129
157
  for (const at of spelled) {
130
- const location = resolved.get(at);
131
- if (location !== undefined && !own.has(fold(location)) && !locations.includes(location))
132
- locations.push(location);
158
+ for (const location of [at, resolved.get(at)]) {
159
+ if (location !== undefined && !own.has(fold(location)) && !locations.includes(location))
160
+ locations.push(location);
161
+ }
133
162
  }
134
163
  if (locations.length > 0)
135
164
  out.set(pattern, locations);
@@ -203,19 +232,24 @@ async function readAuxiliaryHome(env, home, opts, signal) {
203
232
  let reading;
204
233
  if (spelled?.canonical === undefined) {
205
234
  const answered = spelled?.answered === true;
206
- reading = { place: [home], chain: answered ? { ancestors: [], whole: true } : NO_CHAIN, linked: new Map(), answered };
235
+ const linked = opts.configRoot === true && opts.entries !== undefined ? configRootSpelledLinks(opts.entries, [home]) : new Map();
236
+ reading = { place: [home], chain: answered ? { ancestors: [], whole: true } : NO_CHAIN, linked, answered };
207
237
  }
208
238
  else {
209
239
  const place = spelled.spellings;
210
240
  const identity = env.fileIdentity;
211
241
  const [chain, heads] = await Promise.all([
212
242
  typeof identity === "function" ? chainAbove(env, identity, spelled.canonical, signal, deadline) : Promise.resolve({ ancestors: [], whole: true }),
213
- opts.entries !== undefined ? readDenyHomeLinksOf(env, opts.entries, place, signal, deadline) : Promise.resolve({ linked: new Map(), whole: true }),
243
+ opts.entries === undefined
244
+ ? Promise.resolve({ linked: new Map(), whole: true })
245
+ : opts.configRoot === true
246
+ ? readDenyConfigRootLinksOf(env, opts.entries, place, signal, deadline)
247
+ : readDenyHomeLinksOf(env, opts.entries, place, signal, deadline),
214
248
  ]);
215
249
  reading = { place, chain, linked: heads.linked, answered: chain.whole && heads.whole };
216
250
  }
217
251
  if (!reading.answered)
218
- sayAuxiliary("a probe did not answer");
252
+ sayAuxiliary("a probe did not answer", opts.configRoot === true ? "config_root" : "auxiliary");
219
253
  return reading;
220
254
  }
221
255
  function homeViewSourceOver(env, auxiliary) {
@@ -325,31 +359,35 @@ export function homeViewsOf(env, judge, signal) {
325
359
  export function carryHomeViews(judge, source) {
326
360
  return { ...judge, homeViews: source };
327
361
  }
328
- export async function placeReadDenyAtHome(env, compile, homes, signal) {
362
+ export async function placeReadDenyAtHome(env, compile, homes, signal, configRoot) {
329
363
  const entries = compile([]).entries;
330
364
  const declared = declaredHomeOf(env);
331
- const heldAnywhere = (whole) => placementOf(carryHomeViews(compile([]), homeViewSourceOver(env, [])), [], new Map(), whole);
365
+ const relocated = configRoot === undefined ? undefined : readAuxiliaryHome(env, configRoot, { entries, configRoot: true }, signal);
366
+ const heldAnywhere = async (whole) => {
367
+ const linked = (await relocated)?.linked ?? new Map();
368
+ return placementOf(carryHomeViews(linked.size === 0 ? compile([]) : compile([], linked), homeViewSourceOver(env, relocated === undefined ? [] : [relocated])), [], linked, whole);
369
+ };
332
370
  const listed = homes === undefined ? declaredReadDenyHomesAnswered(env) : { homes, whole: true };
333
371
  if (!listed.whole)
334
- return heldAnywhere(false);
372
+ return await heldAnywhere(false);
335
373
  const own = listed.homes.filter((h) => isDeclared(declared, h));
336
374
  let declaredAt = [];
337
375
  if (own.length > 0) {
338
376
  const ownSpellings = await withGateTimeout(homeSpellingsOf(env, own[0], signal), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "readDenyHomes");
339
377
  if (!ownSpellings.answered)
340
- return heldAnywhere(false);
378
+ return await heldAnywhere(false);
341
379
  declaredAt = [...new Set([...own, ...ownSpellings.spellings])];
342
380
  }
343
381
  const auxiliary = readAuxiliaryHomes(env, listed.homes, { entries }, signal);
344
- const [heads, readings] = await Promise.all([readDenyHomeLinksOf(env, entries, declaredAt, signal), Promise.all(auxiliary)]);
382
+ const [heads, readings, root] = await Promise.all([readDenyHomeLinksOf(env, entries, declaredAt, signal), Promise.all(auxiliary), relocated]);
345
383
  const at = [...new Set([...declaredAt, ...readings.flatMap((r) => r.place)])];
346
384
  const linked = new Map();
347
- for (const map of [heads.linked, ...readings.map((r) => r.linked)]) {
385
+ for (const map of [heads.linked, ...readings.map((r) => r.linked), ...(root === undefined ? [] : [root.linked])]) {
348
386
  for (const [pattern, locations] of map)
349
387
  linked.set(pattern, [...new Set([...(linked.get(pattern) ?? []), ...locations])]);
350
388
  }
351
389
  const matcher = linked.size === 0 ? compile(at) : compile(at, linked);
352
- return placementOf(carryHomeViews(matcher, homeViewSourceOver(env, auxiliary)), at, linked, heads.whole);
390
+ return placementOf(carryHomeViews(matcher, homeViewSourceOver(env, relocated === undefined ? auxiliary : [...auxiliary, relocated])), at, linked, heads.whole);
353
391
  }
354
392
  export function readDenyPlacedLater(unplaced, place) {
355
393
  let current = unplaced;
@@ -70,26 +70,66 @@ export interface PathRuleDenyRow {
70
70
  * row, or one whose head cannot be read: it is judged as spelled. */
71
71
  readonly head?: string;
72
72
  }
73
+ /** A row's reach over ONE absolute spelling, three-valued: `unreadable` = the row cannot be judged there (a base its anchor
74
+ * lacks) — a tightening judgment holds on it, an allow grants nothing on it. */
75
+ export type PathRowReach = {
76
+ readonly reach: "reached" | "not_reached";
77
+ } | {
78
+ readonly reach: "unreadable";
79
+ readonly reason: string;
80
+ };
81
+ /** A rule row as the one judgment reads it: the deny-table row, and — on a row a rule binding produced — its three-valued
82
+ * reach (a row built by hand answers through `reaches` alone: reached or not). */
83
+ export interface JudgedPathRow extends PathRuleDenyRow {
84
+ readonly reach?: (absolutePath: string) => PathRowReach;
85
+ }
86
+ /** A call's target as a judgment reads it: the absolute spellings of its views (lexical, canonical, the leaf chain's hops —
87
+ * any order the caller holds), or `unreadable` when the call names a target this reading cannot locate. */
88
+ export interface TargetViews {
89
+ readonly spellings: readonly string[];
90
+ readonly unreadable?: string;
91
+ }
92
+ /** What {@link pathRowsReach} found: the row, the view it met there (absent on an unreadable hold), and — when nothing was
93
+ * reached and some row could not be judged — the reason of the first such row. */
94
+ export interface PathRowsHit<R> {
95
+ readonly row: R;
96
+ readonly view?: string;
97
+ readonly unreadable?: string;
98
+ }
99
+ /**
100
+ * THE one judgment of path rule rows over a call's target, for the gate's rule lanes and the tool's deny table alike: rows
101
+ * outer, views inner — the first row (in the caller's order) that some view reaches is the answer, so the rule a refusal
102
+ * names does not depend on which view happened to meet it. `tighten` (deny / ask): any view reaching the row reaches it; a
103
+ * row that cannot be judged (or a target that cannot be read) is held and answered only when no row reaches. `allow`: a
104
+ * row admits the call only when it admits EVERY view (a link out of an allowed directory is not admitted through its
105
+ * spelling); an unreadable target is admitted by nothing.
106
+ */
107
+ export declare function pathRowsReach<R extends JudgedPathRow>(rows: readonly R[], views: TargetViews, direction: "tighten" | "allow"): PathRowsHit<R> | undefined;
108
+ /** The locations a row is also judged at (each leading part of its HEAD: the anchor's children down to the head itself,
109
+ * a share's root for a UNC head) — what a leg resolves once, for every row whose head it holds. */
110
+ export declare function ruleHeadParts(rows: readonly Pick<PathRuleDenyRow, "head">[]): string[];
73
111
  /**
74
112
  * The rule rows as a leg judges them: each row as spelled, and — for every location on the way to the row's HEAD (each
75
- * leading part of it: the anchor's children down to the head itself) that the environment resolves to another spelling
76
- * (a link or firmlink: `/tmp` → `/private/tmp`, `/var` → `/private/var`, a project root, a home entry or a directory
77
- * inside the rule reached through a symlink) — the same row at that spelling: a path at or below the resolved location
78
- * is judged as its spelling at or below that part. Every read and write face judges the CANONICAL key of a target beside
79
- * its spelled view, so a row whose head is spelled through a link would otherwise never meet a target named by its real
80
- * location — whether the row was written with the short spelling and the call with the long one, or the reverse. Every
81
- * part, not the head alone: a link inside the rule's literal prefix that is re-pointed after the rows were placed still
82
- * meets the row through the parts above it (`<real root>/alias/**` holds when `alias` moves). One rule for every row
83
- * form (absolute, home, root, working directory, directory): the reference compiles the same "physical twin" of every
84
- * deny / ask rule's literal prefix (cli280.js `ei` 9766583, compiled in `mt` 9767083). `leadsTo` is the leg's resolution,
85
- * the one the faces mint their keys with (`undefined` = could not resolve: no twin there); each location is resolved
86
- * once, and a part that only extends its parent's resolution adds no row.
113
+ * leading part of it) that the environment resolves to another spelling (a link or firmlink: `/tmp` → `/private/tmp`,
114
+ * `/var` → `/private/var`, a project root, a home entry or a directory inside the rule reached through a symlink) — the
115
+ * same row at that spelling, right after it: a path at or below the resolved location is judged as its spelling at or
116
+ * below that part. Every part, not the head alone: a link inside the rule's literal prefix that is re-pointed after the
117
+ * rows were placed still meets the row through the parts above it. One rule for every row form and every judge (the
118
+ * gate's lanes and the deny tables): the reference compiles the same "physical twin" of every deny / ask rule's literal
119
+ * prefix (cli280.js `ei` 9766583, compiled in `mt` 9767083). `leads` is the leg's resolution of each part (absent or
120
+ * `undefined` = no twin there); a part that only extends its parent's resolution adds no row. A twin is the row's own
121
+ * kind (it keeps every member but the head), so a judgment reports the rule the twin stands for.
87
122
  */
88
- export declare function withRuleRowsWhereTheyLead(rows: readonly PathRuleDenyRow[], leadsTo: (head: string) => Promise<string | undefined>): Promise<PathRuleDenyRow[]>;
123
+ export declare function rowsWhereTheyLead<R extends JudgedPathRow>(rows: readonly R[], leads: ReadonlyMap<string, string | undefined>): R[];
124
+ /** Where the location `path` itself leads in a leg's resolution (`leads`, keyed by each leading part — the parts
125
+ * {@link ruleHeadParts} names): `undefined` when the leg did not resolve it or it leads to its own spelling. */
126
+ export declare function locationLeadsTo(path: string, leads: ReadonlyMap<string, string | undefined>): string | undefined;
127
+ /** {@link rowsWhereTheyLead} with each part resolved by `leadsTo` (the leg's resolution; each part once). */
128
+ export declare function withRuleRowsWhereTheyLead<R extends JudgedPathRow>(rows: readonly R[], leadsTo: (head: string) => Promise<string | undefined>): Promise<R[]>;
89
129
  /**
90
130
  * The WRITE face of the deny table: the rule rows whose faces include `write` — the judge the shell station reads for a
91
131
  * command's write targets (a redirection, `tee`, `sed -i`, `cp` / `mv` / `touch` / `mkdir` / `rm` operands). Same row
92
- * type, same judgment (`ruleRowHit`) as the read face's rule rows; a hit names the row by its rule text.
132
+ * type, same judgment ({@link pathRowsReach}) as the read face's rule rows; a hit names the row by its rule text.
93
133
  */
94
134
  export interface WriteDenyMatcher {
95
135
  /** Which face this judge is — the write tools' path resolution reads it to word and code the refusal as a write. */
@@ -12,32 +12,62 @@ export function readDenyRunHead(run) {
12
12
  return wild === -1 ? segments.length : wild;
13
13
  }
14
14
  export const DENY_FACES = Object.freeze(["read", "write"]);
15
- export async function withRuleRowsWhereTheyLead(rows, leadsTo) {
16
- const leads = new Map();
17
- const partsOf = (head) => {
18
- const family = pathFamilyOf({ root: head });
19
- const id = pathIdentityOf(head, family, head);
20
- if (!id.ok)
21
- return [head];
22
- const unc = id.identity.startsWith("//");
23
- const segments = id.identity.split("/").filter((seg) => seg !== "");
24
- const parts = [];
25
- for (let k = unc ? 2 : 1; k <= segments.length; k++)
26
- parts.push(spellingOfIdentity(`${unc ? "//" : "/"}${segments.slice(0, k).join("/")}`, family));
27
- return parts.length > 0 ? parts : [head];
28
- };
29
- const partsByRow = rows.map((row) => (row.head === undefined ? [] : partsOf(row.head)));
30
- for (const parts of partsByRow)
31
- for (const part of parts)
32
- if (!leads.has(part))
33
- leads.set(part, leadsTo(part).catch(() => undefined));
15
+ const REACHED = { reach: "reached" };
16
+ const NOT_REACHED = { reach: "not_reached" };
17
+ const reachOf = (row, spelled) => row.reach?.(spelled) ?? (row.reaches(spelled) ? REACHED : NOT_REACHED);
18
+ export function pathRowsReach(rows, views, direction) {
19
+ if (direction === "allow") {
20
+ if (views.unreadable !== undefined || views.spellings.length === 0)
21
+ return undefined;
22
+ for (const row of rows)
23
+ if (views.spellings.every((v) => reachOf(row, v).reach === "reached"))
24
+ return { row };
25
+ return undefined;
26
+ }
27
+ let held;
28
+ for (const row of rows) {
29
+ if (views.unreadable !== undefined)
30
+ held ??= { row, unreadable: views.unreadable };
31
+ for (const view of views.spellings) {
32
+ const r = reachOf(row, view);
33
+ if (r.reach === "reached")
34
+ return { row, view };
35
+ if (r.reach === "unreadable")
36
+ held ??= { row, unreadable: r.reason };
37
+ }
38
+ }
39
+ return held;
40
+ }
41
+ export function ruleHeadParts(rows) {
42
+ const out = new Set();
43
+ for (const row of rows)
44
+ if (row.head !== undefined)
45
+ for (const part of partsOf(row.head))
46
+ out.add(part);
47
+ return [...out];
48
+ }
49
+ function partsOf(head) {
50
+ const family = pathFamilyOf({ root: head });
51
+ const id = pathIdentityOf(head, family, head);
52
+ if (!id.ok)
53
+ return [head];
54
+ const unc = id.identity.startsWith("//");
55
+ const segments = id.identity.split("/").filter((seg) => seg !== "");
56
+ const parts = [];
57
+ for (let k = unc ? 2 : 1; k <= segments.length; k++)
58
+ parts.push(spellingOfIdentity(`${unc ? "//" : "/"}${segments.slice(0, k).join("/")}`, family));
59
+ return parts.length > 0 ? parts : [head];
60
+ }
61
+ export function rowsWhereTheyLead(rows, leads) {
34
62
  const out = [];
35
- for (const [i, row] of rows.entries()) {
63
+ for (const row of rows) {
36
64
  out.push(row);
65
+ if (row.head === undefined || leads.size === 0)
66
+ continue;
37
67
  let parentLed;
38
68
  let parentPart;
39
- for (const part of partsByRow[i]) {
40
- const led = await leads.get(part);
69
+ for (const part of partsOf(row.head)) {
70
+ const led = leads.get(part);
41
71
  const extendsParent = led !== undefined && parentLed !== undefined && parentPart !== undefined && sameLocation(led, `${parentLed.replace(/[\\/]+$/, "")}/${part.slice(parentPart.replace(/[\\/]+$/, "").length).replace(/^[\\/]+/, "")}`);
42
72
  const twin = extendsParent ? undefined : rowLedTo(row, part, led);
43
73
  if (twin !== undefined)
@@ -48,6 +78,15 @@ export async function withRuleRowsWhereTheyLead(rows, leadsTo) {
48
78
  }
49
79
  return out;
50
80
  }
81
+ export function locationLeadsTo(path, leads) {
82
+ const parts = partsOf(path);
83
+ return leads.get(parts[parts.length - 1]);
84
+ }
85
+ export async function withRuleRowsWhereTheyLead(rows, leadsTo) {
86
+ const parts = ruleHeadParts(rows);
87
+ const led = await Promise.all(parts.map((part) => leadsTo(part).catch(() => undefined)));
88
+ return rowsWhereTheyLead(rows, new Map(parts.map((part, i) => [part, led[i]])));
89
+ }
51
90
  function sameLocation(a, b) {
52
91
  const family = pathFamilyOf({ root: a });
53
92
  const x = pathIdentityOf(a, family, a);
@@ -65,50 +104,60 @@ function rowLedTo(row, head, at) {
65
104
  const base = from.identity;
66
105
  const under = base.endsWith("/") ? base : `${base}/`;
67
106
  const onto = to.identity.endsWith("/") ? to.identity.slice(0, -1) : to.identity;
107
+ const mapped = (absolutePath) => {
108
+ const id = pathIdentityOf(absolutePath, family, head);
109
+ if (!id.ok)
110
+ return undefined;
111
+ if (id.identity === base)
112
+ return to.identity;
113
+ return id.identity.startsWith(under) ? `${onto}/${id.identity.slice(under.length)}` : undefined;
114
+ };
115
+ const { head: _head, ...rest } = row;
116
+ const reach = row.reach;
68
117
  return {
69
- rule: row.rule,
70
- tool: row.tool,
71
- pattern: row.pattern,
72
- faces: row.faces,
118
+ ...rest,
73
119
  reaches: (absolutePath) => {
74
- const id = pathIdentityOf(absolutePath, family, head);
75
- if (!id.ok)
76
- return false;
77
- if (id.identity === base)
78
- return row.reaches(to.identity);
79
- return id.identity.startsWith(under) && row.reaches(`${onto}/${id.identity.slice(under.length)}`);
120
+ const m = mapped(absolutePath);
121
+ return m !== undefined && row.reaches(m);
80
122
  },
123
+ ...(reach !== undefined
124
+ ? {
125
+ reach: (absolutePath) => {
126
+ const m = mapped(absolutePath);
127
+ return m === undefined ? NOT_REACHED : reach(m);
128
+ },
129
+ }
130
+ : {}),
81
131
  };
82
132
  }
83
- function ruleRowHit(rows, face, path) {
84
- for (const r of rows)
85
- if (r.faces.includes(face) && r.reaches(path))
86
- return r;
87
- return undefined;
133
+ function ruleRowsHit(rows, face, spellings) {
134
+ const onFace = rows.filter((r) => r.faces.includes(face));
135
+ return onFace.length === 0 ? undefined : pathRowsReach(onFace, { spellings }, "tighten");
88
136
  }
89
137
  export function compileWriteDeny(rules = []) {
90
138
  const ruleRows = rules.filter((r) => r.faces.includes("write"));
91
139
  const matchPath = (path) => {
92
- const hit = ruleRowHit(ruleRows, "write", path);
93
- return hit === undefined ? null : { pattern: hit.rule };
140
+ const hit = ruleRowsHit(ruleRows, "write", [path]);
141
+ return hit === undefined ? null : { pattern: hit.row.rule };
94
142
  };
95
- return { face: "write", ruleRows, matchPath, matchTarget: (canonicalKey, lexicalView, opts) => matchTargetOver(matchPath, canonicalKey, lexicalView, opts) };
143
+ return { face: "write", ruleRows, matchPath, matchTarget: (canonicalKey, lexicalView, opts) => matchTargetOver(() => null, ruleRows, "write", canonicalKey, lexicalView, opts) };
96
144
  }
97
- function matchTargetOver(matchPath, canonicalKey, lexicalView, opts) {
98
- const canonical = matchPath(canonicalKey, opts);
145
+ function matchTargetOver(segmentPath, ruleRows, face, canonicalKey, lexicalView, opts) {
146
+ const canonical = segmentPath(canonicalKey, opts);
99
147
  if (canonical !== null)
100
148
  return { ...canonical, matchedView: canonicalKey };
101
149
  if (lexicalView !== undefined) {
102
- const lexical = matchPath(lexicalView);
150
+ const lexical = segmentPath(lexicalView);
103
151
  if (lexical !== null)
104
152
  return { ...lexical, matchedView: lexicalView };
105
153
  }
106
154
  for (const hop of opts?.hops ?? []) {
107
- const viaHop = matchPath(hop);
155
+ const viaHop = segmentPath(hop);
108
156
  if (viaHop !== null)
109
157
  return { ...viaHop, matchedView: hop };
110
158
  }
111
- return null;
159
+ const hit = ruleRowsHit(ruleRows, face, [canonicalKey, ...(lexicalView !== undefined ? [lexicalView] : []), ...(opts?.hops ?? [])]);
160
+ return hit === undefined ? null : { pattern: hit.row.rule, matchedView: hit.view ?? canonicalKey };
112
161
  }
113
162
  export function readDenySeat(matcher) {
114
163
  const entries = [...(matcher?.entries ?? []).map((e) => e.pattern), ...(matcher?.ruleRows ?? []).map((r) => r.rule)];
@@ -195,6 +244,7 @@ export const READ_FACE_BUILTIN_DENY_TABLE = [
195
244
  { pattern: "~/.mcp.json", tier: "agent-config" },
196
245
  { pattern: "~/.ai-agent/.env", tier: "agent-config" },
197
246
  { pattern: "~/.sema/engine-data/.env", tier: "agent-config" },
247
+ { pattern: "~/.sema/config-lkg-*.json", tier: "agent-config" },
198
248
  { pattern: "~/.codex/config.toml", tier: "agent-config" },
199
249
  { pattern: "~/.cursor/mcp.json", tier: "agent-config" },
200
250
  { pattern: "~/.continue/config.json", tier: "agent-config" },
@@ -461,18 +511,22 @@ export function compileReadDeny(additions = [], layer = "additions", builtin, ru
461
511
  return { pattern: hit };
462
512
  return ruleRows.length === 0 ? null : { pattern: ruleRows[0].rule };
463
513
  };
464
- const matchPath = (path, opts) => {
514
+ const segmentPath = (path, opts) => {
465
515
  const hit = matchSegmentPatterns(path, judged, opts);
516
+ return hit === null ? null : { pattern: hit };
517
+ };
518
+ const matchPath = (path, opts) => {
519
+ const hit = segmentPath(path, opts);
466
520
  if (hit !== null)
467
- return { pattern: hit };
468
- const rule = ruleRows.length === 0 || opts?.walked === true ? undefined : ruleRowHit(ruleRows, "read", path);
469
- return rule === undefined ? null : { pattern: rule.rule };
521
+ return hit;
522
+ const rule = ruleRows.length === 0 || opts?.walked === true ? undefined : ruleRowsHit(ruleRows, "read", [path]);
523
+ return rule === undefined ? null : { pattern: rule.row.rule };
470
524
  };
471
525
  return {
472
526
  entries: normalized,
473
527
  ...(ruleRows.length > 0 ? { ruleRows } : {}),
474
528
  matchPath,
475
- matchTarget: (canonicalKey, lexicalView, opts) => matchTargetOver(matchPath, canonicalKey, lexicalView, opts),
529
+ matchTarget: (canonicalKey, lexicalView, opts) => matchTargetOver(segmentPath, ruleRows, "read", canonicalKey, lexicalView, opts),
476
530
  rgExclusionGlobs,
477
531
  rgProbeGlobs,
478
532
  unexcludableUnder,
@@ -43,12 +43,13 @@ export type RemovalFinding = (DangerousRemoval & {
43
43
  /**
44
44
  * The removal findings of an established plan — ONE walk, nested plans included (a `bash -c` / `sh -c` text, a substitution
45
45
  * body), two words. For every `rm` / `rmdir` segment, each of its OPERANDS (every write target that is not one of its
46
- * option words, read off its argv with {@link REMOVAL_GRAMMAR}) as the plan established it on the WRITE face (the entry the
47
- * program unlinks, and the deny-floor shadow of a home word the plan did not place) is judged by {@link dangerousRemovalOf};
48
- * an operand the plan could not place that expands a name the command itself binds is judged in every spelling those
49
- * bindings give it ({@link boundSpellingsOf}), and one with a binding this reading cannot read is an unverifiable target (a
50
- * recursive force `rm`). A shell handed a script built only when the command runs is a dangerous removal of its own
51
- * ({@link runtimeScriptOf}). The read-face copy of an operand is not judged: it follows a link the removal does not follow.
46
+ * option words: a word some implementation's reading of the program's row writes, {@link writeReadings}) as the plan
47
+ * established it on the WRITE face (the entry the program unlinks, and the deny-floor shadow of a home word the plan did
48
+ * not place) is judged by {@link dangerousRemovalOf}; an operand the plan could not place that expands a name the command
49
+ * itself binds is judged in every spelling those bindings give it ({@link boundSpellingsOf}), and one with a binding this
50
+ * reading cannot read is an unverifiable target (a recursive forced removal, {@link recursiveForce}). A shell handed a
51
+ * script built only when the command runs is a dangerous removal of its own ({@link runtimeScriptOf}). The read-face copy
52
+ * of an operand is not judged: it follows a link the removal does not follow.
52
53
  */
53
54
  export declare function removalFindingsOf(established: EstablishedPlan, seat: RemovalSeat): RemovalFinding[];
54
55
  /** Does any segment of this plan (nested plans included) run a removal program, or hand a shell a script built when it
@@ -1,13 +1,14 @@
1
1
  import { planShellReads } from "./shell-read-plan.js";
2
+ import { literalTextOf } from "../../shell/lex.js";
2
3
  import { createShellReadEstablisher, establishShellReadsLexically } from "./shell-read-establish.js";
3
4
  import { isAbsolutePathForm, isWinFormPath, normalizeAbsPathLexically } from "./safety.js";
4
5
  import { reentryText } from "./shell-reentry-text.js";
5
- import { bashClassifyDefaultAllow, SHELL_REENTRY_PROGRAM_RE } from "./bash-readonly-classifier.js";
6
- import { programArgs, readArgv } from "./shell-read-write-targets.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";
7
9
  import { inlineUntrusted } from "../../core/untrusted-text.js";
8
10
  export const SHELL_REMOVAL_COMMANDS = Object.freeze(["rm", "rmdir"]);
9
11
  const REMOVAL_COMMAND_SET = new Set(SHELL_REMOVAL_COMMANDS);
10
- const REMOVAL_GRAMMAR = { required: "", optional: "", longRequired: [] };
11
12
  function removalKey(path) {
12
13
  let k = normalizeAbsPathLexically(path.normalize("NFC")).replace(/\\/g, "/").toLowerCase();
13
14
  k = k.replace(/(?:\/+\*+)+\/*$/, "");
@@ -54,9 +55,6 @@ function removalKindOf(read, seat) {
54
55
  }
55
56
  return read.established === "unestablishable" && tildeNamesAHome(read.candidate.spelled) ? "home" : undefined;
56
57
  }
57
- function literalOf(pieces) {
58
- return pieces.every((q) => q.literal !== undefined) ? pieces.map((q) => q.literal).join("") : undefined;
59
- }
60
58
  const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
61
59
  const NAME_BINDERS = new Set(["unset", "local", "declare", "typeset", "readonly", "export", "read"]);
62
60
  function bindingsOf(plan) {
@@ -67,11 +65,11 @@ function bindingsOf(plan) {
67
65
  for (const segment of plan.segments) {
68
66
  for (const w of segment.argv)
69
67
  if (w.assignment !== undefined)
70
- bind(w.assignment.name, w.assignment.settlesWholeName ? literalOf(w.assignment.value.pieces) : undefined);
68
+ bind(w.assignment.name, w.assignment.settlesWholeName ? literalTextOf(w.assignment.value.pieces) : undefined);
71
69
  const [head, name] = segment.argv;
72
70
  if ((head?.folded === "for" || head?.folded === "select") && name !== undefined && NAME.test(name.folded)) {
73
71
  const at = segment.argv.findIndex((w, i) => i > 1 && w.folded === "in");
74
- const words = at < 0 ? [undefined] : segment.argv.slice(at + 1).map((w) => literalOf(w.pieces));
72
+ const words = at < 0 ? [undefined] : segment.argv.slice(at + 1).map((w) => literalTextOf(w.pieces));
75
73
  for (const v of words.length > 0 ? words : [undefined])
76
74
  bind(name.folded, v);
77
75
  }
@@ -105,20 +103,23 @@ function boundSpellingsOf(word, bindings) {
105
103
  }
106
104
  return { spellings };
107
105
  }
108
- function recursiveForce(args) {
109
- const { options } = readArgv(args, REMOVAL_GRAMMAR);
110
- const has = (long, short) => options.some((o) => (o.long ? long.startsWith(o.name) : short.test(o.name)));
111
- return has("recursive", /^[rR]$/) && has("force", /^f$/);
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
+ });
112
111
  }
113
112
  export function removalFindingsOf(established, seat) {
114
113
  const out = [];
115
114
  const bindings = bindingsOf(established.plan);
116
115
  for (const segment of established.plan.segments) {
117
116
  const program = segment.verb?.basename;
118
- if (program !== undefined && REMOVAL_COMMAND_SET.has(program)) {
117
+ const row = program !== undefined && REMOVAL_COMMAND_SET.has(program) ? programRow(program) : undefined;
118
+ if (program !== undefined && row !== undefined) {
119
119
  const args = programArgs(segment);
120
- const operands = new Set(readArgv(args, REMOVAL_GRAMMAR).operands);
121
- const options = new Set(args.filter((w) => !operands.has(w)).map((w) => w.raw));
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));
122
123
  for (const read of established.reads.get(segment) ?? []) {
123
124
  if (read.candidate.face !== "write" || options.has(read.candidate.raw))
124
125
  continue;
@@ -134,7 +135,7 @@ export function removalFindingsOf(established, seat) {
134
135
  if (bound === undefined)
135
136
  continue;
136
137
  if ("unreadable" in bound) {
137
- if (program === "rm" && recursiveForce(args))
138
+ if (recursiveForce(row, spelled))
138
139
  out.push({ guard: "unverifiable_delete", program, operand: read.candidate.raw, name: bound.unreadable });
139
140
  continue;
140
141
  }
@@ -164,15 +165,14 @@ export function removalFindingsOf(established, seat) {
164
165
  }
165
166
  function runtimeScriptOf(segment) {
166
167
  const program = segment.verb;
167
- if (segment.reentry !== "shell" || program === undefined || !SHELL_REENTRY_PROGRAM_RE.test(program.basename))
168
+ if (segment.reentry !== "shell" || program === undefined || !runsShellText(program.basename))
168
169
  return undefined;
169
170
  const at = segment.argv.findIndex((w) => w.folded === program.name);
170
- if (at < 0)
171
- return undefined;
172
171
  const MARK = "\u0000";
173
- const words = segment.argv.slice(at + 1).map((w) => literalOf(w.pieces) ?? `${MARK}${w.raw}`);
174
- const text = reentryText(program.basename, words);
175
- return text === undefined || !text.includes(MARK) ? undefined : text.split(MARK).join("");
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
176
  }
177
177
  export function planRunsRemoval(plan) {
178
178
  return plan.segments.some((s) => (s.verb !== undefined && REMOVAL_COMMAND_SET.has(s.verb.basename)) || runtimeScriptOf(s) !== undefined || s.nested.some(planRunsRemoval));