@sema-agent/core 7.46.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 (57) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/core/allow-entry.d.ts +1 -1
  3. package/dist/core/allow-entry.js +3 -4
  4. package/dist/core/ask-origin.d.ts +35 -12
  5. package/dist/core/ask-origin.js +23 -6
  6. package/dist/core/engine-notice.d.ts +7 -4
  7. package/dist/core/gate-fold.js +2 -2
  8. package/dist/core/gate-lanes.js +2 -1
  9. package/dist/core/gate-outcome.d.ts +4 -2
  10. package/dist/core/gate-outcome.js +2 -2
  11. package/dist/core/permission-rule-model.d.ts +12 -6
  12. package/dist/core/permission-rule-model.js +10 -2
  13. package/dist/core/permission-rules.d.ts +4 -5
  14. package/dist/core/permission-rules.js +28 -15
  15. package/dist/core/runner/gate-exit.d.ts +4 -1
  16. package/dist/core/runner/gate-exit.js +5 -1
  17. package/dist/core/runner/prepare-hands-readface.js +5 -5
  18. package/dist/core/runner/prepare-policy-chain.js +2 -1
  19. package/dist/core/shell-wrapper-projection.js +1 -1
  20. package/dist/core/shell-wrapper-table.d.ts +6 -6
  21. package/dist/core/shell-wrapper-table.js +2 -2
  22. package/dist/core/tool-catalog-entries.js +3 -2
  23. package/dist/core/tool-face.d.ts +10 -1
  24. package/dist/core/tool-name-readings.d.ts +8 -0
  25. package/dist/core/tool-name-readings.js +2 -0
  26. package/dist/core/tool-policy.d.ts +10 -9
  27. package/dist/core/tool-policy.js +2 -2
  28. package/dist/core/tool-registry.d.ts +9 -0
  29. package/dist/core/tool-registry.js +10 -0
  30. package/dist/core/write-protect.d.ts +10 -3
  31. package/dist/core/write-protect.js +11 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +1 -1
  34. package/dist/shell/lex.d.ts +15 -0
  35. package/dist/shell/lex.js +58 -13
  36. package/dist/tools/fs/bash-program-position.d.ts +9 -6
  37. package/dist/tools/fs/bash-program-position.js +13 -10
  38. package/dist/tools/fs/program-read-model.d.ts +24 -7
  39. package/dist/tools/fs/program-read-model.js +7 -0
  40. package/dist/tools/fs/program-read-model.rows.js +18 -7
  41. package/dist/tools/fs/removal-targets.js +14 -4
  42. package/dist/tools/fs/shell-read-argv.d.ts +5 -4
  43. package/dist/tools/fs/shell-read-argv.js +5 -3
  44. package/dist/tools/fs/shell-read-bindings.d.ts +46 -19
  45. package/dist/tools/fs/shell-read-bindings.js +38 -29
  46. package/dist/tools/fs/shell-read-establish.js +3 -3
  47. package/dist/tools/fs/shell-read-plan.d.ts +2 -1
  48. package/dist/tools/fs/shell-read-plan.js +32 -36
  49. package/dist/tools/fs/shell-read-word.d.ts +38 -7
  50. package/dist/tools/fs/shell-read-word.js +26 -8
  51. package/dist/tools/fs/shell-read-write-targets.js +4 -3
  52. package/dist/tools/fs/shell-reentry-text.d.ts +98 -0
  53. package/dist/tools/fs/shell-reentry-text.js +115 -2
  54. package/dist/tools/monitor.d.ts +3 -3
  55. package/package.json +1 -1
  56. package/test/export-surface.snapshot.json +9 -3
  57. package/wire-changes/core@7.47.0.json +412 -0
package/dist/shell/lex.js CHANGED
@@ -357,6 +357,7 @@ class WordBuilder {
357
357
  marks = [];
358
358
  subscript;
359
359
  compound = false;
360
+ widenedClasses = [];
360
361
  begin(src) { if (!this.started)
361
362
  this.at = src; }
362
363
  push(w, f, by, src) {
@@ -402,6 +403,17 @@ class WordBuilder {
402
403
  mark(reason, rawAt) { this.marks.push({ at: rawAt, reason }); }
403
404
  inSubscript() { return this.subscript !== undefined && this.subscript.rawEnd === undefined; }
404
405
  foldedOf(r) { return r >= this.foldedAt.length ? this.folded.length : this.foldedAt[r]; }
406
+ classSpelled(from, to) {
407
+ let out = "";
408
+ let at = from;
409
+ for (const [a, b] of this.widenedClasses) {
410
+ if (a < at || b > to)
411
+ continue;
412
+ out += `${this.folded.slice(at, a)}?`;
413
+ at = b;
414
+ }
415
+ return out + this.folded.slice(at, to);
416
+ }
405
417
  slice(from, to) {
406
418
  const out = [];
407
419
  for (const p of this.pieces) {
@@ -519,9 +531,23 @@ class WordBuilder {
519
531
  else if (stack.length > 0 && (d === "," || (d === "." && this.raw[j + 1] === ".")))
520
532
  stack[stack.length - 1].listed = true;
521
533
  }
522
- const nextBracketClose = new Array(n + 1).fill(-1);
523
- for (let j = n - 1; j >= 0; j--)
524
- nextBracketClose[j] = readable(j) && this.raw[j] === "]" ? j : nextBracketClose[j + 1];
534
+ const text = this.folded;
535
+ const nf = text.length;
536
+ const readableAt = (f) => f < nf && readable(this.rawOfFolded[f]);
537
+ const unitClose = (d) => {
538
+ const at = new Array(nf + 2).fill(-1);
539
+ for (let f = nf - 2; f >= 0; f--)
540
+ at[f] = readableAt(f) && readableAt(f + 1) && text[f] === d && text[f + 1] === "]" ? f : at[f + 1];
541
+ return at;
542
+ };
543
+ const unitCloses = new Map([":", "=", "."].map((d) => [d, unitClose(d)]));
544
+ const nextBracketClose = new Array(nf + 2).fill(-1);
545
+ const nextSlash = new Array(nf + 1).fill(nf);
546
+ for (let f = nf - 1; f >= 0; f--) {
547
+ const unitEnd = readableAt(f) && text[f] === "[" && readableAt(f + 1) ? unitCloses.get(text[f + 1] ?? "")?.[f + 2] ?? -1 : -1;
548
+ nextBracketClose[f] = readableAt(f) && text[f] === "]" ? f : unitEnd >= 0 ? nextBracketClose[unitEnd + 2] : nextBracketClose[f + 1];
549
+ nextSlash[f] = text[f] === "/" ? f : nextSlash[f + 1];
550
+ }
525
551
  const ranges = [];
526
552
  for (let k = 0; k < n; k++) {
527
553
  if (!readable(k))
@@ -536,19 +562,35 @@ class WordBuilder {
536
562
  continue;
537
563
  }
538
564
  if (c === "*" || c === "?") {
539
- ranges.push({ from: k, to: k + 1, exp: { kind: "glob", yields: "many" } });
565
+ ranges.push({ from: k, to: k + 1, exp: { kind: "glob", yields: "many", pattern: { folded: c, written: c, judged: c } } });
540
566
  continue;
541
567
  }
542
568
  if (c === "[") {
543
- let j = k + 1;
544
- if (this.raw[j] === "!" || this.raw[j] === "^")
569
+ if (this.rawOfFolded[this.foldedOf(k)] !== k)
570
+ continue;
571
+ let j = this.foldedOf(k) + 1;
572
+ if ((text[j] === "!" || text[j] === "^") && readableAt(j))
545
573
  j++;
546
- if (this.raw[j] === "]")
574
+ if (text[j] === "]" && readableAt(j))
547
575
  j++;
548
- const close = j < n ? nextBracketClose[j] : -1;
549
- if (close < 0)
576
+ const closeAt = j < nf ? nextBracketClose[j] : -1;
577
+ if (closeAt < 0 || nextSlash[j] < closeAt)
550
578
  continue;
551
- ranges.push({ from: k, to: close + 1, exp: { kind: "glob", yields: "many", body: this.raw.slice(k + 1, close) } });
579
+ const close = this.rawOfFolded[closeAt];
580
+ let expands = false;
581
+ for (let m = k + 1; m < close && !expands; m++)
582
+ expands = inExp[m] || braceClose.get(m)?.listed === true;
583
+ let widened = false;
584
+ for (let m = k + 1; m < close && !widened; m++)
585
+ widened = this.rawQuoted[m] !== undefined;
586
+ for (let f = j; f < closeAt && !widened; f++)
587
+ widened = readableAt(f) && text[f] === "[" && readableAt(f + 1) && (unitCloses.get(text[f + 1] ?? "")?.[f + 2] ?? -1) >= 0;
588
+ const from = this.foldedOf(k);
589
+ const to = this.foldedOf(close + 1);
590
+ if (!expands && widened)
591
+ this.widenedClasses.push([from, to]);
592
+ const pattern = expands ? {} : { pattern: { folded: this.folded.slice(from, to), written: this.raw.slice(k, close + 1), judged: widened ? "?" : this.folded.slice(from, to) } };
593
+ ranges.push({ from: k, to: close + 1, exp: { kind: "glob", yields: "many", body: this.raw.slice(k + 1, close), ...pattern } });
552
594
  k = close;
553
595
  }
554
596
  }
@@ -647,18 +689,20 @@ class WordBuilder {
647
689
  }
648
690
  finish(atCommandPosition, tildeSpelling = true) {
649
691
  const rawText = this.raw;
650
- let foldedText = this.folded;
692
+ let literalTilde = false;
651
693
  let assignment;
652
694
  const tildeEnd = this.tildePrefixEnd(0, false);
653
695
  if (tildeEnd !== undefined)
654
696
  this.carveTilde(0, tildeEnd);
655
697
  else {
656
- if (tildeSpelling)
657
- foldedText = literalTildeSpelling(foldedText);
698
+ literalTilde = tildeSpelling;
658
699
  assignment = this.readAssignment();
659
700
  }
660
701
  if (!(assignment !== undefined && assignment.bare && atCommandPosition))
661
702
  this.carveBraceAndGlob();
703
+ const spelled = (text) => (literalTilde ? literalTildeSpelling(text) : text);
704
+ const foldedText = spelled(this.folded);
705
+ const judged = this.widenedClasses.length > 0 ? spelled(this.classSpelled(0, this.folded.length)) : undefined;
662
706
  if (assignment !== undefined && this.subscript?.text)
663
707
  this.mark("subscript_text", this.subscript.rawStart);
664
708
  const marks = this.marks.filter((m) => m.reason !== "subscript_text" || assignment?.subscript !== undefined).sort((a, b) => a.at - b.at);
@@ -679,6 +723,7 @@ class WordBuilder {
679
723
  kind: "word",
680
724
  raw: rawText,
681
725
  folded: foldedText,
726
+ ...(judged !== undefined ? { judged } : {}),
682
727
  pieces,
683
728
  quotedSpans: spans,
684
729
  expansions: pieces.filter((p) => p.expansion !== undefined),
@@ -53,12 +53,15 @@ export type ProgramPositionRefusal = {
53
53
  readonly kind: "control_syntax_at_program_position";
54
54
  readonly owner: string;
55
55
  readonly word: string;
56
- /** Not a DECLINE ({@link programPositionRunsShell}): the row WAS read and a SHELL stands at the program
57
- * position. Absent ⇒ genuine control syntax, which IS one. */
58
- readonly runsShell?: true;
59
- /** The launcher hands the shell its operands as text (`watch "S"`); absent beside `runsShell`, the shell is handed
60
- * nothing (`sudo -s`, `chroot DIR`) and runs its standard input. */
61
- readonly handsText?: true;
56
+ /** Not a DECLINE ({@link programPositionRunsShell}): the row WAS read and a SHELL — or a program the launcher
57
+ * names by an option of its own (`su -s PROGRAM`) — stands at the program position, run by the launcher at argv
58
+ * index `launcher` and handed the words from index `from` on (none where `from` is the argv's end: `sudo -s`,
59
+ * `chroot DIR`), as that launcher's row composes them (`launchedCommands`, shell-reentry-text.ts). Absent ⇒
60
+ * genuine control syntax, which IS one. */
61
+ readonly runsShell?: {
62
+ readonly launcher: number;
63
+ readonly from: number;
64
+ };
62
65
  };
63
66
  export interface ProgramPosition {
64
67
  /** Indices of the bare control words stripped in front (`!`, `if`, `time`, …), in order; their declared options
@@ -1,5 +1,5 @@
1
1
  import { SHELL_CONTROL_PREFIXES } from "../../shell/lex.js";
2
- import { BLOCK_WORD_NAMES, LAUNCHER_NAMES, feedsArgvFromStdin, launcherOptionStep, programRow, selectedOptions, streamDefaultProgram } from "./program-read-model.js";
2
+ import { BLOCK_WORD_NAMES, LAUNCHER_NAMES, feedsArgvFromStdin, launcherOptionStep, launcherRunsShell, programRow, selectedOptions, streamDefaultProgram } from "./program-read-model.js";
3
3
  export const NOT_AUTO_ALLOWED = "— not auto-allowed";
4
4
  export function programBasename(word) {
5
5
  return word.includes("/") ? (word.split("/").pop() ?? word) : word;
@@ -79,12 +79,12 @@ export function readProgramPosition(words) {
79
79
  }
80
80
  const launchers = [{ index: i, name: basename }];
81
81
  const owner = head;
82
- const shellRefusal = (handsText) => ({ kind: "control_syntax_at_program_position", owner, word: "sh", runsShell: true, ...(handsText !== undefined ? { handsText } : {}) });
82
+ const shellRefusal = (launcher, from) => ({ kind: "control_syntax_at_program_position", owner, word: "sh", runsShell: { launcher, from } });
83
83
  let lastRow = launcherRowOf(basename)?.launcher;
84
84
  let owedOperands = lastRow?.requiredOperands ?? 0;
85
85
  const chain0 = (program, refusal) => ({ controlWords, written, assignments, launchers, argvFromStdin: false, runsDefaultProgram: false, program, refusal });
86
- if (lastRow?.shellText === true)
87
- return chain0(undefined, shellRefusal(true));
86
+ if (launcherRunsShell(launcherRowOf(basename), folded.slice(i + 1)))
87
+ return chain0(undefined, shellRefusal(i, i + 1));
88
88
  let argvFromStdin = feedsArgvFromStdin(basename);
89
89
  let lastReentry = argvFromStdin ? i : -1;
90
90
  let reentryDefault = argvFromStdin ? streamDefaultProgram(basename) : undefined;
@@ -92,6 +92,7 @@ export function readProgramPosition(words) {
92
92
  const runsDefaultProgram = lastReentry === folded.length - 1;
93
93
  return { controlWords, written, assignments, launchers, argvFromStdin, runsDefaultProgram, ...(runsDefaultProgram && reentryDefault !== undefined ? { defaultProgram: reentryDefault } : {}), program, refusal };
94
94
  };
95
+ const selectsShell = (l, end) => selectedOptions(launcherRowOf(l.name), folded.slice(l.index + 1, end)).some((o) => o.mode === "interactive");
95
96
  for (let j = pastLauncherOptions(basename, folded, i + 1); j < folded.length; j++) {
96
97
  const t = folded[j];
97
98
  if (owedOperands > 0 && !t.startsWith("-")) {
@@ -100,6 +101,8 @@ export function readProgramPosition(words) {
100
101
  }
101
102
  if (lastRow?.assignmentOperands === true && !t.startsWith("-") && t.includes("="))
102
103
  continue;
104
+ if (lastRow?.shellCommand !== undefined && !t.startsWith("-") && selectsShell(launchers[launchers.length - 1], j))
105
+ return chain(undefined, shellRefusal(launchers[launchers.length - 1].index, j));
103
106
  const wordBasename = programBasename(t);
104
107
  if (feedsArgvFromStdin(wordBasename)) {
105
108
  argvFromStdin = true;
@@ -112,8 +115,8 @@ export function readProgramPosition(words) {
112
115
  if (LAUNCHER_NAMES.has(wordBasename)) {
113
116
  launchers.push({ index: j, name: wordBasename });
114
117
  lastRow = launcherRowOf(wordBasename)?.launcher;
115
- if (lastRow?.shellText === true)
116
- return chain(undefined, shellRefusal(true));
118
+ if (launcherRunsShell(launcherRowOf(wordBasename), folded.slice(j + 1)))
119
+ return chain(undefined, shellRefusal(j, j + 1));
117
120
  owedOperands = lastRow?.requiredOperands ?? 0;
118
121
  j = pastLauncherOptions(wordBasename, folded, j + 1) - 1;
119
122
  continue;
@@ -126,8 +129,8 @@ export function readProgramPosition(words) {
126
129
  return chain({ index: j, name: t, basename: wordBasename });
127
130
  }
128
131
  const last = launchers[launchers.length - 1];
129
- if (lastRow?.whenNoProgram === "shell" || selectedOptions(launcherRowOf(last.name), folded.slice(last.index + 1)).some((o) => o.mode === "interactive"))
130
- return chain(undefined, shellRefusal());
132
+ if (lastRow?.whenNoProgram === "shell" || selectsShell(last, folded.length))
133
+ return chain(undefined, shellRefusal(last.index, folded.length));
131
134
  return chain(undefined);
132
135
  }
133
136
  export function argvReentryUnjudged(pos, acceptsTarget) {
@@ -139,12 +142,12 @@ export function argvReentryUnjudged(pos, acceptsTarget) {
139
142
  export const SHELL_SCAN_DECLINES = Object.freeze(["unreadable_program_position", "stream_fed_operands"]);
140
143
  export function programPositionRunsShell(pos, runsShell) {
141
144
  if (pos.refusal !== undefined)
142
- return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell === true;
145
+ return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell !== undefined;
143
146
  return pos.program !== undefined && runsShell(pos.program.basename);
144
147
  }
145
148
  export function shellScanDeclineOf(pos, acceptsStreamTarget) {
146
149
  if (pos.refusal !== undefined) {
147
- return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell === true ? undefined : "unreadable_program_position";
150
+ return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell !== undefined ? undefined : "unreadable_program_position";
148
151
  }
149
152
  return argvReentryUnjudged(pos, acceptsStreamTarget) ? "stream_fed_operands" : undefined;
150
153
  }
@@ -75,7 +75,8 @@ export type ArgRole =
75
75
  | {
76
76
  readonly kind: "shell-text";
77
77
  }
78
- /** The program word a launcher runs; the words after it are that program's argv. */
78
+ /** The program a launcher or a builtin runs by name: a function (`compgen -F`), the program a launcher runs in place of
79
+ * a shell (`su -s PROGRAM`). */
79
80
  | {
80
81
  readonly kind: "command";
81
82
  }
@@ -163,10 +164,10 @@ export interface OptionSpec {
163
164
  readonly endsOptions?: true;
164
165
  /** `banner`: prints and exits; `interactive`: reads commands from its standard input (after its program, `python -i`;
165
166
  * instead of a script file, `sh -s`; a launcher's shell, run where no program follows, `sudo -s`); `names-program`: selects
166
- * the program to run without a value (`node --test`); `follows`: follows its files as they grow, until killed
167
- * (`tail -f`); `repeats`: repeats on an interval until killed (`free -s`); `quiet`: prints only what the program's own
168
- * text tells it to (`sed -n`); `forced`: removes without asking, and passes over an operand that is not there
169
- * (`rm -f`). */
167
+ * the program to run without a value (`node --test`; a launcher's, from the caller's environment: `su -m`); `follows`:
168
+ * follows its files as they grow, until killed (`tail -f`); `repeats`: repeats on an interval until killed (`free -s`);
169
+ * `quiet`: prints only what the program's own text tells it to (`sed -n`); `forced`: removes without asking, and passes
170
+ * over an operand that is not there (`rm -f`). */
170
171
  readonly mode?: OptionMode;
171
172
  /** This switch turns the program's own value glob off (`curl -g`): every {@link ArgRole} `programGlob` value of the
172
173
  * invocation is read literally. */
@@ -250,10 +251,22 @@ export interface LauncherSpec {
250
251
  readonly assignmentOperands?: true;
251
252
  /** A bare negative integer in its option section is its own argument (`nice -5 <cmd>`). */
252
253
  readonly negativeIntegerIsOwnArgument?: true;
253
- /** It hands its operands to a SHELL as text (`watch "cat /x"`): there is no program word. */
254
- readonly shellText?: true;
254
+ /** It runs a SHELL in place of a program word, and how the words after it become that shell's argv: `joined` — its
255
+ * non-option words, joined by a space, are the shell's `-c` text (`watch "cat /x"`); `user` — its first operand (past
256
+ * a lone `-`) names the user it runs as, and the words after the user are the shell's own argv, behind a `-c TEXT` its
257
+ * own shell-text option gives (`su [-] [USER [ARG…]]`: an implementation that reads its options anywhere takes them
258
+ * past the user too, one that stops at the user hands them on); the program is its option of role `command` where
259
+ * one is given (`su -s PROGRAM`), or a `names-program` switch's (`su -m`: the caller's `$SHELL`). */
260
+ readonly shellText?: "joined" | "user";
261
+ /** Option spellings that make a {@link shellText} launcher run the program its operands name instead
262
+ * (`runuser -u USER CMD…`). */
263
+ readonly programWith?: readonly string[];
255
264
  /** With no program word it runs a shell. */
256
265
  readonly whenNoProgram?: "shell";
266
+ /** Where a switch selecting its shell (mode `interactive`: `sudo -s`) has words after it, they are that shell's `-c`
267
+ * text: `escaped` — joined by a space, every character of each but a letter, a digit, `_`, `-` and `$` escaped with a
268
+ * backslash. Absent, the launcher takes no such words (`doas -s CMD` is refused) and they are read as the program. */
269
+ readonly shellCommand?: "escaped";
257
270
  /** The spelling a required operand must have (a word spelled otherwise is not one the launcher accepts). Absent: any
258
271
  * word that is not option-shaped. */
259
272
  readonly requiredOperandSpelling?: RegExp;
@@ -652,6 +665,10 @@ export declare function launcherOptionPathRole(row: ProgramRow, tok: string): Ex
652
665
  * separated (2), a value fused to its long name or short letter (1); a switch, an attached-only option, or a cluster of
653
666
  * switches (1); a lone `-` where the launcher reads it as its own switch (1). */
654
667
  export declare function launcherOptionStep(row: ProgramRow, tok: string): 1 | 2 | undefined;
668
+ /** Does a launcher that runs a shell ({@link LauncherSpec.shellText}) run one over these words — the words after it —
669
+ * rather than the program they name? Not where an implementation selects one of its {@link LauncherSpec.programWith}
670
+ * options (as that implementation reads them: past `--` a word is an operand). */
671
+ export declare function launcherRunsShell(row: ProgramRow, args: readonly string[]): boolean;
655
672
  /** The spellings of a launcher's command-line options (`env -S` / `--split-string`): the value is a command line the
656
673
  * launcher splits and runs. */
657
674
  export declare function commandLineOptionSpellings(row: ProgramRow | undefined): readonly string[];
@@ -1055,6 +1055,13 @@ export function launcherOptionStep(row, tok) {
1055
1055
  return 1;
1056
1056
  return undefined;
1057
1057
  }
1058
+ export function launcherRunsShell(row, args) {
1059
+ const l = row.launcher;
1060
+ if (l?.shellText === undefined)
1061
+ return false;
1062
+ const specs = new Set((l.programWith ?? []).map((s) => (s.startsWith("--") ? mergedTable(row, "long").get(s.slice(2)) : mergedTable(row, "short").get(s.slice(1)))?.spec));
1063
+ return !implementationsOf(row).some((view) => selectedOptions(view, args, "one-implementation").some((o) => specs.has(o)));
1064
+ }
1058
1065
  export function commandLineOptionSpellings(row) {
1059
1066
  if (row === undefined)
1060
1067
  return [];
@@ -360,8 +360,19 @@ const launcherGrammar = (g, extra = {}) => {
360
360
  put(spelling, val(SHELL_TEXT));
361
361
  for (const spelling of g.shells ?? [])
362
362
  put(spelling, { arity: 0, mode: "interactive" });
363
+ for (const spelling of g.programs ?? [])
364
+ put(spelling, { arity: 0, mode: "names-program" });
363
365
  return { dialect: "any", short, long, afterFirstOperand: "either", ...extra };
364
366
  };
367
+ const SU_OPTIONS = {
368
+ switches: ["-f", "-l", "-P", "-T", "-h", "-V", "--fast", "--login", "--pty", "--no-pty", "--help", "--version"],
369
+ banners: ["-h", "-V"],
370
+ values: { "-g": LABEL, "-G": LABEL, "-w": LABEL, "-s": COMMAND, "--group": LABEL, "--supp-group": LABEL, "--whitelist-environment": LABEL, "--shell": COMMAND },
371
+ shellText: ["-c", "--command", "--session-command"],
372
+ programs: ["-m", "-p", "--preserve-environment"],
373
+ };
374
+ const SU = launcherGrammar(SU_OPTIONS, { stepsDashDash: true });
375
+ const SU_LINUX_ONLY = [...SU_OPTIONS.switches, ...Object.keys(SU_OPTIONS.values), ...SU_OPTIONS.shellText, ...SU_OPTIONS.programs].filter((s) => !["-f", "-l", "-m"].includes(s));
365
376
  const launchers = [
366
377
  { key: "env", grammars: implementations(launcherGrammar({
367
378
  switches: ["-i", "-v", "--ignore-environment", "--debug", "--list-signal-handling", "--help", "--version"],
@@ -380,18 +391,18 @@ const launchers = [
380
391
  optional: { "-i": LABEL, "-l": COUNT, "--replace": LABEL, "--eof": LABEL, "--max-lines": COUNT },
381
392
  }, {}), { reads: { "-a": val(READ, { delivers: "operands" }), "--arg-file": val(READ, { delivers: "operands" }) } }, { lacks: ["-d", "-i", "-l", "-a", "--arg-file", "--null", "--no-run-if-empty", "--verbose", "--exit", "--interactive", "--open-tty", "--show-limits", "--help", "--version", "--max-args", "--max-procs", "--delimiter", "--max-chars", "--process-slot-var", "--replace", "--eof", "--max-lines"] }), launcher: {}, reentry: { kind: "argv", defaultProgram: "echo" }, witness: BINARY },
382
393
  launcher("busybox"),
383
- launcher("sudo", { assignmentOperands: true }, launcherGrammar({
394
+ launcher("sudo", { assignmentOperands: true, shellCommand: "escaped" }, launcherGrammar({
384
395
  switches: ["-A", "-b", "-E", "-H", "-K", "-k", "-l", "-n", "-P", "-S", "-V", "-v", "--askpass", "--background", "--preserve-env", "--set-home", "--remove-timestamp", "--reset-timestamp", "--list", "--non-interactive", "--preserve-groups", "--stdin", "--version", "--validate", "--bell", "--help"],
385
396
  shells: ["-i", "-s", "--login", "--shell"],
386
397
  values: {
387
398
  "-u": LABEL, "-g": LABEL, "-U": LABEL, "-C": COUNT, "-D": WORKDIR, "-h": LABEL, "-p": LABEL, "-r": LABEL, "-R": ROOTDIR, "-t": LABEL, "-T": COUNT, "-a": LABEL,
388
399
  "--user": LABEL, "--group": LABEL, "--other-user": LABEL, "--close-from": COUNT, "--chdir": WORKDIR, "--host": LABEL, "--prompt": LABEL, "--role": LABEL, "--chroot": ROOTDIR, "--type": LABEL, "--command-timeout": COUNT, "--auth-type": LABEL,
389
400
  },
390
- }), { witness: PRIVILEGED }),
391
- launcher("doas", {}, launcherGrammar({ switches: ["-L", "-n"], shells: ["-s"], values: { "-a": LABEL, "-u": LABEL, "-C": READ } }), { witness: PRIVILEGED }),
401
+ }, { stepsDashDash: true }), { witness: PRIVILEGED }),
402
+ launcher("doas", {}, launcherGrammar({ switches: ["-L", "-n"], shells: ["-s"], values: { "-a": LABEL, "-u": LABEL, "-C": READ } }, { stepsDashDash: true }), { witness: PRIVILEGED }),
392
403
  launcher("pkexec", { whenNoProgram: "shell" }, launcherGrammar({ switches: ["--disable-internal-agent", "--keep-cwd", "--version", "--help"], values: { "--user": LABEL } }), { witness: PRIVILEGED }),
393
- launcher("su", { shellText: true }, undefined, { witness: PRIVILEGED }),
394
- launcher("runuser", { shellText: true }, undefined, { witness: PRIVILEGED }),
404
+ { key: "su", grammars: implementations(SU, { afterFirstOperand: "options" }, { lacks: SU_LINUX_ONLY, afterFirstOperand: "operands" }), launcher: { shellText: "user" }, witness: PRIVILEGED },
405
+ { key: "runuser", grammars: [launcherGrammar({ ...SU_OPTIONS, values: { ...SU_OPTIONS.values, "-u": LABEL, "--user": LABEL } }, { afterFirstOperand: "options", stepsDashDash: true })], launcher: { shellText: "user", programWith: ["-u", "--user"] }, witness: PRIVILEGED },
395
406
  launcher("chroot", { requiredOperands: 1, whenNoProgram: "shell" }, undefined, { witness: PRIVILEGED }),
396
407
  launcher("setpriv", {}, undefined, { witness: PRIVILEGED }),
397
408
  launcher("nohup", {}, grammar({}, BANNERS)),
@@ -409,7 +420,7 @@ const launchers = [
409
420
  { key: "stdbuf", grammars: implementations(grammar({ i: val(LABEL), o: val(LABEL), e: val(LABEL) }, { input: val(LABEL), output: val(LABEL), error: val(LABEL), ...BANNERS }), {}, { lacks: ["--input", "--output", "--error", "--help", "--version"] }), launcher: {}, witness: BINARY },
410
421
  launcher("timeout", { requiredOperands: 1, requiredOperandSpelling: DURATION_SPELLING }, grammar({ v: SWITCH, k: val(COUNT), s: val(LABEL) }, { foreground: SWITCH, "preserve-status": SWITCH, verbose: SWITCH, "kill-after": val(COUNT), signal: val(LABEL), ...BANNERS })),
411
422
  { key: "time", grammars: implementations(grammar({ p: SWITCH, a: SWITCH, v: SWITCH, q: SWITCH, f: val(LABEL), o: val(WRITE) }, { portability: SWITCH, append: SWITCH, verbose: SWITCH, quiet: SWITCH, help: SWITCH, version: SWITCH, format: val(LABEL), output: val(WRITE) }, { stepsDashDash: true }), {}, { lacks: ["-v", "-q", "-f", "--portability", "--append", "--verbose", "--quiet", "--help", "--version", "--format", "--output"] }), launcher: {}, witness: BINARY },
412
- launcher("watch", { shellText: true }, undefined, { witness: SHELL_TEXT_LAUNCHER }),
423
+ launcher("watch", { shellText: "joined" }, undefined, { witness: SHELL_TEXT_LAUNCHER }),
413
424
  launcher("setsid", {}, launcherGrammar({ switches: ["-c", "-f", "-w", "--ctty", "--fork", "--wait", "--help", "--version"] })),
414
425
  launcher("flock", { requiredOperands: 1 }, launcherGrammar({
415
426
  switches: ["-s", "-x", "-u", "-n", "-o", "-F", "--shared", "--exclusive", "--unlock", "--nonblock", "--nb", "--close", "--no-fork", "--verbose", "--help", "--version"],
@@ -425,7 +436,7 @@ const launchers = [
425
436
  values: { "-t": COUNT, "-S": LABEL, "-G": LABEL, "--target": COUNT, "--setuid": LABEL, "--setgid": LABEL },
426
437
  optional: { "-r": ROOTDIR, "-w": WORKDIR, "--root": ROOTDIR, "--wd": WORKDIR },
427
438
  }), { witness: LINUX_NAMESPACE }),
428
- launcher("script", { shellText: true }, launcherGrammar({ shellText: ["-c", "--command"] }), { witness: SHELL_TEXT_LAUNCHER }),
439
+ launcher("script", { shellText: "joined" }, launcherGrammar({ shellText: ["-c", "--command"] }), { witness: SHELL_TEXT_LAUNCHER }),
429
440
  launcher("numactl", {}, undefined, { witness: LINUX_NAMESPACE }),
430
441
  launcher("prlimit", {}, undefined, { witness: LINUX_NAMESPACE }),
431
442
  launcher("systemd-run", {}, undefined, { witness: { declared: "hands the program to a service manager" } }),
@@ -2,7 +2,8 @@ import { planShellReads } from "./shell-read-plan.js";
2
2
  import { literalTextOf } from "../../shell/lex.js";
3
3
  import { createShellReadEstablisher, establishShellReadsLexically } from "./shell-read-establish.js";
4
4
  import { isAbsolutePathForm, isWinFormPath, normalizeAbsPathLexically } from "./safety.js";
5
- import { reentryText } from "./shell-reentry-text.js";
5
+ import { launchedCommands, launchedReadings, reentryText, spellLaunched } from "./shell-reentry-text.js";
6
+ import { readProgramPosition } from "./bash-program-position.js";
6
7
  import { bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
7
8
  import { programArgs } from "./shell-read-write-targets.js";
8
9
  import { implementationsOf, programRow, runsShellText, selectedOptions, writeReadings } from "./program-read-model.js";
@@ -167,10 +168,19 @@ function runtimeScriptOf(segment) {
167
168
  const program = segment.verb;
168
169
  if (segment.reentry !== "shell" || program === undefined || !runsShellText(program.basename))
169
170
  return undefined;
170
- const at = segment.argv.findIndex((w) => w.folded === program.name);
171
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}`))];
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}`))]);
174
184
  const text = texts.find((t) => t?.includes(MARK) === true);
175
185
  return text === undefined ? undefined : text.split(MARK).join("");
176
186
  }
@@ -7,14 +7,15 @@
7
7
  */
8
8
  import { type LexedWord } from "../../shell/lex.js";
9
9
  import { type Read } from "./shell-read-word.js";
10
- import { type BindingTable, type BoundValue } from "./shell-read-bindings.js";
10
+ import { type BindingTable, type FloorValue } from "./shell-read-bindings.js";
11
11
  import { subcommandOf, type ArgRole, type ProgramRow } from "./program-read-model.js";
12
12
  import type { ReadCandidate, ReadCandidateProvenance } from "./shell-read-plan.js";
13
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. */
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. */
15
16
  export type Bare = Omit<ReadCandidate, "bases"> & {
16
- readonly alternatives?: readonly string[];
17
- readonly floors?: readonly BoundValue[];
17
+ readonly alternatives?: readonly Pick<ReadCandidate, "spelled" | "pattern">[];
18
+ readonly floors?: readonly FloorValue[];
18
19
  };
19
20
  /** What reading a segment's words found beside its candidates. */
20
21
  export interface WordContext {
@@ -1,5 +1,5 @@
1
1
  import { literalTildeSpelling } from "../../shell/lex.js";
2
- import { expandsOf, readOfText, rewritten } from "./shell-read-word.js";
2
+ import { expandsOf, patternOf, readOfText, rewritten, spellingOf } from "./shell-read-word.js";
3
3
  import { arithmeticSubstitutions } from "./shell-read-bindings.js";
4
4
  import { argvProgramOf, attachedOptionPayloads, isPathShapedToken } from "./bash-readonly-classifier.js";
5
5
  import { assignmentArgumentsAreNotPaths, evaluatesOperands, messageWords, moduleTailOf, objectPathsOf, opensNoOperand, programRow, programTextMayOpen, programTextOpensOf, readArgs, selectsContentsTraversal, slotValueCandidates, subcommandOf, valuesAreLiteral, walksCwdWithoutOperand, wordsReadBySomeImplementations, } from "./program-read-model.js";
@@ -10,14 +10,16 @@ export function pathSubcommandOf(row, args) {
10
10
  export const programTextSentence = (language) => `runs inline ${language} program text, which may open files this station does not read`;
11
11
  export function bare(r, provenance, spelled = r.folded) {
12
12
  const whole = spelled === r.folded;
13
- if (!whole && r.values !== undefined)
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)
14
15
  return { spelled, raw: r.raw, provenance, expands: "unresolvable" };
15
16
  return {
16
17
  spelled,
17
18
  raw: r.raw,
18
19
  provenance,
19
20
  expands: r.expands,
20
- ...(whole && r.values !== undefined ? { alternatives: r.values.slice(1) } : {}),
21
+ ...(whole && r.pattern !== undefined ? { pattern: r.pattern } : {}),
22
+ ...(whole && alternatives !== undefined ? { alternatives } : {}),
21
23
  ...(whole && r.homeFloor !== undefined ? { floorSpelled: r.homeFloor } : {}),
22
24
  ...(whole && r.floorValues !== undefined ? { floors: r.floorValues } : {}),
23
25
  };
@@ -26,9 +26,34 @@
26
26
  */
27
27
  import { type LexedRedirect, type LexedSegment, type LexedToken, type LexedWord } from "../../shell/lex.js";
28
28
  import { type DeclaredAttribute } from "./program-read-model.js";
29
- /** A value a name may hold: the text it holds, and whether that text is a PATTERN whose matches are the values (a `for`
30
- * list word the shell globs before the loop binds it). */
31
- export interface BoundValue {
29
+ /** A value a name may hold — the text the program receives ({@link spelled}: what a shell re-parsing it reads), literal
30
+ * throughout, or a PATTERN whose matches are the values (a `for` list word the shell globs before the loop binds it;
31
+ * `spelled` is then the pattern as the program receives it where it matches nothing): `glob`, with its `parts` — the
32
+ * value in runs: a literal run (quoted or literal text, a settled value) and a pattern run (glob pieces as written,
33
+ * quoting kept, and as a reader judges them — {@link WordExpansion.pattern}). The one producer is the piece walk
34
+ * ({@link valueOf}). */
35
+ export type BoundValue = {
36
+ readonly spelled: string;
37
+ readonly glob?: never;
38
+ readonly parts?: never;
39
+ } | {
40
+ readonly spelled: string;
41
+ readonly glob: true;
42
+ readonly parts: readonly ValuePart[];
43
+ };
44
+ /** One run of a pattern {@link BoundValue}: literal text, or a pattern — the shell expands its characters — `text` as
45
+ * written (what a shell re-reading it matches by), `judged` as a reader judges the names it matches. */
46
+ export type ValuePart = {
47
+ readonly text: string;
48
+ readonly pattern: false;
49
+ } | {
50
+ readonly text: string;
51
+ readonly pattern: true;
52
+ readonly judged: string;
53
+ };
54
+ /** A spelling the deny judge alone reads ({@link Binding.floor}, {@link wordFloorValues}): a value, or a word as written
55
+ * where its names are unknown — nothing re-reads it, so a pattern says no more than that it is one. */
56
+ export interface FloorValue {
32
57
  readonly spelled: string;
33
58
  readonly glob?: true;
34
59
  }
@@ -58,7 +83,7 @@ export interface Binding {
58
83
  readonly scope?: "local" | "unread";
59
84
  /** Spellings the name may hold that the deny judge still reads where the name is otherwise unknown (an array's
60
85
  * elements: what `${A[k]}` expands to is one of them). */
61
- readonly floor?: readonly BoundValue[];
86
+ readonly floor?: readonly FloorValue[];
62
87
  }
63
88
  /** An attribute a declaration gives a name — or every name (`name` absent: the declaration computed it): a later
64
89
  * assignment to it is evaluated as arithmetic (`integer`), stores a case change or an array element (`transformed`), or
@@ -104,27 +129,29 @@ export declare function projectSegment(segment: LexedSegment): ProjectedSegment;
104
129
  * token, a head whose body opens in a later segment). */
105
130
  export declare function dropDefinitionHeads(tokens: LexedToken[]): readonly number[];
106
131
  /**
107
- * What a word holds through the names it refers to — `$NAME`, `${NAME}`, a leading `~` (the value of `HOME`):
108
- * · `undefined` — it refers to no name (it reads as written);
109
- * · its values — every combination of the values its names may hold here (a piece the shell globs keeps its written
110
- * pattern, and the value is then a pattern);
111
- * · `"unknown"` — a name it refers to is not settled here, it carries an expansion no name answers (a modifier, a
112
- * substitution, a brace list, `~user`), an UNQUOTED expansion's value is not one word or `IFS` may have changed
113
- * (`quoted` = the whole word is read as one value: an assignment's value, which is never split), or it would list
114
- * more than {@link WORD_VALUES_MAX} values.
115
- * `table.stateOf` is read after `overlay` (the names the same segment bound earlier).
132
+ * What a word holds through the names it refers to — `$NAME`, `${NAME}`, a leading `~` (the value of `HOME`): `undefined`
133
+ * where it refers to no name (it reads as written), else {@link valueOf}. `table.stateOf` is read after `overlay` (the
134
+ * names the same segment bound earlier).
116
135
  */
117
136
  export declare function wordValues(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined, quoted: boolean, overlay?: ReadonlyMap<string, NameState>): readonly BoundValue[] | "unknown" | undefined;
137
+ /**
138
+ * What a VALUE word holds (an assignment's value, a `for` list word, a word handed to a program) — THE piece walk, the one
139
+ * producer of {@link BoundValue}s: every combination of the values its pieces take — a literal piece its text, a glob
140
+ * piece its pattern (as the program receives it, as written for a shell re-reading it, and as a reader judges it — the
141
+ * lexer's three texts, never re-cut here), `~` and `$NAME` the values the table gives:
142
+ * · its values — a piece the shell globs makes the value a pattern;
143
+ * · `"unknown"` — a name it refers to is not settled here, it carries an expansion no name answers (a modifier, a
144
+ * substitution, a brace list, `~user`, an expansion inside a bracket pattern), an UNQUOTED expansion's value is not one
145
+ * word or `IFS` may have changed (`quoted` = the whole word is read as one value: an assignment's value, which is never
146
+ * split), or it would list more than {@link WORD_VALUES_MAX} values.
147
+ */
148
+ export declare function valueOf(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined, quoted: boolean, overlay?: ReadonlyMap<string, NameState>): NameState;
118
149
  /**
119
150
  * The spellings a word the table leaves unknown may still take through the deny floors of the names it refers to
120
151
  * ({@link BindingTable.floorOf}) — `${A[k]}`, `${A[@]}`, `$A` reading an array's elements — or through their values where
121
152
  * known: for the deny judge alone. Empty where some name it refers to leaves neither.
122
153
  */
123
- export declare function wordFloorValues(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined): readonly BoundValue[];
124
- /** What a VALUE word holds (an assignment's value, a `for` list word): {@link wordValues}, or — a word referring to no
125
- * name — its own text (a word the shell globs is a pattern; one carrying any other expansion holds what the text does
126
- * not say). */
127
- export declare function valueOf(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined, quoted: boolean, overlay?: ReadonlyMap<string, NameState>): NameState;
154
+ export declare function wordFloorValues(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined): readonly FloorValue[];
128
155
  /**
129
156
  * What ONE segment does to the names (see the module header). `argv` = the segment's argv projection (structure
130
157
  * dropped); `readValue` folds a value word against the table (a reader that answers `unknown` gives the names alone).
@@ -164,7 +191,7 @@ export interface BindingTable {
164
191
  /** `HOME` may hold something other than the declared home here. */
165
192
  readonly homeTouched: boolean;
166
193
  /** Every spelling any binding of this name before here left for the deny judge alone ({@link Binding.floor}). */
167
- floorOf(name: string): readonly BoundValue[];
194
+ floorOf(name: string): readonly FloorValue[];
168
195
  /** What a substitution body here starts with. */
169
196
  readonly inherited: InheritedNames;
170
197
  }