@sema-agent/core 7.45.0 → 7.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +298 -1
  2. package/dist/agents/extra-tool-names.d.ts +65 -0
  3. package/dist/agents/extra-tool-names.js +124 -0
  4. package/dist/agents/subagent.d.ts +17 -5
  5. package/dist/agents/subagent.js +33 -25
  6. package/dist/agents/tool-filter.d.ts +17 -1
  7. package/dist/agents/tool-filter.js +7 -0
  8. package/dist/core/agent-config-dir.d.ts +20 -0
  9. package/dist/core/agent-config-dir.js +49 -0
  10. package/dist/core/allow-entry.d.ts +6 -4
  11. package/dist/core/allow-entry.js +6 -5
  12. package/dist/core/ask-origin.d.ts +35 -12
  13. package/dist/core/ask-origin.js +24 -7
  14. package/dist/core/checkpoint-store.d.ts +39 -2
  15. package/dist/core/effective-path-target.d.ts +136 -4
  16. package/dist/core/effective-path-target.js +110 -2
  17. package/dist/core/engine-notice.d.ts +12 -4
  18. package/dist/core/gate-fold.js +5 -2
  19. package/dist/core/gate-lanes.js +11 -4
  20. package/dist/core/gate-outcome.d.ts +4 -2
  21. package/dist/core/gate-outcome.js +2 -2
  22. package/dist/core/governance-codes.d.ts +3 -1
  23. package/dist/core/governance-codes.js +3 -0
  24. package/dist/core/hooks.d.ts +3 -0
  25. package/dist/core/path-rule-binding.d.ts +65 -0
  26. package/dist/core/path-rule-binding.js +99 -0
  27. package/dist/core/permission-rule-model.d.ts +59 -24
  28. package/dist/core/permission-rule-model.js +48 -44
  29. package/dist/core/permission-rule-provider.d.ts +7 -0
  30. package/dist/core/permission-rule-provider.js +10 -3
  31. package/dist/core/permission-rule-store.d.ts +8 -0
  32. package/dist/core/permission-rule-store.js +6 -2
  33. package/dist/core/permission-rule-sync.js +2 -2
  34. package/dist/core/permission-rules.d.ts +26 -45
  35. package/dist/core/permission-rules.js +77 -57
  36. package/dist/core/runner/child-tree.d.ts +97 -0
  37. package/dist/core/runner/child-tree.js +104 -0
  38. package/dist/core/runner/construction-doors.d.ts +5 -0
  39. package/dist/core/runner/construction-doors.js +6 -0
  40. package/dist/core/runner/contracts.d.ts +46 -0
  41. package/dist/core/runner/gate-exit.d.ts +4 -1
  42. package/dist/core/runner/gate-exit.js +5 -1
  43. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  44. package/dist/core/runner/permission-rule-lanes.js +18 -12
  45. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  46. package/dist/core/runner/persisted-path-rows.js +31 -12
  47. package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
  48. package/dist/core/runner/prepare-ask-lane.js +4 -2
  49. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  50. package/dist/core/runner/prepare-execution-env.js +10 -3
  51. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  52. package/dist/core/runner/prepare-gate-stations.js +2 -1
  53. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  54. package/dist/core/runner/prepare-hands-readface.js +11 -11
  55. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  56. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  57. package/dist/core/runner/prepare-park-ask.d.ts +3 -0
  58. package/dist/core/runner/prepare-park-ask.js +5 -3
  59. package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
  60. package/dist/core/runner/prepare-policy-chain.js +13 -5
  61. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  62. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  63. package/dist/core/runner/prepare-task.js +37 -18
  64. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  65. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  66. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  67. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  68. package/dist/core/runner/resume-apply.js +17 -3
  69. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  70. package/dist/core/runner/run-leg.js +5 -1
  71. package/dist/core/runner/runtask.js +2 -2
  72. package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
  73. package/dist/core/runner/write-protection-station.d.ts +3 -2
  74. package/dist/core/runner/write-protection-station.js +1 -0
  75. package/dist/core/runner-deps.d.ts +14 -0
  76. package/dist/core/sensitive-path-policy.d.ts +8 -0
  77. package/dist/core/sensitive-path-policy.js +18 -2
  78. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  79. package/dist/core/shell-wrapper-projection.js +56 -0
  80. package/dist/core/shell-wrapper-table.d.ts +144 -34
  81. package/dist/core/shell-wrapper-table.js +40 -20
  82. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  83. package/dist/core/synthetic-read-verdict.js +3 -1
  84. package/dist/core/tool-catalog-entries.d.ts +21 -0
  85. package/dist/core/tool-catalog-entries.js +12 -2
  86. package/dist/core/tool-face.d.ts +10 -1
  87. package/dist/core/tool-name-readings.d.ts +8 -0
  88. package/dist/core/tool-name-readings.js +2 -0
  89. package/dist/core/tool-policy.d.ts +40 -17
  90. package/dist/core/tool-policy.js +11 -9
  91. package/dist/core/tool-registry.d.ts +9 -0
  92. package/dist/core/tool-registry.js +10 -0
  93. package/dist/core/tool-spec.d.ts +3 -2
  94. package/dist/core/write-protect.d.ts +14 -3
  95. package/dist/core/write-protect.js +41 -10
  96. package/dist/index.d.ts +6 -4
  97. package/dist/index.js +5 -3
  98. package/dist/orchestration/run-spec.js +8 -7
  99. package/dist/orchestration/workflow.js +1 -1
  100. package/dist/shell/lex.d.ts +22 -0
  101. package/dist/shell/lex.js +75 -13
  102. package/dist/tools/fs/bash-program-position.d.ts +14 -90
  103. package/dist/tools/fs/bash-program-position.js +41 -74
  104. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  105. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  106. package/dist/tools/fs/index.d.ts +3 -2
  107. package/dist/tools/fs/program-read-model.d.ts +711 -0
  108. package/dist/tools/fs/program-read-model.js +1259 -0
  109. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  110. package/dist/tools/fs/program-read-model.rows.js +624 -0
  111. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  112. package/dist/tools/fs/read-deny-home.js +62 -24
  113. package/dist/tools/fs/read-deny.d.ts +54 -14
  114. package/dist/tools/fs/read-deny.js +104 -50
  115. package/dist/tools/fs/removal-targets.d.ts +7 -6
  116. package/dist/tools/fs/removal-targets.js +34 -24
  117. package/dist/tools/fs/safety.d.ts +3 -0
  118. package/dist/tools/fs/safety.js +10 -3
  119. package/dist/tools/fs/shell-read-argv.d.ts +86 -0
  120. package/dist/tools/fs/shell-read-argv.js +388 -0
  121. package/dist/tools/fs/shell-read-bindings.d.ts +300 -0
  122. package/dist/tools/fs/shell-read-bindings.js +755 -0
  123. package/dist/tools/fs/shell-read-check.js +23 -15
  124. package/dist/tools/fs/shell-read-establish.js +82 -8
  125. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  126. package/dist/tools/fs/shell-read-judge.js +9 -1
  127. package/dist/tools/fs/shell-read-plan.d.ts +33 -8
  128. package/dist/tools/fs/shell-read-plan.js +309 -407
  129. package/dist/tools/fs/shell-read-word.d.ts +60 -26
  130. package/dist/tools/fs/shell-read-word.js +47 -37
  131. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
  132. package/dist/tools/fs/shell-read-write-targets.js +85 -137
  133. package/dist/tools/fs/shell-reentry-text.d.ts +136 -6
  134. package/dist/tools/fs/shell-reentry-text.js +203 -53
  135. package/dist/tools/monitor.d.ts +3 -3
  136. package/package.json +1 -1
  137. package/test/export-surface.snapshot.json +25 -5
  138. package/wire-changes/core@7.46.0.json +1522 -0
  139. package/wire-changes/core@7.47.0.json +412 -0
@@ -1,13 +1,15 @@
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
- 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";
5
+ import { launchedCommands, launchedReadings, reentryText, spellLaunched } from "./shell-reentry-text.js";
6
+ import { readProgramPosition } from "./bash-program-position.js";
7
+ import { bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
8
+ import { programArgs } from "./shell-read-write-targets.js";
9
+ import { implementationsOf, programRow, runsShellText, selectedOptions, writeReadings } from "./program-read-model.js";
7
10
  import { inlineUntrusted } from "../../core/untrusted-text.js";
8
11
  export const SHELL_REMOVAL_COMMANDS = Object.freeze(["rm", "rmdir"]);
9
12
  const REMOVAL_COMMAND_SET = new Set(SHELL_REMOVAL_COMMANDS);
10
- const REMOVAL_GRAMMAR = { required: "", optional: "", longRequired: [] };
11
13
  function removalKey(path) {
12
14
  let k = normalizeAbsPathLexically(path.normalize("NFC")).replace(/\\/g, "/").toLowerCase();
13
15
  k = k.replace(/(?:\/+\*+)+\/*$/, "");
@@ -54,9 +56,6 @@ function removalKindOf(read, seat) {
54
56
  }
55
57
  return read.established === "unestablishable" && tildeNamesAHome(read.candidate.spelled) ? "home" : undefined;
56
58
  }
57
- function literalOf(pieces) {
58
- return pieces.every((q) => q.literal !== undefined) ? pieces.map((q) => q.literal).join("") : undefined;
59
- }
60
59
  const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
61
60
  const NAME_BINDERS = new Set(["unset", "local", "declare", "typeset", "readonly", "export", "read"]);
62
61
  function bindingsOf(plan) {
@@ -67,11 +66,11 @@ function bindingsOf(plan) {
67
66
  for (const segment of plan.segments) {
68
67
  for (const w of segment.argv)
69
68
  if (w.assignment !== undefined)
70
- bind(w.assignment.name, w.assignment.settlesWholeName ? literalOf(w.assignment.value.pieces) : undefined);
69
+ bind(w.assignment.name, w.assignment.settlesWholeName ? literalTextOf(w.assignment.value.pieces) : undefined);
71
70
  const [head, name] = segment.argv;
72
71
  if ((head?.folded === "for" || head?.folded === "select") && name !== undefined && NAME.test(name.folded)) {
73
72
  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));
73
+ const words = at < 0 ? [undefined] : segment.argv.slice(at + 1).map((w) => literalTextOf(w.pieces));
75
74
  for (const v of words.length > 0 ? words : [undefined])
76
75
  bind(name.folded, v);
77
76
  }
@@ -105,20 +104,23 @@ function boundSpellingsOf(word, bindings) {
105
104
  }
106
105
  return { spellings };
107
106
  }
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$/);
107
+ function recursiveForce(row, args) {
108
+ return implementationsOf(row).some((view) => {
109
+ const selected = selectedOptions(view, args, "one-implementation");
110
+ return selected.some((o) => o.selects === "traversal") && selected.some((o) => o.mode === "forced");
111
+ });
112
112
  }
113
113
  export function removalFindingsOf(established, seat) {
114
114
  const out = [];
115
115
  const bindings = bindingsOf(established.plan);
116
116
  for (const segment of established.plan.segments) {
117
117
  const program = segment.verb?.basename;
118
- if (program !== undefined && REMOVAL_COMMAND_SET.has(program)) {
118
+ const row = program !== undefined && REMOVAL_COMMAND_SET.has(program) ? programRow(program) : undefined;
119
+ if (program !== undefined && row !== undefined) {
119
120
  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));
121
+ const spelled = args.map((w) => w.spelled);
122
+ const removed = new Set(writeReadings(row, spelled).flatMap((r) => r.written.map((w) => w.at)));
123
+ const options = new Set(args.filter((_, at) => !removed.has(at)).map((w) => w.raw));
122
124
  for (const read of established.reads.get(segment) ?? []) {
123
125
  if (read.candidate.face !== "write" || options.has(read.candidate.raw))
124
126
  continue;
@@ -134,7 +136,7 @@ export function removalFindingsOf(established, seat) {
134
136
  if (bound === undefined)
135
137
  continue;
136
138
  if ("unreadable" in bound) {
137
- if (program === "rm" && recursiveForce(args))
139
+ if (recursiveForce(row, spelled))
138
140
  out.push({ guard: "unverifiable_delete", program, operand: read.candidate.raw, name: bound.unreadable });
139
141
  continue;
140
142
  }
@@ -164,15 +166,23 @@ export function removalFindingsOf(established, seat) {
164
166
  }
165
167
  function runtimeScriptOf(segment) {
166
168
  const program = segment.verb;
167
- if (segment.reentry !== "shell" || program === undefined || !SHELL_REENTRY_PROGRAM_RE.test(program.basename))
168
- return undefined;
169
- const at = segment.argv.findIndex((w) => w.folded === program.name);
170
- if (at < 0)
169
+ if (segment.reentry !== "shell" || program === undefined || !runsShellText(program.basename))
171
170
  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 spell = (k, from = 0) => {
173
+ const w = segment.argv[k];
174
+ const literal = literalTextOf(w.pieces);
175
+ return literal !== undefined ? literal.slice(from) : `${MARK}${w.raw.slice(from)}`;
176
+ };
177
+ const pos = readProgramPosition(segment.argv);
178
+ const named = pos.program !== undefined && pos.refusal === undefined ? [{ shell: program.basename, words: segment.argv.slice(pos.program.index + 1).map((_, k) => spell(pos.program.index + 1 + k)) }] : [];
179
+ const launched = launchedCommands(segment.argv, pos).flatMap((c) => {
180
+ const shell = launchedReadings(segment.argv, c).shell;
181
+ return shell === undefined ? [] : spellLaunched(segment.argv, c.args, { word: spell }).map((words) => ({ shell, words }));
182
+ });
183
+ const texts = [...named, ...launched].flatMap(({ shell, words }) => [reentryText(shell, words), ...(segment.stdin ?? []).map((s) => reentryText(shell, words, s.literal ?? `${MARK}${s.from}`))]);
184
+ const text = texts.find((t) => t?.includes(MARK) === true);
185
+ return text === undefined ? undefined : text.split(MARK).join("");
176
186
  }
177
187
  export function planRunsRemoval(plan) {
178
188
  return plan.segments.some((s) => (s.verb !== undefined && REMOVAL_COMMAND_SET.has(s.verb.basename)) || runtimeScriptOf(s) !== undefined || s.nested.some(planRunsRemoval));
@@ -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,86 @@
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 FloorValue } 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), each with the pattern its names are matched by — one candidate each once the segment's reads
15
+ * are read. */
16
+ export type Bare = Omit<ReadCandidate, "bases"> & {
17
+ readonly alternatives?: readonly Pick<ReadCandidate, "spelled" | "pattern">[];
18
+ readonly floors?: readonly FloorValue[];
19
+ };
20
+ /** What reading a segment's words found beside its candidates. */
21
+ export interface WordContext {
22
+ /** The names at this segment. */
23
+ readonly table: BindingTable;
24
+ /** Shell text the arithmetic evaluator runs (a substitution in a subscript it evaluates): the segment re-enters it. */
25
+ readonly reentered: string[];
26
+ /** Program text the station does not read — inline text in a language that may open files, or a program read from
27
+ * standard input: why the segment's reads are not established. */
28
+ programText?: string;
29
+ /** Some program text was found in a slot (whether or not it may open files). */
30
+ programTextSeen: boolean;
31
+ }
32
+ /** The subcommand whose row says how ITS words name paths (operands or a traversal of its own), or `undefined`: a
33
+ * subcommand whose row states only its grammar — its message slots, read by {@link messageWords} — leaves its words to
34
+ * the program's own reading. */
35
+ export declare function pathSubcommandOf(row: ProgramRow | undefined, args: readonly string[]): ReturnType<typeof subcommandOf>;
36
+ /** The sentence of inline program text the station does not read. */
37
+ export declare const programTextSentence: (language: string) => string;
38
+ /** One candidate without its bases (filled by the threading pass): the word's spelling — or `spelled`, a caller's
39
+ * reading of PART of it (an option payload, a `key=value` value) — with the word's own expansion facts. A word whose
40
+ * names may hold several values offers each as its whole spelling; a PART of such a word is not followed through them
41
+ * (it is unresolvable). */
42
+ export declare function bare(r: Read, provenance: ReadCandidateProvenance, spelled?: string): Bare;
43
+ /** The reads one value of a declared role hands over: a candidate per file it names, or — a value the program expands
44
+ * past the listed bound — the word itself, unresolvable. A mount source is a read the daemon makes of the whole tree
45
+ * under it (a read and a traversal root); an object path
46
+ * `<rev>:<path>` offers the word as a path and the `<path>` it spells to the deny judge; program text is never a path
47
+ * ({@link programTextReads}). `alsoOperand`: the walk read the option past the first operand, where the program may
48
+ * have stopped reading options — a path role is still offered (more candidates), but no program text is read there:
49
+ * the word is the operands' (the script's own argument), as the program's delivery reading says. */
50
+ export declare function roleReads(r: Read, provenance: ReadCandidateProvenance, value: string, role: ArgRole, globs: "expanded" | "literal", ctx: WordContext, alsoOperand?: boolean): Bare[];
51
+ /**
52
+ * The candidates a `NAME=value` word hands the judge, both facts about its VALUE (the word is a path to nobody):
53
+ * what path it NAMES, and what the text cannot SETTLE — placed as an operand is wherever an environment value
54
+ * names ONE path (a leading tilde against the declared home; a `:` joins a second). Where the text runs nothing able
55
+ * to open a file, {@link planShellReads} drops them: the value names a path to nobody.
56
+ *
57
+ * Bash applies its assignment-word expansion to such a word even when it is an ARGUMENT to a simple command, so
58
+ * the program receives `NAME=<expanded value>`, and a program that reads its value as a path (`dd if=…`, a build
59
+ * or tool variable, a launcher's environment word) opens what the value names. Two facts of the lexer's
60
+ * {@link AssignmentFact} are read: a tilde the shell EXPANDS in the value (at its start and after each `:`, where
61
+ * unquoted — `value.expansions`) leaves the path unsettled, and the append form (`NAME+=value`) names `<whatever
62
+ * NAME held>` + value, so the whole value is not in the text — but the text it appends IS, and an unset or empty
63
+ * NAME makes it the whole value, so it is a candidate too. A value that is a substitution is the nested plan
64
+ * its body already is, never a candidate of its own text (its placeholder is not a path).
65
+ */
66
+ export declare function pushAssignment(reads: Bare[], w: LexedWord, provenance: ReadCandidateProvenance): void;
67
+ /** Every word of a segment whose program's grammar this parser does not model, from `from` onward: option
68
+ * payloads and path-shaped option words included, the launcher chain's own words excluded, a program declared
69
+ * to open no path operand handing only the environment words in front of it. A program with NO row is read the
70
+ * speculative way: every word is still handed over, but the filesystem decides it
71
+ * ({@link ReadCandidate.speculative}). */
72
+ export declare function unlistedWords(reads: Bare[], toks: readonly Read[], from: number, launcherIndices: ReadonlySet<number>, provenance: ReadCandidateProvenance, ctx: WordContext, program?: {
73
+ readonly index: number;
74
+ readonly basename: string;
75
+ }): void;
76
+ /** The positional operands and declared option-value slots of a listed reader — the program read model's argv walk,
77
+ * one candidate per word the program may open, plus the traversal roots of a recursive form. A subcommand's words
78
+ * are read by its own row; the words a module takes as its program text are program text. */
79
+ export declare function listedVerbCandidates(name: string, argReads: readonly Read[], reads: Bare[], ctx: WordContext): void;
80
+ /** The subscript of a variable-name word (`a[…]`), as the arithmetic text that is evaluated; the empty text for a plain
81
+ * name. */
82
+ export declare function subscriptOnly(word: string): string;
83
+ /** An assignment word's arithmetic: its own subscript (`a[$(…)]=1`, quoted or not — not for a builtin that takes the
84
+ * name `asWritten`), and its value where the name carries the integer attribute (`declare -i n; n='a[$(…)]'`, bash's
85
+ * own `OPTIND` / `RANDOM` / …). */
86
+ export declare function assignmentArithmetic(w: LexedWord, integer: boolean, ctx: WordContext, asWritten?: boolean): void;
@@ -0,0 +1,388 @@
1
+ import { literalTildeSpelling } from "../../shell/lex.js";
2
+ import { expandsOf, patternOf, readOfText, rewritten, spellingOf } from "./shell-read-word.js";
3
+ import { arithmeticSubstitutions } from "./shell-read-bindings.js";
4
+ import { argvProgramOf, attachedOptionPayloads, isPathShapedToken } from "./bash-readonly-classifier.js";
5
+ import { assignmentArgumentsAreNotPaths, evaluatesOperands, messageWords, moduleTailOf, objectPathsOf, opensNoOperand, programRow, programTextMayOpen, programTextOpensOf, readArgs, selectsContentsTraversal, slotValueCandidates, subcommandOf, valuesAreLiteral, walksCwdWithoutOperand, wordsReadBySomeImplementations, } from "./program-read-model.js";
6
+ export function pathSubcommandOf(row, args) {
7
+ const sub = subcommandOf(row, args);
8
+ return sub !== undefined && (sub.row.operands !== undefined || sub.row.traversal !== undefined) ? sub : undefined;
9
+ }
10
+ export const programTextSentence = (language) => `runs inline ${language} program text, which may open files this station does not read`;
11
+ export function bare(r, provenance, spelled = r.folded) {
12
+ const whole = spelled === r.folded;
13
+ const alternatives = r.values !== undefined && r.values.length > 1 ? r.values.slice(1).map((v) => { const pattern = patternOf(v); return { spelled: spellingOf(v), ...(pattern !== undefined ? { pattern } : {}) }; }) : undefined;
14
+ if (!whole && alternatives !== undefined)
15
+ return { spelled, raw: r.raw, provenance, expands: "unresolvable" };
16
+ return {
17
+ spelled,
18
+ raw: r.raw,
19
+ provenance,
20
+ expands: r.expands,
21
+ ...(whole && r.pattern !== undefined ? { pattern: r.pattern } : {}),
22
+ ...(whole && alternatives !== undefined ? { alternatives } : {}),
23
+ ...(whole && r.homeFloor !== undefined ? { floorSpelled: r.homeFloor } : {}),
24
+ ...(whole && r.floorValues !== undefined ? { floors: r.floorValues } : {}),
25
+ };
26
+ }
27
+ export function roleReads(r, provenance, value, role, globs, ctx, alsoOperand = false) {
28
+ if (role.kind === "program-text")
29
+ return alsoOperand ? [] : programTextReads(r, provenance, value, role.language, ctx);
30
+ if (role.kind === "object-path")
31
+ return [bare(r, provenance, value), ...objectPathsOf(value).map((c) => ({ ...bare(r, "object_path", c), denyOnly: true }))];
32
+ const named = slotValueCandidates(value, role, globs);
33
+ if (named === undefined)
34
+ return [{ ...bare(r, provenance), expands: "unresolvable" }];
35
+ if (role.kind === "mount-source")
36
+ return named.flatMap((c) => [bare(r, "mount_source", c), bare(r, "traversal_root", c)]);
37
+ return named.map((c) => bare(r, provenance, c));
38
+ }
39
+ function programTextReads(r, provenance, text, language, ctx) {
40
+ ctx.programTextSeen = true;
41
+ const opens = programTextOpensOf(language);
42
+ if (opens === "arithmetic") {
43
+ ctx.reentered.push(...arithmeticSubstitutions(text, ctx.table));
44
+ return [];
45
+ }
46
+ if (opens === "parsed" || opens === "never")
47
+ return [];
48
+ if (programTextMayOpen(language, text))
49
+ ctx.programText ??= programTextSentence(language);
50
+ return [{ ...bare(r, provenance, text), denyOnly: true }];
51
+ }
52
+ export function pushAssignment(reads, w, provenance) {
53
+ const a = w.assignment;
54
+ if (a === undefined || a.form === "compound")
55
+ return;
56
+ const expandsTilde = a.value.expansions.some((p) => p.expansion?.kind === "tilde");
57
+ const value = expandsTilde ? a.value.folded : literalTildeSpelling(a.value.folded);
58
+ const settled = a.op === "=" && !expandsTilde;
59
+ const named = a.value.folded === "" || (a.op === "+=" ? expandsTilde : !settled) ? undefined : value;
60
+ if (a.value.folded === "" && settled)
61
+ return;
62
+ const v = { spelled: value, raw: a.bare ? a.value.raw : w.raw, provenance };
63
+ const unsettled = provenance !== "env_assignment" ? { ...v, expands: "unresolvable" } : v.spelled.startsWith("~") && !v.spelled.includes(":") ? { ...v, expands: expandsOf(a.value) } : undefined;
64
+ if (!settled && unsettled !== undefined)
65
+ reads.push(unsettled);
66
+ const substitutes = a.value.expansions.some((p) => p.expansion?.kind === "command" || p.expansion?.kind === "process" || p.expansion?.kind === "arithmetic");
67
+ if (named !== undefined && !substitutes)
68
+ reads.push({ spelled: named, raw: w.raw, provenance, expands: expandsOf(w) });
69
+ if (a.value.folded.includes(":"))
70
+ for (const m of a.value.folded.split(":"))
71
+ if (m !== "") {
72
+ const r = readOfText(m);
73
+ reads.push({ spelled: r.folded, raw: r.raw, provenance, expands: r.expands, denyOnly: true });
74
+ }
75
+ }
76
+ export function unlistedWords(reads, toks, from, launcherIndices, provenance, ctx, program) {
77
+ const argv = program !== undefined ? { program: program.basename, from: program.index } : argvProgramOf(toks.map((r) => r.word));
78
+ const argvStart = argv?.from ?? toks.length;
79
+ const slotVerb = argv?.program;
80
+ const slotRow = slotVerb === undefined ? undefined : programRow(slotVerb);
81
+ const noRow = slotVerb !== undefined && slotRow === undefined;
82
+ const speculate = (start, how) => {
83
+ if (!noRow)
84
+ return;
85
+ for (let i = start; i < reads.length; i++) {
86
+ const c = reads[i];
87
+ if (c.expands === "unresolvable" || c.speculative !== undefined || c.denyOnly === true || c.provenance === "program_path" || c.provenance === "env_assignment")
88
+ continue;
89
+ reads[i] = { ...c, speculative: how };
90
+ }
91
+ };
92
+ const message = slotVerb === undefined || argv === undefined ? new Set() : messageWords(slotVerb, toks, argvStart);
93
+ let declaredNotPaths = assignmentArgumentsAreNotPaths(slotVerb ?? "");
94
+ const noPathOperands = slotVerb !== undefined && opensNoOperand(slotVerb);
95
+ const expanding = new Set();
96
+ for (let j = from; j < toks.length; j++) {
97
+ if (launcherIndices.has(j))
98
+ continue;
99
+ if (noPathOperands && j > argvStart && !evaluatesOperands(slotVerb))
100
+ break;
101
+ if (toks[j].expands !== "unresolvable")
102
+ continue;
103
+ expanding.add(j);
104
+ reads.push(bare(toks[j], j === argvStart ? "program_path" : provenance));
105
+ if (j < argvStart)
106
+ pushAssignment(reads, toks[j].word, "env_assignment");
107
+ else if (j > argvStart && !declaredNotPaths)
108
+ pushAssignment(reads, toks[j].word, provenance);
109
+ }
110
+ const positional = [];
111
+ const pastOperand = new Set();
112
+ let stdinOperand = false;
113
+ let globs = "expanded";
114
+ let walkRow = slotRow;
115
+ let walkFrom = argvStart + 1;
116
+ let relocated = false;
117
+ if (argvStart >= toks.length) {
118
+ let endOfOptions = false;
119
+ for (let j = from; j < toks.length; j++) {
120
+ if (launcherIndices.has(j) || expanding.has(j) || message.has(j))
121
+ continue;
122
+ const r = toks[j];
123
+ const word = r.folded;
124
+ if (j > from && !endOfOptions && word === "--") {
125
+ endOfOptions = true;
126
+ continue;
127
+ }
128
+ if (!endOfOptions && word.startsWith("-") && word !== "-") {
129
+ for (const payload of attachedOptionPayloads(word))
130
+ reads.push(bare(r, provenance, payload));
131
+ if (isPathShapedToken(word))
132
+ reads.push(bare(r, provenance));
133
+ continue;
134
+ }
135
+ reads.push(bare(r, provenance));
136
+ pushAssignment(reads, r.word, "env_assignment");
137
+ }
138
+ }
139
+ else if (!noPathOperands) {
140
+ const programWord = toks[argvStart];
141
+ if (!expanding.has(argvStart) && !launcherIndices.has(argvStart) && !message.has(argvStart)) {
142
+ const word = programWord.folded;
143
+ if (isPathShapedToken(word)) {
144
+ if (word.startsWith("-") && word !== "-") {
145
+ for (const payload of attachedOptionPayloads(word))
146
+ reads.push(bare(programWord, provenance, payload));
147
+ reads.push(bare(programWord, provenance));
148
+ }
149
+ else {
150
+ reads.push(bare(programWord, "program_path"));
151
+ if (!declaredNotPaths)
152
+ pushAssignment(reads, programWord.word, provenance);
153
+ }
154
+ }
155
+ }
156
+ const args = toks.slice(argvStart + 1).map((r) => r.folded);
157
+ const sub = pathSubcommandOf(slotRow, args);
158
+ const tail = moduleTailOf(slotRow, args);
159
+ const skipped = (j) => launcherIndices.has(j) || expanding.has(j) || message.has(j) || (tail !== undefined && j >= argvStart + 1 + tail.from);
160
+ const walk = (row, first, last, slotKind) => {
161
+ const at = (k) => first + k;
162
+ const words = toks.slice(first, last).map((r, k) => ({ folded: r.folded, skip: skipped(at(k)) }));
163
+ globs = valuesAreLiteral(row, words.map((w) => w.folded)) ? "literal" : "expanded";
164
+ const someRead = wordsReadBySomeImplementations(row, words.map((w) => w.folded));
165
+ for (const e of readArgs(row, words, { firstOperandSlot: slotKind })) {
166
+ const start = reads.length;
167
+ handleUnlistedEvent(e, at(e.at));
168
+ if (someRead.has(e.at))
169
+ for (let i = start; i < reads.length; i++)
170
+ if (reads[i].expands !== "unresolvable" && reads[i].denyOnly !== true)
171
+ reads[i] = { ...reads[i], speculative: "containment" };
172
+ }
173
+ };
174
+ if (sub !== undefined) {
175
+ walk(slotRow, argvStart + 1, argvStart + 1 + sub.at, false);
176
+ declaredNotPaths = sub.row.operands?.assignmentArguments === "not-paths";
177
+ walkRow = sub.row;
178
+ walkFrom = argvStart + 2 + sub.at;
179
+ relocated = sub.relocated;
180
+ positional.length = 0;
181
+ stdinOperand = false;
182
+ walk(sub.row, walkFrom, toks.length, true);
183
+ }
184
+ else
185
+ walk(slotRow, argvStart + 1, toks.length, "program-text");
186
+ if (tail !== undefined)
187
+ for (let j = argvStart + 1 + tail.from; j < toks.length; j++)
188
+ if (!expanding.has(j))
189
+ reads.push(...roleReads(toks[j], "operand", toks[j].folded, tail.role, globs, ctx));
190
+ }
191
+ function handleUnlistedEvent(e, j) {
192
+ const r = toks[j];
193
+ const start = reads.length;
194
+ switch (e.kind) {
195
+ case "end-of-options":
196
+ return;
197
+ case "dash-word-operand":
198
+ pastOperand.add(j);
199
+ reads.push(bare(r, provenance));
200
+ speculate(start, "whole");
201
+ return;
202
+ case "value": {
203
+ if (!e.consumed) {
204
+ if (r.expands !== "unresolvable")
205
+ reads.push(...roleReads(r, "option_value", r.folded, e.role, globs, ctx, true));
206
+ return;
207
+ }
208
+ if (expanding.has(j))
209
+ return;
210
+ const named = roleReads(r, "option_value", r.folded, e.role, globs, ctx);
211
+ reads.push(...named);
212
+ if (named.some((c) => c.denyOnly !== true))
213
+ positional.push(r);
214
+ return;
215
+ }
216
+ case "option":
217
+ if (e.reading === "no-value")
218
+ return;
219
+ if (e.reading === "speculative") {
220
+ for (const payload of attachedOptionPayloads(r.folded))
221
+ reads.push(bare(r, provenance, payload));
222
+ speculate(start, "containment");
223
+ const whole = reads.length;
224
+ if (isPathShapedToken(r.folded))
225
+ reads.push(bare(r, provenance));
226
+ speculate(whole, "whole");
227
+ return;
228
+ }
229
+ reads.push(...roleReads(r, "option_value", literalTildeSpelling(e.reading.attached), e.reading.role, globs, ctx, pastOperand.has(j)));
230
+ return;
231
+ case "operand": {
232
+ const role = e.dash === undefined ? (e.slot ?? e.role) : undefined;
233
+ const named = role !== undefined ? roleReads(r, provenance, r.folded, role, globs, ctx) : [bare(r, provenance)];
234
+ if (e.dash === "stdin")
235
+ stdinOperand = true;
236
+ else if (named.some((c) => c.denyOnly !== true))
237
+ positional.push(r);
238
+ reads.push(...named);
239
+ speculate(start, r.word.assignment !== undefined ? "whole" : "containment");
240
+ const values = reads.length;
241
+ if (!declaredNotPaths && e.slot === undefined)
242
+ pushAssignment(reads, r.word, provenance);
243
+ speculate(values, "containment");
244
+ return;
245
+ }
246
+ }
247
+ }
248
+ if (walkRow !== undefined && argv !== undefined && selectsContentsTraversal(walkRow, toks.slice(walkFrom).map((x) => x.folded))) {
249
+ const roots = positional.length > 0 ? positional : stdinOperand || !walksCwdWithoutOperand(walkRow) ? [] : [readOfText(".")];
250
+ for (const root of roots)
251
+ reads.push(relocated ? { ...bare(root, "traversal_root"), expands: "unresolvable" } : bare(root, "traversal_root"));
252
+ }
253
+ for (let j = from; j < toks.length; j++) {
254
+ if (launcherIndices.has(j) || j === argvStart)
255
+ continue;
256
+ if (noPathOperands && j > argvStart)
257
+ break;
258
+ const start = reads.length;
259
+ if (toks[j].expands === "glob")
260
+ reads.push(bare(toks[j], provenance));
261
+ speculate(start, "containment");
262
+ }
263
+ }
264
+ export function listedVerbCandidates(name, argReads, reads, ctx) {
265
+ if (opensNoOperand(name))
266
+ return;
267
+ const row = programRow(name);
268
+ const args = argReads.map((r) => r.folded);
269
+ const expanding = new Set();
270
+ argReads.forEach((r, k) => {
271
+ if (!rewritten(r))
272
+ return;
273
+ expanding.add(k);
274
+ reads.push({ ...bare(r, "operand"), expands: "unresolvable" });
275
+ });
276
+ const sub = pathSubcommandOf(row, args);
277
+ const tail = moduleTailOf(row, args);
278
+ if (sub !== undefined) {
279
+ walkListed(row, argReads, 0, sub.at, expanding, reads, ctx, false);
280
+ walkListed(sub.row, argReads, sub.at + 1, argReads.length, expanding, reads, ctx, sub.relocated);
281
+ }
282
+ else
283
+ walkListed(row, argReads, 0, tail !== undefined ? tail.from : argReads.length, expanding, reads, ctx, false);
284
+ if (tail !== undefined)
285
+ for (let k = tail.from; k < argReads.length; k++)
286
+ if (!expanding.has(k))
287
+ reads.push(...roleReads(argReads[k], "operand", args[k], tail.role, "expanded", ctx));
288
+ for (let k = 0; k < args.length; k++) {
289
+ if (expanding.has(k))
290
+ continue;
291
+ if (argReads[k].expands === "glob")
292
+ reads.push(bare(argReads[k], "operand"));
293
+ }
294
+ }
295
+ function walkListed(row, argReads, first, last, expanding, reads, ctx, relocated) {
296
+ const slice = argReads.slice(first, last);
297
+ const args = slice.map((r) => r.folded);
298
+ const positional = [];
299
+ let stdinOperand = false;
300
+ const globs = valuesAreLiteral(row, args) ? "literal" : "expanded";
301
+ const someRead = wordsReadBySomeImplementations(row, args);
302
+ const pastOperand = new Set();
303
+ const offer = (k, value, role, provenance, alsoOperand = false) => {
304
+ const named = roleReads(slice[k], provenance, value, role, globs, ctx, alsoOperand);
305
+ reads.push(...named);
306
+ return named.filter((c) => c.denyOnly !== true).length;
307
+ };
308
+ for (const e of readArgs(row, args.map((folded, k) => ({ folded, skip: expanding.has(first + k) })), { firstOperandSlot: true })) {
309
+ const t = args[e.at];
310
+ const start = reads.length;
311
+ switch (e.kind) {
312
+ case "end-of-options":
313
+ break;
314
+ case "dash-word-operand":
315
+ pastOperand.add(e.at);
316
+ reads.push(bare(slice[e.at], "operand"));
317
+ break;
318
+ case "value": {
319
+ if (expanding.has(first + e.at))
320
+ break;
321
+ if (offer(e.at, t, e.role, "option_value", !e.consumed) > 0 && e.consumed)
322
+ positional.push(slice[e.at]);
323
+ break;
324
+ }
325
+ case "option":
326
+ if (e.reading === "no-value")
327
+ break;
328
+ if (e.reading === "speculative") {
329
+ for (const payload of attachedOptionPayloads(t))
330
+ reads.push(bare(slice[e.at], "option_value", payload));
331
+ break;
332
+ }
333
+ offer(e.at, literalTildeSpelling(e.reading.attached), e.reading.role, "option_value", pastOperand.has(e.at));
334
+ break;
335
+ case "operand": {
336
+ if (e.dash !== undefined) {
337
+ if (e.slot !== undefined)
338
+ break;
339
+ if (e.dash === "stdin")
340
+ stdinOperand = true;
341
+ else
342
+ positional.push(slice[e.at]);
343
+ break;
344
+ }
345
+ if (e.slot !== undefined) {
346
+ if (offer(e.at, t, e.slot, "operand") > 0)
347
+ positional.push(slice[e.at]);
348
+ break;
349
+ }
350
+ let named = 1;
351
+ if (e.role !== undefined)
352
+ named = offer(e.at, t, e.role, "operand");
353
+ else
354
+ reads.push(bare(slice[e.at], "operand"));
355
+ if (named > 0)
356
+ positional.push(slice[e.at]);
357
+ if (row?.operands?.assignmentArguments !== "not-paths")
358
+ pushAssignment(reads, slice[e.at].word, "operand");
359
+ break;
360
+ }
361
+ }
362
+ if (someRead.has(e.at))
363
+ for (let i = start; i < reads.length; i++)
364
+ if (reads[i].expands !== "unresolvable" && reads[i].denyOnly !== true)
365
+ reads[i] = { ...reads[i], speculative: "containment" };
366
+ }
367
+ if (selectsContentsTraversal(row, args)) {
368
+ const roots = positional.length > 0 ? positional : stdinOperand || !walksCwdWithoutOperand(row) ? [] : [readOfText(".")];
369
+ for (const root of roots)
370
+ reads.push(relocated ? { ...bare(root, "traversal_root"), expands: "unresolvable" } : bare(root, "traversal_root"));
371
+ }
372
+ }
373
+ export function subscriptOnly(word) {
374
+ const at = word.indexOf("[");
375
+ return at < 0 ? "" : word.slice(at);
376
+ }
377
+ export function assignmentArithmetic(w, integer, ctx, asWritten = false) {
378
+ const a = w.assignment;
379
+ if (a === undefined) {
380
+ if (!asWritten)
381
+ ctx.reentered.push(...arithmeticSubstitutions(subscriptOnly(w.folded), ctx.table));
382
+ return;
383
+ }
384
+ if (a.subscript !== undefined && !asWritten)
385
+ ctx.reentered.push(...arithmeticSubstitutions(`[${a.subscript.folded}]`, ctx.table));
386
+ if (integer)
387
+ ctx.reentered.push(...arithmeticSubstitutions(a.value.folded, ctx.table));
388
+ }