@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.
- package/CHANGELOG.md +54 -0
- package/dist/core/allow-entry.js +1 -1
- package/dist/core/checkpoint-store.d.ts +1 -1
- package/dist/core/mcp-server-spec.d.ts +1 -1
- package/dist/core/runner/park-commit.d.ts +1 -1
- package/dist/core/safety-axis-vocab.d.ts +1 -1
- package/dist/core/safety-merge-corpus.d.ts +1 -1
- package/dist/core/session-policy-store.d.ts +6 -3
- package/dist/core/session-policy-store.js +28 -10
- package/dist/core/shell-wrapper-table.d.ts +2 -2
- package/dist/core/shell-wrapper-table.js +1 -1
- package/dist/core/tool-name-aliases.d.ts +9 -8
- package/dist/core/tool-name-aliases.js +2 -0
- package/dist/core/tool-policy.js +4 -5
- package/dist/orchestration/workflow.js +2 -2
- package/dist/prompt-assembly/types.d.ts +1 -1
- package/dist/shell/lex.js +30 -4
- package/dist/tools/fs/bash-program-position.d.ts +7 -0
- package/dist/tools/fs/bash-program-position.js +5 -2
- package/dist/tools/fs/index.d.ts +3 -2
- package/dist/tools/fs/index.js +2 -2
- package/dist/tools/fs/program-read-model.d.ts +60 -5
- package/dist/tools/fs/program-read-model.js +100 -36
- package/dist/tools/fs/program-read-model.rows.js +13 -5
- package/dist/tools/fs/read-deny-home.d.ts +20 -1
- package/dist/tools/fs/read-deny-home.js +28 -7
- package/dist/tools/fs/read-deny.d.ts +13 -6
- package/dist/tools/fs/read-deny.js +12 -3
- package/dist/tools/fs/removal-targets.js +12 -3
- package/dist/tools/fs/shell-read-argv.d.ts +4 -7
- package/dist/tools/fs/shell-read-argv.js +5 -7
- package/dist/tools/fs/shell-read-bindings.d.ts +34 -9
- package/dist/tools/fs/shell-read-bindings.js +166 -60
- package/dist/tools/fs/shell-read-establish.js +3 -4
- package/dist/tools/fs/shell-read-plan.d.ts +1 -14
- package/dist/tools/fs/shell-read-plan.js +102 -101
- package/dist/tools/fs/shell-read-start.d.ts +57 -0
- package/dist/tools/fs/shell-read-start.js +92 -0
- package/dist/tools/fs/shell-read-word.d.ts +35 -6
- package/dist/tools/fs/shell-read-word.js +85 -9
- package/dist/tools/fs/shell-reentry-text.d.ts +23 -21
- package/dist/tools/fs/shell-reentry-text.js +31 -26
- package/package.json +1 -1
- 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,
|
|
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
|
-
|
|
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 =
|
|
68
|
-
let baseLost =
|
|
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
|
|
193
|
-
if (
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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 (
|
|
240
|
+
if (loopListHeadAt(p.argv) !== undefined)
|
|
236
241
|
return "head";
|
|
237
|
-
if (head
|
|
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,
|
|
256
|
-
const
|
|
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 {
|
|
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 [
|
|
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
|
|
304
|
+
...(move !== undefined && typeof move.target !== "string" ? { baseMove: { target: unsettle(move.target) } } : {}),
|
|
282
305
|
};
|
|
283
306
|
}
|
|
284
|
-
function segmentFactsRead(segment, p, decl, table, nestedPlan,
|
|
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 (
|
|
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,
|
|
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,
|
|
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 (
|
|
367
|
-
|
|
368
|
-
|
|
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,
|
|
465
|
-
const scripts =
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
|
479
|
-
if (pos.refusal.kind === "option_at_program_position" &&
|
|
480
|
-
const text = commandStringValue(
|
|
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(
|
|
665
|
+
function commandStringValue(row, toks, option) {
|
|
659
666
|
const at = toks.findIndex((r) => r.folded === option);
|
|
660
|
-
|
|
667
|
+
const line = at < 0 ? undefined : commandLineOf(row, option);
|
|
668
|
+
if (line === undefined)
|
|
661
669
|
return undefined;
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
|
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
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
|
|
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 {
|