@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
|
@@ -272,6 +272,23 @@ export interface LauncherSpec {
|
|
|
272
272
|
readonly requiredOperandSpelling?: RegExp;
|
|
273
273
|
/** A lone `-` in its option section is its own switch (`env -`, the obsolete spelling of `env -i`). */
|
|
274
274
|
readonly bareDash?: true;
|
|
275
|
+
/** It runs its shell as ANOTHER user ({@link TargetUser}): the user an option of `userWith` names (`sudo -u USER`), or —
|
|
276
|
+
* a {@link shellText} `user` launcher — its user operand; root where none is named. The shell's `HOME` is that user's
|
|
277
|
+
* home (`target`), or the caller's or that user's as the launcher's policy decides (`policy`: `sudo -s`); a switch of
|
|
278
|
+
* `loginWith` (`sudo -i`, `su -l`, su's lone `-` operand) starts it as a LOGIN shell — its `HOME` that user's, and it
|
|
279
|
+
* stands in that home. A `names-program` switch (`su -m`) keeps the caller's environment, `HOME` with it. */
|
|
280
|
+
readonly asUser?: {
|
|
281
|
+
readonly userWith?: readonly string[];
|
|
282
|
+
readonly home: "target" | "policy";
|
|
283
|
+
readonly loginWith: readonly string[];
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/** The user a launcher runs its shell as ({@link LauncherSpec.asUser}): the name, where a word spells it literally; the
|
|
287
|
+
* shell's `HOME` (`caller` — kept; `target` — that user's; `either` — one of the two); whether it is a login shell. */
|
|
288
|
+
export interface TargetUser {
|
|
289
|
+
readonly name?: string;
|
|
290
|
+
readonly home: "caller" | "target" | "either";
|
|
291
|
+
readonly login: boolean;
|
|
275
292
|
}
|
|
276
293
|
/** How a program receives the PROGRAM it runs (an interpreter): an option value, an operand (`operands.positional`),
|
|
277
294
|
* or standard input when it names none. */
|
|
@@ -563,6 +580,20 @@ export declare function attachedStdinFile(row: ProgramRow | undefined, tok: stri
|
|
|
563
580
|
/** The row read under each implementation's grammar alone (one view per grammar), or the row itself when it declares
|
|
564
581
|
* at most one. A reader that needs to know what ONE implementation does with an argv reads these views. */
|
|
565
582
|
export declare function implementationsOf(row: ProgramRow): readonly ProgramRow[];
|
|
583
|
+
/** What a scan over an argv's option words found: the specs selected, the values they took (argv index; the value
|
|
584
|
+
* inside the option word when it is attached), and the operands (argv indices). */
|
|
585
|
+
interface OptionScan {
|
|
586
|
+
readonly specs: readonly OptionSpec[];
|
|
587
|
+
/** Each selected option's spelling as its row declares it (`-x`, `--exec`), in step with `specs`. */
|
|
588
|
+
readonly names: readonly string[];
|
|
589
|
+
readonly values: readonly {
|
|
590
|
+
readonly at: number;
|
|
591
|
+
readonly value?: string;
|
|
592
|
+
readonly spec: OptionSpec;
|
|
593
|
+
readonly name: string;
|
|
594
|
+
}[];
|
|
595
|
+
readonly operands: readonly number[];
|
|
596
|
+
}
|
|
566
597
|
/**
|
|
567
598
|
* Every option spec an argv selects. A short cluster is read as getopt reads it, scanning left to right: a declared
|
|
568
599
|
* letter is selected; a value-taking letter owns the rest of the token (and the next word, when it ends the token); a
|
|
@@ -574,6 +605,14 @@ export declare function implementationsOf(row: ProgramRow): readonly ProgramRow[
|
|
|
574
605
|
* option, and a word past `--`, included. The reading a check that only ever refuses takes: it errs to more options.
|
|
575
606
|
*/
|
|
576
607
|
export declare function selectedOptions(row: ProgramRow | undefined, args: readonly string[], under?: "one-implementation"): OptionSpec[];
|
|
608
|
+
/** The options ONE implementation's reading of an argv selects ({@link selectedOptions}), named by the spelling its row
|
|
609
|
+
* declares them under (`-x`, `--exec`), and the values they take. An option is told apart by its spelling, never by its
|
|
610
|
+
* spec: spellings share specs (every switch of a row is one). */
|
|
611
|
+
export declare function selectedSpellings(row: ProgramRow, args: readonly string[]): Pick<OptionScan, "names" | "values">;
|
|
612
|
+
/** Does ONE implementation of a row (`view`, {@link implementationsOf}) reject these option words — a spelling the row
|
|
613
|
+
* declares (another implementation's) that this one does not? getopt calls it an invalid option: the program runs
|
|
614
|
+
* nothing. */
|
|
615
|
+
export declare function implementationRejects(row: ProgramRow, view: ProgramRow, words: readonly string[]): boolean;
|
|
577
616
|
/** A path role whose effect writes. */
|
|
578
617
|
export type WritingRole = Extract<ArgRole, {
|
|
579
618
|
kind: "path";
|
|
@@ -656,22 +695,38 @@ export declare function moduleTailOf(row: ProgramRow | undefined, args: readonly
|
|
|
656
695
|
* value exists only attached (spelled bare, it takes nothing), 1 for one that takes the next word; `undefined` for any
|
|
657
696
|
* other spelling and for an option the walk does not step over. */
|
|
658
697
|
export declare function launcherOptionArity(row: ProgramRow, tok: string): 0 | 1 | undefined;
|
|
659
|
-
/** The PATH role of the value a launcher option
|
|
660
|
-
* when its value names no path. Such a value is a word the launcher opens or
|
|
698
|
+
/** The PATH role of the value a launcher option word takes (`xargs -a FILE`, `strace -fo FILE`, `strace -oFILE`), or
|
|
699
|
+
* `undefined` when its value names no path ({@link readLauncherOption}). Such a value is a word the launcher opens or
|
|
700
|
+
* writes, not an operand of its own. */
|
|
661
701
|
export declare function launcherOptionPathRole(row: ProgramRow, tok: string): Extract<ArgRole, {
|
|
662
702
|
kind: "path";
|
|
663
703
|
}> | undefined;
|
|
664
|
-
/** How many
|
|
665
|
-
*
|
|
666
|
-
* switches (1); a lone `-` where the launcher reads it as its own switch (1). */
|
|
704
|
+
/** How many words one launcher option occupies ({@link readLauncherOption}), or `undefined` where the row does not
|
|
705
|
+
* explain it. */
|
|
667
706
|
export declare function launcherOptionStep(row: ProgramRow, tok: string): 1 | 2 | undefined;
|
|
668
707
|
/** Does a launcher that runs a shell ({@link LauncherSpec.shellText}) run one over these words — the words after it —
|
|
669
708
|
* rather than the program they name? Not where an implementation selects one of its {@link LauncherSpec.programWith}
|
|
670
709
|
* options (as that implementation reads them: past `--` a word is an operand). */
|
|
671
710
|
export declare function launcherRunsShell(row: ProgramRow, args: readonly string[]): boolean;
|
|
711
|
+
/** The user a launcher runs its shell as ({@link LauncherSpec.asUser}), read off its own words (`words`, as the program
|
|
712
|
+
* receives them) by its row's walk: the value of its last `userWith` option, else its user operand (`operand`; a lone `-`
|
|
713
|
+
* in front of it starts a login shell, `dash`), else root — a name only where it is spelled as one (`root`, `#0`, a login
|
|
714
|
+
* name); `undefined` for a launcher that runs no other user. */
|
|
715
|
+
export declare function targetUserOf(row: ProgramRow, words: readonly string[], operand?: {
|
|
716
|
+
readonly name?: string;
|
|
717
|
+
readonly dash: boolean;
|
|
718
|
+
}): TargetUser | undefined;
|
|
672
719
|
/** The spellings of a launcher's command-line options (`env -S` / `--split-string`): the value is a command line the
|
|
673
720
|
* launcher splits and runs. */
|
|
674
721
|
export declare function commandLineOptionSpellings(row: ProgramRow | undefined): readonly string[];
|
|
722
|
+
/** The command line ONE launcher option word carries, read as getopt reads the word ({@link readLauncherOption}): a
|
|
723
|
+
* command-line spelling ({@link commandLineOptionSpellings}) exactly — `"next"`, the next word is the line — or with its
|
|
724
|
+
* value after `=` (long) or after the letter (short, `attached`); in a short cluster every letter before the
|
|
725
|
+
* command-line letter is a switch the walk steps (`env -vS 'S'`, `env -iS'S'`). `undefined` where the word carries
|
|
726
|
+
* none. */
|
|
727
|
+
export declare function commandLineOf(row: ProgramRow, tok: string): {
|
|
728
|
+
readonly attached: string;
|
|
729
|
+
} | "next" | undefined;
|
|
675
730
|
/** One implementation's reading of where the program text is: the text (every text option's value joined by newlines,
|
|
676
731
|
* or the first operand), `from-file` (an option names a FILE holding it), `unparsed` (an option the grammar does not
|
|
677
732
|
* declare, or a text option with nothing left to take), or `none` (no program text at all). */
|
|
@@ -682,6 +682,16 @@ export function implementationsOf(row) {
|
|
|
682
682
|
export function selectedOptions(row, args, under) {
|
|
683
683
|
return row === undefined ? [] : [...scanOptions(row, args, under === "one-implementation").specs];
|
|
684
684
|
}
|
|
685
|
+
export function selectedSpellings(row, args) {
|
|
686
|
+
const scan = scanOptions(row, args, true);
|
|
687
|
+
return { names: scan.names, values: scan.values };
|
|
688
|
+
}
|
|
689
|
+
export function implementationRejects(row, view, words) {
|
|
690
|
+
if (view === row)
|
|
691
|
+
return false;
|
|
692
|
+
const declares = (n) => (n.startsWith("--") ? mergedTable(view, "long").has(n.slice(2)) : mergedTable(view, n.length === 2 ? "short" : "dashWords").has(n.slice(1)));
|
|
693
|
+
return selectedSpellings(row, words).names.some((n) => !declares(n));
|
|
694
|
+
}
|
|
685
695
|
function scanOptions(row, args, one) {
|
|
686
696
|
const stopsAtOperand = one && row.grammars[0]?.afterFirstOperand === "operands";
|
|
687
697
|
const shorts = mergedTable(row, "short");
|
|
@@ -689,8 +699,10 @@ function scanOptions(row, args, one) {
|
|
|
689
699
|
const dashWords = mergedTable(row, "dashWords");
|
|
690
700
|
const obsolete = row.grammars.flatMap((g) => (g.obsoleteFirst === undefined ? [] : [g.obsoleteFirst]));
|
|
691
701
|
const specs = [];
|
|
702
|
+
const names = [];
|
|
692
703
|
const values = [];
|
|
693
704
|
const operands = [];
|
|
705
|
+
const select = (spec, name) => { specs.push(spec); names.push(name); };
|
|
694
706
|
let endOfOptions = false;
|
|
695
707
|
for (let k = 0; k < args.length; k++) {
|
|
696
708
|
const t = args[k];
|
|
@@ -704,7 +716,7 @@ function scanOptions(row, args, one) {
|
|
|
704
716
|
for (const ch of t.replace(/^.\d+/, "")) {
|
|
705
717
|
const spec = o.letters[ch];
|
|
706
718
|
if (spec !== undefined)
|
|
707
|
-
|
|
719
|
+
select(spec, `-${ch}`);
|
|
708
720
|
}
|
|
709
721
|
continue;
|
|
710
722
|
}
|
|
@@ -719,13 +731,13 @@ function scanOptions(row, args, one) {
|
|
|
719
731
|
endOfOptions = true;
|
|
720
732
|
continue;
|
|
721
733
|
}
|
|
722
|
-
const take = (spec, attached) => {
|
|
734
|
+
const take = (spec, name, attached) => {
|
|
723
735
|
if (attached !== undefined) {
|
|
724
|
-
values.push({ at: k, value: attached, spec });
|
|
736
|
+
values.push({ at: k, value: attached, spec, name });
|
|
725
737
|
return;
|
|
726
738
|
}
|
|
727
739
|
if (one && k + 1 < args.length) {
|
|
728
|
-
values.push({ at: k + 1, spec });
|
|
740
|
+
values.push({ at: k + 1, spec, name });
|
|
729
741
|
k++;
|
|
730
742
|
}
|
|
731
743
|
};
|
|
@@ -735,22 +747,23 @@ function scanOptions(row, args, one) {
|
|
|
735
747
|
continue;
|
|
736
748
|
const eq = t.indexOf("=");
|
|
737
749
|
const exact = longs.get(name);
|
|
738
|
-
const hits = exact !== undefined ? [exact] : [...longs].filter(([full]) => isLongOptionAbbrevOf(name, full))
|
|
739
|
-
for (const m of hits)
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
750
|
+
const hits = exact !== undefined ? [[name, exact]] : [...longs].filter(([full]) => isLongOptionAbbrevOf(name, full));
|
|
751
|
+
for (const [full, m] of hits)
|
|
752
|
+
select(m.spec, `--${full}`);
|
|
753
|
+
const [full, only] = hits[0] ?? [];
|
|
754
|
+
if (hits.length === 1 && (only.arity === 1 || (eq >= 0 && only.arity === "attached")))
|
|
755
|
+
take(only.spec, `--${full}`, eq >= 0 ? t.slice(eq + 1) : undefined);
|
|
756
|
+
else if (one && eq < 0 && hits.length > 1 && hits.every(([, m]) => m.arity === 1))
|
|
744
757
|
k++;
|
|
745
758
|
continue;
|
|
746
759
|
}
|
|
747
760
|
const word = /^-([A-Za-z_][\w-]+)$/.exec(t)?.[1];
|
|
748
761
|
const dash = word === undefined ? undefined : dashWords.get(word);
|
|
749
762
|
if (dash !== undefined) {
|
|
750
|
-
|
|
763
|
+
select(dash.spec, t);
|
|
751
764
|
if (one) {
|
|
752
765
|
if (dash.arity === 1)
|
|
753
|
-
take(dash.spec, undefined);
|
|
766
|
+
take(dash.spec, t, undefined);
|
|
754
767
|
continue;
|
|
755
768
|
}
|
|
756
769
|
}
|
|
@@ -761,18 +774,18 @@ function scanOptions(row, args, one) {
|
|
|
761
774
|
continue;
|
|
762
775
|
break;
|
|
763
776
|
}
|
|
764
|
-
|
|
777
|
+
select(m.spec, `-${t[i]}`);
|
|
765
778
|
if (m.arity === 0)
|
|
766
779
|
continue;
|
|
767
780
|
const rest = t.slice(i + 1);
|
|
768
781
|
if (rest !== "")
|
|
769
|
-
values.push({ at: k, value: rest, spec: m.spec });
|
|
782
|
+
values.push({ at: k, value: rest, spec: m.spec, name: `-${t[i]}` });
|
|
770
783
|
else if (m.arity === 1 || m.arity === "suffix")
|
|
771
|
-
take(m.spec, undefined);
|
|
784
|
+
take(m.spec, `-${t[i]}`, undefined);
|
|
772
785
|
break;
|
|
773
786
|
}
|
|
774
787
|
}
|
|
775
|
-
return { specs, values, operands };
|
|
788
|
+
return { specs, names, values, operands };
|
|
776
789
|
}
|
|
777
790
|
export function writesPath(role) {
|
|
778
791
|
return role?.kind === "path" && WRITING_EFFECTS.has(role.effect);
|
|
@@ -1023,7 +1036,14 @@ function steppableLauncherOption(row, tok) {
|
|
|
1023
1036
|
const m = tok.startsWith("--") ? (tok.includes("=") ? undefined : mergedTable(row, "long").get(tok.slice(2))) : tok.length === 2 && tok.startsWith("-") ? mergedTable(row, "short").get(tok[1]) : undefined;
|
|
1024
1037
|
if (m === undefined || m.spec.value?.kind === "shell-text" || (m.spec.value?.kind === "path" && m.spec.value.base !== undefined))
|
|
1025
1038
|
return undefined;
|
|
1026
|
-
return m.arity
|
|
1039
|
+
return m.arity !== "disputed" || takesNoWordBare(row, tok) ? m : undefined;
|
|
1040
|
+
}
|
|
1041
|
+
function takesNoWordBare(row, spelling) {
|
|
1042
|
+
const key = spelling.startsWith("--") ? spelling.slice(2) : spelling.slice(1);
|
|
1043
|
+
return row.grammars.every((g) => {
|
|
1044
|
+
const s = (spelling.startsWith("--") ? g.long : g.short)[key];
|
|
1045
|
+
return s === undefined || s.arity === 0 || s.arity === "attached";
|
|
1046
|
+
});
|
|
1027
1047
|
}
|
|
1028
1048
|
export function launcherOptionArity(row, tok) {
|
|
1029
1049
|
if (row.grammars.length === 0)
|
|
@@ -1033,34 +1053,59 @@ export function launcherOptionArity(row, tok) {
|
|
|
1033
1053
|
const m = steppableLauncherOption(row, tok);
|
|
1034
1054
|
return m === undefined ? undefined : m.arity === 1 ? 1 : 0;
|
|
1035
1055
|
}
|
|
1056
|
+
function readLauncherOption(row, tok) {
|
|
1057
|
+
if (tok === "-")
|
|
1058
|
+
return row.launcher?.bareDash === true ? { step: 1 } : undefined;
|
|
1059
|
+
if (tok === "--")
|
|
1060
|
+
return launcherOptionArity(row, tok) === 0 ? { step: 1 } : undefined;
|
|
1061
|
+
if (tok.startsWith("--")) {
|
|
1062
|
+
const eq = tok.indexOf("=");
|
|
1063
|
+
const m = steppableLauncherOption(row, eq > 2 ? tok.slice(0, eq) : tok);
|
|
1064
|
+
if (m === undefined)
|
|
1065
|
+
return undefined;
|
|
1066
|
+
if (eq > 2)
|
|
1067
|
+
return m.arity === 0 || m.arity === "disputed" ? undefined : { step: 1, valued: m };
|
|
1068
|
+
return m.arity === 1 ? { step: 2, valued: m } : { step: 1 };
|
|
1069
|
+
}
|
|
1070
|
+
if (!/^-[^-]/.test(tok))
|
|
1071
|
+
return undefined;
|
|
1072
|
+
for (let i = 1; i < tok.length; i++) {
|
|
1073
|
+
const m = steppableLauncherOption(row, `-${tok[i]}`);
|
|
1074
|
+
if (m === undefined)
|
|
1075
|
+
return undefined;
|
|
1076
|
+
if (m.arity === 0)
|
|
1077
|
+
continue;
|
|
1078
|
+
if (i + 1 < tok.length)
|
|
1079
|
+
return m.arity === "disputed" ? undefined : { step: 1, valued: m };
|
|
1080
|
+
return m.arity === 1 ? { step: 2, valued: m } : { step: 1 };
|
|
1081
|
+
}
|
|
1082
|
+
return { step: 1 };
|
|
1083
|
+
}
|
|
1036
1084
|
export function launcherOptionPathRole(row, tok) {
|
|
1037
|
-
const role =
|
|
1085
|
+
const role = readLauncherOption(row, tok)?.valued?.spec.value;
|
|
1038
1086
|
return role?.kind === "path" ? role : undefined;
|
|
1039
1087
|
}
|
|
1040
1088
|
export function launcherOptionStep(row, tok) {
|
|
1041
|
-
|
|
1042
|
-
return 1;
|
|
1043
|
-
const exact = launcherOptionArity(row, tok);
|
|
1044
|
-
if (exact !== undefined)
|
|
1045
|
-
return exact === 1 ? 2 : 1;
|
|
1046
|
-
const eq = tok.startsWith("--") ? tok.indexOf("=") : -1;
|
|
1047
|
-
if (eq > 2 && steppableLauncherOption(row, tok.slice(0, eq))?.arity !== undefined && steppableLauncherOption(row, tok.slice(0, eq)).arity !== 0)
|
|
1048
|
-
return 1;
|
|
1049
|
-
if (/^-[A-Za-z]./.test(tok) && !tok.startsWith("--")) {
|
|
1050
|
-
const first = steppableLauncherOption(row, tok.slice(0, 2));
|
|
1051
|
-
if (first !== undefined && first.arity !== 0)
|
|
1052
|
-
return 1;
|
|
1053
|
-
}
|
|
1054
|
-
if (/^-[A-Za-z]+$/.test(tok) && [...tok.slice(1)].every((ch) => launcherOptionArity(row, `-${ch}`) === 0 && steppableLauncherOption(row, `-${ch}`)?.arity === 0))
|
|
1055
|
-
return 1;
|
|
1056
|
-
return undefined;
|
|
1089
|
+
return readLauncherOption(row, tok)?.step;
|
|
1057
1090
|
}
|
|
1058
1091
|
export function launcherRunsShell(row, args) {
|
|
1059
1092
|
const l = row.launcher;
|
|
1060
1093
|
if (l?.shellText === undefined)
|
|
1061
1094
|
return false;
|
|
1062
|
-
const
|
|
1063
|
-
return !implementationsOf(row).some((view) =>
|
|
1095
|
+
const programWith = l.programWith ?? [];
|
|
1096
|
+
return !implementationsOf(row).some((view) => selectedSpellings(view, args).names.some((n) => programWith.includes(n)));
|
|
1097
|
+
}
|
|
1098
|
+
export function targetUserOf(row, words, operand) {
|
|
1099
|
+
const as = row.launcher?.asUser;
|
|
1100
|
+
if (as === undefined)
|
|
1101
|
+
return undefined;
|
|
1102
|
+
const own = selectedSpellings(row, words);
|
|
1103
|
+
const user = own.values.filter((v) => as.userWith?.includes(v.name) === true).at(-1);
|
|
1104
|
+
const named = user !== undefined ? user.value ?? words[user.at] : operand === undefined ? "root" : operand.name;
|
|
1105
|
+
const login = operand?.dash === true || own.names.some((n) => as.loginWith.includes(n));
|
|
1106
|
+
const keepsCaller = selectedOptions(row, words).some((o) => o.mode === "names-program");
|
|
1107
|
+
const name = named === "#0" ? "root" : named !== undefined && /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(named) ? named : undefined;
|
|
1108
|
+
return { ...(name !== undefined ? { name } : {}), home: login ? "target" : as.home === "policy" ? "either" : keepsCaller ? "caller" : "target", login };
|
|
1064
1109
|
}
|
|
1065
1110
|
export function commandLineOptionSpellings(row) {
|
|
1066
1111
|
if (row === undefined)
|
|
@@ -1070,6 +1115,25 @@ export function commandLineOptionSpellings(row) {
|
|
|
1070
1115
|
...[...mergedTable(row, "long")].filter(([, m]) => m.spec.value?.kind === "shell-text").map(([n]) => `--${n}`),
|
|
1071
1116
|
];
|
|
1072
1117
|
}
|
|
1118
|
+
export function commandLineOf(row, tok) {
|
|
1119
|
+
const lines = new Set(commandLineOptionSpellings(row));
|
|
1120
|
+
if (tok.startsWith("--")) {
|
|
1121
|
+
const eq = tok.indexOf("=");
|
|
1122
|
+
if (!lines.has(eq > 2 ? tok.slice(0, eq) : tok))
|
|
1123
|
+
return undefined;
|
|
1124
|
+
return eq > 2 ? { attached: tok.slice(eq + 1) } : "next";
|
|
1125
|
+
}
|
|
1126
|
+
if (!/^-[^-]/.test(tok))
|
|
1127
|
+
return undefined;
|
|
1128
|
+
for (let i = 1; i < tok.length; i++) {
|
|
1129
|
+
const letter = `-${tok[i]}`;
|
|
1130
|
+
if (lines.has(letter))
|
|
1131
|
+
return i + 1 < tok.length ? { attached: tok.slice(i + 1) } : "next";
|
|
1132
|
+
if (steppableLauncherOption(row, letter)?.arity !== 0)
|
|
1133
|
+
return undefined;
|
|
1134
|
+
}
|
|
1135
|
+
return undefined;
|
|
1136
|
+
}
|
|
1073
1137
|
export function readProgramTextLanguage(row) {
|
|
1074
1138
|
const first = row?.operands?.positional?.[0];
|
|
1075
1139
|
return first?.kind === "program-text" && READ_LANGUAGES.has(first.language) ? first.language : undefined;
|
|
@@ -391,7 +391,7 @@ const launchers = [
|
|
|
391
391
|
optional: { "-i": LABEL, "-l": COUNT, "--replace": LABEL, "--eof": LABEL, "--max-lines": COUNT },
|
|
392
392
|
}, {}), { reads: { "-a": val(READ, { delivers: "operands" }), "--arg-file": val(READ, { delivers: "operands" }) } }, { lacks: ["-d", "-i", "-l", "-a", "--arg-file", "--null", "--no-run-if-empty", "--verbose", "--exit", "--interactive", "--open-tty", "--show-limits", "--help", "--version", "--max-args", "--max-procs", "--delimiter", "--max-chars", "--process-slot-var", "--replace", "--eof", "--max-lines"] }), launcher: {}, reentry: { kind: "argv", defaultProgram: "echo" }, witness: BINARY },
|
|
393
393
|
launcher("busybox"),
|
|
394
|
-
launcher("sudo", { assignmentOperands: true, shellCommand: "escaped" }, launcherGrammar({
|
|
394
|
+
launcher("sudo", { assignmentOperands: true, shellCommand: "escaped", asUser: { userWith: ["-u", "--user"], home: "policy", loginWith: ["-i", "--login"] } }, launcherGrammar({
|
|
395
395
|
switches: ["-A", "-b", "-E", "-H", "-K", "-k", "-l", "-n", "-P", "-S", "-V", "-v", "--askpass", "--background", "--preserve-env", "--set-home", "--remove-timestamp", "--reset-timestamp", "--list", "--non-interactive", "--preserve-groups", "--stdin", "--version", "--validate", "--bell", "--help"],
|
|
396
396
|
shells: ["-i", "-s", "--login", "--shell"],
|
|
397
397
|
values: {
|
|
@@ -399,10 +399,10 @@ const launchers = [
|
|
|
399
399
|
"--user": LABEL, "--group": LABEL, "--other-user": LABEL, "--close-from": COUNT, "--chdir": WORKDIR, "--host": LABEL, "--prompt": LABEL, "--role": LABEL, "--chroot": ROOTDIR, "--type": LABEL, "--command-timeout": COUNT, "--auth-type": LABEL,
|
|
400
400
|
},
|
|
401
401
|
}, { stepsDashDash: true }), { witness: PRIVILEGED }),
|
|
402
|
-
launcher("doas", {}, launcherGrammar({ switches: ["-L", "-n"], shells: ["-s"], values: { "-a": LABEL, "-u": LABEL, "-C": READ } }, { stepsDashDash: true }), { witness: PRIVILEGED }),
|
|
402
|
+
launcher("doas", { asUser: { userWith: ["-u"], home: "target", loginWith: [] } }, launcherGrammar({ switches: ["-L", "-n"], shells: ["-s"], values: { "-a": LABEL, "-u": LABEL, "-C": READ } }, { stepsDashDash: true }), { witness: PRIVILEGED }),
|
|
403
403
|
launcher("pkexec", { whenNoProgram: "shell" }, launcherGrammar({ switches: ["--disable-internal-agent", "--keep-cwd", "--version", "--help"], values: { "--user": LABEL } }), { witness: PRIVILEGED }),
|
|
404
|
-
{ key: "su", grammars: implementations(SU, { afterFirstOperand: "options" }, { lacks: SU_LINUX_ONLY, afterFirstOperand: "operands" }), launcher: { shellText: "user" }, witness: PRIVILEGED },
|
|
405
|
-
{ key: "runuser", grammars: [launcherGrammar({ ...SU_OPTIONS, values: { ...SU_OPTIONS.values, "-u": LABEL, "--user": LABEL } }, { afterFirstOperand: "options", stepsDashDash: true })], launcher: { shellText: "user", programWith: ["-u", "--user"] }, witness: PRIVILEGED },
|
|
404
|
+
{ key: "su", grammars: implementations(SU, { afterFirstOperand: "options" }, { lacks: SU_LINUX_ONLY, afterFirstOperand: "operands" }), launcher: { shellText: "user", asUser: { home: "target", loginWith: ["-l", "--login"] } }, witness: PRIVILEGED },
|
|
405
|
+
{ key: "runuser", grammars: [launcherGrammar({ ...SU_OPTIONS, values: { ...SU_OPTIONS.values, "-u": LABEL, "--user": LABEL } }, { afterFirstOperand: "options", stepsDashDash: true })], launcher: { shellText: "user", programWith: ["-u", "--user"], asUser: { home: "target", loginWith: ["-l", "--login"] } }, witness: PRIVILEGED },
|
|
406
406
|
launcher("chroot", { requiredOperands: 1, whenNoProgram: "shell" }, undefined, { witness: PRIVILEGED }),
|
|
407
407
|
launcher("setpriv", {}, undefined, { witness: PRIVILEGED }),
|
|
408
408
|
launcher("nohup", {}, grammar({}, BANNERS)),
|
|
@@ -420,7 +420,15 @@ const launchers = [
|
|
|
420
420
|
{ key: "stdbuf", grammars: implementations(grammar({ i: val(LABEL), o: val(LABEL), e: val(LABEL) }, { input: val(LABEL), output: val(LABEL), error: val(LABEL), ...BANNERS }), {}, { lacks: ["--input", "--output", "--error", "--help", "--version"] }), launcher: {}, witness: BINARY },
|
|
421
421
|
launcher("timeout", { requiredOperands: 1, requiredOperandSpelling: DURATION_SPELLING }, grammar({ v: SWITCH, k: val(COUNT), s: val(LABEL) }, { foreground: SWITCH, "preserve-status": SWITCH, verbose: SWITCH, "kill-after": val(COUNT), signal: val(LABEL), ...BANNERS })),
|
|
422
422
|
{ key: "time", grammars: implementations(grammar({ p: SWITCH, a: SWITCH, v: SWITCH, q: SWITCH, f: val(LABEL), o: val(WRITE) }, { portability: SWITCH, append: SWITCH, verbose: SWITCH, quiet: SWITCH, help: SWITCH, version: SWITCH, format: val(LABEL), output: val(WRITE) }, { stepsDashDash: true }), {}, { lacks: ["-v", "-q", "-f", "--portability", "--append", "--verbose", "--quiet", "--help", "--version", "--format", "--output"] }), launcher: {}, witness: BINARY },
|
|
423
|
-
|
|
423
|
+
{ key: "watch", grammars: [
|
|
424
|
+
launcherGrammar({
|
|
425
|
+
switches: ["-b", "-C", "-c", "-e", "-f", "-g", "-h", "-p", "-r", "-t", "-w", "-v", "-x", "--beep", "--color", "--no-color", "--errexit", "--follow", "--chgexit", "--exec", "--precise", "--no-rerun", "--no-title", "--no-wrap", "--help", "--version"],
|
|
426
|
+
banners: ["-h", "-v"],
|
|
427
|
+
values: { "-n": COUNT, "--interval": COUNT, "-q": COUNT, "--equexit": COUNT, "-s": WRITE, "--shotsdir": WRITE },
|
|
428
|
+
optional: { "-d": LABEL, "--differences": LABEL },
|
|
429
|
+
}, { afterFirstOperand: "operands", stepsDashDash: true }),
|
|
430
|
+
launcherGrammar({ switches: ["-d", "-t"], values: { "-n": COUNT } }, { afterFirstOperand: "operands", stepsDashDash: true }),
|
|
431
|
+
], launcher: { shellText: "joined", programWith: ["-x", "--exec"] }, witness: SHELL_TEXT_LAUNCHER },
|
|
424
432
|
launcher("setsid", {}, launcherGrammar({ switches: ["-c", "-f", "-w", "--ctty", "--fork", "--wait", "--help", "--version"] })),
|
|
425
433
|
launcher("flock", { requiredOperands: 1 }, launcherGrammar({
|
|
426
434
|
switches: ["-s", "-x", "-u", "-n", "-o", "-F", "--shared", "--exclusive", "--unlock", "--nonblock", "--nb", "--close", "--no-fork", "--verbose", "--help", "--version"],
|
|
@@ -38,6 +38,23 @@ export declare function readDenyHomeLinksOf(env: ExecutionEnv, entries: readonly
|
|
|
38
38
|
readonly linked: Map<string, string[]>;
|
|
39
39
|
readonly whole: boolean;
|
|
40
40
|
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Where the ROOTED rows of a table (`/app/config-lkg.json`, read-deny.ts) lead — the same `linked` answer
|
|
43
|
+
* {@link readDenyHomeLinksOf} gives for home-anchored rows, by the same machine ({@link linkedLocationsOf}): the row's
|
|
44
|
+
* LITERAL HEAD is spelled under the root and resolved the way the read faces mint their keys, and a location that differs
|
|
45
|
+
* from the spelling is where one of the head's segments is a link (`/app` a link to `/data/app`) — the row is placed
|
|
46
|
+
* there too, so a read of the link's target meets it. The laws are `readDenyHomeLinksOf`'s: a head that resolves to its own
|
|
47
|
+
* spelling adds nothing, a head with nothing below it adds nothing, a head the environment does not answer adds
|
|
48
|
+
* nothing and the answer is not WHOLE, a row whose head is empty (`/*.json`) has no location to resolve. A root is no
|
|
49
|
+
* home, so the answer does not depend on the homes: it is read whether or not a home answered.
|
|
50
|
+
*/
|
|
51
|
+
export declare function readDenyRootLinksOf(env: ExecutionEnv, entries: readonly {
|
|
52
|
+
readonly pattern: string;
|
|
53
|
+
readonly caseSensitive?: boolean;
|
|
54
|
+
}[], signal?: AbortSignal, deadline?: number): Promise<{
|
|
55
|
+
readonly linked: Map<string, string[]>;
|
|
56
|
+
readonly whole: boolean;
|
|
57
|
+
}>;
|
|
41
58
|
/**
|
|
42
59
|
* Where the rows that open with the configuration directory's name (`~/.sema/…`, {@link agentConfigDirTail}) lead when
|
|
43
60
|
* the host declares the directory's root (`RunnerDeps.agentConfigDir`) — the same `linked` answer
|
|
@@ -138,7 +155,9 @@ export declare function carryHomeViews<T extends object>(judge: T, source: HomeV
|
|
|
138
155
|
* with no answer, or a declared home that did not answer, places nothing: the rows are held anywhere (`compile([])`)
|
|
139
156
|
* rather than at a spelling that may miss the home's canonical one;
|
|
140
157
|
* · each AUXILIARY home (see the module head) read once under {@link AUXILIARY_HOME_PROBE_MS} — its spellings, its
|
|
141
|
-
* chain, its rows' heads — at the same time; whatever of it did not answer costs that home alone and never `whole
|
|
158
|
+
* chain, its rows' heads — at the same time; whatever of it did not answer costs that home alone and never `whole`;
|
|
159
|
+
* · the ROOTED rows' heads ({@link readDenyRootLinksOf}), read whether or not a home answered: a head that did not answer
|
|
160
|
+
* leaves the placement not WHOLE, and a placement that held the rows anywhere still places them.
|
|
142
161
|
* `compile` is the mount's own compile; the rows' patterns are read off its compile at no home. */
|
|
143
162
|
export declare function placeReadDenyAtHome(env: ExecutionEnv, compile: (homes: readonly string[], linked?: ReadonlyMap<string, readonly string[]>) => ReadDenyMatcher, homes?: readonly string[], signal?: AbortSignal, configRoot?: string): Promise<{
|
|
144
163
|
readonly matcher: PlacedReadDenyMatcher;
|
|
@@ -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,8 +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 { launchedCommands, launchedReadings, reentryText, spellLaunched } from "./shell-reentry-text.js";
|
|
6
|
-
import { readProgramPosition } from "./bash-program-position.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";
|
|
7
7
|
import { bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
|
|
8
8
|
import { programArgs } from "./shell-read-write-targets.js";
|
|
9
9
|
import { implementationsOf, programRow, runsShellText, selectedOptions, writeReadings } from "./program-read-model.js";
|
|
@@ -68,7 +68,7 @@ function bindingsOf(plan) {
|
|
|
68
68
|
if (w.assignment !== undefined)
|
|
69
69
|
bind(w.assignment.name, w.assignment.settlesWholeName ? literalTextOf(w.assignment.value.pieces) : undefined);
|
|
70
70
|
const [head, name] = segment.argv;
|
|
71
|
-
if ((head
|
|
71
|
+
if (isShellSyntaxWord(head, LOOP_LIST_HEADS) && name !== undefined && NAME.test(name.folded)) {
|
|
72
72
|
const at = segment.argv.findIndex((w, i) => i > 1 && w.folded === "in");
|
|
73
73
|
const words = at < 0 ? [undefined] : segment.argv.slice(at + 1).map((w) => literalTextOf(w.pieces));
|
|
74
74
|
for (const v of words.length > 0 ? words : [undefined])
|
|
@@ -175,6 +175,15 @@ function runtimeScriptOf(segment) {
|
|
|
175
175
|
return literal !== undefined ? literal.slice(from) : `${MARK}${w.raw.slice(from)}`;
|
|
176
176
|
};
|
|
177
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
|
+
}
|
|
178
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)) }] : [];
|
|
179
188
|
const launched = launchedCommands(segment.argv, pos).flatMap((c) => {
|
|
180
189
|
const shell = launchedReadings(segment.argv, c).shell;
|