@sema-agent/core 7.17.6 → 7.18.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 (164) hide show
  1. package/CHANGELOG.md +165 -13
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -0
  5. package/dist/agents/teacher.js +1 -0
  6. package/dist/agents/verify.js +3 -0
  7. package/dist/brain/route-adjudicator.d.ts +18 -0
  8. package/dist/brain/route-adjudicator.js +16 -0
  9. package/dist/core/agent-definition.d.ts +10 -0
  10. package/dist/core/artifact-host.d.ts +41 -4
  11. package/dist/core/artifact-host.js +18 -2
  12. package/dist/core/auto-mode.d.ts +141 -45
  13. package/dist/core/auto-mode.js +109 -3
  14. package/dist/core/checkpoint-store.d.ts +16 -2
  15. package/dist/core/engine-notice.d.ts +47 -1
  16. package/dist/core/engine-notice.js +7 -0
  17. package/dist/core/gate-lanes.js +2 -1
  18. package/dist/core/gate-outcome.d.ts +17 -2
  19. package/dist/core/gate-outcome.js +16 -0
  20. package/dist/core/governance-codes.d.ts +1 -1
  21. package/dist/core/governance-codes.js +8 -0
  22. package/dist/core/hook-seats.d.ts +66 -0
  23. package/dist/core/hook-seats.js +37 -0
  24. package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
  25. package/dist/core/runner/assemble-result.d.ts +2 -1
  26. package/dist/core/runner/assemble-result.js +1 -1
  27. package/dist/core/runner/classifier-ladder.d.ts +89 -0
  28. package/dist/core/runner/classifier-ladder.js +65 -0
  29. package/dist/core/runner/contracts.d.ts +69 -13
  30. package/dist/core/runner/decide-continuation.d.ts +1 -3
  31. package/dist/core/runner/decide-continuation.js +1 -7
  32. package/dist/core/runner/deferral-census.d.ts +52 -0
  33. package/dist/core/runner/deferral-census.js +44 -0
  34. package/dist/core/runner/derived-route-fallback.d.ts +8 -4
  35. package/dist/core/runner/derived-route-fallback.js +8 -3
  36. package/dist/core/runner/gate-exit.d.ts +12 -0
  37. package/dist/core/runner/gate-exit.js +6 -3
  38. package/dist/core/runner/prepare-artifact.d.ts +10 -2
  39. package/dist/core/runner/prepare-artifact.js +5 -4
  40. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  41. package/dist/core/runner/prepare-caps-and-workflow.js +30 -31
  42. package/dist/core/runner/prepare-config-doors.d.ts +1 -1
  43. package/dist/core/runner/prepare-config-doors.js +5 -3
  44. package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
  45. package/dist/core/runner/prepare-defer-classify.js +8 -3
  46. package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
  47. package/dist/core/runner/prepare-gate-stations.js +1 -1
  48. package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
  49. package/dist/core/runner/prepare-hands-readface.js +60 -24
  50. package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
  51. package/dist/core/runner/prepare-inherited-gate.js +10 -2
  52. package/dist/core/runner/prepare-lsp.d.ts +2 -2
  53. package/dist/core/runner/prepare-lsp.js +1 -1
  54. package/dist/core/runner/prepare-policy-chain.js +2 -0
  55. package/dist/core/runner/prepare-project-context.js +4 -2
  56. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
  57. package/dist/core/runner/prepare-prompt-inputs.js +4 -1
  58. package/dist/core/runner/prepare-run-refs.d.ts +9 -1
  59. package/dist/core/runner/prepare-run-refs.js +3 -2
  60. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
  61. package/dist/core/runner/prepare-suspend-saga.js +5 -2
  62. package/dist/core/runner/prepare-task.js +18 -13
  63. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
  64. package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
  65. package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
  66. package/dist/core/runner/prepare-turn-wiring.js +3 -2
  67. package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
  68. package/dist/core/runner/prepare-wiring-manifest.js +5 -2
  69. package/dist/core/runner/resume-checkpoint-screen.js +7 -0
  70. package/dist/core/runner/run-harness-handlers.js +4 -1
  71. package/dist/core/runner/run-identity-wiring.js +6 -2
  72. package/dist/core/runner/run-leg.d.ts +2 -0
  73. package/dist/core/runner/run-leg.js +35 -41
  74. package/dist/core/runner/run-notification-lane.js +55 -27
  75. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  76. package/dist/core/runner/run-terminal-adoption.js +16 -13
  77. package/dist/core/runner/run-turn-boundary.js +3 -1
  78. package/dist/core/runner/runtask.d.ts +6 -5
  79. package/dist/core/runner/runtask.js +26 -14
  80. package/dist/core/runner/skill-base-dir.d.ts +54 -0
  81. package/dist/core/runner/skill-base-dir.js +42 -0
  82. package/dist/core/runner/synthetic-tools.d.ts +16 -19
  83. package/dist/core/runner/synthetic-tools.js +18 -10
  84. package/dist/core/runner/tool-defer-gate.d.ts +22 -9
  85. package/dist/core/runner/tool-defer-gate.js +15 -5
  86. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  87. package/dist/core/runner/tool-disclosure.js +2 -0
  88. package/dist/core/runner/tool-progress-lane.d.ts +42 -0
  89. package/dist/core/runner/tool-progress-lane.js +61 -0
  90. package/dist/core/runner-deps.d.ts +48 -7
  91. package/dist/core/swappable-deps.d.ts +9 -7
  92. package/dist/core/swappable-deps.js +7 -3
  93. package/dist/core/task-event.d.ts +96 -29
  94. package/dist/core/task-notification.d.ts +142 -16
  95. package/dist/core/task-notification.js +106 -0
  96. package/dist/core/task-result.d.ts +9 -1
  97. package/dist/core/task-spec.d.ts +20 -3
  98. package/dist/core/tighten-task-spec.js +7 -5
  99. package/dist/core/tool-model-gate.d.ts +1 -1
  100. package/dist/core/tool-search-setting.d.ts +78 -0
  101. package/dist/core/tool-search-setting.js +90 -0
  102. package/dist/core/tool-spec.d.ts +8 -0
  103. package/dist/core/types.d.ts +1 -1
  104. package/dist/core/types.js +1 -1
  105. package/dist/core/wiring-manifest.d.ts +46 -0
  106. package/dist/core/wiring-manifest.js +9 -1
  107. package/dist/engine/execution-env/node-execution-env.js +10 -4
  108. package/dist/engine/harness/agent-harness.d.ts +42 -0
  109. package/dist/engine/harness/agent-harness.js +61 -29
  110. package/dist/engine/loop/agent-loop.d.ts +68 -3
  111. package/dist/engine/loop/agent-loop.js +109 -16
  112. package/dist/index.d.ts +10 -5
  113. package/dist/index.js +9 -5
  114. package/dist/internal/harness.d.ts +1 -1
  115. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  116. package/dist/orchestration/run-workflow-tool.js +6 -1
  117. package/dist/orchestration/workflow-governance.d.ts +7 -1
  118. package/dist/orchestration/workflow-governance.js +9 -5
  119. package/dist/orchestration/workflow-primitives.d.ts +3 -0
  120. package/dist/orchestration/workflow-primitives.js +6 -1
  121. package/dist/orchestration/workflow-types.d.ts +48 -13
  122. package/dist/orchestration/workflow-types.js +1 -1
  123. package/dist/orchestration/workflow.js +45 -17
  124. package/dist/prompt-assembly/epoch.js +2 -0
  125. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  126. package/dist/prompt-assembly/section-usage.d.ts +26 -0
  127. package/dist/prompt-assembly/section-usage.js +10 -0
  128. package/dist/prompt-assembly/types.d.ts +8 -6
  129. package/dist/prompts/default.d.ts +13 -0
  130. package/dist/prompts/default.js +16 -8
  131. package/dist/tools/artifact/artifact-text.d.ts +10 -1
  132. package/dist/tools/artifact/artifact-text.js +6 -3
  133. package/dist/tools/artifact/artifact-tool.d.ts +12 -2
  134. package/dist/tools/artifact/artifact-tool.js +203 -70
  135. package/dist/tools/artifact/conformance.d.ts +56 -0
  136. package/dist/tools/artifact/conformance.js +349 -0
  137. package/dist/tools/artifact/local-stub.js +1 -1
  138. package/dist/tools/artifact/reject-breaker.d.ts +28 -0
  139. package/dist/tools/artifact/reject-breaker.js +42 -0
  140. package/dist/tools/fs/bash-lexer.d.ts +132 -0
  141. package/dist/tools/fs/bash-lexer.js +563 -0
  142. package/dist/tools/fs/bash-program-position.d.ts +142 -0
  143. package/dist/tools/fs/bash-program-position.js +125 -0
  144. package/dist/tools/fs/bash-readonly-classifier.d.ts +54 -16
  145. package/dist/tools/fs/bash-readonly-classifier.js +375 -530
  146. package/dist/tools/fs/containment-face.d.ts +89 -0
  147. package/dist/tools/fs/containment-face.js +81 -0
  148. package/dist/tools/fs/fs-bash.d.ts +46 -1
  149. package/dist/tools/fs/fs-bash.js +158 -22
  150. package/dist/tools/fs/fs-read.d.ts +1 -1
  151. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  152. package/dist/tools/fs/fs-write.d.ts +4 -3
  153. package/dist/tools/fs/fs-write.js +9 -9
  154. package/dist/tools/fs/index.d.ts +24 -5
  155. package/dist/tools/fs/index.js +18 -6
  156. package/dist/tools/fs/repo-map.d.ts +2 -2
  157. package/dist/tools/fs/safety.d.ts +3 -3
  158. package/dist/tools/fs/safety.js +4 -4
  159. package/package.json +1 -1
  160. package/test/export-surface.snapshot.json +85 -9
  161. package/wire-changes/core@7.17.6.json +81 -0
  162. package/wire-changes/core@7.17.7.json +49 -0
  163. package/dist/tools/fs/read-face.d.ts +0 -68
  164. package/dist/tools/fs/read-face.js +0 -47
@@ -1,5 +1,7 @@
1
1
  import { isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf, withinAnyRoot } from "./safety.js";
2
- export const NOT_AUTO_ALLOWED = "— not auto-allowed";
2
+ import { lexShellText, literalTildeSpelling, quoteMask, substitutionsInHeredocBody, SUBSTITUTION_PLACEHOLDER, wordsOf, } from "./bash-lexer.js";
3
+ import { ASSIGNMENT_WORD, COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
4
+ export { NOT_AUTO_ALLOWED } from "./bash-program-position.js";
3
5
  export const BASH_READONLY_DEFAULT_ALLOW = [
4
6
  "ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
5
7
  "grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
@@ -8,24 +10,6 @@ export const BASH_READONLY_DEFAULT_ALLOW = [
8
10
  ];
9
11
  export const BASH_CLASSIFY_DEFAULT_ALLOW = [...BASH_READONLY_DEFAULT_ALLOW, "find", "sed", "cd"];
10
12
  const SHELL_OPERATORS = /[;&|<>$()`\n\r\\]/;
11
- function quoteMask(s) {
12
- const quoted = new Array(s.length).fill(false);
13
- let open;
14
- for (let i = 0; i < s.length; i++) {
15
- const ch = s[i];
16
- if (open === undefined && (ch === '"' || ch === "'")) {
17
- open = ch;
18
- quoted[i] = true;
19
- continue;
20
- }
21
- if (open !== undefined) {
22
- quoted[i] = true;
23
- if (open === ch)
24
- open = undefined;
25
- }
26
- }
27
- return { quoted, balanced: open === undefined };
28
- }
29
13
  function hasBareShellOperator(s, quotedOperatorsAreText) {
30
14
  if (!quotedOperatorsAreText)
31
15
  return SHELL_OPERATORS.test(s);
@@ -108,48 +92,11 @@ export function splitShellCompoundSegments(source, options) {
108
92
  return { reject: "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed" };
109
93
  }
110
94
  const scanned = options?.trailingTerminator !== "keep" ? source.replace(/;\s*$/, "") : source;
111
- const segments = [];
112
- const connectors = ["start"];
113
- const mask = quoteMask(scanned);
114
- const quoted = (i) => mask.balanced && mask.quoted[i] === true;
115
- let cur = "";
116
- for (let i = 0; i < scanned.length; i++) {
117
- const c = scanned[i];
118
- if (quoted(i)) {
119
- cur += c;
120
- }
121
- else if (c === "&") {
122
- if (scanned[i + 1] === "&") {
123
- segments.push(cur);
124
- cur = "";
125
- connectors.push("&&");
126
- i++;
127
- }
128
- else
129
- return { reject: "`&` backgrounding is not allowed — a backgrounded process outlives the command" };
130
- }
131
- else if (c === "|") {
132
- if (scanned[i + 1] === "|") {
133
- segments.push(cur);
134
- cur = "";
135
- connectors.push("||");
136
- i++;
137
- }
138
- else {
139
- segments.push(cur);
140
- cur = "";
141
- connectors.push("|");
142
- }
143
- }
144
- else if (c === ";") {
145
- segments.push(cur);
146
- cur = "";
147
- connectors.push(";");
148
- }
149
- else
150
- cur += c;
151
- }
152
- segments.push(cur);
95
+ const lexed = lexShellText(scanned, { mode: "strict" });
96
+ if ("reject" in lexed)
97
+ return lexed;
98
+ const segments = lexed.segments.map((s) => s.text);
99
+ const connectors = lexed.segments.map((s) => s.connector);
153
100
  return { segments, pipeFed: connectors.map((k) => k === "|"), connectors };
154
101
  }
155
102
  const EXEMPT_REDIRECTION = /^(?:2>\/dev\/null|[12]?>&[12])$/;
@@ -181,24 +128,6 @@ function stripExemptRedirections(command) {
181
128
  flush();
182
129
  return out;
183
130
  }
184
- function foldQuoteRemovalToken(tok) {
185
- if (!tok.includes('"') && !tok.includes("'"))
186
- return tok;
187
- let out = "";
188
- let open;
189
- for (const ch of tok) {
190
- if (open === undefined && (ch === '"' || ch === "'")) {
191
- open = ch;
192
- continue;
193
- }
194
- if (open === ch) {
195
- open = undefined;
196
- continue;
197
- }
198
- out += ch;
199
- }
200
- return out;
201
- }
202
131
  const POLL_LOOP_CAUSE_MAX_PATHS = 3;
203
132
  export function formatOutOfRootReadApprovalOption(directory) {
204
133
  const trimmed = directory.replace(/[/\\]+$/, "");
@@ -245,30 +174,25 @@ function spellingsOf(absolute) {
245
174
  function reheadSegment(toks, head, from = 0) {
246
175
  return { folded: [head, ...toks.folded.slice(from + 1)], raw: [head, ...toks.raw.slice(from + 1)] };
247
176
  }
248
- const SHELL_CONTROL_PREFIXES = new Set(["!", "if", "then", "elif", "else", "while", "until", "do", "time"]);
249
- const SHELL_BLOCK_WORDS = new Set(["for", "select", "case", "esac", "fi", "done", "function", "coproc", "[[", "]]", "in"]);
250
- function isBareControlWord(toks, i, words) {
251
- const word = toks.folded[i];
252
- return word !== undefined && toks.raw[i] === word && words.has(word);
253
- }
254
- function tokensMoveBase(toks, allow) {
255
- let from = 0;
256
- while (from < toks.folded.length && isBareControlWord(toks, from, SHELL_CONTROL_PREFIXES))
257
- from++;
258
- const head = toks.folded[from];
259
- if (head === undefined)
177
+ function tokensMoveBase(toks) {
178
+ const pos = readProgramPosition(toks);
179
+ if (pos.written === undefined)
260
180
  return false;
261
- if (toks.raw[from] !== head)
181
+ const written = toks.folded[pos.written];
182
+ if (toks.raw[pos.written] !== written)
262
183
  return true;
263
- if (head === "cd")
264
- return true;
265
- if (!COMMAND_LAUNCHERS.has(head))
184
+ if (pos.assignments.length > 0)
185
+ return false;
186
+ if (pos.launchers.length === 0) {
187
+ if (written.startsWith("-") && written !== "-")
188
+ return true;
189
+ return written === "cd";
190
+ }
191
+ if (!COMMAND_LAUNCHERS.has(written))
266
192
  return false;
267
- const unwrapped = unwrapLauncher(toks.folded.slice(from), allow);
268
- return unwrapped.kind === "refused" || (unwrapped.kind === "reader" && unwrapped.head === "cd");
193
+ return pos.refusal !== undefined || pos.program?.basename === "cd";
269
194
  }
270
- const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi)?sh|eval|source|\.|parallel)$/;
271
- const ARGV_REENTRY_PROGRAM_RE = /^xargs$/;
195
+ const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
272
196
  export function refusedCommandMayRead(command, allow, boundary) {
273
197
  return !refusedCommandExempt(command, allow, boundary, 0);
274
198
  }
@@ -276,245 +200,49 @@ const STDIN_ONLY_READER_FLAGS = new Map([
276
200
  ["cat", /^-[AbeEnstTuv]+$/],
277
201
  ["wc", /^-[clmwL]+$/],
278
202
  ]);
279
- const SUBSTITUTION_PLACEHOLDER = "\u0000subst";
280
- function withoutHeredocBodies(text) {
281
- const lines = text.split("\n");
282
- const out = [];
283
- const expandedBodies = [];
284
- let terminator;
285
- let expanded = false;
286
- let body = [];
287
- for (const line of lines) {
288
- if (terminator !== undefined) {
289
- if (line.trim() === terminator) {
290
- if (expanded)
291
- expandedBodies.push(body.join("\n"));
292
- terminator = undefined;
293
- body = [];
294
- }
295
- else
296
- body.push(line);
297
- continue;
298
- }
299
- out.push(line);
300
- const header = heredocHeaderOf(line);
301
- if (header === null)
302
- return undefined;
303
- if (header !== undefined) {
304
- terminator = header.word;
305
- expanded = !header.quoted;
306
- }
307
- }
308
- return terminator === undefined ? { outer: out.join("\n"), expandedBodies } : undefined;
309
- }
310
- function heredocHeaderOf(line) {
311
- let inSingle = false;
312
- let inDouble = false;
313
- for (let i = 0; i < line.length; i++) {
314
- const ch = line[i];
315
- if (ch === "\\" && !inSingle) {
316
- i++;
317
- continue;
318
- }
319
- if (ch === "'" && !inDouble) {
320
- inSingle = !inSingle;
321
- continue;
322
- }
323
- if (ch === '"' && !inSingle) {
324
- inDouble = !inDouble;
325
- continue;
326
- }
327
- if (inSingle || inDouble)
328
- continue;
329
- if (ch === "#")
330
- return undefined;
331
- if (ch === "<" && line[i + 1] === "<") {
332
- if (line[i + 2] === "<") {
333
- i += 2;
334
- continue;
335
- }
336
- const m = /^<<-?\s*([^\s;|&<>()]+)/.exec(line.slice(i));
337
- if (m === null)
338
- return null;
339
- const word = m[1];
340
- if (/[$`]/.test(word))
341
- return null;
342
- const delimiter = word.replace(/\\(.)/g, "$1").replace(/["']/g, "");
343
- if (delimiter === "")
344
- return null;
345
- return { word: delimiter, quoted: /["'\\]/.test(word) };
346
- }
347
- }
348
- return undefined;
349
- }
350
- function splitSubstitutions(text, quotesAreLiteral = false) {
351
- let outer = "";
352
- const bodies = [];
353
- let i = 0;
354
- let inSingle = false;
355
- let inDouble = false;
356
- while (i < text.length) {
357
- const ch = text[i];
358
- if (ch === "\\" && !inSingle) {
359
- outer += text.slice(i, i + 2);
360
- i += 2;
361
- continue;
362
- }
363
- if (ch === "'" && !inDouble && !quotesAreLiteral) {
364
- inSingle = !inSingle;
365
- outer += ch;
366
- i++;
367
- continue;
368
- }
369
- if (ch === '"' && !inSingle && !quotesAreLiteral) {
370
- inDouble = !inDouble;
371
- outer += ch;
372
- i++;
373
- continue;
374
- }
375
- if (inSingle) {
376
- outer += ch;
377
- i++;
378
- continue;
379
- }
380
- if (ch === "`") {
381
- const close = text.indexOf("`", i + 1);
382
- if (close < 0)
383
- return undefined;
384
- bodies.push(text.slice(i + 1, close));
385
- outer += SUBSTITUTION_PLACEHOLDER;
386
- i = close + 1;
387
- continue;
388
- }
389
- if ((ch === "$" || ch === "<" || ch === ">") && text[i + 1] === "(") {
390
- let depth = 0;
391
- let j = i + 1;
392
- let quoted;
393
- for (; j < text.length; j++) {
394
- const c = text[j];
395
- if (quoted !== undefined) {
396
- if (c === "\\" && quoted === '"')
397
- j++;
398
- else if (c === quoted)
399
- quoted = undefined;
400
- continue;
401
- }
402
- if (c === "'" || c === '"') {
403
- quoted = c;
404
- continue;
405
- }
406
- if (c === "(")
407
- depth++;
408
- else if (c === ")") {
409
- depth--;
410
- if (depth === 0)
411
- break;
412
- }
413
- }
414
- if (j >= text.length)
415
- return undefined;
416
- bodies.push(text.slice(i + 2, j));
417
- outer += SUBSTITUTION_PLACEHOLDER;
418
- i = j + 1;
419
- continue;
420
- }
421
- outer += ch;
422
- i++;
423
- }
424
- if (inSingle)
425
- return undefined;
426
- return { outer, bodies };
203
+ function whitelistSegment(segment) {
204
+ const tokens = [...segment.tokens];
205
+ for (let k = 0; k + 2 < tokens.length; k++) {
206
+ const [a, b, c] = [tokens[k], tokens[k + 1], tokens[k + 2]];
207
+ if (a.kind === "word" && b.kind === "group" && b.op === "(" && c.kind === "group" && c.op === ")" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(a.folded))
208
+ tokens.splice(k, 3);
209
+ }
210
+ const words = tokens.filter((t) => t.kind === "word" && !(t.raw === t.folded && (t.folded === "{" || t.folded === "}")));
211
+ let from = 0;
212
+ while (from < words.length && SHELL_BLOCK_WORDS.has(words[from].folded))
213
+ from++;
214
+ return {
215
+ words: { folded: words.slice(from).map((w) => w.folded), raw: words.slice(from).map((w) => w.raw) },
216
+ redirects: tokens.filter((t) => t.kind === "redirect"),
217
+ };
427
218
  }
428
219
  function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside = false) {
429
220
  if (depth > 4)
430
221
  return false;
431
- const stripped = withoutHeredocBodies(command);
432
- if (stripped === undefined)
433
- return false;
434
- if (!whitelistReadsTheSameWords(command, stripped.outer))
222
+ const lexed = lexShellText(command, { mode: "lenient" });
223
+ if ("reject" in lexed)
435
224
  return false;
436
- const split = splitSubstitutions(stripped.outer);
437
- if (split === undefined)
438
- return false;
439
- const bodies = [...split.bodies];
440
- for (const expandedBody of stripped.expandedBodies) {
441
- const inner = splitSubstitutions(expandedBody, true);
442
- if (inner === undefined)
443
- return false;
444
- bodies.push(...inner.bodies);
445
- }
446
- const fold = (w) => w.replace(/\\(.)/g, "$1").replace(/["']/g, "");
447
- const programOf = (w) => {
448
- const inner = fold(w);
449
- return inner.includes("/") ? inner.slice(inner.lastIndexOf("/") + 1) : inner;
450
- };
451
- const segments = split.outer.split(/\n|;|&&|\|\||\||(?<![>&\d])&(?![>&])/).map((segment) => {
452
- const toks = [];
453
- for (const word of segment.trim().split(/\s+/)) {
454
- let rest = word;
455
- while (rest !== "") {
456
- const m = /^(\d*(?:<<<|<<|<|>>|>)(?:&\d*)?|&>>?)(.*)$/.exec(rest);
457
- if (m !== null) {
458
- toks.push(m[1].replace(/^\d+/, ""));
459
- rest = m[2];
460
- continue;
461
- }
462
- const at = rest.search(/\d*(?:<<<|<<|<|>>|>)|&>/);
463
- if (at <= 0) {
464
- toks.push(rest);
465
- break;
466
- }
467
- toks.push(rest.slice(0, at));
468
- rest = rest.slice(at);
469
- }
470
- }
471
- const commentAt = toks.findIndex((t) => t.startsWith("#"));
472
- return (commentAt >= 0 ? toks.slice(0, commentAt) : toks)
473
- .map((t) => (/^[A-Za-z_][A-Za-z0-9_]*\(\)$/.test(t) ? "{" : t.replace(/^[({]+|[)}]+$/g, "")))
474
- .filter((t) => t !== "");
475
- });
476
- const headIndex = (toks) => {
477
- let i = 0;
478
- while (i < toks.length) {
479
- const w = fold(toks[i]);
480
- if (ASSIGNMENT_WORD.test(w) || SHELL_CONTROL_PREFIXES.has(w) || SHELL_BLOCK_WORDS.has(w) || /^[{}]$/.test(w)) {
481
- i++;
482
- continue;
483
- }
484
- if (/^(?:<<<|<<|<|>>?|&>>?)$/.test(w)) {
485
- i += 2;
486
- continue;
487
- }
488
- if (/^(?:>>?|<)&\d*$/.test(w)) {
489
- i++;
225
+ const bodies = [...lexed.substitutions];
226
+ const segments = [];
227
+ for (const segment of lexed.segments) {
228
+ for (const t of segment.tokens) {
229
+ if (t.kind !== "heredoc" || t.quoted)
490
230
  continue;
491
- }
492
- break;
231
+ const inner = substitutionsInHeredocBody(t.body);
232
+ if (inner === undefined)
233
+ return false;
234
+ for (const body of inner)
235
+ bodies.push(body);
493
236
  }
494
- return i;
495
- };
496
- const runProgramOf = (toks, i) => {
497
- let head = programOf(toks[i] ?? "");
498
- let guard = 0;
499
- while (COMMAND_LAUNCHERS.has(head) && guard++ < 8) {
500
- let j = i + 1;
501
- while (j < toks.length && (ASSIGNMENT_WORD.test(fold(toks[j])) || /^\d+[smhd]?$/.test(fold(toks[j]))))
502
- j++;
503
- const next = toks[j];
504
- if (next === undefined || fold(next).startsWith("-"))
505
- return undefined;
506
- i = j;
507
- head = programOf(next);
508
- }
509
- return head;
510
- };
237
+ segments.push(whitelistSegment(segment));
238
+ }
511
239
  const baseMoved = baseMovedOutside ||
512
- segments.some((toks) => {
513
- const i = headIndex(toks);
514
- if (i >= toks.length)
515
- return false;
516
- const head = runProgramOf(toks, i);
517
- return head === undefined || head === "cd" || UNMODELLED_BASE_MOVERS.has(head);
240
+ segments.some(({ words }) => {
241
+ const pos = readProgramPosition(words);
242
+ if (pos.refusal !== undefined)
243
+ return true;
244
+ const runs = pos.program?.basename;
245
+ return runs === "cd" || (runs !== undefined && UNMODELLED_BASE_MOVERS.has(runs));
518
246
  });
519
247
  for (const body of bodies) {
520
248
  const walked = classifyCompoundReadonlyDetailed(body, allow, boundary);
@@ -525,33 +253,22 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
525
253
  if (!refusedCommandExempt(body, allow, boundary, depth + 1, baseMoved))
526
254
  return false;
527
255
  }
528
- const stdinFileJudgedInside = (raw) => {
529
- if (raw === undefined || raw === "" || raw.includes(SUBSTITUTION_PLACEHOLDER) || UNREADABLE_WHITELIST_WORD.test(raw))
256
+ const stdinFileJudgedInside = (word) => {
257
+ if (word === undefined || word === "" || word.includes(SUBSTITUTION_PLACEHOLDER) || UNREADABLE_WHITELIST_WORD.test(word))
530
258
  return false;
531
259
  if (boundary === undefined || baseMoved)
532
260
  return false;
533
- const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], fold(raw), boundary.homeDir);
261
+ const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], word, boundary.homeDir);
534
262
  if (resolved === undefined || boundary.denyMatch?.(resolved) != null)
535
263
  return false;
536
264
  return withinAnyRoot(boundary.roots, resolved);
537
265
  };
538
- const unlistedSegmentJudgedInside = (toks, from) => {
266
+ const unlistedSegmentJudgedInside = (words, from) => {
539
267
  if (boundary === undefined)
540
268
  return true;
541
269
  let endOfOptions = false;
542
- for (let j = from; j < toks.length; j++) {
543
- const t = toks[j];
544
- if (t === "<<<" || t === "<<" || t === "<") {
545
- j++;
546
- continue;
547
- }
548
- if (/^(?:>>?|&>>?)$/.test(t)) {
549
- j++;
550
- continue;
551
- }
552
- if (/^(?:>>?|<)&\d*$/.test(t))
553
- continue;
554
- const word = fold(t);
270
+ for (let j = from; j < words.folded.length; j++) {
271
+ const word = words.folded[j];
555
272
  if (j > from && !endOfOptions && word === "--") {
556
273
  endOfOptions = true;
557
274
  continue;
@@ -570,40 +287,49 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
570
287
  }
571
288
  return true;
572
289
  };
573
- for (const toks of segments) {
574
- for (let j = 0; j < toks.length; j++)
575
- if (toks[j] === "<" && !stdinFileJudgedInside(toks[j + 1]))
290
+ for (const { words, redirects } of segments) {
291
+ for (const r of redirects)
292
+ if (r.op === "<" && !stdinFileJudgedInside(r.target?.folded))
576
293
  return false;
577
- let i = headIndex(toks);
578
- if (i >= toks.length)
579
- continue;
580
- const headWord = fold(toks[i]);
581
- if (headWord.includes(SUBSTITUTION_PLACEHOLDER) || /[$`]/.test(headWord))
294
+ const pos = readProgramPosition(words);
295
+ if (pos.refusal !== undefined)
582
296
  return false;
583
- let head = programOf(headWord);
584
- if (COMMAND_LAUNCHERS.has(head)) {
585
- const wrapped = runProgramOf(toks, i);
586
- if (wrapped === undefined || /[$`]/.test(wrapped) || SHELL_REENTRY_PROGRAM_RE.test(wrapped))
297
+ for (const k of words.folded.keys()) {
298
+ const word = words.folded[k];
299
+ if (ASSIGNMENT_WORD.test(word) && /\$\(|`/.test(word) && (pos.assignments.includes(k) || pos.launchers.some((l) => l.index < k)))
587
300
  return false;
588
- const unwrapped = unwrapLauncher(toks.slice(i).map(fold), allow);
589
- if (unwrapped.kind === "refused")
301
+ }
302
+ const program = pos.program;
303
+ const listed = (name) => allow.has(name) || name === "cd";
304
+ if (program === undefined) {
305
+ if (pos.launchers.length === 0)
306
+ continue;
307
+ if (argvReentryUnjudged(pos, NO_PATH_OPERAND_COMMANDS))
590
308
  return false;
591
- if (argvReentryUnjudged(unwrapped))
309
+ if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index))
592
310
  return false;
593
- if (unwrapped.kind === "unlisted") {
594
- if (!unlistedSegmentJudgedInside(toks, i))
311
+ continue;
312
+ }
313
+ if (program.name.includes(SUBSTITUTION_PLACEHOLDER) || /[$`]/.test(program.name))
314
+ return false;
315
+ if (argumentAsProgramRefusal(program.basename, program.basename, words.folded.slice(program.index + 1)) !== undefined)
316
+ return false;
317
+ if (SHELL_REENTRY_PROGRAM_RE.test(program.basename))
318
+ return false;
319
+ if (pos.launchers.length > 0) {
320
+ if (argvReentryUnjudged(pos, NO_PATH_OPERAND_COMMANDS))
321
+ return false;
322
+ if (!listed(program.basename)) {
323
+ if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index))
595
324
  return false;
596
325
  continue;
597
326
  }
598
- head = unwrapped.head;
599
- i += unwrapped.index;
600
327
  }
601
- if (SHELL_REENTRY_PROGRAM_RE.test(head))
602
- return false;
328
+ const head = program.basename;
603
329
  if (head === "cd" || UNMODELLED_BASE_MOVERS.has(head))
604
330
  continue;
605
331
  if (!allow.has(head)) {
606
- if (!unlistedSegmentJudgedInside(toks, i))
332
+ if (!unlistedSegmentJudgedInside(words, program.index))
607
333
  return false;
608
334
  continue;
609
335
  }
@@ -612,110 +338,18 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
612
338
  const flags = STDIN_ONLY_READER_FLAGS.get(head);
613
339
  if (flags === undefined)
614
340
  return false;
615
- for (let j = i + 1; j < toks.length; j++) {
616
- const t = toks[j];
617
- if (t === "<<<" || t === "<<" || t === "<") {
618
- j++;
619
- continue;
620
- }
621
- if (/^(?:>>?|&>>?)$/.test(t)) {
622
- j++;
623
- continue;
624
- }
625
- if (/^(?:>>?|<)&\d*$/.test(t))
626
- continue;
627
- if (flags.test(fold(t)))
628
- continue;
629
- return false;
341
+ for (let j = program.index + 1; j < words.folded.length; j++) {
342
+ if (!flags.test(words.folded[j]))
343
+ return false;
630
344
  }
631
345
  }
632
346
  return true;
633
347
  }
634
- function unwrapLauncher(folded, allow) {
635
- let argvFromStdin = ARGV_REENTRY_PROGRAM_RE.test(programBasename(folded[0] ?? ""));
636
- let lastReentry = argvFromStdin ? 0 : -1;
637
- for (let i = 1; i < folded.length; i++) {
638
- const t = folded[i];
639
- if (ASSIGNMENT_WORD.test(t) || /^\d+[smhd]?$/.test(t))
640
- continue;
641
- const wordBasename = programBasename(t);
642
- if (ARGV_REENTRY_PROGRAM_RE.test(wordBasename)) {
643
- argvFromStdin = true;
644
- lastReentry = i;
645
- continue;
646
- }
647
- if (COMMAND_LAUNCHERS.has(wordBasename))
648
- continue;
649
- if (t.startsWith("-"))
650
- return { kind: "refused", reason: `"${folded[0]}" is given an option before the program it runs — which program runs is not readable without the launcher's option table ${NOT_AUTO_ALLOWED}` };
651
- const head = wordBasename;
652
- if (SHELL_CONTROL_PREFIXES.has(head) || SHELL_BLOCK_WORDS.has(head))
653
- return { kind: "refused", reason: `"${folded[0]}" is given shell control syntax ("${head}") where the program it runs would be ${NOT_AUTO_ALLOWED}` };
654
- return allow.has(head) || head === "cd" ? { kind: "reader", index: i, head, argvFromStdin } : { kind: "unlisted", head, index: i, argvFromStdin };
655
- }
656
- return { kind: "unlisted", argvFromStdin, runsDefaultProgram: lastReentry === folded.length - 1 };
657
- }
658
- const LAUNCHER_OPTION_GRAMMAR = new Map([
659
- ["time", {
660
- plain: new Set(["-p", "-a", "-v", "-q", "--portability", "--append", "--verbose", "--quiet", "--help", "--version"]),
661
- valued: new Set(["-f", "-o", "--format", "--output"]),
662
- }],
663
- ]);
664
- function launcherOptionSpan(launcher, folded, i) {
665
- const row = launcher === undefined ? undefined : LAUNCHER_OPTION_GRAMMAR.get(launcher);
666
- const tok = folded[i];
667
- if (row === undefined || tok === undefined)
668
- return 0;
669
- if (row.plain.has(tok))
670
- return 1;
671
- return row.valued.has(tok) && folded[i + 1] !== undefined ? 2 : 0;
672
- }
673
- const ARGV_REENTRY_DEFAULT_PROGRAM = "echo";
674
- function argvReentryUnjudged(unwrapped) {
675
- if (!unwrapped.argvFromStdin)
676
- return false;
677
- const runs = unwrapped.head ?? (unwrapped.runsDefaultProgram === true ? ARGV_REENTRY_DEFAULT_PROGRAM : undefined);
678
- return runs === undefined || !NO_PATH_OPERAND_COMMANDS.has(runs);
679
- }
680
348
  function tokenizeSegment(segment) {
681
- const raw = splitWordsQuoteAware(segment);
682
- return { folded: raw.map((r) => { const f = foldQuoteRemovalToken(r); return tildeIsLiteral(r) ? literalTildeSpelling(f) : f; }), raw };
683
- }
684
- function literalTildeSpelling(word) {
685
- return word.startsWith("~") ? `./${word}` : word;
686
- }
687
- function tildeIsLiteral(raw) {
688
- if (!raw.startsWith("~"))
689
- return true;
690
- for (let i = 1; i < raw.length; i++) {
691
- const ch = raw[i];
692
- if (ch === '"' || ch === "'")
693
- return true;
694
- if (ch === "/")
695
- return false;
696
- }
697
- return false;
698
- }
699
- function splitWordsQuoteAware(segment) {
700
- const s = segment.trim();
701
- const mask = quoteMask(s);
702
- if (!mask.balanced)
703
- return s.split(/\s+/).filter((t) => t.length > 0);
704
- const out = [];
705
- let cur = "";
706
- for (let i = 0; i < s.length; i++) {
707
- const ch = s[i];
708
- if (!mask.quoted[i] && /\s/.test(ch)) {
709
- if (cur.length > 0)
710
- out.push(cur);
711
- cur = "";
712
- continue;
713
- }
714
- cur += ch;
715
- }
716
- if (cur.length > 0)
717
- out.push(cur);
718
- return out;
349
+ const lexed = lexShellText(segment, { mode: "strict", connectors: "text" });
350
+ if ("reject" in lexed)
351
+ return { folded: [], raw: [] };
352
+ return wordsOf(lexed.segments[0]);
719
353
  }
720
354
  function hasUnquotedExpansionMetachar(rawToken) {
721
355
  let open;
@@ -1159,14 +793,259 @@ const SCRIPT_SLOT_VERBS = new Map([
1159
793
  return readings.every((reading) => reading === "unparsed");
1160
794
  }],
1161
795
  ]);
796
+ const INLINE_PROGRAM_TEXT = "runs its argument as an interpreted program";
797
+ const INLINE_PROGRAM_STDIN = "reads the program it runs from its standard input";
798
+ const INTERPRETER_BANNER_OPTIONS = new Set(["--version", "--help", "--usage", "-version", "-help"]);
799
+ const BLANK_INLINE = {
800
+ longText: new Set(), longFile: new Set(), longNamesProgram: new Set(), longSupplemental: new Set(),
801
+ longValue: new Set(), longPlain: new Set(), fileOptionEndsOptions: false,
802
+ textLetters: "", fileLetters: "", separatedValueLetters: "", attachedValueLetters: "",
803
+ plainLetters: "", bannerLetters: "", interactiveLetters: "",
804
+ programOperand: { at: 0, isText: false },
805
+ };
806
+ const PERL_INLINE = {
807
+ ...BLANK_INLINE, textLetters: "eEmM", separatedValueLetters: "I", attachedValueLetters: "iCFxDV",
808
+ plainLetters: "nplawcsStTUWX", bannerLetters: "vVh", stdinWhenUnnamed: true,
809
+ };
810
+ const RUBY_INLINE = {
811
+ ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "IrECFKT", plainLetters: "nplawcsSU",
812
+ bannerLetters: "vh", stdinWhenUnnamed: true,
813
+ };
814
+ const PYTHON_INLINE = {
815
+ ...BLANK_INLINE, textLetters: "c", fileLetters: "m", separatedValueLetters: "WXQ",
816
+ plainLetters: "uBEIsSqbdvxOtR", bannerLetters: "Vh?", interactiveLetters: "i",
817
+ fileOptionEndsOptions: true, stdinWhenUnnamed: true,
818
+ };
819
+ const NODE_INLINE = {
820
+ ...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "ep",
821
+ longValue: new Set(["--import", "--loader", "--experimental-loader", "--require", "--env-file", "--conditions", "--max-old-space-size"]),
822
+ longNamesProgram: new Set(["--test"]),
823
+ longPlain: new Set(["--inspect", "--inspect-brk", "--no-warnings", "--trace-warnings", "--watch", "--enable-source-maps", "--experimental-modules", "--no-deprecation", "--check"]),
824
+ separatedValueLetters: "r", plainLetters: "c", bannerLetters: "vh", interactiveLetters: "i",
825
+ stdinWhenUnnamed: true,
826
+ };
827
+ const PHP_INLINE = {
828
+ ...BLANK_INLINE, longText: new Set(["--run", "--process-begin", "--process-code", "--process-end"]),
829
+ textLetters: "rRBE", fileLetters: "fF", separatedValueLetters: "dcz", plainLetters: "nqswl",
830
+ bannerLetters: "vhi", interactiveLetters: "a", stdinWhenUnnamed: true,
831
+ };
832
+ const LUA_INLINE = {
833
+ ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "l", bannerLetters: "vh",
834
+ interactiveLetters: "i", stdinWhenUnnamed: true,
835
+ };
836
+ const TCL_INLINE = { ...BLANK_INLINE, stdinWhenUnnamed: true };
837
+ const EXPECT_INLINE = {
838
+ ...BLANK_INLINE, textLetters: "c", separatedValueLetters: "Db", plainLetters: "dN",
839
+ bannerLetters: "vh", interactiveLetters: "i", stdinWhenUnnamed: true,
840
+ };
841
+ const AWK_INLINE = {
842
+ ...BLANK_INLINE, longText: new Set(["--source"]), textLetters: "e",
843
+ longFile: new Set(["--file", "--exec"]), fileLetters: "fE", separatedValueLetters: "vF",
844
+ bannerLetters: "V", programOperand: { at: 0, isText: true }, stdinWhenUnnamed: false,
845
+ };
846
+ const JQ_INLINE = {
847
+ ...BLANK_INLINE, longFile: new Set(["--from-file"]), fileLetters: "f", separatedValueLetters: "L",
848
+ plainLetters: "nrjcsSaCMRe", bannerLetters: "h", programOperand: { at: 0, isText: true },
849
+ stdinWhenUnnamed: false,
850
+ };
851
+ const BC_INLINE = {
852
+ ...BLANK_INLINE, longText: new Set(["--expression"]), textLetters: "e", plainLetters: "lqsw",
853
+ bannerLetters: "vh", interactiveLetters: "i", programOperand: undefined, stdinWhenUnnamed: true,
854
+ };
855
+ const DC_INLINE = {
856
+ ...BLANK_INLINE, longText: new Set(["--expression"]), textLetters: "e",
857
+ longFile: new Set(["--file"]), fileLetters: "f", bannerLetters: "hV",
858
+ programOperand: undefined, stdinWhenUnnamed: true,
859
+ };
860
+ const SQLITE_INLINE = {
861
+ ...BLANK_INLINE, longText: new Set(["-cmd"]), longSupplemental: new Set(["-init"]),
862
+ programOperand: { at: 1, isText: true }, stdinWhenUnnamed: true,
863
+ };
864
+ const PSQL_INLINE = {
865
+ ...BLANK_INLINE, longText: new Set(["--command"]), textLetters: "c",
866
+ longFile: new Set(["--file"]), fileLetters: "f", separatedValueLetters: "hpUdoF",
867
+ plainLetters: "aeEqtxXwW", bannerLetters: "lV?", programOperand: undefined, stdinWhenUnnamed: true,
868
+ };
869
+ const OSASCRIPT_INLINE = {
870
+ ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "ls", plainLetters: "i", bannerLetters: "h",
871
+ stdinWhenUnnamed: true,
872
+ };
873
+ const RSCRIPT_INLINE = {
874
+ ...BLANK_INLINE, textLetters: "e", bannerLetters: "h", stdinWhenUnnamed: false,
875
+ };
876
+ const JULIA_INLINE = {
877
+ ...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "eE",
878
+ separatedValueLetters: "JLp", plainLetters: "q", bannerLetters: "vh", interactiveLetters: "i",
879
+ stdinWhenUnnamed: true,
880
+ };
881
+ const GHCI_INLINE = {
882
+ ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "io", programOperand: undefined,
883
+ stdinWhenUnnamed: true,
884
+ };
885
+ const GROOVY_INLINE = {
886
+ ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "Dcl", plainLetters: "n",
887
+ bannerLetters: "vh", interactiveLetters: "i", stdinWhenUnnamed: false,
888
+ };
889
+ const BUN_INLINE = {
890
+ ...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "ep",
891
+ separatedValueLetters: "r", bannerLetters: "vh", stdinWhenUnnamed: false,
892
+ };
893
+ const INTERPRETER_INLINE_PROGRAMS = new Map([
894
+ ["perl", PERL_INLINE],
895
+ ["ruby", RUBY_INLINE],
896
+ ["python", PYTHON_INLINE],
897
+ ["node", NODE_INLINE],
898
+ ["nodejs", NODE_INLINE],
899
+ ["php", PHP_INLINE],
900
+ ["lua", LUA_INLINE],
901
+ ["luajit", LUA_INLINE],
902
+ ["tclsh", TCL_INLINE],
903
+ ["wish", TCL_INLINE],
904
+ ["expect", EXPECT_INLINE],
905
+ ["awk", AWK_INLINE],
906
+ ["gawk", AWK_INLINE],
907
+ ["mawk", AWK_INLINE],
908
+ ["nawk", AWK_INLINE],
909
+ ["jq", JQ_INLINE],
910
+ ["bc", BC_INLINE],
911
+ ["dc", DC_INLINE],
912
+ ["sqlite", SQLITE_INLINE],
913
+ ["psql", PSQL_INLINE],
914
+ ["osascript", OSASCRIPT_INLINE],
915
+ ["Rscript", RSCRIPT_INLINE],
916
+ ["julia", JULIA_INLINE],
917
+ ["ghci", GHCI_INLINE],
918
+ ["groovy", GROOVY_INLINE],
919
+ ["bun", BUN_INLINE],
920
+ ]);
921
+ function interpreterKey(name) {
922
+ return name.replace(/\d+(?:\.\d+)*$/, "");
923
+ }
924
+ function inlineProgramClause(model, args) {
925
+ let endOfOptions = false;
926
+ let banner = false;
927
+ let interactive = false;
928
+ let named = false;
929
+ let stdin = false;
930
+ let explained = true;
931
+ let operands = 0;
932
+ scan: for (let i = 0; i < args.length; i++) {
933
+ const arg = args[i];
934
+ if (!endOfOptions && arg === "--") {
935
+ endOfOptions = true;
936
+ continue;
937
+ }
938
+ if (arg === "-") {
939
+ if (model.programOperand?.isText !== true)
940
+ stdin = true;
941
+ continue;
942
+ }
943
+ if (!endOfOptions && arg.startsWith("-")) {
944
+ const eq = arg.indexOf("=");
945
+ const whole = eq < 0 ? arg : arg.slice(0, eq);
946
+ const valueOf = () => (eq < 0 ? args[++i] : arg.slice(eq + 1));
947
+ if (model.longText.has(whole))
948
+ return INLINE_PROGRAM_TEXT;
949
+ if (model.longFile.has(whole)) {
950
+ if (valueOf() === "-") {
951
+ stdin = true;
952
+ continue;
953
+ }
954
+ named = true;
955
+ if (explained && model.fileOptionEndsOptions)
956
+ break scan;
957
+ continue;
958
+ }
959
+ if (model.longNamesProgram.has(whole)) {
960
+ named = true;
961
+ continue;
962
+ }
963
+ if (model.longSupplemental.has(whole)) {
964
+ valueOf();
965
+ continue;
966
+ }
967
+ if (INTERPRETER_BANNER_OPTIONS.has(whole)) {
968
+ banner = true;
969
+ continue;
970
+ }
971
+ if (model.longValue.has(whole)) {
972
+ valueOf();
973
+ continue;
974
+ }
975
+ if (model.longPlain.has(whole))
976
+ continue;
977
+ if (arg.startsWith("--")) {
978
+ if (eq < 0)
979
+ explained = false;
980
+ continue;
981
+ }
982
+ for (let k = 1; k < arg.length; k++) {
983
+ const ch = arg[k];
984
+ if (ch >= "0" && ch <= "9")
985
+ continue;
986
+ if (model.textLetters.includes(ch))
987
+ return INLINE_PROGRAM_TEXT;
988
+ if (model.fileLetters.includes(ch)) {
989
+ const value = k + 1 < arg.length ? arg.slice(k + 1) : args[++i];
990
+ if (value === "-") {
991
+ stdin = true;
992
+ break;
993
+ }
994
+ named = true;
995
+ if (explained && model.fileOptionEndsOptions)
996
+ break scan;
997
+ break;
998
+ }
999
+ if (model.attachedValueLetters.includes(ch)) {
1000
+ if (model.bannerLetters.includes(ch))
1001
+ banner = true;
1002
+ break;
1003
+ }
1004
+ if (model.bannerLetters.includes(ch)) {
1005
+ banner = true;
1006
+ continue;
1007
+ }
1008
+ if (model.interactiveLetters.includes(ch)) {
1009
+ interactive = true;
1010
+ continue;
1011
+ }
1012
+ if (model.separatedValueLetters.includes(ch)) {
1013
+ if (k + 1 >= arg.length)
1014
+ i++;
1015
+ break;
1016
+ }
1017
+ if (!model.plainLetters.includes(ch))
1018
+ explained = false;
1019
+ }
1020
+ continue;
1021
+ }
1022
+ operands++;
1023
+ if (!named && explained && model.programOperand !== undefined && operands === model.programOperand.at + 1) {
1024
+ if (model.programOperand.isText)
1025
+ return INLINE_PROGRAM_TEXT;
1026
+ named = true;
1027
+ break;
1028
+ }
1029
+ }
1030
+ if (stdin || interactive)
1031
+ return INLINE_PROGRAM_STDIN;
1032
+ if (named || banner)
1033
+ return undefined;
1034
+ return model.stdinWhenUnnamed ? INLINE_PROGRAM_STDIN : undefined;
1035
+ }
1162
1036
  function unjudgedProgramTextRefusal(written, clause) {
1163
1037
  return `command "${written}" ${clause}, which this walk does not judge ${NOT_AUTO_ALLOWED}`;
1164
1038
  }
1165
- function scriptSlotRefusal(written, name, args) {
1166
- const recognizer = SCRIPT_SLOT_VERBS.get(name);
1167
- if (recognizer === undefined || !recognizer(args))
1039
+ function argumentAsProgramRefusal(written, name, args) {
1040
+ const slot = SCRIPT_SLOT_VERBS.get(name);
1041
+ if (slot !== undefined && slot(args)) {
1042
+ return unjudgedProgramTextRefusal(written, "names files or runs commands inside its script text");
1043
+ }
1044
+ const interpreter = INTERPRETER_INLINE_PROGRAMS.get(interpreterKey(name));
1045
+ if (interpreter === undefined)
1168
1046
  return undefined;
1169
- return unjudgedProgramTextRefusal(written, "names files or runs commands inside its script text");
1047
+ const clause = inlineProgramClause(interpreter, args);
1048
+ return clause === undefined ? undefined : unjudgedProgramTextRefusal(written, clause);
1170
1049
  }
1171
1050
  function guardedVerbFormReason(name, toks, raw) {
1172
1051
  if (name === "find") {
@@ -1558,13 +1437,8 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
1558
1437
  for (let si = 0; si < segments.length; si++) {
1559
1438
  const toks = tokenizeSegment(segments[si]);
1560
1439
  const folded = toks.folded;
1561
- let from = 0;
1562
- while (from < folded.length && isBareControlWord(toks, from, SHELL_CONTROL_PREFIXES)) {
1563
- const controlWord = programBasename(folded[from] ?? "");
1564
- from++;
1565
- for (let span = launcherOptionSpan(controlWord, folded, from); span > 0; span = launcherOptionSpan(controlWord, folded, from))
1566
- from += span;
1567
- }
1440
+ const pos = readProgramPosition(toks);
1441
+ const from = pos.written ?? folded.length;
1568
1442
  const written = folded[from];
1569
1443
  if (written === undefined)
1570
1444
  return { reason: "empty command" };
@@ -1582,17 +1456,13 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
1582
1456
  const name = pathPrefixed ? parsed.name.slice(parsed.name.lastIndexOf("/") + 1) : parsed.name;
1583
1457
  if (pathPrefixed)
1584
1458
  nameRefusal ??= "the command must be a bare name resolved via PATH (no path prefix)";
1585
- if (name.startsWith("-") && name !== "-") {
1586
- return { reason: `"${folded[0]}" is given an option where the program it runs would be — which program runs is not readable without its option table ${NOT_AUTO_ALLOWED}`, refusedWhole: true };
1587
- }
1588
- const launcherUnwrap = COMMAND_LAUNCHERS.has(name) ? unwrapLauncher(folded.slice(from), allow) : undefined;
1589
- if (launcherUnwrap?.kind === "refused")
1590
- return { reason: launcherUnwrap.reason, refusedWhole: true };
1591
- const runs = launcherUnwrap === undefined ? { head: name, index: 0 } : { head: launcherUnwrap.head, index: launcherUnwrap.index };
1592
- if (runs?.head !== undefined && runs.index !== undefined) {
1593
- const slotRefusal = scriptSlotRefusal(runs.head, runs.head, folded.slice(from + runs.index + 1));
1594
- if (slotRefusal !== undefined)
1595
- return { reason: slotRefusal, refusedWhole: true };
1459
+ if (pos.refusal !== undefined)
1460
+ return { reason: programPositionRefusalSentence(pos.refusal), refusedWhole: true };
1461
+ const program = pos.program;
1462
+ if (program !== undefined) {
1463
+ const familyRefusal = argumentAsProgramRefusal(program.basename, program.basename, folded.slice(program.index + 1));
1464
+ if (familyRefusal !== undefined)
1465
+ return { reason: familyRefusal, refusedWhole: true };
1596
1466
  }
1597
1467
  if (allow.has(name)) {
1598
1468
  if (pathPrefixed || from > 0)
@@ -1604,20 +1474,18 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
1604
1474
  nameRefusal ??= `command "${parsed.name}" is not in the read-only allowlist`;
1605
1475
  if (SHELL_REENTRY_PROGRAM_RE.test(name))
1606
1476
  return { reason: unjudgedProgramTextRefusal(parsed.name, "runs its argument as shell text"), refusedWhole: true };
1607
- if (COMMAND_LAUNCHERS.has(name)) {
1608
- const unwrapped = launcherUnwrap ?? unwrapLauncher(folded.slice(from), allow);
1609
- if (unwrapped.kind === "refused")
1610
- return { reason: unwrapped.reason, refusedWhole: true };
1611
- if (unwrapped.kind === "unlisted" && unwrapped.head !== undefined && SHELL_REENTRY_PROGRAM_RE.test(unwrapped.head)) {
1612
- return { reason: `"${written}" runs "${unwrapped.head}", which runs its argument as shell text this walk does not judge ${NOT_AUTO_ALLOWED}`, refusedWhole: true };
1477
+ if (pos.launchers.length > 0) {
1478
+ const listed = program !== undefined && (allow.has(program.basename) || program.basename === "cd");
1479
+ if (program !== undefined && !listed && SHELL_REENTRY_PROGRAM_RE.test(program.basename)) {
1480
+ return { reason: `"${written}" runs "${program.basename}", which runs its argument as shell text this walk does not judge ${NOT_AUTO_ALLOWED}`, refusedWhole: true };
1613
1481
  }
1614
- if (argvReentryUnjudged(unwrapped)) {
1482
+ if (argvReentryUnjudged(pos, NO_PATH_OPERAND_COMMANDS)) {
1615
1483
  return { reason: `"${written}" takes the operands of the program it runs from its standard input, which this walk does not judge ${NOT_AUTO_ALLOWED}`, refusedWhole: true };
1616
1484
  }
1617
- if (unwrapped.kind === "unlisted")
1485
+ if (!listed || program === undefined)
1618
1486
  unlistedSegments.add(si);
1619
1487
  else
1620
- reheaded.set(si, { from: from + unwrapped.index, head: unwrapped.head });
1488
+ reheaded.set(si, { from: program.index, head: program.basename });
1621
1489
  continue;
1622
1490
  }
1623
1491
  unlistedSegments.add(si);
@@ -1949,44 +1817,21 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
1949
1817
  const toks = tokenizeSegment(command);
1950
1818
  if (toks.folded.length === 0)
1951
1819
  return {};
1952
- const head = programBasename(toks.folded[0]);
1953
- let past = 0;
1954
- for (let span = launcherOptionSpan(head, toks.folded, past + 1); span > 0; span = launcherOptionSpan(head, toks.folded, past + 1))
1955
- past += span;
1956
- const chain = past === 0 ? toks.folded : [toks.folded[0], ...toks.folded.slice(past + 1)];
1957
- const runs = COMMAND_LAUNCHERS.has(head) ? unwrapLauncher(chain, new Set()) : { kind: "reader", head, index: 0 };
1958
- if (runs.kind === "refused")
1959
- return { reason: runs.reason, refusedWhole: true };
1960
- if (runs.head !== undefined && runs.index !== undefined) {
1961
- const slotRefusal = scriptSlotRefusal(runs.head, runs.head, toks.folded.slice(runs.index + past + 1));
1962
- if (slotRefusal !== undefined)
1963
- return { reason: slotRefusal, refusedWhole: true };
1820
+ const pos = readProgramPosition(toks);
1821
+ if (pos.refusal !== undefined)
1822
+ return { reason: programPositionRefusalSentence(pos.refusal), refusedWhole: true };
1823
+ if (pos.program !== undefined) {
1824
+ const familyRefusal = argumentAsProgramRefusal(pos.program.basename, pos.program.basename, toks.folded.slice(pos.program.index + 1));
1825
+ if (familyRefusal !== undefined)
1826
+ return { reason: familyRefusal, refusedWhole: true };
1964
1827
  }
1965
1828
  return evaluateReadBoundary([toks], boundary);
1966
1829
  }
1967
1830
  const NOT_GATED = { gated: false, paths: [] };
1968
- const ASSIGNMENT_WORD = /^[A-Za-z_][A-Za-z0-9_]*=/;
1969
1831
  const UNREADABLE_WHITELIST_WORD = /[$`()*?[\]{}~]/;
1970
- function whitelistReadsTheSameWords(command, outsideHeredocBodies) {
1971
- if (/\\\r?\n/.test(command))
1972
- return false;
1973
- if (/\\[<>&|;()]/.test(outsideHeredocBodies))
1974
- return false;
1975
- return !(outsideHeredocBodies.includes("#") && /["']/.test(outsideHeredocBodies));
1976
- }
1977
1832
  const UNMODELLED_BASE_MOVERS = new Set(["pushd", "popd"]);
1978
- const COMMAND_LAUNCHERS = new Set([
1979
- "env", "command", "builtin", "exec", "noglob", "xargs",
1980
- "sudo", "doas", "pkexec", "su", "runuser", "chroot", "setpriv",
1981
- "nohup", "nice", "ionice", "chrt", "taskset", "stdbuf", "timeout", "time", "watch",
1982
- "setsid", "flock", "unshare", "nsenter", "script", "numactl", "prlimit", "systemd-run",
1983
- "strace", "ltrace", "valgrind", "firejail", "bwrap",
1984
- ]);
1985
1833
  export const COMMAND_LAUNCHERS_FOR_TEST = COMMAND_LAUNCHERS;
1986
1834
  const LAUNCHER_SCAN_HEAD = "cat";
1987
- function programBasename(word) {
1988
- return word.includes("/") ? (word.split("/").pop() ?? word) : word;
1989
- }
1990
1835
  export function classifyOutOfRootReadGate(command, allow, boundary) {
1991
1836
  const containmentOnly = {
1992
1837
  roots: boundary.roots,
@@ -2141,7 +1986,7 @@ export function classifyBoundedReadonlyPollLoopDetailed(command, allow, boundary
2141
1986
  if (readSegments.length === 0) {
2142
1987
  return { reason: "the loop body has no read command — a sleep-only loop observes nothing and is not auto-allowed" };
2143
1988
  }
2144
- const bodyHasCd = readSegments.some((seg) => tokensMoveBase(tokenizeSegment(seg), allow));
1989
+ const bodyHasCd = readSegments.some((seg) => tokensMoveBase(tokenizeSegment(seg)));
2145
1990
  const modelled = bodyHasCd ? Array.from({ length: beats }, () => readSegments.join("; ")).join("; ") : readSegments.join("; ");
2146
1991
  const verdict = classifyCompoundReadonlyDetailed(modelled, allow, boundary, { iterated: bodyHasCd });
2147
1992
  if (verdict.reason !== undefined)