@sema-agent/core 7.44.0 → 7.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/CHANGELOG.md +418 -1
  2. package/dist/agents/extra-tool-names.d.ts +65 -0
  3. package/dist/agents/extra-tool-names.js +124 -0
  4. package/dist/agents/subagent.d.ts +17 -5
  5. package/dist/agents/subagent.js +65 -38
  6. package/dist/agents/tool-filter.d.ts +82 -5
  7. package/dist/agents/tool-filter.js +134 -4
  8. package/dist/core/agent-config-dir.d.ts +20 -0
  9. package/dist/core/agent-config-dir.js +49 -0
  10. package/dist/core/agent-definition.d.ts +2 -2
  11. package/dist/core/allow-entry.d.ts +30 -4
  12. package/dist/core/allow-entry.js +32 -5
  13. package/dist/core/ask-origin.d.ts +23 -4
  14. package/dist/core/ask-origin.js +11 -5
  15. package/dist/core/checkpoint-store.d.ts +41 -4
  16. package/dist/core/decision-receipt.js +1 -1
  17. package/dist/core/effective-path-target.d.ts +143 -10
  18. package/dist/core/effective-path-target.js +113 -9
  19. package/dist/core/engine-notice.d.ts +25 -3
  20. package/dist/core/engine-notice.js +12 -6
  21. package/dist/core/gate-fold.js +3 -0
  22. package/dist/core/gate-lanes.js +11 -3
  23. package/dist/core/gate-outcome.d.ts +5 -0
  24. package/dist/core/gate-outcome.js +2 -0
  25. package/dist/core/governance-codes.d.ts +3 -1
  26. package/dist/core/governance-codes.js +3 -0
  27. package/dist/core/hooks.d.ts +5 -0
  28. package/dist/core/memory-engine/content-origin.d.ts +0 -34
  29. package/dist/core/memory-engine/content-origin.js +9 -4
  30. package/dist/core/memory-engine/layout.d.ts +5 -1
  31. package/dist/core/memory-engine/layout.js +1 -0
  32. package/dist/core/path-rule-binding.d.ts +65 -0
  33. package/dist/core/path-rule-binding.js +99 -0
  34. package/dist/core/permission-rule-model.d.ts +47 -18
  35. package/dist/core/permission-rule-model.js +38 -42
  36. package/dist/core/permission-rule-provider.d.ts +7 -0
  37. package/dist/core/permission-rule-provider.js +10 -3
  38. package/dist/core/permission-rule-store.d.ts +8 -0
  39. package/dist/core/permission-rule-store.js +6 -2
  40. package/dist/core/permission-rule-sync.js +2 -2
  41. package/dist/core/permission-rules.d.ts +22 -36
  42. package/dist/core/permission-rules.js +50 -40
  43. package/dist/core/real-approval.d.ts +9 -6
  44. package/dist/core/real-approval.js +5 -5
  45. package/dist/core/runner/child-tree.d.ts +110 -0
  46. package/dist/core/runner/child-tree.js +107 -0
  47. package/dist/core/runner/construction-doors.d.ts +5 -0
  48. package/dist/core/runner/construction-doors.js +6 -0
  49. package/dist/core/runner/contracts.d.ts +54 -2
  50. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  51. package/dist/core/runner/permission-rule-lanes.js +18 -12
  52. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  53. package/dist/core/runner/persisted-path-rows.js +31 -12
  54. package/dist/core/runner/prepare-ask-lane.d.ts +6 -0
  55. package/dist/core/runner/prepare-ask-lane.js +4 -2
  56. package/dist/core/runner/prepare-config-doors.js +3 -1
  57. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  58. package/dist/core/runner/prepare-execution-env.js +11 -3
  59. package/dist/core/runner/prepare-gate-stations.d.ts +6 -0
  60. package/dist/core/runner/prepare-gate-stations.js +3 -1
  61. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  62. package/dist/core/runner/prepare-hands-readface.js +6 -6
  63. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  64. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  65. package/dist/core/runner/prepare-listings.d.ts +4 -4
  66. package/dist/core/runner/prepare-listings.js +2 -22
  67. package/dist/core/runner/prepare-park-ask.d.ts +5 -0
  68. package/dist/core/runner/prepare-park-ask.js +6 -4
  69. package/dist/core/runner/prepare-policy-chain.d.ts +16 -2
  70. package/dist/core/runner/prepare-policy-chain.js +14 -5
  71. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  72. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  73. package/dist/core/runner/prepare-task.js +41 -18
  74. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  75. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  76. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  77. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  78. package/dist/core/runner/resume-apply.js +17 -3
  79. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  80. package/dist/core/runner/resume-policy-outcome.js +2 -1
  81. package/dist/core/runner/run-leg.js +5 -1
  82. package/dist/core/runner/run-notification-lane.js +2 -2
  83. package/dist/core/runner/runtask.d.ts +2 -0
  84. package/dist/core/runner/runtask.js +4 -2
  85. package/dist/core/runner/stream-steer-verb.js +2 -2
  86. package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
  87. package/dist/core/runner/sub-agent-deny-lists.js +121 -0
  88. package/dist/core/runner/synthetic-tools.d.ts +1 -1
  89. package/dist/core/runner/synthetic-tools.js +3 -2
  90. package/dist/core/runner/write-protection-station.d.ts +3 -2
  91. package/dist/core/runner/write-protection-station.js +1 -0
  92. package/dist/core/runner-deps.d.ts +14 -0
  93. package/dist/core/sensitive-path-policy.d.ts +8 -0
  94. package/dist/core/sensitive-path-policy.js +18 -2
  95. package/dist/core/session-policy-store.d.ts +6 -2
  96. package/dist/core/session-policy-store.js +2 -1
  97. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  98. package/dist/core/shell-wrapper-projection.js +56 -0
  99. package/dist/core/shell-wrapper-table.d.ts +144 -34
  100. package/dist/core/shell-wrapper-table.js +40 -20
  101. package/dist/core/skill-grant.d.ts +10 -2
  102. package/dist/core/skill-grant.js +20 -4
  103. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  104. package/dist/core/synthetic-read-verdict.js +3 -1
  105. package/dist/core/task-notification.d.ts +12 -1
  106. package/dist/core/task-notification.js +13 -1
  107. package/dist/core/task-spec.d.ts +5 -3
  108. package/dist/core/tool-catalog-entries.d.ts +21 -0
  109. package/dist/core/tool-catalog-entries.js +9 -0
  110. package/dist/core/tool-policy.d.ts +45 -12
  111. package/dist/core/tool-policy.js +18 -20
  112. package/dist/core/tool-spec.d.ts +23 -4
  113. package/dist/core/types.d.ts +1 -1
  114. package/dist/core/types.js +1 -1
  115. package/dist/core/write-protect.d.ts +4 -0
  116. package/dist/core/write-protect.js +30 -9
  117. package/dist/index.d.ts +10 -6
  118. package/dist/index.js +9 -5
  119. package/dist/orchestration/run-spec.js +8 -7
  120. package/dist/orchestration/workflow.js +3 -2
  121. package/dist/shell/lex.d.ts +7 -0
  122. package/dist/shell/lex.js +17 -0
  123. package/dist/tools/fs/bash-program-position.d.ts +8 -87
  124. package/dist/tools/fs/bash-program-position.js +33 -69
  125. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  126. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  127. package/dist/tools/fs/index.d.ts +3 -2
  128. package/dist/tools/fs/program-read-model.d.ts +694 -0
  129. package/dist/tools/fs/program-read-model.js +1252 -0
  130. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  131. package/dist/tools/fs/program-read-model.rows.js +613 -0
  132. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  133. package/dist/tools/fs/read-deny-home.js +62 -24
  134. package/dist/tools/fs/read-deny.d.ts +54 -14
  135. package/dist/tools/fs/read-deny.js +104 -50
  136. package/dist/tools/fs/removal-targets.d.ts +83 -0
  137. package/dist/tools/fs/removal-targets.js +236 -0
  138. package/dist/tools/fs/safety.d.ts +3 -0
  139. package/dist/tools/fs/safety.js +10 -3
  140. package/dist/tools/fs/shell-read-argv.d.ts +85 -0
  141. package/dist/tools/fs/shell-read-argv.js +386 -0
  142. package/dist/tools/fs/shell-read-bindings.d.ts +273 -0
  143. package/dist/tools/fs/shell-read-bindings.js +746 -0
  144. package/dist/tools/fs/shell-read-check.js +23 -15
  145. package/dist/tools/fs/shell-read-establish.js +79 -5
  146. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  147. package/dist/tools/fs/shell-read-judge.js +9 -1
  148. package/dist/tools/fs/shell-read-plan.d.ts +31 -7
  149. package/dist/tools/fs/shell-read-plan.js +299 -393
  150. package/dist/tools/fs/shell-read-word.d.ts +29 -26
  151. package/dist/tools/fs/shell-read-word.js +30 -38
  152. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -37
  153. package/dist/tools/fs/shell-read-write-targets.js +83 -136
  154. package/dist/tools/fs/shell-reentry-text.d.ts +38 -6
  155. package/dist/tools/fs/shell-reentry-text.js +90 -53
  156. package/package.json +1 -1
  157. package/test/export-surface.snapshot.json +49 -3
  158. package/wire-changes/core@7.45.0.json +1084 -0
  159. package/wire-changes/core@7.46.0.json +1522 -0
@@ -5,7 +5,8 @@ import { combinePolicies } from "../core/tool-policy.js";
5
5
  import { delimitUntrusted } from "../core/untrusted-text.js";
6
6
  import { isDurablePause } from "../agents/suspend-guard.js";
7
7
  import { terminalProjection } from "../core/runner/terminal-projection.js";
8
- import { isAbsoluteForFamily, pathFamilyOf, writeTargetPath } from "../tools/fs/safety.js";
8
+ import { writeTargetPath } from "../tools/fs/safety.js";
9
+ import { callTargetViewsOf } from "../core/effective-path-target.js";
9
10
  import { captureBaseline, resolveFrozenPaths, restoreFrozenPaths, runOracle, snapshotFrozenPaths, specError, } from "../core/spec-contract.js";
10
11
  import { isProtectedWrite } from "../core/tool-registry.js";
11
12
  const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32";
@@ -21,13 +22,13 @@ export function frozenDenyPolicy(rootDir, frozenResolved) {
21
22
  check(req) {
22
23
  if (!isProtectedWrite(req))
23
24
  return { action: "allow" };
24
- const raw = writeTargetPath(req);
25
- if (typeof raw !== "string" || raw.length === 0)
25
+ const views = callTargetViewsOf(req, "tighten", { root: rootDir });
26
+ if (views === undefined || views.spellings.length === 0)
26
27
  return { action: "allow" };
27
- const absForm = isAbsoluteForFamily(pathFamilyOf({ root: rootDir, cwd: req.cwd }), raw);
28
- const abs = pathKey(absForm ? raw : resolve(req.cwd || rootDir, raw));
29
- if (frozen.has(abs)) {
30
- return { action: "deny", message: `${raw} is part of the frozen specification surface (read-only).` };
28
+ const raw = writeTargetPath(req);
29
+ const hit = views.spellings.some((v) => frozen.has(pathKey(v)));
30
+ if (hit) {
31
+ return { action: "deny", message: `${typeof raw === "string" && raw.length > 0 ? raw : views.spellings[0]} is part of the frozen specification surface (read-only).` };
31
32
  }
32
33
  return { action: "allow" };
33
34
  },
@@ -19,6 +19,7 @@ import { boundInputHashOf } from "../core/canonical-json.js";
19
19
  import { boundedRedactedSummary } from "../core/untrusted-egress.js";
20
20
  import { delimitUntrusted } from "../core/untrusted-text.js";
21
21
  import { OUTPUT_TOOL_NAME } from "../core/runner/synthetic-tools.js";
22
+ import { deniedToolNamesOf } from "../core/runner/sub-agent-deny-lists.js";
22
23
  import { compileOutputSchema } from "../core/runner/strict-output-schema.js";
23
24
  import { WorkflowBudgetExceededError, WorkflowNestingError, WorkflowAgentBlockedError, WorkflowAgentParkedError, WorkflowAgentSchemaError, WorkflowAgentStalledError, WorkflowMaxAgentsError, WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } from "./workflow-types.js";
24
25
  import { WorkflowScriptError } from "./workflow-meta.js";
@@ -118,7 +119,7 @@ export function applyWorkflowAgentType(spec, agentType, registry) {
118
119
  if (out.memoryPersistenceCapable === undefined && def.memoryPersistenceCapable !== undefined)
119
120
  out.memoryPersistenceCapable = def.memoryPersistenceCapable;
120
121
  if (def.allowTools !== undefined || def.denyTools !== undefined) {
121
- const perType = createAllowDenyPolicy({ ...(def.allowTools ? { allow: def.allowTools } : {}), ...(def.denyTools ? { deny: def.denyTools } : {}), scope: "narrowing" });
122
+ const perType = createAllowDenyPolicy({ ...(def.allowTools ? { allow: def.allowTools } : {}), ...(def.denyTools ? { deny: deniedToolNamesOf(`workflow agentType "${def.name}"`, def.denyTools) } : {}), scope: "narrowing" });
122
123
  out.toolPolicy = out.toolPolicy ? combinePolicies(out.toolPolicy, perType) : perType;
123
124
  }
124
125
  return out;
@@ -1578,7 +1579,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1578
1579
  opts.onForwardEvent(e.type === "task_progress" ? { ...e, workflowRunId: runId, workflowAgentLabel: label } : e);
1579
1580
  }
1580
1581
  : undefined;
1581
- const baseInternals = { ...(agentOpts.isolation ? { isolation: agentOpts.isolation } : {}), ...(opts.parentCwd !== undefined ? { parentCwd: opts.parentCwd } : {}), ...spawnAttribution, ...(await captureFloorSeatsNow()), ...(enrichedForward !== undefined ? { onForwardEvent: enrichedForward } : {}), delegationTaskType: "workflow", agentName: label, onWorkspaceResolved: createWorkspaceObserver(rec) };
1582
+ const baseInternals = { ...(agentOpts.isolation ? { isolation: agentOpts.isolation } : {}), ...(opts.parentCwd !== undefined ? { parentCwd: opts.parentCwd, callerRoot: opts.parentCwd } : {}), ...spawnAttribution, ...(await captureFloorSeatsNow()), ...(enrichedForward !== undefined ? { onForwardEvent: enrichedForward } : {}), delegationTaskType: "workflow", agentName: label, onWorkspaceResolved: createWorkspaceObserver(rec) };
1582
1583
  let attempts = 0;
1583
1584
  let throttleRetried = false;
1584
1585
  let lastAttemptReason;
@@ -248,6 +248,13 @@ export declare function substitutionsInHeredocBody(body: string): {
248
248
  readonly body: string;
249
249
  readonly kind: "command" | "arithmetic";
250
250
  }[] | undefined;
251
+ /** The text a word's pieces hand the program when every piece is literal; `undefined` when one is an expansion or
252
+ * unresolved. */
253
+ export declare function literalTextOf(pieces: readonly WordPiece[]): string | undefined;
254
+ /** The text an UNQUOTED here-document body delivers when the shell expands nothing in it: a backslash before `$`, a
255
+ * backquote or a backslash is removed (a line continuation was joined when the body was read), every other character
256
+ * stands. `undefined` when an unescaped `$` or backquote stands in it: the program reads what the expansion makes. */
257
+ export declare function literalHeredocBody(body: string): string | undefined;
251
258
  /** Lex a command: its segments, or the one reason bash (or this lexer) cannot delimit them. */
252
259
  export declare function lexShell(source: string): ShellLex;
253
260
  /**
package/dist/shell/lex.js CHANGED
@@ -319,6 +319,23 @@ export function substitutionsInHeredocBody(body) {
319
319
  }
320
320
  return out;
321
321
  }
322
+ export function literalTextOf(pieces) {
323
+ return pieces.every((q) => q.literal !== undefined) ? pieces.map((q) => q.literal).join("") : undefined;
324
+ }
325
+ export function literalHeredocBody(body) {
326
+ let out = "";
327
+ for (let i = 0; i < body.length; i++) {
328
+ const ch = body[i];
329
+ if (ch === "$" || ch === "`")
330
+ return undefined;
331
+ if (ch === "\\" && i + 1 < body.length && "$`\\".includes(body[i + 1])) {
332
+ out += body[++i];
333
+ continue;
334
+ }
335
+ out += ch;
336
+ }
337
+ return out;
338
+ }
322
339
  const BLANK = /[ \t]/;
323
340
  const fail = (reason, at) => ({ parseFailure: { reason, at } });
324
341
  const publicPiece = (p) => ({
@@ -31,8 +31,6 @@ export declare function writtenAssignmentPrefix(token: string): {
31
31
  readonly settlesWholeName: boolean;
32
32
  } | undefined;
33
33
  export { SHELL_CONTROL_PREFIXES };
34
- /** Shell block words that name no command of their own (a loop head, a block close, a case head). */
35
- export declare const SHELL_BLOCK_WORDS: ReadonlySet<string>;
36
34
  /** A control word is a keyword only when bash sees it BARE: `'if'` / `"do"` / `\\!` are command names (a program
37
35
  * looked up on PATH, whose failure changes nothing), so a quoted or escaped spelling never counts as one. */
38
36
  export declare function isBareControlWord(words: readonly LexedWord[], i: number, set: ReadonlySet<string>): boolean;
@@ -43,88 +41,6 @@ export declare function isBareControlWord(words: readonly LexedWord[], i: number
43
41
  * over control words and earlier assignments re-traces). One fact, two readers that must agree.
44
42
  */
45
43
  export declare function isLeadingAssignmentWord(words: readonly LexedWord[], i: number): boolean;
46
- /**
47
- * Programs that turn their STANDARD INPUT into the OPERANDS of another program, and EXEC that program directly.
48
- *
49
- * The same class as the shell re-entry programs, one level down: there the unseen text is COMMANDS; here the
50
- * command's name IS visible and its argv is not — `echo <path> | xargs cat` opens a file no token of this walk
51
- * ever named, because the operand arrives on a stream. The membership criterion is that behaviour and only that
52
- * behaviour; a program that consumes stdin as DATA (`sort`, `wc`, a reader the deployment never listed) is NOT a
53
- * member, because its stdin never becomes a filename. Spelled as a pattern — these are program names, never tool
54
- * names.
55
- *
56
- * "EXECS DIRECTLY" is what separates this class from the shell re-entry class, and it is why GNU `parallel` is
57
- * NOT a member: parallel BUILDS A COMMAND LINE and hands it to a shell, and with no command word at all it runs
58
- * each input line as a command — so parallel belongs to the shell re-entry class and is refused whole with no
59
- * exemption at all. A spelling this file has no name for (an aliased or wrapped `xargs`) is judged as any other
60
- * unlisted program is — the name-table floor the re-entry class records for itself.
61
- */
62
- export declare const ARGV_REENTRY_PROGRAM_RE: RegExp;
63
- /** The program an {@link ARGV_REENTRY_PROGRAM_RE} launcher runs when the chain names none: `xargs` defaults to
64
- * `echo`, which is why a bare one reads nothing. */
65
- export declare const ARGV_REENTRY_DEFAULT_PROGRAM = "echo";
66
- /**
67
- * What a launcher declares about its OWN options: each exact spelling with its ARITY — `0` for a switch, `1` for an
68
- * option whose value is the next token. Every spelling not in the map falls to the refusal (the conservative side,
69
- * and the verdict those spellings always had); a `1` with no token left to take is not readable either.
70
- *
71
- * THREE SPELLINGS OF ONE DECLARATION, because getopt has three: an arity-1 entry is honoured separated (`-k 5`),
72
- * fused to its long name (`--signal=TERM`) and fused to its short letter (`-k5`), and an arity-0 short entry inside
73
- * a cluster of arity-0 letters (`-pv`). Reading only the exact spelling made one program two.
74
- *
75
- * WHAT A ROW MAY NOT GET WRONG: declaring an arity-1 option as arity 0 steps onto its VALUE and reads the value as
76
- * the program — the one direction that turns a refusal into a wrong answer rather than a lost one. A row carries
77
- * only options whose arity every supported implementation agrees on; a flag no row declares still refuses.
78
- */
79
- export interface LauncherRow {
80
- /** Consulted ONLY for the launcher the command line itself names — a bare keyword in front of the command
81
- * (`time -p …`) or the written command word (`"time" -p …`, `X=1 time -p …`, `/usr/bin/time -p …`). A launcher
82
- * nested behind another (`env time -p …`) is that launcher's operand, and its options refuse: resolving past
83
- * them would turn a whole refusal into an ordinary walked segment — a widening, where every other read of this
84
- * table only ever finds MORE program to judge. */
85
- readonly optionArity: ReadonlyMap<string, 0 | 1>;
86
- /** A bare NEGATIVE integer in this launcher's option section is its OWN argument (`nice -5 <cmd>`), not a flag
87
- * the table cannot read. Per row: elsewhere a dash-leading token is an option, and reading one as a number
88
- * would step over a flag whose meaning this table does not know. */
89
- readonly negativeIntegerIsOwnArgument?: true;
90
- /** Positional operands the launcher takes BEFORE the program, consumed whatever they spell (`timeout DURATION
91
- * COMMAND`); a spelling-based reading under-accepted, and every spelling it missed became the program word. */
92
- readonly requiredOperands?: number;
93
- /** The launcher reads NAME=VALUE operands with ITS OWN name grammar (`env 1=x …`), wider than the shell's. */
94
- readonly assignmentOperands?: true;
95
- /** The launcher hands its operands to a SHELL as text (`watch "cat /x"` runs `sh -c`): no program word to read. */
96
- readonly shellText?: true;
97
- /** With no program word the launcher runs a SHELL: piped text would reach it, so an empty program position refuses. */
98
- readonly whenNoProgram?: "shell";
99
- /** Options whose value is a WHOLE COMMAND LINE the launcher splits and runs (`env -S "cmd arg"`,
100
- * `--split-string=…`). Not an arity this reader steps over — the program is inside the value, so the
101
- * program-position walk still refuses here — but the candidate extraction reads the value as nested shell
102
- * text instead of as one opaque word. */
103
- readonly commandString?: readonly string[];
104
- }
105
- /**
106
- * The ONE launcher truth: programs that RUN ANOTHER PROGRAM named in ordinary argv tokens, so a declared reader can
107
- * sit behind one (`env cat <path>`, `sudo cat <path>`, `timeout 5 cat <path>`), each with the option grammar this
108
- * file reads for it.
109
- *
110
- * Membership is the key set. A row with no declared options means every option-shaped word after that launcher
111
- * refuses ("which program runs is not readable without the launcher's option table") — the answer every launcher
112
- * but `time` has always had, now stated per row rather than by omission. Declaring a row's options is a behaviour
113
- * decision with its own criteria; adding a launcher is a row.
114
- *
115
- * `time` carries both spellings a deployment can meet: the shell builtin takes `-p` alone, and the standalone
116
- * program adds a format, an output file, and three switches.
117
- *
118
- * A deliberate SUBSET of the rule lane's own `BARE_INTERPRETER_NAMES`, mirrored rather than imported because that
119
- * module imports the classifier — the dependency runs that way, and a cycle is a worse problem than a mirror whose
120
- * subset relation is pinned by test. GNU `parallel` is absent for the reason the shells are absent: it does not run
121
- * a program named in token-visible argv — it BUILDS a command line and hands it to a shell — so it is a shell
122
- * re-entry member instead. Language interpreters and shells are excluded as a scope statement: their program
123
- * argument is a script path or a quoted code string, not a token-visible program with token-visible operands.
124
- */
125
- export declare const LAUNCHER_TABLE: ReadonlyMap<string, LauncherRow>;
126
- /** The launcher names — derived from the table, never kept beside it. */
127
- export declare const COMMAND_LAUNCHERS: ReadonlySet<string>;
128
44
  export declare const PROGRAM_POSITION_REFUSAL_KINDS: readonly ["option_at_program_position", "control_syntax_at_program_position"];
129
45
  export type ProgramPositionRefusalKind = (typeof PROGRAM_POSITION_REFUSAL_KINDS)[number];
130
46
  /** Why the program could not be read — a closed set, each member minted into one sentence. `owner` is the word
@@ -140,6 +56,9 @@ export type ProgramPositionRefusal = {
140
56
  /** Not a DECLINE ({@link programPositionRunsShell}): the row WAS read and a SHELL stands at the program
141
57
  * position. Absent ⇒ genuine control syntax, which IS one. */
142
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;
143
62
  };
144
63
  export interface ProgramPosition {
145
64
  /** Indices of the bare control words stripped in front (`!`, `if`, `time`, …), in order; their declared options
@@ -159,6 +78,8 @@ export interface ProgramPosition {
159
78
  readonly argvFromStdin: boolean;
160
79
  /** The chain ends ON the re-entry program (nothing follows it), so it runs its default program. */
161
80
  readonly runsDefaultProgram: boolean;
81
+ /** The program the stream-fed chain member runs when the chain names none (`xargs` runs `echo`). */
82
+ readonly defaultProgram?: string;
162
83
  /** The program bash RUNS: the first word past the chain. `name` is the folded word as written, `basename` what
163
84
  * bash resolves (`/usr/bin/cat` is `cat`). `undefined` when the chain names none or the read was refused. */
164
85
  readonly program: {
@@ -184,7 +105,7 @@ export declare function readProgramPosition(words: readonly LexedWord[]): Progra
184
105
  /** Does this program position hand a program operands the walk cannot see? True when the chain re-enters through
185
106
  * stdin AND the program it runs is not one the caller accepts as a stream target. ONE reading, shared by every
186
107
  * consumer; WHICH targets are accepted is the caller's declaration, not this reader's. */
187
- export declare function argvReentryUnjudged(pos: ProgramPosition, acceptedTargets: ReadonlySet<string>): boolean;
108
+ export declare function argvReentryUnjudged(pos: ProgramPosition, acceptsTarget: (basename: string) => boolean): boolean;
188
109
  /**
189
110
  * The two ways this reader DECLINES rather than judges — a CLOSED set (`docs/CLOSED-SETS.md`), because what a
190
111
  * seat does with a declined segment is a ROW of the shell read decision table (`shell-read-judge.ts`, the
@@ -197,9 +118,9 @@ export type ShellScanDecline = (typeof SHELL_SCAN_DECLINES)[number];
197
118
  /** Does a SHELL stand where this segment's program would be — as the program itself (`sh -c`), or as a launcher
198
119
  * ROW handing its operands to one (`watch "…"`)? ONE predicate for the re-entry class, so the two spellings
199
120
  * cannot be answered differently; which names are shells is the caller's declaration. Not a decline. */
200
- export declare function programPositionRunsShell(pos: ProgramPosition, shellProgram: RegExp): boolean;
121
+ export declare function programPositionRunsShell(pos: ProgramPosition, runsShell: (basename: string) => boolean): boolean;
201
122
  /** Which decline, if any, this program position raises — ONE reading of the two facts above, so no consumer
202
123
  * re-derives either. A refusal that says a SHELL runs is NOT one: it names what runs. */
203
- export declare function shellScanDeclineOf(pos: ProgramPosition, acceptedStreamTargets: ReadonlySet<string>): ShellScanDecline | undefined;
124
+ export declare function shellScanDeclineOf(pos: ProgramPosition, acceptsStreamTarget: (basename: string) => boolean): ShellScanDecline | undefined;
204
125
  /** ONE sentence per refusal kind, minted here and nowhere else. */
205
126
  export declare function programPositionRefusalSentence(refusal: ProgramPositionRefusal): string;
@@ -1,4 +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
3
  export const NOT_AUTO_ALLOWED = "— not auto-allowed";
3
4
  export function programBasename(word) {
4
5
  return word.includes("/") ? (word.split("/").pop() ?? word) : word;
@@ -14,7 +15,6 @@ export function writtenAssignmentPrefix(token) {
14
15
  return { prefix, name: bracket < 0 ? written : written.slice(0, bracket), appends, settlesWholeName: !appends && bracket < 0 };
15
16
  }
16
17
  export { SHELL_CONTROL_PREFIXES };
17
- export const SHELL_BLOCK_WORDS = new Set(["for", "select", "case", "esac", "fi", "done", "function", "coproc", "[[", "]]", "in"]);
18
18
  export function isBareControlWord(words, i, set) {
19
19
  const w = words[i];
20
20
  return w !== undefined && w.raw === w.folded && set.has(w.folded);
@@ -23,48 +23,19 @@ export function isLeadingAssignmentWord(words, i) {
23
23
  const w = words[i];
24
24
  return w !== undefined && w.assignment !== undefined && w.assignment.bare && w.atCommandPosition;
25
25
  }
26
- export const ARGV_REENTRY_PROGRAM_RE = /^xargs$/;
27
- export const ARGV_REENTRY_DEFAULT_PROGRAM = "echo";
28
- const NO_DECLARED_OPTIONS = { optionArity: new Map() };
29
- const launcherRow = (pairs, extra = {}) => ({ optionArity: new Map(pairs), ...extra });
30
- export const LAUNCHER_TABLE = new Map([
31
- ["env", launcherRow([["-i", 0], ["-0", 0], ["-v", 0], ["--ignore-environment", 0], ["--null", 0], ["--debug", 0], ["-u", 1], ["--unset", 1]], { assignmentOperands: true, commandString: ["-S", "--split-string"] })],
32
- ["command", launcherRow([["-p", 0]])],
33
- ["builtin", NO_DECLARED_OPTIONS], ["exec", NO_DECLARED_OPTIONS],
34
- ["noglob", NO_DECLARED_OPTIONS], ["xargs", NO_DECLARED_OPTIONS], ["busybox", NO_DECLARED_OPTIONS],
35
- ["sudo", { optionArity: new Map(), assignmentOperands: true }], ["doas", NO_DECLARED_OPTIONS], ["pkexec", { optionArity: new Map(), whenNoProgram: "shell" }],
36
- ["su", { optionArity: new Map(), shellText: true }], ["runuser", { optionArity: new Map(), shellText: true }],
37
- ["chroot", { optionArity: new Map(), requiredOperands: 1, whenNoProgram: "shell" }], ["setpriv", NO_DECLARED_OPTIONS],
38
- ["nohup", NO_DECLARED_OPTIONS],
39
- ["nice", launcherRow([["-n", 1], ["--adjustment", 1]], { negativeIntegerIsOwnArgument: true })],
40
- ["ionice", NO_DECLARED_OPTIONS], ["chrt", { optionArity: new Map(), requiredOperands: 1 }], ["taskset", { optionArity: new Map(), requiredOperands: 1 }],
41
- ["stdbuf", launcherRow([["-i", 1], ["-o", 1], ["-e", 1], ["--input", 1], ["--output", 1], ["--error", 1]])],
42
- ["timeout", launcherRow([
43
- ["--foreground", 0], ["--preserve-status", 0], ["--verbose", 0], ["-v", 0],
44
- ["-k", 1], ["-s", 1], ["--kill-after", 1], ["--signal", 1],
45
- ], { requiredOperands: 1 })],
46
- ["time", {
47
- optionArity: new Map([
48
- ["-p", 0], ["-a", 0], ["-v", 0], ["-q", 0], ["--portability", 0], ["--append", 0], ["--verbose", 0], ["--quiet", 0], ["--help", 0], ["--version", 0], ["--", 0],
49
- ["-f", 1], ["-o", 1], ["--format", 1], ["--output", 1],
50
- ]),
51
- }],
52
- ["watch", { optionArity: new Map(), shellText: true }], ["setsid", NO_DECLARED_OPTIONS], ["flock", { optionArity: new Map(), requiredOperands: 1 }], ["unshare", { optionArity: new Map(), whenNoProgram: "shell" }],
53
- ["nsenter", { optionArity: new Map(), whenNoProgram: "shell" }], ["script", { optionArity: new Map(), shellText: true }], ["numactl", NO_DECLARED_OPTIONS], ["prlimit", NO_DECLARED_OPTIONS],
54
- ["systemd-run", NO_DECLARED_OPTIONS],
55
- ["strace", NO_DECLARED_OPTIONS], ["ltrace", NO_DECLARED_OPTIONS], ["valgrind", NO_DECLARED_OPTIONS], ["firejail", { optionArity: new Map(), whenNoProgram: "shell" }],
56
- ["bwrap", NO_DECLARED_OPTIONS],
57
- ]);
58
- export const COMMAND_LAUNCHERS = new Set(LAUNCHER_TABLE.keys());
26
+ function launcherRowOf(name) {
27
+ const row = programRow(name);
28
+ return row?.launcher !== undefined ? row : undefined;
29
+ }
59
30
  function pastLauncherOptions(launcher, folded, i) {
60
- const row = LAUNCHER_TABLE.get(launcher);
31
+ const row = launcherRowOf(launcher);
61
32
  if (row === undefined)
62
33
  return i;
63
34
  for (;;) {
64
35
  const tok = folded[i];
65
36
  if (tok === undefined)
66
37
  return i;
67
- if (row.negativeIntegerIsOwnArgument === true && /^-\d+$/.test(tok)) {
38
+ if (row.launcher?.negativeIntegerIsOwnArgument === true && /^-\d+$/.test(tok)) {
68
39
  i += 1;
69
40
  continue;
70
41
  }
@@ -76,19 +47,6 @@ function pastLauncherOptions(launcher, folded, i) {
76
47
  i += step;
77
48
  }
78
49
  }
79
- function launcherOptionStep(row, tok) {
80
- const exact = row.optionArity.get(tok);
81
- if (exact !== undefined)
82
- return exact === 1 ? 2 : 1;
83
- const eq = tok.startsWith("--") ? tok.indexOf("=") : -1;
84
- if (eq > 2 && row.optionArity.get(tok.slice(0, eq)) === 1)
85
- return 1;
86
- if (/^-[A-Za-z]./.test(tok) && row.optionArity.get(tok.slice(0, 2)) === 1)
87
- return 1;
88
- if (/^-[A-Za-z]+$/.test(tok) && [...tok.slice(1)].every((ch) => row.optionArity.get(`-${ch}`) === 0))
89
- return 1;
90
- return undefined;
91
- }
92
50
  export const PROGRAM_POSITION_REFUSAL_KINDS = Object.freeze(["option_at_program_position", "control_syntax_at_program_position"]);
93
51
  export function readProgramPosition(words) {
94
52
  const folded = words.map((w) => w.folded);
@@ -116,20 +74,24 @@ export function readProgramPosition(words) {
116
74
  const owner = controlWords.length > 0 ? folded[controlWords[controlWords.length - 1]] : head;
117
75
  return none(written, assignments, { kind: "option_at_program_position", owner, option: head });
118
76
  }
119
- if (!COMMAND_LAUNCHERS.has(basename)) {
77
+ if (!LAUNCHER_NAMES.has(basename)) {
120
78
  return { controlWords, written, assignments, launchers: [], argvFromStdin: false, runsDefaultProgram: false, program: { index: i, name: head, basename }, refusal: undefined };
121
79
  }
122
80
  const launchers = [{ index: i, name: basename }];
123
81
  const owner = head;
124
- const shellRefusal = () => ({ kind: "control_syntax_at_program_position", owner, word: "sh", runsShell: true });
125
- let owedOperands = LAUNCHER_TABLE.get(basename)?.requiredOperands ?? 0;
126
- let lastRow = LAUNCHER_TABLE.get(basename);
82
+ const shellRefusal = (handsText) => ({ kind: "control_syntax_at_program_position", owner, word: "sh", runsShell: true, ...(handsText !== undefined ? { handsText } : {}) });
83
+ let lastRow = launcherRowOf(basename)?.launcher;
84
+ let owedOperands = lastRow?.requiredOperands ?? 0;
127
85
  const chain0 = (program, refusal) => ({ controlWords, written, assignments, launchers, argvFromStdin: false, runsDefaultProgram: false, program, refusal });
128
86
  if (lastRow?.shellText === true)
129
- return chain0(undefined, shellRefusal());
130
- let argvFromStdin = ARGV_REENTRY_PROGRAM_RE.test(basename);
87
+ return chain0(undefined, shellRefusal(true));
88
+ let argvFromStdin = feedsArgvFromStdin(basename);
131
89
  let lastReentry = argvFromStdin ? i : -1;
132
- const chain = (program, refusal) => ({ controlWords, written, assignments, launchers, argvFromStdin, runsDefaultProgram: lastReentry === folded.length - 1, program, refusal });
90
+ let reentryDefault = argvFromStdin ? streamDefaultProgram(basename) : undefined;
91
+ const chain = (program, refusal) => {
92
+ const runsDefaultProgram = lastReentry === folded.length - 1;
93
+ return { controlWords, written, assignments, launchers, argvFromStdin, runsDefaultProgram, ...(runsDefaultProgram && reentryDefault !== undefined ? { defaultProgram: reentryDefault } : {}), program, refusal };
94
+ };
133
95
  for (let j = pastLauncherOptions(basename, folded, i + 1); j < folded.length; j++) {
134
96
  const t = folded[j];
135
97
  if (owedOperands > 0 && !t.startsWith("-")) {
@@ -139,50 +101,52 @@ export function readProgramPosition(words) {
139
101
  if (lastRow?.assignmentOperands === true && !t.startsWith("-") && t.includes("="))
140
102
  continue;
141
103
  const wordBasename = programBasename(t);
142
- if (ARGV_REENTRY_PROGRAM_RE.test(wordBasename)) {
104
+ if (feedsArgvFromStdin(wordBasename)) {
143
105
  argvFromStdin = true;
144
106
  lastReentry = j;
107
+ reentryDefault = streamDefaultProgram(wordBasename);
145
108
  lastRow = undefined;
146
109
  owedOperands = 0;
147
110
  continue;
148
111
  }
149
- if (COMMAND_LAUNCHERS.has(wordBasename)) {
112
+ if (LAUNCHER_NAMES.has(wordBasename)) {
150
113
  launchers.push({ index: j, name: wordBasename });
151
- lastRow = LAUNCHER_TABLE.get(wordBasename);
114
+ lastRow = launcherRowOf(wordBasename)?.launcher;
152
115
  if (lastRow?.shellText === true)
153
- return chain(undefined, shellRefusal());
116
+ return chain(undefined, shellRefusal(true));
154
117
  owedOperands = lastRow?.requiredOperands ?? 0;
155
118
  j = pastLauncherOptions(wordBasename, folded, j + 1) - 1;
156
119
  continue;
157
120
  }
158
121
  if (t.startsWith("-"))
159
122
  return chain(undefined, { kind: "option_at_program_position", owner, option: t });
160
- if (SHELL_CONTROL_PREFIXES.has(wordBasename) || SHELL_BLOCK_WORDS.has(wordBasename)) {
123
+ if (SHELL_CONTROL_PREFIXES.has(wordBasename) || BLOCK_WORD_NAMES.has(wordBasename)) {
161
124
  return chain(undefined, { kind: "control_syntax_at_program_position", owner, word: wordBasename });
162
125
  }
163
126
  return chain({ index: j, name: t, basename: wordBasename });
164
127
  }
165
- if (lastRow?.whenNoProgram === "shell")
128
+ 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"))
166
130
  return chain(undefined, shellRefusal());
167
131
  return chain(undefined);
168
132
  }
169
- export function argvReentryUnjudged(pos, acceptedTargets) {
133
+ export function argvReentryUnjudged(pos, acceptsTarget) {
170
134
  if (!pos.argvFromStdin)
171
135
  return false;
172
- const runs = pos.program?.basename ?? (pos.runsDefaultProgram ? ARGV_REENTRY_DEFAULT_PROGRAM : undefined);
173
- return runs === undefined || !acceptedTargets.has(runs);
136
+ const runs = pos.program?.basename ?? pos.defaultProgram;
137
+ return runs === undefined || !acceptsTarget(runs);
174
138
  }
175
139
  export const SHELL_SCAN_DECLINES = Object.freeze(["unreadable_program_position", "stream_fed_operands"]);
176
- export function programPositionRunsShell(pos, shellProgram) {
140
+ export function programPositionRunsShell(pos, runsShell) {
177
141
  if (pos.refusal !== undefined)
178
142
  return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell === true;
179
- return pos.program !== undefined && shellProgram.test(pos.program.basename);
143
+ return pos.program !== undefined && runsShell(pos.program.basename);
180
144
  }
181
- export function shellScanDeclineOf(pos, acceptedStreamTargets) {
145
+ export function shellScanDeclineOf(pos, acceptsStreamTarget) {
182
146
  if (pos.refusal !== undefined) {
183
147
  return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell === true ? undefined : "unreadable_program_position";
184
148
  }
185
- return argvReentryUnjudged(pos, acceptedStreamTargets) ? "stream_fed_operands" : undefined;
149
+ return argvReentryUnjudged(pos, acceptsStreamTarget) ? "stream_fed_operands" : undefined;
186
150
  }
187
151
  export function programPositionRefusalSentence(refusal) {
188
152
  switch (refusal.kind) {