@sema-agent/core 7.47.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 (44) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/core/allow-entry.js +1 -1
  3. package/dist/core/checkpoint-store.d.ts +1 -1
  4. package/dist/core/mcp-server-spec.d.ts +1 -1
  5. package/dist/core/runner/park-commit.d.ts +1 -1
  6. package/dist/core/safety-axis-vocab.d.ts +1 -1
  7. package/dist/core/safety-merge-corpus.d.ts +1 -1
  8. package/dist/core/session-policy-store.d.ts +6 -3
  9. package/dist/core/session-policy-store.js +28 -10
  10. package/dist/core/shell-wrapper-table.d.ts +2 -2
  11. package/dist/core/shell-wrapper-table.js +1 -1
  12. package/dist/core/tool-name-aliases.d.ts +9 -8
  13. package/dist/core/tool-name-aliases.js +2 -0
  14. package/dist/core/tool-policy.js +4 -5
  15. package/dist/orchestration/workflow.js +2 -2
  16. package/dist/prompt-assembly/types.d.ts +1 -1
  17. package/dist/shell/lex.js +30 -4
  18. package/dist/tools/fs/bash-program-position.d.ts +7 -0
  19. package/dist/tools/fs/bash-program-position.js +5 -2
  20. package/dist/tools/fs/index.d.ts +3 -2
  21. package/dist/tools/fs/index.js +2 -2
  22. package/dist/tools/fs/program-read-model.d.ts +60 -5
  23. package/dist/tools/fs/program-read-model.js +100 -36
  24. package/dist/tools/fs/program-read-model.rows.js +13 -5
  25. package/dist/tools/fs/read-deny-home.d.ts +20 -1
  26. package/dist/tools/fs/read-deny-home.js +28 -7
  27. package/dist/tools/fs/read-deny.d.ts +13 -6
  28. package/dist/tools/fs/read-deny.js +12 -3
  29. package/dist/tools/fs/removal-targets.js +12 -3
  30. package/dist/tools/fs/shell-read-argv.d.ts +4 -7
  31. package/dist/tools/fs/shell-read-argv.js +5 -7
  32. package/dist/tools/fs/shell-read-bindings.d.ts +34 -9
  33. package/dist/tools/fs/shell-read-bindings.js +166 -60
  34. package/dist/tools/fs/shell-read-establish.js +3 -4
  35. package/dist/tools/fs/shell-read-plan.d.ts +1 -14
  36. package/dist/tools/fs/shell-read-plan.js +102 -101
  37. package/dist/tools/fs/shell-read-start.d.ts +57 -0
  38. package/dist/tools/fs/shell-read-start.js +92 -0
  39. package/dist/tools/fs/shell-read-word.d.ts +35 -6
  40. package/dist/tools/fs/shell-read-word.js +85 -9
  41. package/dist/tools/fs/shell-reentry-text.d.ts +23 -21
  42. package/dist/tools/fs/shell-reentry-text.js +31 -26
  43. package/package.json +1 -1
  44. 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 { programTextOf, readOfText, readsOf, renamed, rewritten, settledFieldsOf, 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
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((a) => ({ ...renamed(one, a.spelled), ...a }))].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);
@@ -461,11 +485,26 @@ function segmentFactsRead(segment, p, decl, table, nestedPlan, piped) {
461
485
  if (pos.argvFromStdin)
462
486
  reentry = "argv";
463
487
  const reenterShell = (verb, argvs) => {
464
- const stdin = stdinTextsOf(segment, piped);
465
- const scripts = argvs.flatMap(({ shell, texts: t, args: a }) => [reentryText(shell, t), reentryText(shell, a), ...stdin.map((s) => (s.literal === undefined ? undefined : reentryText(shell, t, s.literal)))]);
466
- for (const t of new Set(scripts))
467
- if (t !== undefined)
468
- nested.push(nestedPlan(t, "later"));
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
+ }
469
508
  for (const { shell, args: a, words } of argvs) {
470
509
  const at = shell === "source" || shell === "." ? a.findIndex((w) => !w.startsWith("-") || w === "-") : scriptFileAt(shell, a);
471
510
  const file = at === undefined ? undefined : words[at];
@@ -475,9 +514,9 @@ function segmentFactsRead(segment, p, decl, table, nestedPlan, piped) {
475
514
  return done({ empty: false, launcherChain, verb, reentry: "shell", declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), ...(stdin.length > 0 ? { stdin } : {}) });
476
515
  };
477
516
  if (pos.refusal !== undefined) {
478
- const commandLine = commandLineOptionSpellings(programRow(programBasename(pos.refusal.owner)));
479
- if (pos.refusal.kind === "option_at_program_position" && commandLine.length > 0) {
480
- 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);
481
520
  if (text !== undefined) {
482
521
  nested.push(nestedPlan(text.value, "later"));
483
522
  reentry = "shell";
@@ -596,38 +635,6 @@ function segmentFactsRead(segment, p, decl, table, nestedPlan, piped) {
596
635
  ...(loopHead !== undefined ? { loopHead } : {}),
597
636
  });
598
637
  }
599
- function rebasePlan(plan, from, to) {
600
- if (to !== "unmodelled" && to.length === 1 && to[0] === from)
601
- return plan;
602
- const fromSep = from.endsWith("/") || from.endsWith("\\") ? from : `${from}/`;
603
- const moveBase = (b) => {
604
- if (to === "unmodelled")
605
- return [];
606
- if (b === from)
607
- return [...to];
608
- if (b.startsWith(fromSep))
609
- return to.map((t) => `${t.endsWith("/") || t.endsWith("\\") ? t : `${t}/`}${b.slice(fromSep.length)}`);
610
- return [b];
611
- };
612
- const moveCandidate = (c) => {
613
- if (c.bases === "unmodelled" || c.bases.length === 0)
614
- return c;
615
- if (to === "unmodelled")
616
- return { ...c, bases: "unmodelled" };
617
- return { ...c, bases: [...new Set(c.bases.flatMap(moveBase))] };
618
- };
619
- return {
620
- ...plan,
621
- segments: plan.segments.map((s) => ({
622
- ...s,
623
- reads: s.reads.map(moveCandidate),
624
- redirects: s.redirects.map((r) => (r.target === undefined ? r : { ...r, target: moveCandidate(r.target) })),
625
- ...(s.baseMove !== undefined ? { baseMove: { target: typeof s.baseMove.target === "string" ? s.baseMove.target : moveCandidate(s.baseMove.target) } } : {}),
626
- bases: s.bases === "unmodelled" || to === "unmodelled" ? "unmodelled" : [...new Set(s.bases.flatMap(moveBase))],
627
- nested: s.nested.map((n) => rebasePlan(n, from, to)),
628
- })),
629
- };
630
- }
631
638
  function dedupe(reads) {
632
639
  const seen = new Set();
633
640
  const out = [];
@@ -655,21 +662,15 @@ function redirectFact(r, table, homeDir) {
655
662
  return { op: r.op, direction: "in" };
656
663
  return { op: r.op, direction: "out", target: bare(readsOf([r.target], table, homeDir)[0], "write_redirect_target") };
657
664
  }
658
- function commandStringValue(options, toks, option) {
665
+ function commandStringValue(row, toks, option) {
659
666
  const at = toks.findIndex((r) => r.folded === option);
660
- if (at < 0)
667
+ const line = at < 0 ? undefined : commandLineOf(row, option);
668
+ if (line === undefined)
661
669
  return undefined;
662
- for (const spelling of options) {
663
- if (option === spelling) {
664
- const value = toks[at + 1] === undefined ? undefined : programTextOf(toks[at + 1]);
665
- return value === undefined ? undefined : { value, next: at + 2 };
666
- }
667
- if (spelling.startsWith("--") && option.startsWith(`${spelling}=`))
668
- return { value: option.slice(spelling.length + 1), next: at + 1 };
669
- if (!spelling.startsWith("--") && option.startsWith(spelling) && option.length > spelling.length)
670
- return { value: option.slice(spelling.length), next: at + 1 };
671
- }
672
- 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 };
673
674
  }
674
675
  function literalBeats(toks) {
675
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
+ }
@@ -18,7 +18,7 @@ export interface Read {
18
18
  readonly raw: string;
19
19
  readonly folded: string;
20
20
  /** A pattern's names are matched by this spelling where it is not {@link folded} (a class the judge widens, read as
21
- * any one character — {@link LexedWord.judged}, {@link patternOf}); `folded` stays the name opened where none match. */
21
+ * any one character — {@link LexedWord.judged}, {@link judgedSpellingOf}); `folded` stays the name opened where none match. */
22
22
  readonly pattern?: string;
23
23
  readonly expands: WordExpands;
24
24
  /** The names the word refers to were read through the table: its expansion IS the spelling above (and, where the
@@ -38,17 +38,46 @@ export interface Read {
38
38
  /** The reads of a segment's words against the binding table at that segment (`homeDir` = the declared home, the
39
39
  * state `HOME` starts in). A word that refers to no name, or only to a `HOME` the text has not touched (`~/x`, placed
40
40
  * by the establishment at the declared home), reads as written. */
41
- export declare function readsOf(words: readonly LexedWord[], table: BindingTable | undefined, homeDir: string | undefined): Read[];
41
+ export declare function readsOf(words: readonly LexedWord[], table: BindingTable | undefined, homeDir: string | undefined, split?: boolean): Read[];
42
+ /** One reading of a segment: the binding table with each name its words read held to ONE value (or, past
43
+ * {@link READINGS_MAX}, made unknown — every value it held stays a deny-floor spelling, {@link BindingTable.floorOf}),
44
+ * the reads of its argv against it ({@link readsOf}), and the argv the shell hands the program in it — a word whose
45
+ * names settle into one value that is no pattern is that value, as a literal word (a reader of the program position,
46
+ * of a launcher's fields, of a program's options reads it as it reads a literal word); any other word as written, its
47
+ * names' deny floors in `floors` (a shell that re-reads the word — `sh -c`, `eval`, a launcher's shell — starts with
48
+ * them: the word names in that text what it names here). */
49
+ export interface SegmentReading {
50
+ readonly table: BindingTable;
51
+ readonly reads: readonly Read[];
52
+ readonly argv: readonly LexedWord[];
53
+ readonly floors: ReadonlyMap<string, readonly FloorValue[]>;
54
+ }
55
+ /**
56
+ * The readings of one segment's words (its argv and its redirection targets): the names they read through the table that
57
+ * may hold several values here, in the order the words read them (the program position first), each held to one value
58
+ * per reading — every combination while the readings stay within {@link READINGS_MAX}, so the program position, a
59
+ * launcher's fields, a re-entered text and an operand are spelled from the same values (`$P` read twice takes one value
60
+ * in a reading: the shell expands both alike); a name past that bound is unknown in every reading — its word as written,
61
+ * every value it held a deny floor — never a sample of its combinations. A segment whose names hold one value each has
62
+ * one reading.
63
+ */
64
+ export declare function segmentReadingsOf(argv: readonly LexedWord[], targets: readonly LexedWord[], table: BindingTable, homeDir: string | undefined): SegmentReading[];
42
65
  /** The spelling a reader names a value by, read directly as a path — for a pattern, the name the program opens where it
43
66
  * matches nothing: its text, a leading `~` spelled as the literal tilde it is ({@link literalTildeSpelling}, the lexer's
44
67
  * spelling of a written one) — the shell never tilde-expands what a quoted piece or an expansion produced, so a value that
45
68
  * starts with `~` starts with a literal one (`X=/w; rm \~$X/*` reads `./~/w/*`, as `rm \~/w/*` does). A shell that
46
69
  * re-parses the value reads {@link programTextOf} instead. */
47
70
  export declare const spellingOf: (v: BoundValue) => string;
48
- /** The spelling a pattern value's names are matched by, where it is not {@link spellingOf}: its runs as a reader judges
49
- * them (a class the judge widens as `?`, the lexer's `WordExpansion.pattern.judged`) — a candidate's
50
- * `pattern`. */
51
- export declare function patternOf(v: BoundValue): string | undefined;
71
+ /** The pattern a PART of a pattern word is matched by (an option value attached to its option word, `--file=["!"a]*`; an
72
+ * assignment-shaped word's value, `if=["!"a]*`): the word's own judged spelling (`pattern`) past the text in front of
73
+ * the part — bash expands the WHOLE word, so the names the part may take are those the word's widened spelling reaches
74
+ * (a superset: a name is judged whether or not the word in front of it matches too). `undefined` where the word carries
75
+ * none, the part is not the end of the word's spelling, or the text in front holds a widened class itself (its judged
76
+ * spelling no longer lines up). */
77
+ export declare function partPattern(word: {
78
+ readonly folded: string;
79
+ readonly pattern?: string;
80
+ }, part: string): string | undefined;
52
81
  /** A candidate under another name (a value of several, a backup, a copy's child): the word's `pattern` matched the word,
53
82
  * not that name — the name is matched by its own text. */
54
83
  export declare function renamed<C extends {