@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
@@ -5,34 +5,94 @@
5
5
  * does to the word before the program sees it.
6
6
  */
7
7
  import { type LexedCommand, type LexedWord, type WordPiece } from "../../shell/lex.js";
8
- import { type BindingTable, type BoundValue } from "./shell-read-bindings.js";
8
+ import { type BindingTable, type BoundValue, type FloorValue } from "./shell-read-bindings.js";
9
9
  /** `"glob"` = an unquoted pathname-expansion character; `"unresolvable"` = the shell rewrites the word (a brace,
10
10
  * variable or command substitution) so its text is not the path. The read candidate's own field, spelled here. */
11
11
  export type WordExpands = false | "glob" | "unresolvable";
12
- /** A word as the candidate readers name it: the lexer's word, the spelling the program receives — the names the word
13
- * refers to read through the binding table ({@link wordValues}: `"$F"` after `F=/x` is `/x`) — and what the shell does to
14
- * it before the program sees it. */
12
+ /** A word as the candidate readers name it: the lexer's word, the spelling a reader names it by — the names the word
13
+ * refers to read through the binding table ({@link wordValues}: `"$F"` after `F=/x` is `/x`; {@link spellingOf}) — and
14
+ * what the shell does to it before the program sees it. A text handed to a shell that re-parses it is the program's
15
+ * ({@link programTextOf}), never this one. */
15
16
  export interface Read {
16
17
  readonly word: LexedWord;
17
18
  readonly raw: string;
18
19
  readonly folded: string;
20
+ /** A pattern's names are matched by this spelling where it is not {@link folded} (a class the judge widens, read as
21
+ * any one character — {@link LexedWord.judged}, {@link judgedSpellingOf}); `folded` stays the name opened where none match. */
22
+ readonly pattern?: string;
19
23
  readonly expands: WordExpands;
20
24
  /** The names the word refers to were read through the table: its expansion IS the spelling above (and, where the
21
25
  * names may hold several values, {@link values}). */
22
26
  readonly settled?: true;
23
- /** Every spelling the word may expand to (the first is {@link folded}); present only where there are several. */
24
- readonly values?: readonly string[];
27
+ /** Every value the word may expand to (the first spelled {@link folded}, {@link spellingOf}; matched by {@link pattern}),
28
+ * each with the parts of it the shell expands as a pattern ({@link BoundValue}); present wherever the names were read
29
+ * through the table ({@link settled}). */
30
+ readonly values?: readonly BoundValue[];
25
31
  /** The word refers to `HOME` and `HOME` may hold something other than the declared home here: the spelling the
26
32
  * word has with the declared home, for the deny judge alone. */
27
33
  readonly homeFloor?: string;
28
34
  /** The word's names are unknown here, but their bindings left spellings for the deny judge (an array's elements):
29
35
  * what the word may then spell, for the deny judge alone. */
30
- readonly floorValues?: readonly BoundValue[];
36
+ readonly floorValues?: readonly FloorValue[];
31
37
  }
32
38
  /** The reads of a segment's words against the binding table at that segment (`homeDir` = the declared home, the
33
39
  * state `HOME` starts in). A word that refers to no name, or only to a `HOME` the text has not touched (`~/x`, placed
34
40
  * by the establishment at the declared home), reads as written. */
35
- export declare function readsOf(words: readonly LexedWord[], table: BindingTable | undefined, homeDir: string | undefined): Read[];
41
+ export declare function readsOf(words: readonly LexedWord[], table: BindingTable | undefined, homeDir: string | undefined, split?: boolean): Read[];
42
+ /** One reading of a segment: the binding table with each name its words read held to ONE value (or, past
43
+ * {@link READINGS_MAX}, made unknown — every value it held stays a deny-floor spelling, {@link BindingTable.floorOf}),
44
+ * the reads of its argv against it ({@link readsOf}), and the argv the shell hands the program in it — a word whose
45
+ * names settle into one value that is no pattern is that value, as a literal word (a reader of the program position,
46
+ * of a launcher's fields, of a program's options reads it as it reads a literal word); any other word as written, its
47
+ * names' deny floors in `floors` (a shell that re-reads the word — `sh -c`, `eval`, a launcher's shell — starts with
48
+ * them: the word names in that text what it names here). */
49
+ export interface SegmentReading {
50
+ readonly table: BindingTable;
51
+ readonly reads: readonly Read[];
52
+ readonly argv: readonly LexedWord[];
53
+ readonly floors: ReadonlyMap<string, readonly FloorValue[]>;
54
+ }
55
+ /**
56
+ * The readings of one segment's words (its argv and its redirection targets): the names they read through the table that
57
+ * may hold several values here, in the order the words read them (the program position first), each held to one value
58
+ * per reading — every combination while the readings stay within {@link READINGS_MAX}, so the program position, a
59
+ * launcher's fields, a re-entered text and an operand are spelled from the same values (`$P` read twice takes one value
60
+ * in a reading: the shell expands both alike); a name past that bound is unknown in every reading — its word as written,
61
+ * every value it held a deny floor — never a sample of its combinations. A segment whose names hold one value each has
62
+ * one reading.
63
+ */
64
+ export declare function segmentReadingsOf(argv: readonly LexedWord[], targets: readonly LexedWord[], table: BindingTable, homeDir: string | undefined): SegmentReading[];
65
+ /** The spelling a reader names a value by, read directly as a path — for a pattern, the name the program opens where it
66
+ * matches nothing: its text, a leading `~` spelled as the literal tilde it is ({@link literalTildeSpelling}, the lexer's
67
+ * spelling of a written one) — the shell never tilde-expands what a quoted piece or an expansion produced, so a value that
68
+ * starts with `~` starts with a literal one (`X=/w; rm \~$X/*` reads `./~/w/*`, as `rm \~/w/*` does). A shell that
69
+ * re-parses the value reads {@link programTextOf} instead. */
70
+ export declare const spellingOf: (v: BoundValue) => string;
71
+ /** The pattern a PART of a pattern word is matched by (an option value attached to its option word, `--file=["!"a]*`; an
72
+ * assignment-shaped word's value, `if=["!"a]*`): the word's own judged spelling (`pattern`) past the text in front of
73
+ * the part — bash expands the WHOLE word, so the names the part may take are those the word's widened spelling reaches
74
+ * (a superset: a name is judged whether or not the word in front of it matches too). `undefined` where the word carries
75
+ * none, the part is not the end of the word's spelling, or the text in front holds a widened class itself (its judged
76
+ * spelling no longer lines up). */
77
+ export declare function partPattern(word: {
78
+ readonly folded: string;
79
+ readonly pattern?: string;
80
+ }, part: string): string | undefined;
81
+ /** A candidate under another name (a value of several, a backup, a copy's child): the word's `pattern` matched the word,
82
+ * not that name — the name is matched by its own text. */
83
+ export declare function renamed<C extends {
84
+ readonly spelled: string;
85
+ readonly pattern?: string;
86
+ }>(c: C, spelled: string): Omit<C, "pattern">;
87
+ /** The text a read's word hands a program, as a shell re-parsing it reads it (`eval`, `sh -c`, a launcher's line): its
88
+ * first established value as the program receives it, or the lexer's folded spelling — never {@link spellingOf}'s path
89
+ * spelling (`X='~'; eval rm -rf "$X"` re-parses `rm -rf ~`, the home). */
90
+ export declare const programTextOf: (r: Read) => string;
91
+ /** What a read's word hands the program, where the reader knows it: each value its names settled into — one field each
92
+ * (an unquoted expansion whose value is not one word is never settled, {@link wordValues}), or, where the word is a
93
+ * pattern, the pattern whose matches are the fields, its parts telling pattern characters from literal ones;
94
+ * `undefined` for a word the table did not settle (or that names nothing). */
95
+ export declare function settledFieldsOf(r: Read): readonly BoundValue[] | undefined;
36
96
  /** The shell REWRITES this read's word before the program sees it (a parameter, command, process, arithmetic or
37
97
  * brace expansion, or the character of one) — a word whose names the table settled excepted: its expansion is its
38
98
  * spelling. */
@@ -1,33 +1,127 @@
1
- import { argvOf, lexShell, UNRESOLVED_SENTENCE } from "../../shell/lex.js";
1
+ import { argvOf, lexShell, literalTildeSpelling, UNRESOLVED_SENTENCE } from "../../shell/lex.js";
2
2
  import { expansionInWord, extglobOpenerIn, globInWord, homeExpansionOf } from "./bash-readonly-classifier.js";
3
- import { wordFloorValues, wordValues } from "./shell-read-bindings.js";
4
- export function readsOf(words, table, homeDir) {
3
+ import { floorOf, judgedSpellingOf, namesIn, wordFloorValues, wordValues } from "./shell-read-bindings.js";
4
+ export function readsOf(words, table, homeDir, split = false) {
5
5
  return words.map((word) => {
6
- const refersToName = word.pieces.some((p) => p.expansion?.kind === "parameter");
6
+ const refersToName = word.pieces.some((p) => p.expansion?.kind === "parameter") || namesIn(word).length > 0;
7
+ const asWritten = { word, raw: word.raw, folded: word.folded, ...(word.judged !== undefined ? { pattern: word.judged } : {}) };
7
8
  if (!refersToName && table?.homeTouched !== true)
8
- return { word, raw: word.raw, folded: word.folded, expands: expandsOf(word) };
9
- const values = wordValues(word, table, false);
9
+ return { ...asWritten, expands: expandsOf(word) };
10
+ const values = wordValues(word, table, false, undefined, split);
10
11
  if (values === undefined)
11
- return { word, raw: word.raw, folded: word.folded, expands: expandsOf(word) };
12
+ return { ...asWritten, expands: expandsOf(word) };
12
13
  if (values === "unknown") {
13
14
  const floors = refersToName ? wordFloorValues(word, table) : [];
14
- return { word, raw: word.raw, folded: word.folded, expands: refersToName ? "unresolvable" : expandsOf(word), ...(floors.length > 0 ? { floorValues: floors } : {}) };
15
+ return { ...asWritten, expands: refersToName ? "unresolvable" : expandsOf(word), ...(floors.length > 0 ? { floorValues: floors } : {}) };
15
16
  }
16
17
  const glob = values.some((v) => v.glob === true) || globInWord(word);
17
18
  const refersToHome = word.pieces.some((p) => p.expansion?.kind === "tilde" || (p.expansion?.kind === "parameter" && p.expansion.name === "HOME"));
18
19
  const declared = refersToHome && homeDir !== undefined && table?.homeTouched === true ? wordValues(word, table, false, new Map([["HOME", [{ spelled: homeDir }]]])) : undefined;
19
- const floor = declared !== undefined && declared !== "unknown" ? declared[0]?.spelled : undefined;
20
+ const floor = declared !== undefined && declared !== "unknown" && declared[0] !== undefined ? spellingOf(declared[0]) : undefined;
21
+ const pattern = judgedSpellingOf(values[0]);
20
22
  return {
21
23
  word,
22
24
  raw: word.raw,
23
- folded: values[0].spelled,
25
+ folded: spellingOf(values[0]),
26
+ ...(pattern !== undefined ? { pattern } : {}),
24
27
  expands: glob ? "glob" : false,
25
28
  settled: true,
26
- ...(values.length > 1 ? { values: values.map((v) => v.spelled) } : {}),
27
- ...(floor !== undefined && !values.every((v) => v.spelled === floor) ? { homeFloor: floor } : {}),
29
+ values,
30
+ ...(floor !== undefined && !values.every((v) => spellingOf(v) === floor) ? { homeFloor: floor } : {}),
28
31
  };
29
32
  });
30
33
  }
34
+ const READINGS_MAX = 64;
35
+ export function segmentReadingsOf(argv, targets, table, homeDir) {
36
+ const several = new Map();
37
+ for (const word of [...argv, ...targets]) {
38
+ for (const name of namesReadIn(word, table)) {
39
+ const state = table.stateOf(name);
40
+ if (state !== "unknown" && state.length > 1)
41
+ several.set(name, state);
42
+ }
43
+ }
44
+ let overlays = [new Map()];
45
+ const past = [];
46
+ for (const [name, values] of several) {
47
+ if (past.length > 0 || overlays.length * values.length > READINGS_MAX) {
48
+ past.push(name);
49
+ continue;
50
+ }
51
+ overlays = overlays.flatMap((o) => values.map((v) => new Map(o).set(name, [v])));
52
+ }
53
+ for (const o of overlays)
54
+ for (const name of past)
55
+ o.set(name, "unknown");
56
+ return overlays.map((overlay) => {
57
+ const reading = overlay.size === 0 ? table : overlaidTable(table, overlay);
58
+ const fielded = readsOf(argv, reading, homeDir, true).flatMap((r) => {
59
+ const fields = r.values?.length === 1 ? r.values[0].fields : undefined;
60
+ return fields === undefined ? [r] : fields.map((f) => ({ word: argumentWordOf(f, f) ?? r.word, raw: f, folded: spellingOf({ spelled: f }), expands: false, settled: true, values: [{ spelled: f }] }));
61
+ });
62
+ const floors = new Map();
63
+ for (const r of fielded) {
64
+ if (r.settled === true)
65
+ continue;
66
+ for (const p of r.word.pieces) {
67
+ const name = p.expansion?.kind === "parameter" ? p.expansion.name : undefined;
68
+ if (name === undefined)
69
+ continue;
70
+ const held = reading.stateOf(name);
71
+ const floor = [...(held === "unknown" ? [] : held.map(floorOf)), ...reading.floorOf(name)];
72
+ if (floor.length > 0)
73
+ floors.set(name, floor);
74
+ }
75
+ }
76
+ return { table: reading, reads: fielded, argv: fielded.map((r) => valuedWord(r.word, r)), floors };
77
+ });
78
+ }
79
+ function namesReadIn(word, table) {
80
+ return [...namesIn(word), ...(table.homeTouched && word.pieces.some((p) => p.expansion?.kind === "tilde" && p.expansion.body === "~") ? ["HOME"] : [])];
81
+ }
82
+ function overlaidTable(table, overlay) {
83
+ return {
84
+ stateOf: (name) => overlay.get(name) ?? table.stateOf(name),
85
+ isInteger: (name) => table.isInteger(name),
86
+ ifsTouched: table.ifsTouched,
87
+ homeTouched: table.homeTouched,
88
+ floorOf: (name) => {
89
+ const held = table.stateOf(name);
90
+ return overlay.get(name) !== "unknown" || held === "unknown" ? table.floorOf(name) : [...table.floorOf(name), ...held.map(floorOf)];
91
+ },
92
+ inherited: table.inherited,
93
+ };
94
+ }
95
+ function valuedWord(word, r) {
96
+ const v = r.values?.length === 1 ? r.values[0] : undefined;
97
+ if (word.assignment !== undefined || v === undefined || v.glob === true || v.spelled === word.folded)
98
+ return word;
99
+ return argumentWordOf(v.spelled, r.raw) ?? word;
100
+ }
101
+ function argumentWordOf(text, raw) {
102
+ const quoted = text.replace(/[^A-Za-z0-9_./+@:%=,-]/g, (c) => (c === "'" ? "\\'" : `'${c}'`));
103
+ const lexed = lexShell(`: ${quoted === "" ? "''" : quoted}`);
104
+ const words = "parseFailure" in lexed || lexed.segments.length !== 1 ? undefined : argvOf(lexed.segments[0]);
105
+ return words?.length === 2 ? { ...words[1], raw } : undefined;
106
+ }
107
+ export const spellingOf = (v) => literalTildeSpelling(v.spelled);
108
+ export function partPattern(word, part) {
109
+ if (word.pattern === undefined || part === word.folded || !word.folded.endsWith(part))
110
+ return undefined;
111
+ const front = word.folded.slice(0, word.folded.length - part.length);
112
+ if (!word.pattern.startsWith(front))
113
+ return undefined;
114
+ const judged = word.pattern.slice(front.length);
115
+ return judged === part ? undefined : judged;
116
+ }
117
+ export function renamed(c, spelled) {
118
+ const { pattern: _matched, ...rest } = c;
119
+ return { ...rest, spelled };
120
+ }
121
+ export const programTextOf = (r) => r.values?.[0]?.spelled ?? r.word.folded;
122
+ export function settledFieldsOf(r) {
123
+ return r.settled === true ? r.values : undefined;
124
+ }
31
125
  export const rewritten = (r) => r.settled !== true && expansionInWord(r.word);
32
126
  export function readOfText(text) {
33
127
  const l = lexShell(text);
@@ -1,4 +1,5 @@
1
1
  import { denyFloorShadowsOf } from "./shell-read-judge.js";
2
+ import { renamed } from "./shell-read-word.js";
2
3
  import { implementationsOf, programRow, selectedOptions, writeReadings } from "./program-read-model.js";
3
4
  import { programBasename, readProgramPosition } from "./bash-program-position.js";
4
5
  export function programArgs(segment) {
@@ -85,8 +86,8 @@ function backupTargetsOf(edited, readings, override) {
85
86
  const suffixes = [...new Set(readings.flatMap((r) => r.suffixes))];
86
87
  const follows = override !== undefined && !override.includes("entry");
87
88
  return edited.flatMap(({ candidate: f, onlyIfExists }) => suffixes.flatMap((suffix) => follows
88
- ? [{ candidate: { ...f, spelled: `${f.spelled}${suffix}`, face: "write" }, seats: ["entry"], followed: { operand: f, suffix }, ...(onlyIfExists !== undefined ? { onlyIfExists } : {}) }]
89
- : sedBackupNames(f.spelled, suffix).map((spelled) => ({ candidate: { ...f, spelled, face: "write" }, seats: ["entry"], ...(onlyIfExists !== undefined ? { onlyIfExists } : {}) }))));
89
+ ? [{ candidate: { ...renamed(f, `${f.spelled}${suffix}`), face: "write" }, seats: ["entry"], followed: { operand: f, suffix }, ...(onlyIfExists !== undefined ? { onlyIfExists } : {}) }]
90
+ : sedBackupNames(f.spelled, suffix).map((spelled) => ({ candidate: { ...renamed(f, spelled), face: "write" }, seats: ["entry"], ...(onlyIfExists !== undefined ? { onlyIfExists } : {}) }))));
90
91
  }
91
92
  function copyDestinationChildren(segment, words, readings) {
92
93
  const candidateOf = (raw) => segment.reads.find((c) => c.raw === raw && WORD_PROVENANCES.has(c.provenance));
@@ -107,7 +108,7 @@ function copyDestinationChildren(segment, words, readings) {
107
108
  const child = `${dest.spelled.replace(/\/+$/, "")}/${src.spelled.replace(/\/+$/, "").split("/").pop() ?? ""}`;
108
109
  if (child.endsWith("/") || children.has(child))
109
110
  continue;
110
- children.set(child, destCandidate !== undefined && source !== undefined ? { ...destCandidate, spelled: child } : { spelled: child, raw: src.raw, provenance: "unlisted_word", expands: "unresolvable", bases: [] });
111
+ children.set(child, destCandidate !== undefined && source !== undefined ? renamed(destCandidate, child) : { spelled: child, raw: src.raw, provenance: "unlisted_word", expands: "unresolvable", bases: [] });
111
112
  }
112
113
  }
113
114
  return [...children.values()];
@@ -5,6 +5,9 @@
5
5
  * re-entry (`shell-read-plan.ts`); this module is the one reader of which operand — or which input — that text is.
6
6
  */
7
7
  import { type LexedSegment, type LexedWord } from "../../shell/lex.js";
8
+ import { type ProgramPosition } from "./bash-program-position.js";
9
+ import { type TargetUser } from "./program-read-model.js";
10
+ import type { BoundValue } from "./shell-read-bindings.js";
8
11
  /**
9
12
  * The code bodies a builtin keeps among its operands, one per body; empty for every other program. The program read
10
13
  * model's row says where the body is: the action of a `trap` (its first operand after the options), the value of an
@@ -24,6 +27,102 @@ export declare function codeBodies(basename: string, args: readonly string[]): r
24
27
  * the argv names no text and the program runs its standard input ({@link scriptSourceOf}: the row runs it unnamed, or
25
28
  * the script file named is standard input), the text is `stdin` — one text that may arrive there, as the caller spells it. */
26
29
  export declare function reentryText(basename: string, args: readonly string[], stdin?: string): string | undefined;
30
+ /** One argument of the argv a launcher hands the SHELL it runs: a word of the command line passed on as it stands (`at`,
31
+ * its argv index; `from` — only the word's characters from there on, an option value attached to its option word), a word
32
+ * of the launcher's own (`fixed`: the `-c` in front of a text it builds), or a text it builds from several words
33
+ * (`joined`: their argv indices, joined by a space; `escaped` — every character of a word's value but a letter, a digit,
34
+ * `_`, `-` and `$` escaped with a backslash, as `sudo -s` builds its shell's `-c` text). */
35
+ export type LaunchedArg = {
36
+ readonly at: number;
37
+ readonly from?: number;
38
+ } | {
39
+ readonly fixed: string;
40
+ } | {
41
+ readonly joined: readonly number[];
42
+ readonly escaped?: true;
43
+ };
44
+ /** What a launcher that runs a program named by no word of its own runs: `program` — absent, a SHELL named by no word
45
+ * (sudo's, chroot's, the user's login shell `su` runs); `at` — the word naming it (`su -s PROGRAM`, from its `from`-th
46
+ * character where the value is attached to its option word); `spelled` — a program the launcher takes from elsewhere,
47
+ * as a shell spells it (`su -m`: the caller's `"$SHELL"`) — and the argv it hands that program. */
48
+ export interface LaunchedCommand {
49
+ readonly program?: {
50
+ readonly at: number;
51
+ readonly from?: number;
52
+ } | {
53
+ readonly spelled: string;
54
+ };
55
+ readonly args: readonly LaunchedArg[];
56
+ /** The user the launcher runs it as, where its row says it runs another ({@link targetUserOf}). */
57
+ readonly as?: TargetUser;
58
+ }
59
+ /**
60
+ * What a launcher that runs a program named by no word of its own runs ({@link readProgramPosition}'s `runsShell`: the
61
+ * launcher, and the first word it hands on), one command per reading of the launcher's row where its implementations
62
+ * read it differently; empty when no launcher of the segment runs one. A shell's argv is read by that shell's row
63
+ * ({@link reentryText}, {@link scriptFileAt}): its `-c` text, its script file, else its standard input. By the launcher
64
+ * row (`LauncherSpec`, program-read-model.ts):
65
+ * · no word handed on (`sudo -s`, `chroot DIR`, a bare `su`): the shell is handed nothing;
66
+ * · `shellCommand: "escaped"` (`sudo -s CMD…`): `-c` and the words, escaped and joined;
67
+ * · `shellText: "joined"` (`watch CMD…`): `-c` and the non-option words, joined — and `-c` with each shell-text value
68
+ * attached to its option word (`script -c'CMD'`);
69
+ * · `shellText: "user"` (`su [-] [USER [ARG…]]`): per implementation — its first operand past a lone `-` names the user;
70
+ * one that reads its options anywhere hands `-c TEXT` for each shell-text value of its own (a command each) and its
71
+ * operands after the user, one that stops at the user hands every word after the user. The program is the value of
72
+ * its option of role `command` (`-s PROGRAM`), else the program a selected `names-program` switch takes from the
73
+ * caller's environment (`-m`: `"$SHELL"`), else the user's login shell.
74
+ */
75
+ export declare function launchedCommands(argv: readonly LexedWord[], pos?: ProgramPosition): readonly LaunchedCommand[];
76
+ /** How a launched command is read: as a SHELL's argv (`shell`: the shell whose row reads it — a program that runs its
77
+ * `-c` text, `sh`, `bash`, `fish`, …, or `sh` for the shell named by no word), as the command line it is (`line`: a
78
+ * program that is no shell, `su -s /bin/rm …`, its words as written, so its expansions are the command line's), or
79
+ * both — a program the text does not spell (`su -s "$X"`, `su -m`: the caller's `"$SHELL"`) may be any program, a shell
80
+ * among them. */
81
+ export declare function launchedReadings(argv: readonly LexedWord[], command: LaunchedCommand): {
82
+ readonly shell?: string;
83
+ readonly line?: string;
84
+ };
85
+ /** What a launcher runs ({@link launchedCommands}, {@link launchedReadings}), as a reader of the command line reads it in
86
+ * one reading of its names: each shell's argv in two spellings — `texts` (a word's literal value, else `spell.text`) and
87
+ * `args` (`spell.folded`) — with the argv index each position stands for (`words`), its words' fields read off
88
+ * `spell.values` ({@link spellLaunched}) and the user it runs as (`as`); the word naming a shell (`su -s /bin/bash`,
89
+ * `programWords`); and each command line a program that is no shell runs (`lines`, once each). */
90
+ export declare function launchedInput(argv: readonly LexedWord[], pos: ProgramPosition, spell: {
91
+ readonly text: (k: number) => string;
92
+ readonly folded: (k: number) => string;
93
+ readonly values: (k: number) => readonly BoundValue[] | undefined;
94
+ }): {
95
+ readonly shells: readonly {
96
+ readonly shell: string;
97
+ readonly texts: readonly string[];
98
+ readonly args: readonly string[];
99
+ readonly words: readonly (number | undefined)[];
100
+ readonly as?: TargetUser;
101
+ }[];
102
+ readonly programWords: readonly number[];
103
+ readonly lines: readonly string[];
104
+ };
105
+ /** How a reader spells the words of a command line it hands a launched shell ({@link spellLaunched}): `word` — a word as
106
+ * it stands, from its `from`-th character; `values` — for a word the shell rewrites before the launcher sees it, the
107
+ * values it may take where the reader knows them (each one field, or a pattern whose matches are the fields — its parts
108
+ * say which characters the shell expands), `undefined` where it does not; absent — the reader knows none. */
109
+ export interface LaunchedSpelling {
110
+ readonly word: (at: number, from: number) => string;
111
+ readonly values?: (at: number) => readonly BoundValue[] | undefined;
112
+ }
113
+ /**
114
+ * The argv a launched shell is handed ({@link launchedCommands}), spelled as `spell` spells the command line's words in ONE
115
+ * reading of their names (the plan reads a segment once per combination of its names' values, `segmentReadingsOf` in
116
+ * shell-read-word.ts — a word's value here is its value in that reading). A word an escaping launcher joins (`sudo -s
117
+ * CMD…`) is handed as the launcher hands it: the FIELD the command line's shell gives the launcher, escaped
118
+ * ({@link sudoEscaped}) — the field a literal word spells, or the field the shell established for a word it rewrites first
119
+ * (`X='#'; sudo -s rm -r "$X" d` hands `rm -r \# d`: the value is an operand, never a comment), a pattern's literal
120
+ * characters escaped and its pattern characters bare ({@link launchedSpellingOf}). A word whose field the reader does not
121
+ * know is handed AS WRITTEN: the launched shell re-reads the expansion with the command line's quoting, so the word stays
122
+ * one argument (an unknown there) and its neighbours stay theirs (`"#$Y" d` is never `#$Y d`). Any other launcher hands a
123
+ * word's text as `spell.word` spells it. One argv, as a list of the one.
124
+ */
125
+ export declare function spellLaunched(argv: readonly LexedWord[], args: readonly LaunchedArg[], spell: LaunchedSpelling): string[][];
27
126
  /** The argv index of the SCRIPT FILE a shell runs where its argv carries no text ({@link scriptSourceOf}). */
28
127
  export declare function scriptFileAt(basename: string, args: readonly string[]): number | undefined;
29
128
  /** One text that may arrive on a segment's standard input: `from` = its source as written (a here-document's body,
@@ -34,9 +133,10 @@ export interface StdinText {
34
133
  }
35
134
  /** What may arrive on a segment's standard input: each here-document (its descriptor is not read: every one counts) —
36
135
  * its body, literal when quoted or when nothing in it expands — each here-string (literal when its word is), `<` file
37
- * or `<&` descriptor written to no other descriptor, and the pipe it is reached through (`piped`) unless one of those
38
- * three replaces it. */
39
- export declare function stdinTextsOf(segment: LexedSegment, piped: StdinText | undefined): StdinText[];
136
+ * or `<&` descriptor written to no other descriptor, and what it inherits (`fed`: the pipe it is reached through, the
137
+ * pipe or the input of the group it stands in, the input of the shell running its text) unless one of those three
138
+ * replaces it. */
139
+ export declare function stdinTextsOf(segment: LexedSegment, fed: readonly StdinText[]): StdinText[];
40
140
  /** What one segment (its argv) writes into the pipe after it: the text a printer spells ({@link printedText}) when its
41
141
  * program word and every argument are literal, else only the segment as written. */
42
142
  export declare function pipedTextOf(segment: LexedSegment, argv: readonly LexedWord[]): StdinText;
@@ -1,6 +1,6 @@
1
1
  import { literalHeredocBody, literalTextOf } from "../../shell/lex.js";
2
- import { readProgramPosition } from "./bash-program-position.js";
3
- import { printedText, programRow, readArgRoles, selectedOptions } from "./program-read-model.js";
2
+ import { programBasename, readProgramPosition } from "./bash-program-position.js";
3
+ import { implementationRejects, implementationsOf, printedText, programRow, readArgRoles, selectedOptions, targetUserOf } from "./program-read-model.js";
4
4
  import { namesStandardInput } from "./safety.js";
5
5
  export function codeBodies(basename, args) {
6
6
  const row = programRow(basename);
@@ -70,6 +70,124 @@ function argvText(row, at, args) {
70
70
  }
71
71
  return undefined;
72
72
  }
73
+ export function launchedCommands(argv, pos = readProgramPosition(argv)) {
74
+ const runs = pos.refusal?.kind === "control_syntax_at_program_position" ? pos.refusal.runsShell : undefined;
75
+ if (runs === undefined)
76
+ return [];
77
+ const row = programRow(programBasename(argv[runs.launcher].folded));
78
+ const words = argv.slice(runs.from).map((w) => w.folded);
79
+ const at = (k) => runs.from + k;
80
+ if (row?.launcher?.shellText === "user")
81
+ return userCommands(row, words, at);
82
+ const user = row === undefined ? undefined : targetUserOf(row, argv.slice(runs.launcher + 1, runs.from).map((w) => w.folded));
83
+ const as = user !== undefined ? { as: user } : {};
84
+ if (words.length === 0 || row?.launcher === undefined)
85
+ return [{ args: [], ...as }];
86
+ if (row.launcher.shellText === "joined") {
87
+ const texts = new Map();
88
+ for (const view of implementationsOf(row)) {
89
+ const events = readArgRoles(view, words);
90
+ const first = events.find((e) => e.kind === "operand" || e.kind === "dash-word-operand")?.at ?? words.length;
91
+ if (implementationRejects(row, view, words.slice(0, first)))
92
+ continue;
93
+ const joined = words.slice(first).map((_, k) => at(first + k));
94
+ const values = events.flatMap((e) => (e.kind === "option" || (e.kind === "value" && e.at < first) ? roleValue(e, "shell-text", words, at) ?? [] : []));
95
+ for (const t of [...(joined.length === 0 ? [] : [{ joined }]), ...values])
96
+ texts.set(JSON.stringify(t), t);
97
+ }
98
+ return texts.size === 0 ? [{ args: [], ...as }] : [...texts.values()].map((t) => ({ args: [{ fixed: "-c" }, t], ...as }));
99
+ }
100
+ return [{ args: [{ fixed: "-c" }, { joined: words.map((_, k) => at(k)), escaped: true }], ...as }];
101
+ }
102
+ function roleValue(e, kind, words, at) {
103
+ if (e.kind === "value" && e.role?.kind === kind)
104
+ return { at: at(e.at) };
105
+ if (e.kind === "option" && typeof e.reading === "object" && e.reading.role?.kind === kind)
106
+ return { at: at(e.at), from: words[e.at].length - e.reading.attached.length };
107
+ return undefined;
108
+ }
109
+ function userCommands(row, words, at) {
110
+ const out = [];
111
+ const valued = (e, kind) => roleValue(e, kind, words, at);
112
+ for (const view of implementationsOf(row)) {
113
+ const events = readArgRoles(view, words);
114
+ const operands = events.filter((e) => e.kind === "operand");
115
+ const user = (operands[0]?.dash !== undefined ? operands[1] : operands[0])?.at;
116
+ const end = user ?? words.length;
117
+ const stops = view.grammars[0]?.afterFirstOperand === "operands";
118
+ const own = stops ? events.filter((e) => e.at < end) : events;
119
+ const named = own.flatMap((e) => valued(e, "command") ?? []).at(-1);
120
+ const fromEnvironment = selectedOptions(view, stops ? words.slice(0, end) : words, "one-implementation").some((o) => o.mode === "names-program");
121
+ const program = named !== undefined ? { program: named } : fromEnvironment ? { program: { spelled: "\"$SHELL\"" } } : {};
122
+ const target = targetUserOf(view, stops ? words.slice(0, end) : words.filter((_, k) => k !== user), { name: user === undefined ? "root" : words[user], dash: operands[0]?.dash !== undefined });
123
+ const runs = { ...program, ...(target !== undefined ? { as: target } : {}) };
124
+ if (stops) {
125
+ out.push({ ...runs, args: user === undefined ? [] : words.slice(user + 1).map((_, k) => ({ at: at(user + 1 + k) })) });
126
+ continue;
127
+ }
128
+ const rest = operands.filter((e) => user !== undefined && e.at > user).map((e) => ({ at: at(e.at) }));
129
+ const texts = own.flatMap((e) => valued(e, "shell-text") ?? []);
130
+ if (texts.length === 0)
131
+ out.push({ ...runs, args: rest });
132
+ for (const text of texts)
133
+ out.push({ ...runs, args: [{ fixed: "-c" }, text, ...rest] });
134
+ }
135
+ return out;
136
+ }
137
+ export function launchedReadings(argv, command) {
138
+ const p = command.program;
139
+ if (p === undefined)
140
+ return { shell: "sh" };
141
+ const raw = (k, from = 0) => {
142
+ if (from === 0)
143
+ return argv[k].raw;
144
+ const literal = literalTextOf(argv[k].pieces);
145
+ return literal === undefined ? argv[k].folded.slice(from) : `'${literal.slice(from).replace(/'/g, "'\\''")}'`;
146
+ };
147
+ const line = [("at" in p ? raw(p.at, p.from) : p.spelled), ...command.args.map((a) => ("fixed" in a ? a.fixed : "at" in a ? raw(a.at, a.from) : a.joined.map((k) => raw(k)).join(" ")))].join(" ");
148
+ const literal = "at" in p ? literalTextOf(argv[p.at].pieces)?.slice(p.from ?? 0) : undefined;
149
+ if (literal === undefined)
150
+ return { shell: "sh", line };
151
+ const basename = programBasename(literal);
152
+ const reentry = programRow(basename)?.reentry;
153
+ return reentry?.kind === "shell" && reentry.text === "c-option" ? { shell: basename } : { line };
154
+ }
155
+ export function launchedInput(argv, pos, spell) {
156
+ const spelled = (word) => (args) => spellLaunched(argv, args, { word: (k, from) => word(k).slice(from), values: spell.values });
157
+ const asTexts = spelled(spell.text);
158
+ const asArgs = spelled(spell.folded);
159
+ const lines = new Set();
160
+ const programWords = [];
161
+ const shells = launchedCommands(argv, pos).flatMap((c) => {
162
+ const { shell, line } = launchedReadings(argv, c);
163
+ if (line !== undefined)
164
+ lines.add(line);
165
+ if (shell === undefined)
166
+ return [];
167
+ if (line === undefined && c.program !== undefined && "at" in c.program && c.program.from === undefined)
168
+ programWords.push(c.program.at);
169
+ const words = c.args.map((x) => ("at" in x && x.from === undefined ? x.at : undefined));
170
+ const args = asArgs(c.args);
171
+ return asTexts(c.args).map((texts, i) => ({ shell, texts, args: args[i], words, ...(c.as !== undefined ? { as: c.as } : {}) }));
172
+ });
173
+ return { shells, programWords, lines: [...lines] };
174
+ }
175
+ function sudoEscaped(field) {
176
+ return field.replace(/[^A-Za-z0-9_$-]/gu, (c) => `\\${c}`);
177
+ }
178
+ function launchedSpellingOf(v) {
179
+ return v.glob === true ? v.parts.map((p) => (p.pattern ? p.text : sudoEscaped(p.text))).join("") : sudoEscaped(v.spelled);
180
+ }
181
+ export function spellLaunched(argv, args, spell) {
182
+ const field = (k) => {
183
+ const literal = literalTextOf(argv[k].pieces);
184
+ if (literal !== undefined)
185
+ return sudoEscaped(literal);
186
+ const values = spell.values?.(k);
187
+ return values?.length === 1 ? launchedSpellingOf(values[0]) : argv[k].raw;
188
+ };
189
+ return [args.map((a) => ("fixed" in a ? a.fixed : "at" in a ? spell.word(a.at, a.from ?? 0) : a.joined.map((k) => (a.escaped === true ? field(k) : spell.word(k, 0))).join(" ")))];
190
+ }
73
191
  export function scriptFileAt(basename, args) {
74
192
  const row = programRow(basename);
75
193
  return row?.reentry?.kind === "shell" && argvText(row, row.reentry.text, args) === undefined ? scriptSourceOf(row, args).file : undefined;
@@ -85,7 +203,7 @@ function scriptSourceOf(row, args) {
85
203
  return { stdin: true };
86
204
  return { file: first.at, stdin: options.some((e) => e.kind === "option" && e.reading === "speculative") };
87
205
  }
88
- export function stdinTextsOf(segment, piped) {
206
+ export function stdinTextsOf(segment, fed) {
89
207
  const out = [];
90
208
  let replacesPipe = false;
91
209
  for (const t of segment.tokens) {
@@ -99,7 +217,7 @@ export function stdinTextsOf(segment, piped) {
99
217
  replacesPipe = true;
100
218
  }
101
219
  }
102
- return piped === undefined || replacesPipe ? out : [piped, ...out];
220
+ return replacesPipe ? out : [...fed, ...out];
103
221
  }
104
222
  export function pipedTextOf(segment, argv) {
105
223
  const from = `${segment.text.trim()} |`;
@@ -28,9 +28,9 @@
28
28
  * The handle is a first-class m* registry task: TaskOutput serves the re-readable spool, TaskStop stops the
29
29
  * watch (design/134 stoppedBy attribution applies automatically).
30
30
  *
31
- * Security posture: `command` is an arbitrary shell script ⇒ effect:"write", same as Bash, and prepare-task
32
- * folds Monitor into the SAME shellGate tighten (irreversibility tier + reversibility probe on the same
33
- * `command` arg) — Monitor is never a policy side-door around a gated Bash.
31
+ * Security posture: `command` is an arbitrary shell script ⇒ effect:"write", same as Bash; the catalog puts Monitor on the
32
+ * SHELL FACE, so the `Bash(<command>)` rules, the shell seat, the read station and the removal guard judge its `command`
33
+ * as they judge Bash's — Monitor is never a policy side-door around a gated Bash.
34
34
  *
35
35
  * 深对比残差 M17 CLOSED (2026-07-09 live-CC probe, docs/cc-probe-198/live-schema-monitor-worktree-2026-07-09.json):
36
36
  * the deep-dive bundle showed a `command? + ws?:{url,protocols}` dual-mode schema; the LIVE current-CC schema is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.46.0",
3
+ "version": "7.48.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",