@sema-agent/core 7.46.0 → 7.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/dist/core/allow-entry.d.ts +1 -1
  3. package/dist/core/allow-entry.js +4 -5
  4. package/dist/core/ask-origin.d.ts +35 -12
  5. package/dist/core/ask-origin.js +23 -6
  6. package/dist/core/checkpoint-store.d.ts +1 -1
  7. package/dist/core/engine-notice.d.ts +7 -4
  8. package/dist/core/gate-fold.js +2 -2
  9. package/dist/core/gate-lanes.js +2 -1
  10. package/dist/core/gate-outcome.d.ts +4 -2
  11. package/dist/core/gate-outcome.js +2 -2
  12. package/dist/core/mcp-server-spec.d.ts +1 -1
  13. package/dist/core/permission-rule-model.d.ts +12 -6
  14. package/dist/core/permission-rule-model.js +10 -2
  15. package/dist/core/permission-rules.d.ts +4 -5
  16. package/dist/core/permission-rules.js +28 -15
  17. package/dist/core/runner/gate-exit.d.ts +4 -1
  18. package/dist/core/runner/gate-exit.js +5 -1
  19. package/dist/core/runner/park-commit.d.ts +1 -1
  20. package/dist/core/runner/prepare-hands-readface.js +5 -5
  21. package/dist/core/runner/prepare-policy-chain.js +2 -1
  22. package/dist/core/safety-axis-vocab.d.ts +1 -1
  23. package/dist/core/safety-merge-corpus.d.ts +1 -1
  24. package/dist/core/session-policy-store.d.ts +6 -3
  25. package/dist/core/session-policy-store.js +28 -10
  26. package/dist/core/shell-wrapper-projection.js +1 -1
  27. package/dist/core/shell-wrapper-table.d.ts +8 -8
  28. package/dist/core/shell-wrapper-table.js +3 -3
  29. package/dist/core/tool-catalog-entries.js +3 -2
  30. package/dist/core/tool-face.d.ts +10 -1
  31. package/dist/core/tool-name-aliases.d.ts +9 -8
  32. package/dist/core/tool-name-aliases.js +2 -0
  33. package/dist/core/tool-name-readings.d.ts +8 -0
  34. package/dist/core/tool-name-readings.js +2 -0
  35. package/dist/core/tool-policy.d.ts +10 -9
  36. package/dist/core/tool-policy.js +6 -7
  37. package/dist/core/tool-registry.d.ts +9 -0
  38. package/dist/core/tool-registry.js +10 -0
  39. package/dist/core/write-protect.d.ts +10 -3
  40. package/dist/core/write-protect.js +11 -1
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/orchestration/workflow.js +2 -2
  44. package/dist/prompt-assembly/types.d.ts +1 -1
  45. package/dist/shell/lex.d.ts +15 -0
  46. package/dist/shell/lex.js +86 -15
  47. package/dist/tools/fs/bash-program-position.d.ts +16 -6
  48. package/dist/tools/fs/bash-program-position.js +18 -12
  49. package/dist/tools/fs/index.d.ts +3 -2
  50. package/dist/tools/fs/index.js +2 -2
  51. package/dist/tools/fs/program-read-model.d.ts +84 -12
  52. package/dist/tools/fs/program-read-model.js +105 -34
  53. package/dist/tools/fs/program-read-model.rows.js +26 -7
  54. package/dist/tools/fs/read-deny-home.d.ts +20 -1
  55. package/dist/tools/fs/read-deny-home.js +28 -7
  56. package/dist/tools/fs/read-deny.d.ts +13 -6
  57. package/dist/tools/fs/read-deny.js +12 -3
  58. package/dist/tools/fs/removal-targets.js +24 -5
  59. package/dist/tools/fs/shell-read-argv.d.ts +6 -8
  60. package/dist/tools/fs/shell-read-argv.js +5 -5
  61. package/dist/tools/fs/shell-read-bindings.d.ts +71 -19
  62. package/dist/tools/fs/shell-read-bindings.js +190 -75
  63. package/dist/tools/fs/shell-read-establish.js +6 -7
  64. package/dist/tools/fs/shell-read-plan.d.ts +3 -15
  65. package/dist/tools/fs/shell-read-plan.js +126 -129
  66. package/dist/tools/fs/shell-read-start.d.ts +57 -0
  67. package/dist/tools/fs/shell-read-start.js +92 -0
  68. package/dist/tools/fs/shell-read-word.d.ts +68 -8
  69. package/dist/tools/fs/shell-read-word.js +106 -12
  70. package/dist/tools/fs/shell-read-write-targets.js +4 -3
  71. package/dist/tools/fs/shell-reentry-text.d.ts +103 -3
  72. package/dist/tools/fs/shell-reentry-text.js +122 -4
  73. package/dist/tools/monitor.d.ts +3 -3
  74. package/package.json +1 -1
  75. package/test/export-surface.snapshot.json +9 -3
  76. package/wire-changes/core@7.47.0.json +412 -0
  77. package/wire-changes/core@7.48.0.json +252 -0
@@ -1,12 +1,13 @@
1
1
  import { lexShell, PARSE_FAILURE_SENTENCE, substitutionsInHeredocBody, literalTextOf, literalTildeSpelling, } from "../../shell/lex.js";
2
- import { readOfText, readsOf, rewritten, spelledWithHome, isArithmeticBody, unreadableSentenceOf } from "./shell-read-word.js";
2
+ import { programTextOf, readOfText, readsOf, rewritten, segmentReadingsOf, settledFieldsOf, spelledWithHome, isArithmeticBody, unreadableSentenceOf } from "./shell-read-word.js";
3
3
  import { assignmentArithmetic, bare, listedVerbCandidates, pathSubcommandOf, programTextSentence, pushAssignment, roleReads, subscriptOnly, unlistedWords } from "./shell-read-argv.js";
4
- import { ARITHMETIC_TEST_OPERATORS, BindingThread, arithmeticSubstitutions, isArithmeticCommand, bindingEffectOf, parameterBodySubstitutions, projectSegment, valueOf, } from "./shell-read-bindings.js";
5
- import { programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, } from "./bash-program-position.js";
4
+ import { ARITHMETIC_TEST_OPERATORS, BindingThread, arithmeticSubstitutions, isArithmeticCommand, bindingEffectOf, loopListHeadAt, parameterBodySubstitutions, projectSegment, valueOf, } from "./shell-read-bindings.js";
5
+ import { isShellSyntaxWord, programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, } from "./bash-program-position.js";
6
6
  import { POLL_LOOP_MAX_BEATS, attachedOptionPayloads, resolveOperandLexically, isPathShapedToken, programTextSlotFiles, } from "./bash-readonly-classifier.js";
7
- import { BLOCK_WORD_NAMES, commandLineOptionSpellings, declaredAttributes, isDeclarationBuiltin, isInertBuiltin, launcherOptionPathRole, launcherOptionStep, readOptionRole, programLanguageOf, programTextOpensOf, selectedOptions, movesBaseUnfollowed, opensNoOperand, programDelivery, programRow, readArgRoles, runsShellText, } from "./program-read-model.js";
7
+ import { BLOCK_WORD_NAMES, commandLineOf, commandLineOptionSpellings, declaredAttributes, isDeclarationBuiltin, isInertBuiltin, launcherOptionPathRole, launcherOptionStep, readOptionRole, programLanguageOf, programTextOpensOf, selectedOptions, movesBaseUnfollowed, opensNoOperand, programDelivery, programRow, readArgRoles, runsShellText, } from "./program-read-model.js";
8
8
  import { isAbsolutePathForm } from "./safety.js";
9
- import { codeBodies, pipedTextOf, reentryText, scriptFileAt, stdinTextsOf } from "./shell-reentry-text.js";
9
+ import { codeBodies, launchedInput, pipedTextOf, reentryText, scriptFileAt, stdinTextsOf } from "./shell-reentry-text.js";
10
+ import { plannedOnce, rebasePlan, startedAt, stdinFeeds } from "./shell-read-start.js";
10
11
  export const READ_CANDIDATE_PROVENANCES = Object.freeze([
11
12
  "operand",
12
13
  "option_value",
@@ -22,6 +23,7 @@ export const READ_CANDIDATE_PROVENANCES = Object.freeze([
22
23
  "object_path",
23
24
  ]);
24
25
  const NESTING_MAX = 4;
26
+ const LOOP_END = new Set(["done"]), COPROC = new Set(["coproc"]);
25
27
  const BASE_CANDIDATE_MAX = 256;
26
28
  export function planShellReads(command, decl, depth = 0, start) {
27
29
  const shape = { grouped: false, backgrounded: false, strayRedirection: false };
@@ -53,19 +55,19 @@ export function planShellReads(command, decl, depth = 0, start) {
53
55
  }
54
56
  const names = new BindingThread(lexed.segments, named, initial, boundAnywhere, loopEndOf, start?.homeTouched === true, start?.inherited);
55
57
  const laterStart = { initial: (name) => (name === "HOME" && !boundAnywhere("HOME") ? declaredHome : "unknown"), boundAnywhere, homeTouched: boundAnywhere("HOME"), inherited: names.inheritedAt() };
56
- const nestedPlanner = (table) => (text, kind) => planShellReads(text, decl, depth + 1, kind === "substitution" ? { initial: (name) => table.stateOf(name), boundAnywhere, homeTouched: table.homeTouched, inherited: table.inherited } : laterStart);
58
+ const nestedPlanner = (table) => (text, kind, at) => planShellReads(text, decl, depth + 1, kind === "substitution" ? { initial: (name) => table.stateOf(name), boundAnywhere, homeTouched: table.homeTouched, inherited: table.inherited } : startedAt(laterStart, at));
57
59
  const empty = lexed.segments.map((segment, si) => segmentIsEmpty(segment, projected[si]));
58
60
  const facts = projected.map(() => undefined);
59
61
  const tables = projected.map(() => undefined);
62
+ const fedAt = stdinFeeds(lexed.segments, start?.stdin ?? [], (si) => (facts[si - 1] === undefined ? [projected[si - 1].argv] : (facts[si - 1].readings ?? [facts[si - 1]]).map((f) => f.argv)).map((argv) => pipedTextOf(lexed.segments[si - 1], argv)));
60
63
  const factsAt = (si, table) => {
61
64
  tables[si] ??= table;
62
- const piped = si > 0 && lexed.segments[si].connector === "|" ? pipedTextOf(lexed.segments[si - 1], projected[si - 1].argv) : undefined;
63
- return (facts[si] ??= segmentFacts(lexed.segments[si], projected[si], decl, table, nestedPlanner(table), piped));
65
+ return (facts[si] ??= segmentFacts(lexed.segments[si], projected[si], decl, table, nestedPlanner(table), fedAt(si)));
64
66
  };
65
67
  const basesOf = projected.map(() => new Set());
66
68
  const unmodelled = projected.map(() => false);
67
- let candidates = [{ base: decl.cwd, skippedCd: -1 }];
68
- let baseLost = false;
69
+ let candidates = (start?.bases !== undefined && start.bases !== "unmodelled" ? start.bases : [decl.cwd]).map((base) => ({ base, skippedCd: -1 }));
70
+ let baseLost = start?.bases === "unmodelled";
69
71
  let floorBases;
70
72
  const loseBase = (reachable, floorTarget) => {
71
73
  baseLost = true;
@@ -182,15 +184,15 @@ export function planShellReads(command, decl, depth = 0, start) {
182
184
  }
183
185
  }
184
186
  const planned = facts.map((f) => f);
185
- const inert = depth === 0 && unreadable === undefined && planned.every(segmentOpensNothing);
186
- const settled = inert ? planned.map((f) => ({ ...f, reads: f.reads.filter((c) => c.provenance !== "env_assignment") })) : planned;
187
+ const inert = depth === 0 && unreadable === undefined && planned.every((f) => (f.readings ?? [f]).every(segmentOpensNothing));
188
+ const settled = inert ? planned.map((f) => ({ ...f, reads: f.reads.filter((c) => c.provenance !== "env_assignment"), ...(f.readings !== undefined ? { readings: f.readings.map((g) => ({ ...g, reads: g.reads.filter((c) => c.provenance !== "env_assignment") })) } : {}) })) : planned;
187
189
  shape.grouped = projected.some((p) => p.grouped);
188
- shape.backgrounded = lexed.segments.some((s) => s.connector === "&") || settled.some((f) => f.backgrounds === true);
190
+ shape.backgrounded = lexed.segments.some((s) => s.connector === "&") || settled.some((f) => (f.readings ?? [f]).some((g) => g.backgrounds === true));
189
191
  shape.strayRedirection = projected.some((p) => p.argv.length === 0 && p.redirects.length > 0);
190
192
  const segments = [];
191
193
  for (let si = 0; si < settled.length; si++) {
192
- const f = settled[si];
193
- if (f.empty)
194
+ const threaded = settled[si];
195
+ if (threaded.empty)
194
196
  continue;
195
197
  const bases = unmodelled[si] ? "unmodelled" : [...basesOf[si]];
196
198
  const floor = unmodelled[si] && floorBases !== undefined && floorBases.length > 0 ? floorBases : undefined;
@@ -199,23 +201,26 @@ export function planShellReads(command, decl, depth = 0, start) {
199
201
  const here = c.provenance === "object_path" && bases !== "unmodelled" ? [...new Set(bases.flatMap(ancestorsOf))] : bases;
200
202
  return { ...c, bases: relative ? here : [], ...(relative && floor !== undefined ? { floorBases: floor } : {}) };
201
203
  };
202
- segments.push({
203
- text: f.text,
204
- connector: f.connector,
205
- argv: f.argv,
206
- launcherChain: f.launcherChain,
207
- ...(f.verb !== undefined ? { verb: f.verb } : {}),
208
- ...(f.reentry !== undefined ? { reentry: f.reentry } : {}),
209
- ...(f.unreadProgramText !== undefined ? { unreadProgramText: f.unreadProgramText } : {}),
210
- ...(f.stdin !== undefined ? { stdin: f.stdin } : {}),
211
- declines: f.declines,
212
- reads: dedupe(f.reads).map(withBases),
213
- redirects: f.redirects.map((r) => (r.target === undefined ? { op: r.op, direction: r.direction } : { op: r.op, direction: r.direction, target: withBases(r.target) })),
214
- ...(f.baseMove !== undefined ? { baseMove: { target: typeof f.baseMove.target === "string" ? f.baseMove.target : withBases(f.baseMove.target) } } : {}),
215
- bases,
216
- nested: f.nested.map((n) => rebasePlan(n, decl.cwd, bases)),
217
- reach: reaches[si] ?? { andJoined: false, orBroken: false, moveProvable: false },
218
- });
204
+ const move = threaded.baseMove;
205
+ for (const f of threaded.readings ?? [threaded]) {
206
+ segments.push({
207
+ text: f.text,
208
+ connector: f.connector,
209
+ argv: f.argv,
210
+ launcherChain: f.launcherChain,
211
+ ...(f.verb !== undefined ? { verb: f.verb } : {}),
212
+ ...(f.reentry !== undefined ? { reentry: f.reentry } : {}),
213
+ ...(f.unreadProgramText !== undefined ? { unreadProgramText: f.unreadProgramText } : {}),
214
+ ...(f.stdin !== undefined ? { stdin: f.stdin } : {}),
215
+ declines: f.declines,
216
+ reads: dedupe(f.reads).map(withBases),
217
+ redirects: f.redirects.map((r) => (r.target === undefined ? { op: r.op, direction: r.direction } : { op: r.op, direction: r.direction, target: withBases(r.target) })),
218
+ ...(move !== undefined ? { baseMove: { target: typeof move.target === "string" ? move.target : withBases(move.target) } } : {}),
219
+ bases,
220
+ nested: f.nested.map((n) => (f.elsewhere?.has(n) === true ? n : rebasePlan(n, decl.cwd, bases))),
221
+ reach: reaches[si] ?? { andJoined: false, orBroken: false, moveProvable: false },
222
+ });
223
+ }
219
224
  }
220
225
  return { segments, shape, ...(unreadable !== undefined ? { unreadable } : {}) };
221
226
  }
@@ -232,9 +237,9 @@ function loopShapeOf(p) {
232
237
  const head = p.argv[0];
233
238
  if (head === undefined)
234
239
  return undefined;
235
- if (head.raw === head.folded && (head.folded === "for" || head.folded === "select") && BLOCK_WORD_NAMES.has(head.folded))
240
+ if (loopListHeadAt(p.argv) !== undefined)
236
241
  return "head";
237
- if (head.raw === head.folded && head.folded === "done" && BLOCK_WORD_NAMES.has(head.folded))
242
+ if (isShellSyntaxWord(head, LOOP_END))
238
243
  return "end";
239
244
  const pos = readProgramPosition(p.argv);
240
245
  return pos.controlWords.some((i) => p.argv[i].folded === "while" || p.argv[i].folded === "until") ? "head" : undefined;
@@ -252,8 +257,26 @@ function segmentOpensNothing(f) {
252
257
  const inertProgram = program === undefined || (program.name === program.basename && isInertBuiltin(program.basename) && !(isDeclarationBuiltin(program.basename) && f.argv.slice(at + 1).some((w) => /^[-+]/.test(w.folded))));
253
258
  return inertProgram && f.argv.every((w) => !/[[\]$`]/.test(w.raw)) && f.nested.length === 0 && f.reentry === undefined && f.declines.length === 0 && f.launcherChain.length === 0 && f.redirects.every((r) => r.direction !== "in") && f.reads.every((c) => c.provenance === "env_assignment");
254
259
  }
255
- function segmentFacts(segment, p, decl, table, nestedPlan, piped) {
256
- const facts = segmentFactsRead(segment, p, decl, table, nestedPlan, piped);
260
+ function segmentFacts(segment, p, decl, table, nestedPlan, fed) {
261
+ const targets = p.redirects.flatMap((r) => (r.target === undefined ? [] : [r.target]));
262
+ const readings = segmentReadingsOf(p.argv, targets, table, decl.homeDir);
263
+ if (readings.length === 1)
264
+ return readingFacts(segment, { ...p, argv: readings[0].argv }, decl, readings[0], nestedPlan, fed);
265
+ const once = plannedOnce(nestedPlan);
266
+ const each = readings.map((reading) => readingFacts(segment, { ...p, argv: reading.argv }, decl, reading, once, fed));
267
+ const first = each[0];
268
+ const move = each.find((f) => f.baseMove !== undefined)?.baseMove;
269
+ return {
270
+ ...first,
271
+ moveProvable: false,
272
+ ...(move !== undefined ? { baseMove: { target: typeof move.target === "string" ? move.target : { ...move.target, expands: "unresolvable" } } } : {}),
273
+ readings: each,
274
+ };
275
+ }
276
+ function readingFacts(segment, p, decl, reading, nestedPlan, fed) {
277
+ const table = reading.table;
278
+ const planner = (text, kind, at) => nestedPlan(text, kind, kind === "later" ? { ...at, floors: reading.floors } : at);
279
+ const facts = segmentFactsRead(segment, p, decl, table, planner, fed, reading.reads);
257
280
  const unresolvedRaw = new Set();
258
281
  for (const t of segment.tokens) {
259
282
  const w = t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined;
@@ -269,20 +292,21 @@ function segmentFacts(segment, p, decl, table, nestedPlan, piped) {
269
292
  };
270
293
  const unsettle = (c) => floor((unresolvedRaw.has(c.raw) || (homeWithheld && c.spelled.startsWith("~"))) && c.expands !== "unresolvable" ? { ...c, expands: "unresolvable" } : c);
271
294
  const each = (c) => {
272
- const { alternatives, floors, ...one } = c;
273
- const shadows = (floors ?? []).map((v) => ({ spelled: v.spelled, raw: one.raw, provenance: one.provenance, expands: v.glob === true ? "glob" : false, denyOnly: true }));
274
- return [...[one, ...(alternatives ?? []).map((spelled) => ({ ...one, spelled }))].map(unsettle), ...shadows];
295
+ const { floors, ...one } = c;
296
+ const shadows = (floors ?? []).map((v) => ({ spelled: v.spelled, ...(v.pattern !== undefined ? { pattern: v.pattern } : {}), raw: one.raw, provenance: one.provenance, expands: v.glob === true ? "glob" : false, denyOnly: true }));
297
+ return [unsettle(one), ...shadows];
275
298
  };
276
299
  const move = facts.baseMove;
277
300
  return {
278
301
  ...facts,
279
302
  reads: facts.reads.flatMap(each),
280
303
  redirects: facts.redirects.map((r) => (r.target === undefined ? r : { ...r, target: unsettle(r.target) })),
281
- ...(move !== undefined && typeof move.target !== "string" ? { baseMove: { target: unsettle(move.target.alternatives !== undefined ? { ...each(move.target)[0], expands: "unresolvable" } : move.target) } } : {}),
304
+ ...(move !== undefined && typeof move.target !== "string" ? { baseMove: { target: unsettle(move.target) } } : {}),
282
305
  };
283
306
  }
284
- function segmentFactsRead(segment, p, decl, table, nestedPlan, piped) {
307
+ function segmentFactsRead(segment, p, decl, table, nestedPlan, fed, toks) {
285
308
  const nested = [];
309
+ const elsewhere = new Set();
286
310
  const ctx = { table, reentered: [], programTextSeen: false };
287
311
  const arithmetic = segment.tokens.flatMap((t) => (t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined)?.pieces ?? []).flatMap((q) => (q.expansion?.kind === "arithmetic" && q.expansion.body !== undefined && isArithmeticBody(q.expansion.body) ? [q.expansion.body] : []));
288
312
  const count = (list, body) => list.filter((b) => b === body).length;
@@ -339,7 +363,6 @@ function segmentFactsRead(segment, p, decl, table, nestedPlan, piped) {
339
363
  redirects,
340
364
  nested,
341
365
  };
342
- const toks = readsOf(p.argv, table, decl.homeDir);
343
366
  const done = (f) => {
344
367
  const reentered = [...new Set(ctx.reentered)];
345
368
  for (const body of reentered)
@@ -349,23 +372,24 @@ function segmentFactsRead(segment, p, decl, table, nestedPlan, piped) {
349
372
  ...f,
350
373
  ...(reentered.length > 0 ? { reentry: "shell" } : {}),
351
374
  ...(f.unreadProgramText === undefined && ctx.programText !== undefined ? { unreadProgramText: ctx.programText } : {}),
375
+ ...(elsewhere.size > 0 ? { elsewhere } : {}),
352
376
  };
353
377
  };
354
378
  const head = toks[0]?.folded;
355
- if (head === "coproc" && toks[0].raw === head && toks.length > 1) {
379
+ if (isShellSyntaxWord(p.argv[0], COPROC) && toks.length > 1) {
356
380
  const tail = (from) => ({ ...p, argv: p.argv.slice(from) });
357
- const first = segmentFacts(segment, tail(1), decl, table, nestedPlan, piped);
381
+ const first = segmentFacts(segment, tail(1), decl, table, nestedPlan, fed);
358
382
  if (toks.length < 3)
359
383
  return { ...first, backgrounds: true };
360
- const second = segmentFacts(segment, tail(2), decl, table, nestedPlan, piped);
384
+ const second = segmentFacts(segment, tail(2), decl, table, nestedPlan, fed);
361
385
  const seen = new Set(first.nested.map((n) => JSON.stringify(n.segments.map((sg) => sg.text))));
362
386
  const extraNested = second.nested.filter((n) => !seen.has(JSON.stringify(n.segments.map((sg) => sg.text))));
363
387
  return { ...first, ...(second.reentry === "shell" ? { reentry: second.reentry } : {}), reads: [...first.reads, ...second.reads], nested: [...first.nested, ...extraNested], backgrounds: true };
364
388
  }
365
389
  const loop = loopShapeOf(p);
366
- if (head !== undefined && toks[0].raw === head && BLOCK_WORD_NAMES.has(head)) {
367
- if (loop === "head")
368
- return done({ empty: false, launcherChain: [], declines: [], reads, moveProvable: false, loopHead: { beats: literalBeats(toks) } });
390
+ if (loopListHeadAt(p.argv) !== undefined)
391
+ return done({ empty: false, launcherChain: [], declines: [], reads, moveProvable: false, loopHead: { beats: literalBeats(toks) } });
392
+ if (head !== undefined && isShellSyntaxWord(p.argv[0], BLOCK_WORD_NAMES)) {
369
393
  if (head === "[[") {
370
394
  toks.forEach((r, i) => {
371
395
  if (ARITHMETIC_TEST_OPERATORS.has(toks[i - 1]?.folded ?? "") || ARITHMETIC_TEST_OPERATORS.has(toks[i + 1]?.folded ?? "") || toks[i - 1]?.folded === "-v")
@@ -413,7 +437,7 @@ function segmentFactsRead(segment, p, decl, table, nestedPlan, piped) {
413
437
  if (!w.startsWith("-") || w === "-")
414
438
  break;
415
439
  const step = launcherOptionStep(row, w);
416
- if (step === undefined)
440
+ if (step === undefined || (step === 2 && j + 1 >= chainEnd))
417
441
  break;
418
442
  if (step === 2) {
419
443
  const role = launcherOptionPathRole(row, w);
@@ -460,10 +484,39 @@ function segmentFactsRead(segment, p, decl, table, nestedPlan, piped) {
460
484
  let reentry;
461
485
  if (pos.argvFromStdin)
462
486
  reentry = "argv";
487
+ const reenterShell = (verb, argvs) => {
488
+ const stdin = stdinTextsOf(segment, fed);
489
+ const scripts = new Map();
490
+ const script = (text, at) => { if (text !== undefined)
491
+ scripts.set(JSON.stringify([text, at]), { ...at, text }); };
492
+ for (const { shell, texts: t, args: a, as } of argvs) {
493
+ const user = as !== undefined ? { as } : {};
494
+ const argvText = [reentryText(shell, t), reentryText(shell, a)];
495
+ for (const text of argvText)
496
+ script(text, { ...user, ...(stdin.length > 0 ? { stdin } : {}) });
497
+ if (argvText.every((text) => text === undefined))
498
+ for (const s of stdin)
499
+ if (s.literal !== undefined)
500
+ script(reentryText(shell, t, s.literal), user);
501
+ }
502
+ for (const { text, ...at } of scripts.values()) {
503
+ const plan = nestedPlan(text, "later", at);
504
+ nested.push(plan);
505
+ if (at.as?.login === true)
506
+ elsewhere.add(plan);
507
+ }
508
+ for (const { shell, args: a, words } of argvs) {
509
+ const at = shell === "source" || shell === "." ? a.findIndex((w) => !w.startsWith("-") || w === "-") : scriptFileAt(shell, a);
510
+ const file = at === undefined ? undefined : words[at];
511
+ if (file !== undefined)
512
+ reads.push(bare(file, "operand"));
513
+ }
514
+ return done({ empty: false, launcherChain, verb, reentry: "shell", declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), ...(stdin.length > 0 ? { stdin } : {}) });
515
+ };
463
516
  if (pos.refusal !== undefined) {
464
- const commandLine = commandLineOptionSpellings(programRow(programBasename(pos.refusal.owner)));
465
- if (pos.refusal.kind === "option_at_program_position" && commandLine.length > 0) {
466
- const text = commandStringValue(commandLine, toks, pos.refusal.option);
517
+ const owner = programRow(programBasename(pos.refusal.owner));
518
+ if (pos.refusal.kind === "option_at_program_position" && owner !== undefined && commandLineOptionSpellings(owner).length > 0) {
519
+ const text = commandStringValue(owner, toks, pos.refusal.option);
467
520
  if (text !== undefined) {
468
521
  nested.push(nestedPlan(text.value, "later"));
469
522
  reentry = "shell";
@@ -472,16 +525,16 @@ function segmentFactsRead(segment, p, decl, table, nestedPlan, piped) {
472
525
  }
473
526
  }
474
527
  if (programPositionRunsShell(pos, runsShellText)) {
475
- reentry = "shell";
476
- const first = pos.launchers[0]?.index ?? written;
477
- const runs = pos.refusal.kind === "control_syntax_at_program_position" ? pos.refusal : undefined;
478
- const text = runs?.handsText === true ? toks.slice(first + 1).map((r) => r.folded).filter((w) => !w.startsWith("-") || w === "-").join(" ") : "";
479
- const shell = { name: runs?.word ?? "sh", basename: "sh", listed: decl.readers.has("sh") };
480
- const stdin = text.length > 0 ? [] : stdinTextsOf(segment, piped);
481
- for (const t of new Set([text, ...stdin.map((s) => (s.literal === undefined ? "" : reentryText(shell.basename, [], s.literal) ?? ""))]))
482
- if (t.length > 0)
483
- nested.push(nestedPlan(t, "later"));
484
- return done({ empty: false, launcherChain, verb: shell, reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), ...(stdin.length > 0 ? { stdin } : {}) });
528
+ const launched = launchedInput(p.argv, pos, { text: (k) => literalTextOf(toks[k].word.pieces) ?? programTextOf(toks[k]), folded: (k) => programTextOf(toks[k]), values: (k) => settledFieldsOf(toks[k]) });
529
+ for (const k of launched.programWords)
530
+ if (isPathShapedToken(toks[k].folded))
531
+ reads.push(bare(toks[k], "program_path"));
532
+ for (const text of launched.lines)
533
+ nested.push(nestedPlan(text, "substitution"));
534
+ const argvs = launched.shells.map((a) => ({ ...a, words: a.words.map((k) => (k === undefined ? undefined : toks[k])) }));
535
+ if (argvs.length === 0)
536
+ return done({ empty: false, launcherChain, reentry: "shell", declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}) });
537
+ return reenterShell({ name: argvs[0].shell, basename: argvs[0].shell, listed: decl.readers.has(argvs[0].shell) }, argvs);
485
538
  }
486
539
  const from = pos.launchers[0]?.index ?? written;
487
540
  unlistedWords(reads, toks, from, launcherIndices, "unlisted_word", ctx);
@@ -493,31 +546,13 @@ function segmentFactsRead(segment, p, decl, table, nestedPlan, piped) {
493
546
  return done({ empty: false, launcherChain, ...(reentry !== undefined ? { reentry } : {}), declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}) });
494
547
  }
495
548
  const argReads = toks.slice(program.index + 1);
496
- const args = argReads.map((r) => r.folded);
497
- const texts = argReads.map((r) => literalTextOf(r.word.pieces) ?? r.folded);
549
+ const args = argReads.map(programTextOf);
550
+ const texts = argReads.map((r) => literalTextOf(r.word.pieces) ?? programTextOf(r));
498
551
  const programRead = toks[program.index];
499
552
  if (rewritten(programRead) || isPathShapedToken(programRead.folded))
500
553
  reads.push(bare(programRead, "program_path"));
501
- if (runsShellText(basename)) {
502
- reentry = "shell";
503
- const stdin = stdinTextsOf(segment, piped);
504
- const text = reentryText(basename, texts);
505
- const scripts = [text, reentryText(basename, args), ...stdin.map((s) => (s.literal === undefined ? undefined : reentryText(basename, texts, s.literal)))];
506
- for (const t of new Set(scripts))
507
- if (t !== undefined)
508
- nested.push(nestedPlan(t, "later"));
509
- if (basename === "source" || basename === ".") {
510
- const file = argReads.find((a) => !a.folded.startsWith("-") || a.folded === "-");
511
- if (file !== undefined)
512
- reads.push(bare(file, "operand"));
513
- }
514
- else {
515
- const file = scriptFileAt(basename, args);
516
- if (file !== undefined)
517
- reads.push(bare(argReads[file], "operand"));
518
- }
519
- return done({ empty: false, launcherChain, verb: verb, reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), ...(stdin.length > 0 ? { stdin } : {}) });
520
- }
554
+ if (runsShellText(basename))
555
+ return reenterShell(verb, [{ shell: basename, texts, args, words: argReads }]);
521
556
  const row = programRow(basename);
522
557
  const delivery = programDelivery(row, args);
523
558
  if (delivery !== undefined)
@@ -600,38 +635,6 @@ function segmentFactsRead(segment, p, decl, table, nestedPlan, piped) {
600
635
  ...(loopHead !== undefined ? { loopHead } : {}),
601
636
  });
602
637
  }
603
- function rebasePlan(plan, from, to) {
604
- if (to !== "unmodelled" && to.length === 1 && to[0] === from)
605
- return plan;
606
- const fromSep = from.endsWith("/") || from.endsWith("\\") ? from : `${from}/`;
607
- const moveBase = (b) => {
608
- if (to === "unmodelled")
609
- return [];
610
- if (b === from)
611
- return [...to];
612
- if (b.startsWith(fromSep))
613
- return to.map((t) => `${t.endsWith("/") || t.endsWith("\\") ? t : `${t}/`}${b.slice(fromSep.length)}`);
614
- return [b];
615
- };
616
- const moveCandidate = (c) => {
617
- if (c.bases === "unmodelled" || c.bases.length === 0)
618
- return c;
619
- if (to === "unmodelled")
620
- return { ...c, bases: "unmodelled" };
621
- return { ...c, bases: [...new Set(c.bases.flatMap(moveBase))] };
622
- };
623
- return {
624
- ...plan,
625
- segments: plan.segments.map((s) => ({
626
- ...s,
627
- reads: s.reads.map(moveCandidate),
628
- redirects: s.redirects.map((r) => (r.target === undefined ? r : { ...r, target: moveCandidate(r.target) })),
629
- ...(s.baseMove !== undefined ? { baseMove: { target: typeof s.baseMove.target === "string" ? s.baseMove.target : moveCandidate(s.baseMove.target) } } : {}),
630
- bases: s.bases === "unmodelled" || to === "unmodelled" ? "unmodelled" : [...new Set(s.bases.flatMap(moveBase))],
631
- nested: s.nested.map((n) => rebasePlan(n, from, to)),
632
- })),
633
- };
634
- }
635
638
  function dedupe(reads) {
636
639
  const seen = new Set();
637
640
  const out = [];
@@ -659,21 +662,15 @@ function redirectFact(r, table, homeDir) {
659
662
  return { op: r.op, direction: "in" };
660
663
  return { op: r.op, direction: "out", target: bare(readsOf([r.target], table, homeDir)[0], "write_redirect_target") };
661
664
  }
662
- function commandStringValue(options, toks, option) {
665
+ function commandStringValue(row, toks, option) {
663
666
  const at = toks.findIndex((r) => r.folded === option);
664
- if (at < 0)
667
+ const line = at < 0 ? undefined : commandLineOf(row, option);
668
+ if (line === undefined)
665
669
  return undefined;
666
- for (const spelling of options) {
667
- if (option === spelling) {
668
- const value = toks[at + 1]?.folded;
669
- return value === undefined ? undefined : { value, next: at + 2 };
670
- }
671
- if (spelling.startsWith("--") && option.startsWith(`${spelling}=`))
672
- return { value: option.slice(spelling.length + 1), next: at + 1 };
673
- if (!spelling.startsWith("--") && option.startsWith(spelling) && option.length > spelling.length)
674
- return { value: option.slice(spelling.length), next: at + 1 };
675
- }
676
- return undefined;
670
+ if (line !== "next")
671
+ return { value: line.attached, next: at + 1 };
672
+ const value = toks[at + 1] === undefined ? undefined : programTextOf(toks[at + 1]);
673
+ return value === undefined ? undefined : { value, next: at + 2 };
677
674
  }
678
675
  function literalBeats(toks) {
679
676
  const at = toks.findIndex((r) => r.folded === "in");
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Where a text the read plan (`shell-read-plan.ts`) plans starts: a nested text's start — its names, the deny floors the
3
+ * words handing it on carry, the user its shell runs as, its standard input and its working directory — and where each
4
+ * segment's standard input comes from. A nested plan parsed against the declarations' working directory is re-based onto
5
+ * the segment it sits in ({@link rebasePlan}), unless it starts in a working directory of its own.
6
+ */
7
+ import type { LexedSegment } from "../../shell/lex.js";
8
+ import type { BindingTable, FloorValue, NameReader } from "./shell-read-bindings.js";
9
+ import type { TargetUser } from "./program-read-model.js";
10
+ import type { StdinText } from "./shell-reentry-text.js";
11
+ import type { ReadCandidate, ShellReadPlan } from "./shell-read-plan.js";
12
+ /** Where a nested text starts its names: a substitution body inherits the table of the segment it sits in (a subshell
13
+ * holds every variable); a text the shell runs later or elsewhere (a code body, `eval`'s text, a child shell's `-c`)
14
+ * starts from the text's own start — every name unknown, `HOME` the declared home unless the enclosing text binds it
15
+ * anywhere. `boundAnywhere` answers for the enclosing texts. */
16
+ export interface PlanStart {
17
+ readonly initial: NameReader;
18
+ readonly boundAnywhere: (name: string) => boolean;
19
+ /** `HOME` may already hold something other than the declared home where the nested text starts. */
20
+ readonly homeTouched: boolean;
21
+ /** The attributes, name references and deny floors in force where the nested text starts. */
22
+ readonly inherited: BindingTable["inherited"];
23
+ /** The working directories it starts in, where they are not the enclosing segment's (a login shell starts in its user's
24
+ * home; `"unmodelled"` — a home the text does not settle). */
25
+ readonly bases?: readonly string[] | "unmodelled";
26
+ /** What arrives on its own standard input (the input of the shell running it). */
27
+ readonly stdin?: readonly StdinText[];
28
+ }
29
+ /** What a segment says of the start of a text it hands a shell to run later: the deny floors of the words it holds as
30
+ * written, the user the shell runs as ({@link TargetUser}), and the shell's standard input. */
31
+ export interface NestedStart {
32
+ readonly floors?: ReadonlyMap<string, readonly FloorValue[]>;
33
+ readonly as?: TargetUser;
34
+ readonly stdin?: readonly StdinText[];
35
+ }
36
+ /** Where a text run later starts ({@link NestedStart}): the later start, with the shell's standard input, the floors of the
37
+ * words that hand it on, and — run as another user — that user's `HOME` (the caller's, the target's, or either) and, for
38
+ * a login shell, the target's home as its working directory. */
39
+ export declare function startedAt(later: PlanStart, at: NestedStart | undefined): PlanStart;
40
+ /** A planner that plans each text once per start: a segment read in several readings hands most of its nested texts (a
41
+ * substitution's body, a launched shell's text) on alike in every reading. */
42
+ export declare function plannedOnce<K, P>(plan: (text: string, kind: K, at?: NestedStart) => P): (text: string, kind: K, at?: NestedStart) => P;
43
+ /**
44
+ * What arrives on each segment's standard input: the pipe it is reached through (`piped`, what the segment before it
45
+ * prints); else the input of the group (`{ …; }`, `( … )`) it stands in — a group a pipe feeds hands each member that
46
+ * pipe; else the input the text itself was handed (`start`: a shell's `-c` text reads that shell's standard input).
47
+ */
48
+ export declare function stdinFeeds(segments: readonly LexedSegment[], start: readonly StdinText[], piped: (si: number) => readonly StdinText[]): (si: number) => readonly StdinText[];
49
+ /**
50
+ * A nested plan (a substitution body, a here-document's substitution, a launcher's command string) was parsed
51
+ * against the declarations' working directory; the segment it sits in may stand elsewhere (`cd sub && cat <<EOF
52
+ * $(cat secret)`), so its relative candidates are re-based onto the ENCLOSING segment's bases — every base the
53
+ * nested text derived from the start is carried over by prefix, an unsettled enclosing base leaves every
54
+ * relative candidate unplaced, and absolute candidates are untouched. Recursive: a nested plan's own nested plans
55
+ * derive from the same start.
56
+ */
57
+ export declare function rebasePlan(plan: ShellReadPlan, from: string, to: ReadCandidate["bases"]): ShellReadPlan;
@@ -0,0 +1,92 @@
1
+ const ROOT_HOMES = ["/root", "/var/root"];
2
+ export function startedAt(later, at) {
3
+ if (at === undefined)
4
+ return later;
5
+ const fed = at.stdin !== undefined && at.stdin.length > 0 ? { ...later, stdin: at.stdin } : later;
6
+ const withFloors = at.floors === undefined || at.floors.size === 0 ? fed : { ...fed, inherited: { ...fed.inherited, floors: joinedFloors(fed.inherited.floors, at.floors) } };
7
+ const as = at.as;
8
+ if (as === undefined || as.home === "caller")
9
+ return withFloors;
10
+ const homes = as.name === "root" ? ROOT_HOMES : undefined;
11
+ const target = homes === undefined ? "unknown" : homes.map((spelled) => ({ spelled }));
12
+ const caller = later.initial("HOME");
13
+ const home = as.home === "target" ? target : caller === "unknown" || target === "unknown" ? "unknown" : [...caller, ...target];
14
+ return {
15
+ ...withFloors,
16
+ initial: (name) => (name === "HOME" ? home : withFloors.initial(name)),
17
+ homeTouched: true,
18
+ ...(as.login ? { bases: homes ?? "unmodelled" } : {}),
19
+ };
20
+ }
21
+ export function plannedOnce(plan) {
22
+ const planned = new Map();
23
+ return (text, kind, at) => {
24
+ const key = JSON.stringify([kind, text, [...(at?.floors ?? [])], at?.as, at?.stdin]);
25
+ if (!planned.has(key))
26
+ planned.set(key, plan(text, kind, at));
27
+ return planned.get(key);
28
+ };
29
+ }
30
+ function joinedFloors(a, b) {
31
+ const out = new Map(a);
32
+ for (const [name, floors] of b)
33
+ out.set(name, [...(out.get(name) ?? []), ...floors]);
34
+ return out;
35
+ }
36
+ export function stdinFeeds(segments, start, piped) {
37
+ const opener = [];
38
+ const enclosing = [];
39
+ const open = [];
40
+ segments.forEach((segment, si) => {
41
+ enclosing[si] = open.at(-1);
42
+ let opens = false;
43
+ for (const t of segment.tokens) {
44
+ if ((t.kind === "group" && t.op === "(") || (t.kind === "word" && t.raw === "{")) {
45
+ open.push(si);
46
+ opens = true;
47
+ }
48
+ else if ((t.kind === "group" && t.op === ")") || (t.kind === "word" && t.raw === "}"))
49
+ open.pop();
50
+ }
51
+ opener[si] = opens ? si : enclosing[si];
52
+ });
53
+ const fedAt = (si) => {
54
+ if (si > 0 && segments[si].connector === "|")
55
+ return [...new Map(piped(si).map((s) => [JSON.stringify(s), s])).values()];
56
+ const group = opener[si] !== si ? opener[si] : enclosing[si];
57
+ return group !== undefined ? fedAt(group) : start;
58
+ };
59
+ return fedAt;
60
+ }
61
+ export function rebasePlan(plan, from, to) {
62
+ if (to !== "unmodelled" && to.length === 1 && to[0] === from)
63
+ return plan;
64
+ const fromSep = from.endsWith("/") || from.endsWith("\\") ? from : `${from}/`;
65
+ const moveBase = (b) => {
66
+ if (to === "unmodelled")
67
+ return [];
68
+ if (b === from)
69
+ return [...to];
70
+ if (b.startsWith(fromSep))
71
+ return to.map((t) => `${t.endsWith("/") || t.endsWith("\\") ? t : `${t}/`}${b.slice(fromSep.length)}`);
72
+ return [b];
73
+ };
74
+ const moveCandidate = (c) => {
75
+ if (c.bases === "unmodelled" || c.bases.length === 0)
76
+ return c;
77
+ if (to === "unmodelled")
78
+ return { ...c, bases: "unmodelled" };
79
+ return { ...c, bases: [...new Set(c.bases.flatMap(moveBase))] };
80
+ };
81
+ return {
82
+ ...plan,
83
+ segments: plan.segments.map((s) => ({
84
+ ...s,
85
+ reads: s.reads.map(moveCandidate),
86
+ redirects: s.redirects.map((r) => (r.target === undefined ? r : { ...r, target: moveCandidate(r.target) })),
87
+ ...(s.baseMove !== undefined ? { baseMove: { target: typeof s.baseMove.target === "string" ? s.baseMove.target : moveCandidate(s.baseMove.target) } } : {}),
88
+ bases: s.bases === "unmodelled" || to === "unmodelled" ? "unmodelled" : [...new Set(s.bases.flatMap(moveBase))],
89
+ nested: s.nested.map((n) => rebasePlan(n, from, to)),
90
+ })),
91
+ };
92
+ }