@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.
- package/CHANGELOG.md +106 -0
- package/dist/core/allow-entry.d.ts +1 -1
- package/dist/core/allow-entry.js +4 -5
- package/dist/core/ask-origin.d.ts +35 -12
- package/dist/core/ask-origin.js +23 -6
- package/dist/core/checkpoint-store.d.ts +1 -1
- package/dist/core/engine-notice.d.ts +7 -4
- package/dist/core/gate-fold.js +2 -2
- package/dist/core/gate-lanes.js +2 -1
- package/dist/core/gate-outcome.d.ts +4 -2
- package/dist/core/gate-outcome.js +2 -2
- package/dist/core/mcp-server-spec.d.ts +1 -1
- package/dist/core/permission-rule-model.d.ts +12 -6
- package/dist/core/permission-rule-model.js +10 -2
- package/dist/core/permission-rules.d.ts +4 -5
- package/dist/core/permission-rules.js +28 -15
- package/dist/core/runner/gate-exit.d.ts +4 -1
- package/dist/core/runner/gate-exit.js +5 -1
- package/dist/core/runner/park-commit.d.ts +1 -1
- package/dist/core/runner/prepare-hands-readface.js +5 -5
- package/dist/core/runner/prepare-policy-chain.js +2 -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-projection.js +1 -1
- package/dist/core/shell-wrapper-table.d.ts +8 -8
- package/dist/core/shell-wrapper-table.js +3 -3
- package/dist/core/tool-catalog-entries.js +3 -2
- package/dist/core/tool-face.d.ts +10 -1
- package/dist/core/tool-name-aliases.d.ts +9 -8
- package/dist/core/tool-name-aliases.js +2 -0
- package/dist/core/tool-name-readings.d.ts +8 -0
- package/dist/core/tool-name-readings.js +2 -0
- package/dist/core/tool-policy.d.ts +10 -9
- package/dist/core/tool-policy.js +6 -7
- package/dist/core/tool-registry.d.ts +9 -0
- package/dist/core/tool-registry.js +10 -0
- package/dist/core/write-protect.d.ts +10 -3
- package/dist/core/write-protect.js +11 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/workflow.js +2 -2
- package/dist/prompt-assembly/types.d.ts +1 -1
- package/dist/shell/lex.d.ts +15 -0
- package/dist/shell/lex.js +86 -15
- package/dist/tools/fs/bash-program-position.d.ts +16 -6
- package/dist/tools/fs/bash-program-position.js +18 -12
- 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 +84 -12
- package/dist/tools/fs/program-read-model.js +105 -34
- package/dist/tools/fs/program-read-model.rows.js +26 -7
- 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 +24 -5
- package/dist/tools/fs/shell-read-argv.d.ts +6 -8
- package/dist/tools/fs/shell-read-argv.js +5 -5
- package/dist/tools/fs/shell-read-bindings.d.ts +71 -19
- package/dist/tools/fs/shell-read-bindings.js +190 -75
- package/dist/tools/fs/shell-read-establish.js +6 -7
- package/dist/tools/fs/shell-read-plan.d.ts +3 -15
- package/dist/tools/fs/shell-read-plan.js +126 -129
- 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 +68 -8
- package/dist/tools/fs/shell-read-word.js +106 -12
- package/dist/tools/fs/shell-read-write-targets.js +4 -3
- package/dist/tools/fs/shell-reentry-text.d.ts +103 -3
- package/dist/tools/fs/shell-reentry-text.js +122 -4
- package/dist/tools/monitor.d.ts +3 -3
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +9 -3
- package/wire-changes/core@7.47.0.json +412 -0
- package/wire-changes/core@7.48.0.json +252 -0
|
@@ -3,7 +3,7 @@ import { canonicalizeTarget, isAbsolutePathForm, joinForFamily, normalizeAbsPath
|
|
|
3
3
|
import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, withGateTimeout } from "../../core/gate-bounded-await.js";
|
|
4
4
|
import { thrownText } from "../../core/thrown-value.js";
|
|
5
5
|
import { agentConfigDirTail } from "../../core/agent-config-dir.js";
|
|
6
|
-
import { readDenyPatternRun, readDenyRunHead } from "./read-deny.js";
|
|
6
|
+
import { isRootedReadDenyPattern, readDenyPatternRun, readDenyRunHead } from "./read-deny.js";
|
|
7
7
|
export const AUXILIARY_HOME_PROBE_MS = 2_000;
|
|
8
8
|
const said = new Set();
|
|
9
9
|
function sayOnce(kind, line) {
|
|
@@ -111,7 +111,7 @@ export async function readDenyHomeLinksOf(env, entries, homes, signal, deadline)
|
|
|
111
111
|
const { homeAnchored, run } = readDenyPatternRun(pattern);
|
|
112
112
|
if (!homeAnchored)
|
|
113
113
|
continue;
|
|
114
|
-
const head =
|
|
114
|
+
const head = literalHeadOf(run);
|
|
115
115
|
if (head.length === 0)
|
|
116
116
|
continue;
|
|
117
117
|
const spelled = homes.map((home) => normalizeAbsPathLexically(joinForFamily(pathFamilyOf({ root: home }), home, head)));
|
|
@@ -119,6 +119,23 @@ export async function readDenyHomeLinksOf(env, entries, homes, signal, deadline)
|
|
|
119
119
|
}
|
|
120
120
|
return await linkedLocationsOf(env, rows, signal, deadline);
|
|
121
121
|
}
|
|
122
|
+
const ROOT = "/";
|
|
123
|
+
export async function readDenyRootLinksOf(env, entries, signal, deadline) {
|
|
124
|
+
const rows = [];
|
|
125
|
+
for (const { pattern, caseSensitive } of entries) {
|
|
126
|
+
if (!isRootedReadDenyPattern(pattern))
|
|
127
|
+
continue;
|
|
128
|
+
const head = literalHeadOf(pattern);
|
|
129
|
+
if (head.length === 0)
|
|
130
|
+
continue;
|
|
131
|
+
const spelled = [normalizeAbsPathLexically(joinForFamily(pathFamilyOf({ root: ROOT }), ROOT, head))];
|
|
132
|
+
rows.push({ pattern, caseSensitive: caseSensitive === true, spelled, anchored: spelled });
|
|
133
|
+
}
|
|
134
|
+
return await linkedLocationsOf(env, rows, signal, deadline);
|
|
135
|
+
}
|
|
136
|
+
function literalHeadOf(run) {
|
|
137
|
+
return run.split("/").filter(Boolean).slice(0, readDenyRunHead(run)).join("/");
|
|
138
|
+
}
|
|
122
139
|
export async function readDenyConfigRootLinksOf(env, entries, roots, signal, deadline) {
|
|
123
140
|
return await linkedLocationsOf(env, configRootRowsOf(entries, roots), signal, deadline);
|
|
124
141
|
}
|
|
@@ -129,7 +146,7 @@ function configRootRowsOf(entries, roots) {
|
|
|
129
146
|
const tail = homeAnchored ? agentConfigDirTail(run, caseSensitive === true) : undefined;
|
|
130
147
|
if (tail === undefined)
|
|
131
148
|
continue;
|
|
132
|
-
const head =
|
|
149
|
+
const head = literalHeadOf(tail);
|
|
133
150
|
const spelled = roots.map((root) => normalizeAbsPathLexically(head.length === 0 ? root : joinForFamily(pathFamilyOf({ root }), root, head)));
|
|
134
151
|
rows.push({ pattern, caseSensitive: caseSensitive === true, spelled, anchored: [] });
|
|
135
152
|
}
|
|
@@ -363,8 +380,12 @@ export async function placeReadDenyAtHome(env, compile, homes, signal, configRoo
|
|
|
363
380
|
const entries = compile([]).entries;
|
|
364
381
|
const declared = declaredHomeOf(env);
|
|
365
382
|
const relocated = configRoot === undefined ? undefined : readAuxiliaryHome(env, configRoot, { entries, configRoot: true }, signal);
|
|
383
|
+
const rooted = readDenyRootLinksOf(env, entries, signal);
|
|
366
384
|
const heldAnywhere = async (whole) => {
|
|
367
|
-
const linked =
|
|
385
|
+
const linked = new Map();
|
|
386
|
+
for (const map of [(await relocated)?.linked, (await rooted).linked])
|
|
387
|
+
for (const [pattern, locations] of map ?? [])
|
|
388
|
+
linked.set(pattern, locations);
|
|
368
389
|
return placementOf(carryHomeViews(linked.size === 0 ? compile([]) : compile([], linked), homeViewSourceOver(env, relocated === undefined ? [] : [relocated])), [], linked, whole);
|
|
369
390
|
};
|
|
370
391
|
const listed = homes === undefined ? declaredReadDenyHomesAnswered(env) : { homes, whole: true };
|
|
@@ -379,15 +400,15 @@ export async function placeReadDenyAtHome(env, compile, homes, signal, configRoo
|
|
|
379
400
|
declaredAt = [...new Set([...own, ...ownSpellings.spellings])];
|
|
380
401
|
}
|
|
381
402
|
const auxiliary = readAuxiliaryHomes(env, listed.homes, { entries }, signal);
|
|
382
|
-
const [heads, readings, root] = await Promise.all([readDenyHomeLinksOf(env, entries, declaredAt, signal), Promise.all(auxiliary), relocated]);
|
|
403
|
+
const [heads, readings, root, rootedHeads] = await Promise.all([readDenyHomeLinksOf(env, entries, declaredAt, signal), Promise.all(auxiliary), relocated, rooted]);
|
|
383
404
|
const at = [...new Set([...declaredAt, ...readings.flatMap((r) => r.place)])];
|
|
384
405
|
const linked = new Map();
|
|
385
|
-
for (const map of [heads.linked, ...readings.map((r) => r.linked), ...(root === undefined ? [] : [root.linked])]) {
|
|
406
|
+
for (const map of [heads.linked, ...readings.map((r) => r.linked), ...(root === undefined ? [] : [root.linked]), rootedHeads.linked]) {
|
|
386
407
|
for (const [pattern, locations] of map)
|
|
387
408
|
linked.set(pattern, [...new Set([...(linked.get(pattern) ?? []), ...locations])]);
|
|
388
409
|
}
|
|
389
410
|
const matcher = linked.size === 0 ? compile(at) : compile(at, linked);
|
|
390
|
-
return placementOf(carryHomeViews(matcher, homeViewSourceOver(env, relocated === undefined ? auxiliary : [...auxiliary, relocated])), at, linked, heads.whole);
|
|
411
|
+
return placementOf(carryHomeViews(matcher, homeViewSourceOver(env, relocated === undefined ? auxiliary : [...auxiliary, relocated])), at, linked, heads.whole && rootedHeads.whole);
|
|
391
412
|
}
|
|
392
413
|
export function readDenyPlacedLater(unplaced, place) {
|
|
393
414
|
let current = unplaced;
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* unless the pattern opens with the HOME word ({@link READ_DENY_HOME_WORD}): `~/<run>` matches only where
|
|
6
6
|
* the run starts right below the execution environment's home directory (`~/.sema/settings.json` is the
|
|
7
7
|
* home's file, never a project's `.sema/settings.json`). Only a FIRST segment that is exactly `~` is the
|
|
8
|
-
* word; `~user` and a `~` further along are literal segments.
|
|
8
|
+
* word; `~user` and a `~` further along are literal segments. A pattern that opens with `/` (and is not home-anchored)
|
|
9
|
+
* is ROOTED, the same kind of anchor with the filesystem root as its place: `/app/config-lkg.json` names the file
|
|
10
|
+
* at exactly that location, never a project's `app/config-lkg.json` — the run must start at the first segment of an
|
|
11
|
+
* absolute path (a path that is not absolute is not placed, and the row does not judge it).
|
|
9
12
|
* String shorthand ≡ `{ pattern }`. Matching is ASCII-case-insensitive by default (a case-insensitive
|
|
10
13
|
* filesystem serves `.SSH` and `.ssh` as the same directory); `caseSensitive: true` opts a single
|
|
11
14
|
* entry out. Unicode normalization (NFC/NFD) and volume-level case semantics are NOT modeled (v1
|
|
@@ -384,8 +387,9 @@ export interface CompiledSegmentPattern {
|
|
|
384
387
|
/** The segment lists of the directories the run is ANCHORED at (folded as the pattern folds): the run must start
|
|
385
388
|
* right after one of them. Absent = the run matches anywhere. */
|
|
386
389
|
anchors?: readonly (readonly string[])[];
|
|
387
|
-
/** Judged in a ROOTED view only: an extra location of a home-anchored row (where its literal head leads)
|
|
388
|
-
*
|
|
390
|
+
/** Judged in a ROOTED view only: an extra location of a home-anchored row (where its literal head leads), or a row that is
|
|
391
|
+
* itself rooted (anchored at the filesystem root, {@link ReadDenyEntry}). A view that is not rooted cannot be placed: a
|
|
392
|
+
* home-anchored row's own run is held anywhere in it (the row cannot be placed, so it holds wider); these two judge it not. */
|
|
389
393
|
rootedOnly?: true;
|
|
390
394
|
}
|
|
391
395
|
/**
|
|
@@ -408,6 +412,9 @@ export declare function compileSegmentPattern(raw: string, fold: "none" | "ascii
|
|
|
408
412
|
export declare function matchSegmentPatterns(path: string, compiled: readonly CompiledSegmentPattern[], opts?: {
|
|
409
413
|
readonly aliasResolved?: boolean;
|
|
410
414
|
}): string | null;
|
|
415
|
+
/** Is a canonical pattern ROOTED — placed at the filesystem root ({@link ReadDenyEntry})? Only {@link canonicalReadDenyPattern}
|
|
416
|
+
* puts a leading `/` on a canonical pattern, so the text is the whole answer. */
|
|
417
|
+
export declare function isRootedReadDenyPattern(canonical: string): boolean;
|
|
411
418
|
/**
|
|
412
419
|
* the PURE validation/canonicality predicate over a PERSISTED deny entry (checkpoint v9
|
|
413
420
|
* face section), for the resume pre-CAS ladder: it must refuse BEFORE the approval CAS everything
|
|
@@ -437,9 +444,9 @@ export declare function checkedReadDenyHomes(homes: unknown, seat?: string): rea
|
|
|
437
444
|
* spellings only — any other shape refuses loudly, naming `readDenyHomes`. EMPTY = the environment declares no
|
|
438
445
|
* home: a home-anchored entry then matches its run anywhere — a row that cannot be placed holds wider, never narrower.
|
|
439
446
|
*
|
|
440
|
-
* `linked` — where a home-anchored row's LITERAL HEAD ({@link readDenyRunHead}) really leads, keyed by the row's
|
|
441
|
-
* pattern: the canonical locations a mount resolved (`readDenyHomeLinksOf`, read-deny-home.ts) where
|
|
442
|
-
* head's segments is a link (`~/.sema` linked into a dotfiles directory). The row is placed at each location too —
|
|
447
|
+
* `linked` — where a home-anchored or a rooted row's LITERAL HEAD ({@link readDenyRunHead}) really leads, keyed by the row's
|
|
448
|
+
* pattern: the canonical locations a mount resolved (`readDenyHomeLinksOf` / `readDenyRootLinksOf`, read-deny-home.ts) where
|
|
449
|
+
* one of the head's segments is a link (`~/.sema` linked into a dotfiles directory; `/app` a link to `/data/app`). The row is placed at each location too —
|
|
443
450
|
* the same row, the same name in every refusal — with the rest of its run matched below it. The read faces judge
|
|
444
451
|
* canonical keys, so every spelling that reaches the linked file meets the row there. Absolute locations only.
|
|
445
452
|
*/
|
|
@@ -418,7 +418,14 @@ function normalizeEntry(entry, layer) {
|
|
|
418
418
|
throw new Error(`readDenyPatterns (${layer}): pattern ${JSON.stringify(shape.pattern)} contains no path segments${homeAnchored ? " below the home word" : ""} and would ${homeAnchored ? "name the whole home directory" : "deny nothing"}. ` +
|
|
419
419
|
`Patterns are "/"-separated runs of path SEGMENTS (e.g. ".ssh", ".config/gcloud", "id_rsa*", "~/.sema/settings.json"); remove the entry or spell the segments.`);
|
|
420
420
|
}
|
|
421
|
-
return { pattern:
|
|
421
|
+
return { pattern: canonicalReadDenyPattern(shape.pattern), caseSensitive: shape.caseSensitive === true };
|
|
422
|
+
}
|
|
423
|
+
function canonicalReadDenyPattern(pattern) {
|
|
424
|
+
const joined = pattern.split("/").filter(Boolean).join("/");
|
|
425
|
+
return pattern.startsWith("/") && !readDenyPatternRun(joined).homeAnchored ? `/${joined}` : joined;
|
|
426
|
+
}
|
|
427
|
+
export function isRootedReadDenyPattern(canonical) {
|
|
428
|
+
return canonical.startsWith("/");
|
|
422
429
|
}
|
|
423
430
|
export function persistedReadDenyEntryProblem(e) {
|
|
424
431
|
if (typeof e !== "object" || e === null)
|
|
@@ -434,7 +441,7 @@ export function persistedReadDenyEntryProblem(e) {
|
|
|
434
441
|
const segments = pattern.split("/").filter(Boolean);
|
|
435
442
|
if (readDenyPatternRun(segments.join("/")).run.length === 0)
|
|
436
443
|
return "pattern has no path segments";
|
|
437
|
-
if (
|
|
444
|
+
if (canonicalReadDenyPattern(pattern) !== pattern)
|
|
438
445
|
return "pattern is not in canonical a/b form";
|
|
439
446
|
return null;
|
|
440
447
|
}
|
|
@@ -473,6 +480,8 @@ export function compileReadDeny(additions = [], layer = "additions", builtin, ru
|
|
|
473
480
|
const c = compileSegmentPattern(run, fold);
|
|
474
481
|
if (c === null)
|
|
475
482
|
throw new Error(`readDenyPatterns: pattern ${JSON.stringify(e.pattern)} compiled to zero segments.`);
|
|
483
|
+
if (isRootedReadDenyPattern(e.pattern))
|
|
484
|
+
return { ...c, raw: e.pattern, anchors: [[]], rootedOnly: true };
|
|
476
485
|
if (!homeAnchored || homeSegments.length === 0)
|
|
477
486
|
return { ...c, raw: e.pattern };
|
|
478
487
|
return { ...c, raw: e.pattern, anchors: homeSegments.map((segs) => (fold === "ascii" ? segs.map(asciiLower) : segs)) };
|
|
@@ -483,7 +492,7 @@ export function compileReadDeny(additions = [], layer = "additions", builtin, ru
|
|
|
483
492
|
normalized.forEach((e, i) => {
|
|
484
493
|
const locations = linked.get(e.pattern);
|
|
485
494
|
const { homeAnchored, run } = readDenyPatternRun(e.pattern);
|
|
486
|
-
if (locations === undefined || !homeAnchored)
|
|
495
|
+
if (locations === undefined || !(homeAnchored || isRootedReadDenyPattern(e.pattern)))
|
|
487
496
|
return;
|
|
488
497
|
const c = compiled[i];
|
|
489
498
|
const rest = c.segments.slice(readDenyRunHead(run));
|
|
@@ -2,7 +2,8 @@ import { planShellReads } from "./shell-read-plan.js";
|
|
|
2
2
|
import { literalTextOf } from "../../shell/lex.js";
|
|
3
3
|
import { createShellReadEstablisher, establishShellReadsLexically } from "./shell-read-establish.js";
|
|
4
4
|
import { isAbsolutePathForm, isWinFormPath, normalizeAbsPathLexically } from "./safety.js";
|
|
5
|
-
import { reentryText } from "./shell-reentry-text.js";
|
|
5
|
+
import { launchedCommands, launchedReadings, reentryText, scriptFileAt, spellLaunched } from "./shell-reentry-text.js";
|
|
6
|
+
import { isShellSyntaxWord, LOOP_LIST_HEADS, readProgramPosition } from "./bash-program-position.js";
|
|
6
7
|
import { bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
|
|
7
8
|
import { programArgs } from "./shell-read-write-targets.js";
|
|
8
9
|
import { implementationsOf, programRow, runsShellText, selectedOptions, writeReadings } from "./program-read-model.js";
|
|
@@ -67,7 +68,7 @@ function bindingsOf(plan) {
|
|
|
67
68
|
if (w.assignment !== undefined)
|
|
68
69
|
bind(w.assignment.name, w.assignment.settlesWholeName ? literalTextOf(w.assignment.value.pieces) : undefined);
|
|
69
70
|
const [head, name] = segment.argv;
|
|
70
|
-
if ((head
|
|
71
|
+
if (isShellSyntaxWord(head, LOOP_LIST_HEADS) && name !== undefined && NAME.test(name.folded)) {
|
|
71
72
|
const at = segment.argv.findIndex((w, i) => i > 1 && w.folded === "in");
|
|
72
73
|
const words = at < 0 ? [undefined] : segment.argv.slice(at + 1).map((w) => literalTextOf(w.pieces));
|
|
73
74
|
for (const v of words.length > 0 ? words : [undefined])
|
|
@@ -167,10 +168,28 @@ function runtimeScriptOf(segment) {
|
|
|
167
168
|
const program = segment.verb;
|
|
168
169
|
if (segment.reentry !== "shell" || program === undefined || !runsShellText(program.basename))
|
|
169
170
|
return undefined;
|
|
170
|
-
const at = segment.argv.findIndex((w) => w.folded === program.name);
|
|
171
171
|
const MARK = "\u0000";
|
|
172
|
-
const
|
|
173
|
-
|
|
172
|
+
const spell = (k, from = 0) => {
|
|
173
|
+
const w = segment.argv[k];
|
|
174
|
+
const literal = literalTextOf(w.pieces);
|
|
175
|
+
return literal !== undefined ? literal.slice(from) : `${MARK}${w.raw.slice(from)}`;
|
|
176
|
+
};
|
|
177
|
+
const pos = readProgramPosition(segment.argv);
|
|
178
|
+
if (pos.program !== undefined && pos.refusal === undefined) {
|
|
179
|
+
const argv = segment.argv.slice(pos.program.index + 1);
|
|
180
|
+
const file = scriptFileAt(program.basename, argv.map((w) => w.folded));
|
|
181
|
+
const source = file === undefined ? undefined : argv[file];
|
|
182
|
+
if (source !== undefined && source.pieces.length === 1 && source.pieces[0].expansion?.kind === "process")
|
|
183
|
+
return source.raw;
|
|
184
|
+
if (pos.argvFromStdin)
|
|
185
|
+
return segment.text.trim();
|
|
186
|
+
}
|
|
187
|
+
const named = pos.program !== undefined && pos.refusal === undefined ? [{ shell: program.basename, words: segment.argv.slice(pos.program.index + 1).map((_, k) => spell(pos.program.index + 1 + k)) }] : [];
|
|
188
|
+
const launched = launchedCommands(segment.argv, pos).flatMap((c) => {
|
|
189
|
+
const shell = launchedReadings(segment.argv, c).shell;
|
|
190
|
+
return shell === undefined ? [] : spellLaunched(segment.argv, c.args, { word: spell }).map((words) => ({ shell, words }));
|
|
191
|
+
});
|
|
192
|
+
const texts = [...named, ...launched].flatMap(({ shell, words }) => [reentryText(shell, words), ...(segment.stdin ?? []).map((s) => reentryText(shell, words, s.literal ?? `${MARK}${s.from}`))]);
|
|
174
193
|
const text = texts.find((t) => t?.includes(MARK) === true);
|
|
175
194
|
return text === undefined ? undefined : text.split(MARK).join("");
|
|
176
195
|
}
|
|
@@ -7,14 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type LexedWord } from "../../shell/lex.js";
|
|
9
9
|
import { type Read } from "./shell-read-word.js";
|
|
10
|
-
import { type BindingTable, type
|
|
10
|
+
import { type BindingTable, type FloorValue } from "./shell-read-bindings.js";
|
|
11
11
|
import { subcommandOf, type ArgRole, type ProgramRow } from "./program-read-model.js";
|
|
12
12
|
import type { ReadCandidate, ReadCandidateProvenance } from "./shell-read-plan.js";
|
|
13
|
-
/** A candidate before the threading fills its bases. `
|
|
14
|
-
*
|
|
13
|
+
/** A candidate before the threading fills its bases. `floors`: the spellings its word's names left for the deny judge
|
|
14
|
+
* alone (a word whose names are unknown here). */
|
|
15
15
|
export type Bare = Omit<ReadCandidate, "bases"> & {
|
|
16
|
-
readonly
|
|
17
|
-
readonly floors?: readonly BoundValue[];
|
|
16
|
+
readonly floors?: readonly FloorValue[];
|
|
18
17
|
};
|
|
19
18
|
/** What reading a segment's words found beside its candidates. */
|
|
20
19
|
export interface WordContext {
|
|
@@ -35,9 +34,8 @@ export declare function pathSubcommandOf(row: ProgramRow | undefined, args: read
|
|
|
35
34
|
/** The sentence of inline program text the station does not read. */
|
|
36
35
|
export declare const programTextSentence: (language: string) => string;
|
|
37
36
|
/** One candidate without its bases (filled by the threading pass): the word's spelling — or `spelled`, a caller's
|
|
38
|
-
* reading of PART of it (an option payload, a `key=value` value) — with the word's own expansion facts.
|
|
39
|
-
*
|
|
40
|
-
* (it is unresolvable). */
|
|
37
|
+
* reading of PART of it (an option payload, a `key=value` value) — with the word's own expansion facts. The word is read
|
|
38
|
+
* in one reading of the segment's names (`segmentReadingsOf`): its names hold one value each, or are unknown. */
|
|
41
39
|
export declare function bare(r: Read, provenance: ReadCandidateProvenance, spelled?: string): Bare;
|
|
42
40
|
/** The reads one value of a declared role hands over: a candidate per file it names, or — a value the program expands
|
|
43
41
|
* past the listed bound — the word itself, unresolvable. A mount source is a read the daemon makes of the whole tree
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { literalTildeSpelling } from "../../shell/lex.js";
|
|
2
|
-
import { expandsOf, readOfText, rewritten } from "./shell-read-word.js";
|
|
2
|
+
import { expandsOf, partPattern, readOfText, rewritten } from "./shell-read-word.js";
|
|
3
3
|
import { arithmeticSubstitutions } from "./shell-read-bindings.js";
|
|
4
4
|
import { argvProgramOf, attachedOptionPayloads, isPathShapedToken } from "./bash-readonly-classifier.js";
|
|
5
5
|
import { assignmentArgumentsAreNotPaths, evaluatesOperands, messageWords, moduleTailOf, objectPathsOf, opensNoOperand, programRow, programTextMayOpen, programTextOpensOf, readArgs, selectsContentsTraversal, slotValueCandidates, subcommandOf, valuesAreLiteral, walksCwdWithoutOperand, wordsReadBySomeImplementations, } from "./program-read-model.js";
|
|
@@ -10,14 +10,13 @@ export function pathSubcommandOf(row, args) {
|
|
|
10
10
|
export const programTextSentence = (language) => `runs inline ${language} program text, which may open files this station does not read`;
|
|
11
11
|
export function bare(r, provenance, spelled = r.folded) {
|
|
12
12
|
const whole = spelled === r.folded;
|
|
13
|
-
|
|
14
|
-
return { spelled, raw: r.raw, provenance, expands: "unresolvable" };
|
|
13
|
+
const pattern = whole ? r.pattern : partPattern(r, spelled);
|
|
15
14
|
return {
|
|
16
15
|
spelled,
|
|
17
16
|
raw: r.raw,
|
|
18
17
|
provenance,
|
|
19
18
|
expands: r.expands,
|
|
20
|
-
...(
|
|
19
|
+
...(pattern !== undefined ? { pattern } : {}),
|
|
21
20
|
...(whole && r.homeFloor !== undefined ? { floorSpelled: r.homeFloor } : {}),
|
|
22
21
|
...(whole && r.floorValues !== undefined ? { floors: r.floorValues } : {}),
|
|
23
22
|
};
|
|
@@ -62,8 +61,9 @@ export function pushAssignment(reads, w, provenance) {
|
|
|
62
61
|
if (!settled && unsettled !== undefined)
|
|
63
62
|
reads.push(unsettled);
|
|
64
63
|
const substitutes = a.value.expansions.some((p) => p.expansion?.kind === "command" || p.expansion?.kind === "process" || p.expansion?.kind === "arithmetic");
|
|
64
|
+
const pattern = named === undefined ? undefined : partPattern({ folded: w.folded, ...(w.judged !== undefined ? { pattern: w.judged } : {}) }, named);
|
|
65
65
|
if (named !== undefined && !substitutes)
|
|
66
|
-
reads.push({ spelled: named, raw: w.raw, provenance, expands: expandsOf(w) });
|
|
66
|
+
reads.push({ spelled: named, raw: w.raw, provenance, expands: expandsOf(w), ...(pattern !== undefined ? { pattern } : {}) });
|
|
67
67
|
if (a.value.folded.includes(":"))
|
|
68
68
|
for (const m of a.value.folded.split(":"))
|
|
69
69
|
if (m !== "") {
|
|
@@ -26,12 +26,46 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { type LexedRedirect, type LexedSegment, type LexedToken, type LexedWord } from "../../shell/lex.js";
|
|
28
28
|
import { type DeclaredAttribute } from "./program-read-model.js";
|
|
29
|
-
/** A value a name may hold
|
|
30
|
-
* list word the shell globs before the loop binds it
|
|
31
|
-
|
|
29
|
+
/** A value a name may hold — the text the program receives ({@link spelled}: what a shell re-parsing it reads), literal
|
|
30
|
+
* throughout, or a PATTERN whose matches are the values (a `for` list word the shell globs before the loop binds it;
|
|
31
|
+
* `spelled` is then the pattern as the program receives it where it matches nothing): `glob`, with its `parts` — the
|
|
32
|
+
* value in runs: a literal run (quoted or literal text, a settled value) and a pattern run (glob pieces as written,
|
|
33
|
+
* quoting kept, and as a reader judges them — {@link WordExpansion.pattern}). The one producer is the piece walk
|
|
34
|
+
* ({@link valueOf}). */
|
|
35
|
+
export type BoundValue = {
|
|
36
|
+
readonly spelled: string;
|
|
37
|
+
readonly glob?: never;
|
|
38
|
+
readonly parts?: never;
|
|
39
|
+
readonly fields?: readonly string[];
|
|
40
|
+
} | {
|
|
41
|
+
readonly spelled: string;
|
|
42
|
+
readonly glob: true;
|
|
43
|
+
readonly parts: readonly ValuePart[];
|
|
44
|
+
readonly fields?: never;
|
|
45
|
+
};
|
|
46
|
+
/** One run of a pattern {@link BoundValue}: literal text, or a pattern — the shell expands its characters — `text` as
|
|
47
|
+
* written (what a shell re-reading it matches by), `judged` as a reader judges the names it matches. */
|
|
48
|
+
export type ValuePart = {
|
|
49
|
+
readonly text: string;
|
|
50
|
+
readonly pattern: false;
|
|
51
|
+
} | {
|
|
52
|
+
readonly text: string;
|
|
53
|
+
readonly pattern: true;
|
|
54
|
+
readonly judged: string;
|
|
55
|
+
};
|
|
56
|
+
/** A spelling the deny judge alone reads ({@link Binding.floor}, {@link wordFloorValues}): a value, or a word as written
|
|
57
|
+
* where its names are unknown — nothing re-reads it, so a pattern says no more than that it is one. */
|
|
58
|
+
export interface FloorValue {
|
|
32
59
|
readonly spelled: string;
|
|
33
60
|
readonly glob?: true;
|
|
61
|
+
/** A pattern's spelling as a reader judges the names it matches, where it is not `spelled` ({@link judgedSpellingOf}). */
|
|
62
|
+
readonly pattern?: string;
|
|
34
63
|
}
|
|
64
|
+
/** A pattern value's names as a reader judges them, where that is not its spelling: its runs, a class the judge widens
|
|
65
|
+
* as `?` (the lexer's `WordExpansion.pattern.judged`), a leading `~` the literal tilde it is. */
|
|
66
|
+
export declare function judgedSpellingOf(v: BoundValue): string | undefined;
|
|
67
|
+
/** A value as a deny-floor spelling, its judged spelling with it. */
|
|
68
|
+
export declare function floorOf(v: BoundValue): FloorValue;
|
|
35
69
|
/** What a name holds at one point of a text: its possible values, or `"unknown"`. */
|
|
36
70
|
type NameState = readonly BoundValue[] | "unknown";
|
|
37
71
|
/**
|
|
@@ -58,7 +92,7 @@ export interface Binding {
|
|
|
58
92
|
readonly scope?: "local" | "unread";
|
|
59
93
|
/** Spellings the name may hold that the deny judge still reads where the name is otherwise unknown (an array's
|
|
60
94
|
* elements: what `${A[k]}` expands to is one of them). */
|
|
61
|
-
readonly floor?: readonly
|
|
95
|
+
readonly floor?: readonly FloorValue[];
|
|
62
96
|
}
|
|
63
97
|
/** An attribute a declaration gives a name — or every name (`name` absent: the declaration computed it): a later
|
|
64
98
|
* assignment to it is evaluated as arithmetic (`integer`), stores a case change or an array element (`transformed`), or
|
|
@@ -104,27 +138,40 @@ export declare function projectSegment(segment: LexedSegment): ProjectedSegment;
|
|
|
104
138
|
* token, a head whose body opens in a later segment). */
|
|
105
139
|
export declare function dropDefinitionHeads(tokens: LexedToken[]): readonly number[];
|
|
106
140
|
/**
|
|
107
|
-
* What a word holds through the names it refers to — `$NAME`, `${NAME}`, a leading `~` (the value of `HOME`):
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
|
|
141
|
+
* What a word holds through the names it refers to — `$NAME`, `${NAME}`, a leading `~` (the value of `HOME`): `undefined`
|
|
142
|
+
* where it refers to no name (it reads as written), else {@link valueOf}. `table.stateOf` is read after `overlay` (the
|
|
143
|
+
* names the same segment bound earlier).
|
|
144
|
+
*/
|
|
145
|
+
export declare function wordValues(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined, quoted: boolean, overlay?: ReadonlyMap<string, NameState>, split?: boolean): readonly BoundValue[] | "unknown" | undefined;
|
|
146
|
+
/** The names a word's expansions read as values — a bare `$NAME` / `${NAME}`, and one written inside a bracket the shell
|
|
147
|
+
* forms its class from after expanding it ({@link valueOf}) — in the order they stand. */
|
|
148
|
+
export declare function namesIn(word: Pick<LexedWord, "pieces">): string[];
|
|
149
|
+
/**
|
|
150
|
+
* What a VALUE word holds (an assignment's value, a `for` list word, a word handed to a program) — THE piece walk, the one
|
|
151
|
+
* producer of {@link BoundValue}s: every combination of the values its pieces take, each field's pattern formed once the
|
|
152
|
+
* whole field has expanded ({@link formedField}) — a literal piece its text, a glob piece its pattern as written (a bracket
|
|
153
|
+
* holding an expansion its brackets and its body, the body's names substituted in place), `~` and `$NAME` the values the
|
|
154
|
+
* table gives:
|
|
155
|
+
* · its values — a field the shell globs makes the value a pattern;
|
|
111
156
|
* · `"unknown"` — a name it refers to is not settled here, it carries an expansion no name answers (a modifier, a
|
|
112
|
-
* substitution, a brace list, `~user`), an UNQUOTED expansion's value is
|
|
113
|
-
* (`quoted` = the whole word is read as one value: an assignment's
|
|
114
|
-
*
|
|
115
|
-
*
|
|
157
|
+
* substitution, a brace list, `~user`), an UNQUOTED expansion's value is no {@link READABLE_VALUE} (or holds
|
|
158
|
+
* whitespace outside `split`), `IFS` may have changed (`quoted` = the whole word is read as one value: an assignment's
|
|
159
|
+
* value, which is never split), a field is not formed or a split one forms a pattern, or it would list more than
|
|
160
|
+
* {@link WORD_VALUES_MAX} values.
|
|
161
|
+
* `split` = the word is read as the ARGUMENTS it becomes: an unquoted value is split into its words on the default `IFS`
|
|
162
|
+
* (a value then carries its {@link BoundValue} `fields`).
|
|
116
163
|
*/
|
|
117
|
-
export declare function
|
|
164
|
+
export declare function valueOf(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined, quoted: boolean, overlay?: ReadonlyMap<string, NameState>, split?: boolean): NameState;
|
|
118
165
|
/**
|
|
119
166
|
* The spellings a word the table leaves unknown may still take through the deny floors of the names it refers to
|
|
120
167
|
* ({@link BindingTable.floorOf}) — `${A[k]}`, `${A[@]}`, `$A` reading an array's elements — or through their values where
|
|
121
168
|
* known: for the deny judge alone. Empty where some name it refers to leaves neither.
|
|
122
169
|
*/
|
|
123
|
-
export declare function wordFloorValues(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined): readonly
|
|
124
|
-
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
export declare function
|
|
170
|
+
export declare function wordFloorValues(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined): readonly FloorValue[];
|
|
171
|
+
/** Where a segment's argv heads a `for` / `select` loop: the index of that bare word, at the segment's start or past the
|
|
172
|
+
* bare control words in front of it (`do for x in …`, `then for …` — a loop nested in a body or a branch); `undefined`
|
|
173
|
+
* for any other segment. */
|
|
174
|
+
export declare function loopListHeadAt(argv: readonly LexedWord[]): number | undefined;
|
|
128
175
|
/**
|
|
129
176
|
* What ONE segment does to the names (see the module header). `argv` = the segment's argv projection (structure
|
|
130
177
|
* dropped); `readValue` folds a value word against the table (a reader that answers `unknown` gives the names alone).
|
|
@@ -153,6 +200,9 @@ export type NameReader = (name: string) => NameState;
|
|
|
153
200
|
export interface InheritedNames {
|
|
154
201
|
readonly attributes: readonly NameAttribute[];
|
|
155
202
|
readonly references: boolean;
|
|
203
|
+
/** The deny floors a name starts the text with ({@link BindingTable.floorOf}): what the words handing it this text as
|
|
204
|
+
* written may name there — or, for a substitution body, the floors its segment reads. */
|
|
205
|
+
readonly floors?: ReadonlyMap<string, readonly FloorValue[]>;
|
|
156
206
|
}
|
|
157
207
|
/** What the thread answers for one reader: what a name may hold, and whether a name carries the integer attribute. */
|
|
158
208
|
export interface BindingTable {
|
|
@@ -164,7 +214,7 @@ export interface BindingTable {
|
|
|
164
214
|
/** `HOME` may hold something other than the declared home here. */
|
|
165
215
|
readonly homeTouched: boolean;
|
|
166
216
|
/** Every spelling any binding of this name before here left for the deny judge alone ({@link Binding.floor}). */
|
|
167
|
-
floorOf(name: string): readonly
|
|
217
|
+
floorOf(name: string): readonly FloorValue[];
|
|
168
218
|
/** What a substitution body here starts with. */
|
|
169
219
|
readonly inherited: InheritedNames;
|
|
170
220
|
}
|
|
@@ -208,6 +258,8 @@ export declare class BindingThread {
|
|
|
208
258
|
private readonly blocks;
|
|
209
259
|
/** Segments that are a pipeline member or run in the background. */
|
|
210
260
|
private readonly detached;
|
|
261
|
+
/** The floors the names start the text with ({@link InheritedNames.floors}). */
|
|
262
|
+
private readonly startFloors;
|
|
211
263
|
constructor(segments: readonly LexedSegment[],
|
|
212
264
|
/** What each segment binds, its names alone (a value is the reader's to read). */
|
|
213
265
|
named: readonly BindingEffect[],
|