@sema-agent/core 7.46.0 → 7.48.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 (77) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/dist/core/allow-entry.d.ts +1 -1
  3. package/dist/core/allow-entry.js +4 -5
  4. package/dist/core/ask-origin.d.ts +35 -12
  5. package/dist/core/ask-origin.js +23 -6
  6. package/dist/core/checkpoint-store.d.ts +1 -1
  7. package/dist/core/engine-notice.d.ts +7 -4
  8. package/dist/core/gate-fold.js +2 -2
  9. package/dist/core/gate-lanes.js +2 -1
  10. package/dist/core/gate-outcome.d.ts +4 -2
  11. package/dist/core/gate-outcome.js +2 -2
  12. package/dist/core/mcp-server-spec.d.ts +1 -1
  13. package/dist/core/permission-rule-model.d.ts +12 -6
  14. package/dist/core/permission-rule-model.js +10 -2
  15. package/dist/core/permission-rules.d.ts +4 -5
  16. package/dist/core/permission-rules.js +28 -15
  17. package/dist/core/runner/gate-exit.d.ts +4 -1
  18. package/dist/core/runner/gate-exit.js +5 -1
  19. package/dist/core/runner/park-commit.d.ts +1 -1
  20. package/dist/core/runner/prepare-hands-readface.js +5 -5
  21. package/dist/core/runner/prepare-policy-chain.js +2 -1
  22. package/dist/core/safety-axis-vocab.d.ts +1 -1
  23. package/dist/core/safety-merge-corpus.d.ts +1 -1
  24. package/dist/core/session-policy-store.d.ts +6 -3
  25. package/dist/core/session-policy-store.js +28 -10
  26. package/dist/core/shell-wrapper-projection.js +1 -1
  27. package/dist/core/shell-wrapper-table.d.ts +8 -8
  28. package/dist/core/shell-wrapper-table.js +3 -3
  29. package/dist/core/tool-catalog-entries.js +3 -2
  30. package/dist/core/tool-face.d.ts +10 -1
  31. package/dist/core/tool-name-aliases.d.ts +9 -8
  32. package/dist/core/tool-name-aliases.js +2 -0
  33. package/dist/core/tool-name-readings.d.ts +8 -0
  34. package/dist/core/tool-name-readings.js +2 -0
  35. package/dist/core/tool-policy.d.ts +10 -9
  36. package/dist/core/tool-policy.js +6 -7
  37. package/dist/core/tool-registry.d.ts +9 -0
  38. package/dist/core/tool-registry.js +10 -0
  39. package/dist/core/write-protect.d.ts +10 -3
  40. package/dist/core/write-protect.js +11 -1
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/orchestration/workflow.js +2 -2
  44. package/dist/prompt-assembly/types.d.ts +1 -1
  45. package/dist/shell/lex.d.ts +15 -0
  46. package/dist/shell/lex.js +86 -15
  47. package/dist/tools/fs/bash-program-position.d.ts +16 -6
  48. package/dist/tools/fs/bash-program-position.js +18 -12
  49. package/dist/tools/fs/index.d.ts +3 -2
  50. package/dist/tools/fs/index.js +2 -2
  51. package/dist/tools/fs/program-read-model.d.ts +84 -12
  52. package/dist/tools/fs/program-read-model.js +105 -34
  53. package/dist/tools/fs/program-read-model.rows.js +26 -7
  54. package/dist/tools/fs/read-deny-home.d.ts +20 -1
  55. package/dist/tools/fs/read-deny-home.js +28 -7
  56. package/dist/tools/fs/read-deny.d.ts +13 -6
  57. package/dist/tools/fs/read-deny.js +12 -3
  58. package/dist/tools/fs/removal-targets.js +24 -5
  59. package/dist/tools/fs/shell-read-argv.d.ts +6 -8
  60. package/dist/tools/fs/shell-read-argv.js +5 -5
  61. package/dist/tools/fs/shell-read-bindings.d.ts +71 -19
  62. package/dist/tools/fs/shell-read-bindings.js +190 -75
  63. package/dist/tools/fs/shell-read-establish.js +6 -7
  64. package/dist/tools/fs/shell-read-plan.d.ts +3 -15
  65. package/dist/tools/fs/shell-read-plan.js +126 -129
  66. package/dist/tools/fs/shell-read-start.d.ts +57 -0
  67. package/dist/tools/fs/shell-read-start.js +92 -0
  68. package/dist/tools/fs/shell-read-word.d.ts +68 -8
  69. package/dist/tools/fs/shell-read-word.js +106 -12
  70. package/dist/tools/fs/shell-read-write-targets.js +4 -3
  71. package/dist/tools/fs/shell-reentry-text.d.ts +103 -3
  72. package/dist/tools/fs/shell-reentry-text.js +122 -4
  73. package/dist/tools/monitor.d.ts +3 -3
  74. package/package.json +1 -1
  75. package/test/export-surface.snapshot.json +9 -3
  76. package/wire-changes/core@7.47.0.json +412 -0
  77. package/wire-changes/core@7.48.0.json +252 -0
@@ -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;
@@ -16,9 +16,12 @@ export function writtenAssignmentPrefix(token) {
16
16
  }
17
17
  export { SHELL_CONTROL_PREFIXES };
18
18
  export function isBareControlWord(words, i, set) {
19
- const w = words[i];
20
- return w !== undefined && w.raw === w.folded && set.has(w.folded);
19
+ return isShellSyntaxWord(words[i], set);
20
+ }
21
+ export function isShellSyntaxWord(w, set) {
22
+ return w !== undefined && w.atCommandPosition && w.raw === w.folded && set.has(w.folded);
21
23
  }
24
+ export const LOOP_LIST_HEADS = new Set(["for", "select"]);
22
25
  export function isLeadingAssignmentWord(words, i) {
23
26
  const w = words[i];
24
27
  return w !== undefined && w.assignment !== undefined && w.assignment.bare && w.atCommandPosition;
@@ -79,12 +82,12 @@ export function readProgramPosition(words) {
79
82
  }
80
83
  const launchers = [{ index: i, name: basename }];
81
84
  const owner = head;
82
- const shellRefusal = (handsText) => ({ kind: "control_syntax_at_program_position", owner, word: "sh", runsShell: true, ...(handsText !== undefined ? { handsText } : {}) });
85
+ const shellRefusal = (launcher, from) => ({ kind: "control_syntax_at_program_position", owner, word: "sh", runsShell: { launcher, from } });
83
86
  let lastRow = launcherRowOf(basename)?.launcher;
84
87
  let owedOperands = lastRow?.requiredOperands ?? 0;
85
88
  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));
89
+ if (launcherRunsShell(launcherRowOf(basename), folded.slice(i + 1)))
90
+ return chain0(undefined, shellRefusal(i, i + 1));
88
91
  let argvFromStdin = feedsArgvFromStdin(basename);
89
92
  let lastReentry = argvFromStdin ? i : -1;
90
93
  let reentryDefault = argvFromStdin ? streamDefaultProgram(basename) : undefined;
@@ -92,6 +95,7 @@ export function readProgramPosition(words) {
92
95
  const runsDefaultProgram = lastReentry === folded.length - 1;
93
96
  return { controlWords, written, assignments, launchers, argvFromStdin, runsDefaultProgram, ...(runsDefaultProgram && reentryDefault !== undefined ? { defaultProgram: reentryDefault } : {}), program, refusal };
94
97
  };
98
+ const selectsShell = (l, end) => selectedOptions(launcherRowOf(l.name), folded.slice(l.index + 1, end)).some((o) => o.mode === "interactive");
95
99
  for (let j = pastLauncherOptions(basename, folded, i + 1); j < folded.length; j++) {
96
100
  const t = folded[j];
97
101
  if (owedOperands > 0 && !t.startsWith("-")) {
@@ -100,6 +104,8 @@ export function readProgramPosition(words) {
100
104
  }
101
105
  if (lastRow?.assignmentOperands === true && !t.startsWith("-") && t.includes("="))
102
106
  continue;
107
+ if (lastRow?.shellCommand !== undefined && !t.startsWith("-") && selectsShell(launchers[launchers.length - 1], j))
108
+ return chain(undefined, shellRefusal(launchers[launchers.length - 1].index, j));
103
109
  const wordBasename = programBasename(t);
104
110
  if (feedsArgvFromStdin(wordBasename)) {
105
111
  argvFromStdin = true;
@@ -112,8 +118,8 @@ export function readProgramPosition(words) {
112
118
  if (LAUNCHER_NAMES.has(wordBasename)) {
113
119
  launchers.push({ index: j, name: wordBasename });
114
120
  lastRow = launcherRowOf(wordBasename)?.launcher;
115
- if (lastRow?.shellText === true)
116
- return chain(undefined, shellRefusal(true));
121
+ if (launcherRunsShell(launcherRowOf(wordBasename), folded.slice(j + 1)))
122
+ return chain(undefined, shellRefusal(j, j + 1));
117
123
  owedOperands = lastRow?.requiredOperands ?? 0;
118
124
  j = pastLauncherOptions(wordBasename, folded, j + 1) - 1;
119
125
  continue;
@@ -126,8 +132,8 @@ export function readProgramPosition(words) {
126
132
  return chain({ index: j, name: t, basename: wordBasename });
127
133
  }
128
134
  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());
135
+ if (lastRow?.whenNoProgram === "shell" || selectsShell(last, folded.length))
136
+ return chain(undefined, shellRefusal(last.index, folded.length));
131
137
  return chain(undefined);
132
138
  }
133
139
  export function argvReentryUnjudged(pos, acceptsTarget) {
@@ -139,12 +145,12 @@ export function argvReentryUnjudged(pos, acceptsTarget) {
139
145
  export const SHELL_SCAN_DECLINES = Object.freeze(["unreadable_program_position", "stream_fed_operands"]);
140
146
  export function programPositionRunsShell(pos, runsShell) {
141
147
  if (pos.refusal !== undefined)
142
- return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell === true;
148
+ return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell !== undefined;
143
149
  return pos.program !== undefined && runsShell(pos.program.basename);
144
150
  }
145
151
  export function shellScanDeclineOf(pos, acceptsStreamTarget) {
146
152
  if (pos.refusal !== undefined) {
147
- return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell === true ? undefined : "unreadable_program_position";
153
+ return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell !== undefined ? undefined : "unreadable_program_position";
148
154
  }
149
155
  return argvReentryUnjudged(pos, acceptsStreamTarget) ? "stream_fed_operands" : undefined;
150
156
  }
@@ -187,8 +187,9 @@ export interface HandsToolkitOptions {
187
187
  * under a short bound of its own, and a probe of it that fails costs that entry alone. The public shell judgment takes the
188
188
  * same list as {@link import("./shell-read-check.js").ShellReadJudgeInput.readDenyHomes}. */
189
189
  readDenyHomes?: readonly string[];
190
- /** Where each home-anchored row's literal head leads beyond the home's spellings
191
- * ({@link import("./read-deny-home.js").readDenyHomeLinksOf}, keyed by the row's pattern) — the Runner passes the
190
+ /** Where each home-anchored row's literal head leads beyond the home's spellings, and each rooted row's (`/…`) beyond its own
191
+ * spelling ({@link import("./read-deny-home.js").readDenyHomeLinksOf} / {@link import("./read-deny-home.js").readDenyRootLinksOf},
192
+ * keyed by the row's pattern) — the Runner passes the
192
193
  * locations its own judge was compiled with, so the band and the gate judge one placement.
193
194
  * Unless a mount carries both, the band resolves the placement itself the way the Runner does (the homes it carries
194
195
  * are its home set — the account is not asked and no home is added;
@@ -14,7 +14,7 @@ export * from "./read-deny.js";
14
14
  export { readDenyHomesOf } from "./read-deny-home.js";
15
15
  import { BASH_READONLY_DEFAULT_ALLOW } from "./bash-readonly-classifier.js";
16
16
  export { BASH_CLASSIFY_EXTRA_ALLOW, bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
17
- import { checkedReadDenyHomes, compileReadDeny, compileWriteDeny, readDenyBuiltinConfigOf, readDenyPatternRun } from "./read-deny.js";
17
+ import { checkedReadDenyHomes, compileReadDeny, compileWriteDeny, isRootedReadDenyPattern, readDenyBuiltinConfigOf, readDenyPatternRun } from "./read-deny.js";
18
18
  import { withGateTimeout } from "../../core/gate-bounded-await.js";
19
19
  import { carryHomeViews, homeViewsAt, placeReadDenyAtHome, placementViewsOf, readDenyPlacedLater, sayPlacementLost } from "./read-deny-home.js";
20
20
  import { deploymentFaceClampNotice, resolveContainmentFace } from "./containment-face.js";
@@ -51,7 +51,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
51
51
  const carriedViews = carried ? placementViewsOf(opts.readDenyHomes) : undefined;
52
52
  if (carried && carriedViews === undefined)
53
53
  sayPlacementLost();
54
- const placesNothing = !unplaced.entries.some((e) => readDenyPatternRun(e.pattern).homeAnchored);
54
+ const placesNothing = !unplaced.entries.some((e) => readDenyPatternRun(e.pattern).homeAnchored || isRootedReadDenyPattern(e.pattern));
55
55
  const late = carried || placesNothing ? undefined : readDenyPlacedLater(carryHomeViews(unplaced, homeViewsAt(env, [])), () => placeReadDenyAtHome(env, compileAt, readDenyHomes));
56
56
  const readDeny = late?.matcher ??
57
57
  (carried
@@ -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,15 +251,44 @@ 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;
260
273
  /** A lone `-` in its option section is its own switch (`env -`, the obsolete spelling of `env -i`). */
261
274
  readonly bareDash?: true;
275
+ /** It runs its shell as ANOTHER user ({@link TargetUser}): the user an option of `userWith` names (`sudo -u USER`), or —
276
+ * a {@link shellText} `user` launcher — its user operand; root where none is named. The shell's `HOME` is that user's
277
+ * home (`target`), or the caller's or that user's as the launcher's policy decides (`policy`: `sudo -s`); a switch of
278
+ * `loginWith` (`sudo -i`, `su -l`, su's lone `-` operand) starts it as a LOGIN shell — its `HOME` that user's, and it
279
+ * stands in that home. A `names-program` switch (`su -m`) keeps the caller's environment, `HOME` with it. */
280
+ readonly asUser?: {
281
+ readonly userWith?: readonly string[];
282
+ readonly home: "target" | "policy";
283
+ readonly loginWith: readonly string[];
284
+ };
285
+ }
286
+ /** The user a launcher runs its shell as ({@link LauncherSpec.asUser}): the name, where a word spells it literally; the
287
+ * shell's `HOME` (`caller` — kept; `target` — that user's; `either` — one of the two); whether it is a login shell. */
288
+ export interface TargetUser {
289
+ readonly name?: string;
290
+ readonly home: "caller" | "target" | "either";
291
+ readonly login: boolean;
262
292
  }
263
293
  /** How a program receives the PROGRAM it runs (an interpreter): an option value, an operand (`operands.positional`),
264
294
  * or standard input when it names none. */
@@ -550,6 +580,20 @@ export declare function attachedStdinFile(row: ProgramRow | undefined, tok: stri
550
580
  /** The row read under each implementation's grammar alone (one view per grammar), or the row itself when it declares
551
581
  * at most one. A reader that needs to know what ONE implementation does with an argv reads these views. */
552
582
  export declare function implementationsOf(row: ProgramRow): readonly ProgramRow[];
583
+ /** What a scan over an argv's option words found: the specs selected, the values they took (argv index; the value
584
+ * inside the option word when it is attached), and the operands (argv indices). */
585
+ interface OptionScan {
586
+ readonly specs: readonly OptionSpec[];
587
+ /** Each selected option's spelling as its row declares it (`-x`, `--exec`), in step with `specs`. */
588
+ readonly names: readonly string[];
589
+ readonly values: readonly {
590
+ readonly at: number;
591
+ readonly value?: string;
592
+ readonly spec: OptionSpec;
593
+ readonly name: string;
594
+ }[];
595
+ readonly operands: readonly number[];
596
+ }
553
597
  /**
554
598
  * Every option spec an argv selects. A short cluster is read as getopt reads it, scanning left to right: a declared
555
599
  * letter is selected; a value-taking letter owns the rest of the token (and the next word, when it ends the token); a
@@ -561,6 +605,14 @@ export declare function implementationsOf(row: ProgramRow): readonly ProgramRow[
561
605
  * option, and a word past `--`, included. The reading a check that only ever refuses takes: it errs to more options.
562
606
  */
563
607
  export declare function selectedOptions(row: ProgramRow | undefined, args: readonly string[], under?: "one-implementation"): OptionSpec[];
608
+ /** The options ONE implementation's reading of an argv selects ({@link selectedOptions}), named by the spelling its row
609
+ * declares them under (`-x`, `--exec`), and the values they take. An option is told apart by its spelling, never by its
610
+ * spec: spellings share specs (every switch of a row is one). */
611
+ export declare function selectedSpellings(row: ProgramRow, args: readonly string[]): Pick<OptionScan, "names" | "values">;
612
+ /** Does ONE implementation of a row (`view`, {@link implementationsOf}) reject these option words — a spelling the row
613
+ * declares (another implementation's) that this one does not? getopt calls it an invalid option: the program runs
614
+ * nothing. */
615
+ export declare function implementationRejects(row: ProgramRow, view: ProgramRow, words: readonly string[]): boolean;
564
616
  /** A path role whose effect writes. */
565
617
  export type WritingRole = Extract<ArgRole, {
566
618
  kind: "path";
@@ -643,18 +695,38 @@ export declare function moduleTailOf(row: ProgramRow | undefined, args: readonly
643
695
  * value exists only attached (spelled bare, it takes nothing), 1 for one that takes the next word; `undefined` for any
644
696
  * other spelling and for an option the walk does not step over. */
645
697
  export declare function launcherOptionArity(row: ProgramRow, tok: string): 0 | 1 | undefined;
646
- /** The PATH role of the value a launcher option spelled EXACTLY takes (`xargs -a FILE`, `strace -o FILE`), or `undefined`
647
- * when its value names no path. Such a value is a word the launcher opens or writes, not an operand of its own. */
698
+ /** The PATH role of the value a launcher option word takes (`xargs -a FILE`, `strace -fo FILE`, `strace -oFILE`), or
699
+ * `undefined` when its value names no path ({@link readLauncherOption}). Such a value is a word the launcher opens or
700
+ * writes, not an operand of its own. */
648
701
  export declare function launcherOptionPathRole(row: ProgramRow, tok: string): Extract<ArgRole, {
649
702
  kind: "path";
650
703
  }> | undefined;
651
- /** How many tokens one launcher option occupies, or `undefined` where the row does not explain it: an arity-1 option
652
- * separated (2), a value fused to its long name or short letter (1); a switch, an attached-only option, or a cluster of
653
- * switches (1); a lone `-` where the launcher reads it as its own switch (1). */
704
+ /** How many words one launcher option occupies ({@link readLauncherOption}), or `undefined` where the row does not
705
+ * explain it. */
654
706
  export declare function launcherOptionStep(row: ProgramRow, tok: string): 1 | 2 | undefined;
707
+ /** Does a launcher that runs a shell ({@link LauncherSpec.shellText}) run one over these words — the words after it —
708
+ * rather than the program they name? Not where an implementation selects one of its {@link LauncherSpec.programWith}
709
+ * options (as that implementation reads them: past `--` a word is an operand). */
710
+ export declare function launcherRunsShell(row: ProgramRow, args: readonly string[]): boolean;
711
+ /** The user a launcher runs its shell as ({@link LauncherSpec.asUser}), read off its own words (`words`, as the program
712
+ * receives them) by its row's walk: the value of its last `userWith` option, else its user operand (`operand`; a lone `-`
713
+ * in front of it starts a login shell, `dash`), else root — a name only where it is spelled as one (`root`, `#0`, a login
714
+ * name); `undefined` for a launcher that runs no other user. */
715
+ export declare function targetUserOf(row: ProgramRow, words: readonly string[], operand?: {
716
+ readonly name?: string;
717
+ readonly dash: boolean;
718
+ }): TargetUser | undefined;
655
719
  /** The spellings of a launcher's command-line options (`env -S` / `--split-string`): the value is a command line the
656
720
  * launcher splits and runs. */
657
721
  export declare function commandLineOptionSpellings(row: ProgramRow | undefined): readonly string[];
722
+ /** The command line ONE launcher option word carries, read as getopt reads the word ({@link readLauncherOption}): a
723
+ * command-line spelling ({@link commandLineOptionSpellings}) exactly — `"next"`, the next word is the line — or with its
724
+ * value after `=` (long) or after the letter (short, `attached`); in a short cluster every letter before the
725
+ * command-line letter is a switch the walk steps (`env -vS 'S'`, `env -iS'S'`). `undefined` where the word carries
726
+ * none. */
727
+ export declare function commandLineOf(row: ProgramRow, tok: string): {
728
+ readonly attached: string;
729
+ } | "next" | undefined;
658
730
  /** One implementation's reading of where the program text is: the text (every text option's value joined by newlines,
659
731
  * or the first operand), `from-file` (an option names a FILE holding it), `unparsed` (an option the grammar does not
660
732
  * declare, or a text option with nothing left to take), or `none` (no program text at all). */
@@ -682,6 +682,16 @@ export function implementationsOf(row) {
682
682
  export function selectedOptions(row, args, under) {
683
683
  return row === undefined ? [] : [...scanOptions(row, args, under === "one-implementation").specs];
684
684
  }
685
+ export function selectedSpellings(row, args) {
686
+ const scan = scanOptions(row, args, true);
687
+ return { names: scan.names, values: scan.values };
688
+ }
689
+ export function implementationRejects(row, view, words) {
690
+ if (view === row)
691
+ return false;
692
+ const declares = (n) => (n.startsWith("--") ? mergedTable(view, "long").has(n.slice(2)) : mergedTable(view, n.length === 2 ? "short" : "dashWords").has(n.slice(1)));
693
+ return selectedSpellings(row, words).names.some((n) => !declares(n));
694
+ }
685
695
  function scanOptions(row, args, one) {
686
696
  const stopsAtOperand = one && row.grammars[0]?.afterFirstOperand === "operands";
687
697
  const shorts = mergedTable(row, "short");
@@ -689,8 +699,10 @@ function scanOptions(row, args, one) {
689
699
  const dashWords = mergedTable(row, "dashWords");
690
700
  const obsolete = row.grammars.flatMap((g) => (g.obsoleteFirst === undefined ? [] : [g.obsoleteFirst]));
691
701
  const specs = [];
702
+ const names = [];
692
703
  const values = [];
693
704
  const operands = [];
705
+ const select = (spec, name) => { specs.push(spec); names.push(name); };
694
706
  let endOfOptions = false;
695
707
  for (let k = 0; k < args.length; k++) {
696
708
  const t = args[k];
@@ -704,7 +716,7 @@ function scanOptions(row, args, one) {
704
716
  for (const ch of t.replace(/^.\d+/, "")) {
705
717
  const spec = o.letters[ch];
706
718
  if (spec !== undefined)
707
- specs.push(spec);
719
+ select(spec, `-${ch}`);
708
720
  }
709
721
  continue;
710
722
  }
@@ -719,13 +731,13 @@ function scanOptions(row, args, one) {
719
731
  endOfOptions = true;
720
732
  continue;
721
733
  }
722
- const take = (spec, attached) => {
734
+ const take = (spec, name, attached) => {
723
735
  if (attached !== undefined) {
724
- values.push({ at: k, value: attached, spec });
736
+ values.push({ at: k, value: attached, spec, name });
725
737
  return;
726
738
  }
727
739
  if (one && k + 1 < args.length) {
728
- values.push({ at: k + 1, spec });
740
+ values.push({ at: k + 1, spec, name });
729
741
  k++;
730
742
  }
731
743
  };
@@ -735,22 +747,23 @@ function scanOptions(row, args, one) {
735
747
  continue;
736
748
  const eq = t.indexOf("=");
737
749
  const exact = longs.get(name);
738
- const hits = exact !== undefined ? [exact] : [...longs].filter(([full]) => isLongOptionAbbrevOf(name, full)).map(([, m]) => m);
739
- for (const m of hits)
740
- specs.push(m.spec);
741
- if (hits.length === 1 && (hits[0].arity === 1 || (eq >= 0 && hits[0].arity === "attached")))
742
- take(hits[0].spec, eq >= 0 ? t.slice(eq + 1) : undefined);
743
- else if (one && eq < 0 && hits.length > 1 && hits.every((m) => m.arity === 1))
750
+ const hits = exact !== undefined ? [[name, exact]] : [...longs].filter(([full]) => isLongOptionAbbrevOf(name, full));
751
+ for (const [full, m] of hits)
752
+ select(m.spec, `--${full}`);
753
+ const [full, only] = hits[0] ?? [];
754
+ if (hits.length === 1 && (only.arity === 1 || (eq >= 0 && only.arity === "attached")))
755
+ take(only.spec, `--${full}`, eq >= 0 ? t.slice(eq + 1) : undefined);
756
+ else if (one && eq < 0 && hits.length > 1 && hits.every(([, m]) => m.arity === 1))
744
757
  k++;
745
758
  continue;
746
759
  }
747
760
  const word = /^-([A-Za-z_][\w-]+)$/.exec(t)?.[1];
748
761
  const dash = word === undefined ? undefined : dashWords.get(word);
749
762
  if (dash !== undefined) {
750
- specs.push(dash.spec);
763
+ select(dash.spec, t);
751
764
  if (one) {
752
765
  if (dash.arity === 1)
753
- take(dash.spec, undefined);
766
+ take(dash.spec, t, undefined);
754
767
  continue;
755
768
  }
756
769
  }
@@ -761,18 +774,18 @@ function scanOptions(row, args, one) {
761
774
  continue;
762
775
  break;
763
776
  }
764
- specs.push(m.spec);
777
+ select(m.spec, `-${t[i]}`);
765
778
  if (m.arity === 0)
766
779
  continue;
767
780
  const rest = t.slice(i + 1);
768
781
  if (rest !== "")
769
- values.push({ at: k, value: rest, spec: m.spec });
782
+ values.push({ at: k, value: rest, spec: m.spec, name: `-${t[i]}` });
770
783
  else if (m.arity === 1 || m.arity === "suffix")
771
- take(m.spec, undefined);
784
+ take(m.spec, `-${t[i]}`, undefined);
772
785
  break;
773
786
  }
774
787
  }
775
- return { specs, values, operands };
788
+ return { specs, names, values, operands };
776
789
  }
777
790
  export function writesPath(role) {
778
791
  return role?.kind === "path" && WRITING_EFFECTS.has(role.effect);
@@ -1023,7 +1036,14 @@ function steppableLauncherOption(row, tok) {
1023
1036
  const m = tok.startsWith("--") ? (tok.includes("=") ? undefined : mergedTable(row, "long").get(tok.slice(2))) : tok.length === 2 && tok.startsWith("-") ? mergedTable(row, "short").get(tok[1]) : undefined;
1024
1037
  if (m === undefined || m.spec.value?.kind === "shell-text" || (m.spec.value?.kind === "path" && m.spec.value.base !== undefined))
1025
1038
  return undefined;
1026
- return m.arity === "disputed" ? undefined : m;
1039
+ return m.arity !== "disputed" || takesNoWordBare(row, tok) ? m : undefined;
1040
+ }
1041
+ function takesNoWordBare(row, spelling) {
1042
+ const key = spelling.startsWith("--") ? spelling.slice(2) : spelling.slice(1);
1043
+ return row.grammars.every((g) => {
1044
+ const s = (spelling.startsWith("--") ? g.long : g.short)[key];
1045
+ return s === undefined || s.arity === 0 || s.arity === "attached";
1046
+ });
1027
1047
  }
1028
1048
  export function launcherOptionArity(row, tok) {
1029
1049
  if (row.grammars.length === 0)
@@ -1033,27 +1053,59 @@ export function launcherOptionArity(row, tok) {
1033
1053
  const m = steppableLauncherOption(row, tok);
1034
1054
  return m === undefined ? undefined : m.arity === 1 ? 1 : 0;
1035
1055
  }
1056
+ function readLauncherOption(row, tok) {
1057
+ if (tok === "-")
1058
+ return row.launcher?.bareDash === true ? { step: 1 } : undefined;
1059
+ if (tok === "--")
1060
+ return launcherOptionArity(row, tok) === 0 ? { step: 1 } : undefined;
1061
+ if (tok.startsWith("--")) {
1062
+ const eq = tok.indexOf("=");
1063
+ const m = steppableLauncherOption(row, eq > 2 ? tok.slice(0, eq) : tok);
1064
+ if (m === undefined)
1065
+ return undefined;
1066
+ if (eq > 2)
1067
+ return m.arity === 0 || m.arity === "disputed" ? undefined : { step: 1, valued: m };
1068
+ return m.arity === 1 ? { step: 2, valued: m } : { step: 1 };
1069
+ }
1070
+ if (!/^-[^-]/.test(tok))
1071
+ return undefined;
1072
+ for (let i = 1; i < tok.length; i++) {
1073
+ const m = steppableLauncherOption(row, `-${tok[i]}`);
1074
+ if (m === undefined)
1075
+ return undefined;
1076
+ if (m.arity === 0)
1077
+ continue;
1078
+ if (i + 1 < tok.length)
1079
+ return m.arity === "disputed" ? undefined : { step: 1, valued: m };
1080
+ return m.arity === 1 ? { step: 2, valued: m } : { step: 1 };
1081
+ }
1082
+ return { step: 1 };
1083
+ }
1036
1084
  export function launcherOptionPathRole(row, tok) {
1037
- const role = steppableLauncherOption(row, tok)?.spec.value;
1085
+ const role = readLauncherOption(row, tok)?.valued?.spec.value;
1038
1086
  return role?.kind === "path" ? role : undefined;
1039
1087
  }
1040
1088
  export function launcherOptionStep(row, tok) {
1041
- if (tok === "-" && row.launcher?.bareDash === true)
1042
- return 1;
1043
- const exact = launcherOptionArity(row, tok);
1044
- if (exact !== undefined)
1045
- return exact === 1 ? 2 : 1;
1046
- const eq = tok.startsWith("--") ? tok.indexOf("=") : -1;
1047
- if (eq > 2 && steppableLauncherOption(row, tok.slice(0, eq))?.arity !== undefined && steppableLauncherOption(row, tok.slice(0, eq)).arity !== 0)
1048
- return 1;
1049
- if (/^-[A-Za-z]./.test(tok) && !tok.startsWith("--")) {
1050
- const first = steppableLauncherOption(row, tok.slice(0, 2));
1051
- if (first !== undefined && first.arity !== 0)
1052
- return 1;
1053
- }
1054
- if (/^-[A-Za-z]+$/.test(tok) && [...tok.slice(1)].every((ch) => launcherOptionArity(row, `-${ch}`) === 0 && steppableLauncherOption(row, `-${ch}`)?.arity === 0))
1055
- return 1;
1056
- return undefined;
1089
+ return readLauncherOption(row, tok)?.step;
1090
+ }
1091
+ export function launcherRunsShell(row, args) {
1092
+ const l = row.launcher;
1093
+ if (l?.shellText === undefined)
1094
+ return false;
1095
+ const programWith = l.programWith ?? [];
1096
+ return !implementationsOf(row).some((view) => selectedSpellings(view, args).names.some((n) => programWith.includes(n)));
1097
+ }
1098
+ export function targetUserOf(row, words, operand) {
1099
+ const as = row.launcher?.asUser;
1100
+ if (as === undefined)
1101
+ return undefined;
1102
+ const own = selectedSpellings(row, words);
1103
+ const user = own.values.filter((v) => as.userWith?.includes(v.name) === true).at(-1);
1104
+ const named = user !== undefined ? user.value ?? words[user.at] : operand === undefined ? "root" : operand.name;
1105
+ const login = operand?.dash === true || own.names.some((n) => as.loginWith.includes(n));
1106
+ const keepsCaller = selectedOptions(row, words).some((o) => o.mode === "names-program");
1107
+ const name = named === "#0" ? "root" : named !== undefined && /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(named) ? named : undefined;
1108
+ return { ...(name !== undefined ? { name } : {}), home: login ? "target" : as.home === "policy" ? "either" : keepsCaller ? "caller" : "target", login };
1057
1109
  }
1058
1110
  export function commandLineOptionSpellings(row) {
1059
1111
  if (row === undefined)
@@ -1063,6 +1115,25 @@ export function commandLineOptionSpellings(row) {
1063
1115
  ...[...mergedTable(row, "long")].filter(([, m]) => m.spec.value?.kind === "shell-text").map(([n]) => `--${n}`),
1064
1116
  ];
1065
1117
  }
1118
+ export function commandLineOf(row, tok) {
1119
+ const lines = new Set(commandLineOptionSpellings(row));
1120
+ if (tok.startsWith("--")) {
1121
+ const eq = tok.indexOf("=");
1122
+ if (!lines.has(eq > 2 ? tok.slice(0, eq) : tok))
1123
+ return undefined;
1124
+ return eq > 2 ? { attached: tok.slice(eq + 1) } : "next";
1125
+ }
1126
+ if (!/^-[^-]/.test(tok))
1127
+ return undefined;
1128
+ for (let i = 1; i < tok.length; i++) {
1129
+ const letter = `-${tok[i]}`;
1130
+ if (lines.has(letter))
1131
+ return i + 1 < tok.length ? { attached: tok.slice(i + 1) } : "next";
1132
+ if (steppableLauncherOption(row, letter)?.arity !== 0)
1133
+ return undefined;
1134
+ }
1135
+ return undefined;
1136
+ }
1066
1137
  export function readProgramTextLanguage(row) {
1067
1138
  const first = row?.operands?.positional?.[0];
1068
1139
  return first?.kind === "program-text" && READ_LANGUAGES.has(first.language) ? first.language : undefined;
@@ -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", asUser: { userWith: ["-u", "--user"], home: "policy", loginWith: ["-i", "--login"] } }, 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", { asUser: { userWith: ["-u"], home: "target", loginWith: [] } }, 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", asUser: { home: "target", loginWith: ["-l", "--login"] } }, 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"], asUser: { home: "target", loginWith: ["-l", "--login"] } }, 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,15 @@ 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
+ { key: "watch", grammars: [
424
+ launcherGrammar({
425
+ switches: ["-b", "-C", "-c", "-e", "-f", "-g", "-h", "-p", "-r", "-t", "-w", "-v", "-x", "--beep", "--color", "--no-color", "--errexit", "--follow", "--chgexit", "--exec", "--precise", "--no-rerun", "--no-title", "--no-wrap", "--help", "--version"],
426
+ banners: ["-h", "-v"],
427
+ values: { "-n": COUNT, "--interval": COUNT, "-q": COUNT, "--equexit": COUNT, "-s": WRITE, "--shotsdir": WRITE },
428
+ optional: { "-d": LABEL, "--differences": LABEL },
429
+ }, { afterFirstOperand: "operands", stepsDashDash: true }),
430
+ launcherGrammar({ switches: ["-d", "-t"], values: { "-n": COUNT } }, { afterFirstOperand: "operands", stepsDashDash: true }),
431
+ ], launcher: { shellText: "joined", programWith: ["-x", "--exec"] }, witness: SHELL_TEXT_LAUNCHER },
413
432
  launcher("setsid", {}, launcherGrammar({ switches: ["-c", "-f", "-w", "--ctty", "--fork", "--wait", "--help", "--version"] })),
414
433
  launcher("flock", { requiredOperands: 1 }, launcherGrammar({
415
434
  switches: ["-s", "-x", "-u", "-n", "-o", "-F", "--shared", "--exclusive", "--unlock", "--nonblock", "--nb", "--close", "--no-fork", "--verbose", "--help", "--version"],
@@ -425,7 +444,7 @@ const launchers = [
425
444
  values: { "-t": COUNT, "-S": LABEL, "-G": LABEL, "--target": COUNT, "--setuid": LABEL, "--setgid": LABEL },
426
445
  optional: { "-r": ROOTDIR, "-w": WORKDIR, "--root": ROOTDIR, "--wd": WORKDIR },
427
446
  }), { witness: LINUX_NAMESPACE }),
428
- launcher("script", { shellText: true }, launcherGrammar({ shellText: ["-c", "--command"] }), { witness: SHELL_TEXT_LAUNCHER }),
447
+ launcher("script", { shellText: "joined" }, launcherGrammar({ shellText: ["-c", "--command"] }), { witness: SHELL_TEXT_LAUNCHER }),
429
448
  launcher("numactl", {}, undefined, { witness: LINUX_NAMESPACE }),
430
449
  launcher("prlimit", {}, undefined, { witness: LINUX_NAMESPACE }),
431
450
  launcher("systemd-run", {}, undefined, { witness: { declared: "hands the program to a service manager" } }),
@@ -38,6 +38,23 @@ 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 ROOTED rows of a table (`/app/config-lkg.json`, read-deny.ts) lead — the same `linked` answer
43
+ * {@link readDenyHomeLinksOf} gives for home-anchored rows, by the same machine ({@link linkedLocationsOf}): the row's
44
+ * LITERAL HEAD is spelled under the root and resolved the way the read faces mint their keys, and a location that differs
45
+ * from the spelling is where one of the head's segments is a link (`/app` a link to `/data/app`) — the row is placed
46
+ * there too, so a read of the link's target meets it. The laws are `readDenyHomeLinksOf`'s: a head that resolves to its own
47
+ * spelling adds nothing, a head with nothing below it adds nothing, a head the environment does not answer adds
48
+ * nothing and the answer is not WHOLE, a row whose head is empty (`/*.json`) has no location to resolve. A root is no
49
+ * home, so the answer does not depend on the homes: it is read whether or not a home answered.
50
+ */
51
+ export declare function readDenyRootLinksOf(env: ExecutionEnv, entries: readonly {
52
+ readonly pattern: string;
53
+ readonly caseSensitive?: boolean;
54
+ }[], signal?: AbortSignal, deadline?: number): Promise<{
55
+ readonly linked: Map<string, string[]>;
56
+ readonly whole: boolean;
57
+ }>;
41
58
  /**
42
59
  * Where the rows that open with the configuration directory's name (`~/.sema/…`, {@link agentConfigDirTail}) lead when
43
60
  * the host declares the directory's root (`RunnerDeps.agentConfigDir`) — the same `linked` answer
@@ -138,7 +155,9 @@ export declare function carryHomeViews<T extends object>(judge: T, source: HomeV
138
155
  * with no answer, or a declared home that did not answer, places nothing: the rows are held anywhere (`compile([])`)
139
156
  * rather than at a spelling that may miss the home's canonical one;
140
157
  * · each AUXILIARY home (see the module head) read once under {@link AUXILIARY_HOME_PROBE_MS} — its spellings, its
141
- * chain, its rows' heads — at the same time; whatever of it did not answer costs that home alone and never `whole`.
158
+ * chain, its rows' heads — at the same time; whatever of it did not answer costs that home alone and never `whole`;
159
+ * · the ROOTED rows' heads ({@link readDenyRootLinksOf}), read whether or not a home answered: a head that did not answer
160
+ * leaves the placement not WHOLE, and a placement that held the rows anywhere still places them.
142
161
  * `compile` is the mount's own compile; the rows' patterns are read off its compile at no home. */
143
162
  export declare function placeReadDenyAtHome(env: ExecutionEnv, compile: (homes: readonly string[], linked?: ReadonlyMap<string, readonly string[]>) => ReadDenyMatcher, homes?: readonly string[], signal?: AbortSignal, configRoot?: string): Promise<{
144
163
  readonly matcher: PlacedReadDenyMatcher;