@sema-agent/core 7.17.5 → 7.17.7
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 +73 -0
- package/dist/core/memory-engine/delegation-settlement.d.ts +27 -9
- package/dist/core/memory-engine/delegation-settlement.js +155 -46
- package/dist/core/memory-engine/engine.js +12 -6
- package/dist/core/remote-env.d.ts +23 -1
- package/dist/core/remote-env.js +4 -1
- package/dist/core/runner/prepare-hands-readface.js +2 -2
- package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -0
- package/dist/tools/fs/bash-readonly-classifier.js +678 -29
- package/dist/tools/fs/fs-bash.js +87 -21
- package/package.json +1 -1
- package/wire-changes/core@7.17.5.json +2 -2
- package/wire-changes/core@7.17.6.json +81 -0
|
@@ -214,6 +214,10 @@ function isPathShapedToken(tok) {
|
|
|
214
214
|
return tok.includes("/") || tok.startsWith("~") || tok === "." || tok === "..";
|
|
215
215
|
}
|
|
216
216
|
export function resolveOperandLexically(base, operand, homeDir) {
|
|
217
|
+
return resolveOperandSpellings(base, operand, homeDir)?.folded;
|
|
218
|
+
}
|
|
219
|
+
const PARENT_SEGMENT_RE = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
|
|
220
|
+
export function resolveOperandSpellings(base, operand, homeDir) {
|
|
217
221
|
let raw = operand;
|
|
218
222
|
if (raw === "~" || raw.startsWith("~/")) {
|
|
219
223
|
if (homeDir === undefined || !isAbsolutePathForm(homeDir))
|
|
@@ -227,12 +231,16 @@ export function resolveOperandLexically(base, operand, homeDir) {
|
|
|
227
231
|
if (!win32NamespaceScreen(raw, family).ok)
|
|
228
232
|
return undefined;
|
|
229
233
|
if (isAbsoluteForFamily(family, raw))
|
|
230
|
-
return
|
|
234
|
+
return spellingsOf(nativeUncSpellingOf(family, raw));
|
|
231
235
|
if (base === undefined || family === undefined || !isAbsoluteForFamily(family, base))
|
|
232
236
|
return undefined;
|
|
233
237
|
if (isShellRootedSpellingUnmapped(family, raw))
|
|
234
238
|
return undefined;
|
|
235
|
-
return
|
|
239
|
+
return spellingsOf(joinForFamily(family, base, raw));
|
|
240
|
+
}
|
|
241
|
+
function spellingsOf(absolute) {
|
|
242
|
+
const folded = normalizeAbsPathLexically(absolute);
|
|
243
|
+
return PARENT_SEGMENT_RE.test(absolute) ? { folded, spelled: absolute } : { folded };
|
|
236
244
|
}
|
|
237
245
|
function reheadSegment(toks, head, from = 0) {
|
|
238
246
|
return { folded: [head, ...toks.folded.slice(from + 1)], raw: [head, ...toks.raw.slice(from + 1)] };
|
|
@@ -245,13 +253,19 @@ function isBareControlWord(toks, i, words) {
|
|
|
245
253
|
}
|
|
246
254
|
function tokensMoveBase(toks, allow) {
|
|
247
255
|
let from = 0;
|
|
248
|
-
while (from < toks.folded.length && isBareControlWord(toks, from, SHELL_CONTROL_PREFIXES))
|
|
256
|
+
while (from < toks.folded.length && isBareControlWord(toks, from, SHELL_CONTROL_PREFIXES)) {
|
|
257
|
+
const controlWord = programBasename(toks.folded[from] ?? "");
|
|
249
258
|
from++;
|
|
259
|
+
for (let span = launcherOptionSpan(controlWord, toks.folded, from); span > 0; span = launcherOptionSpan(controlWord, toks.folded, from))
|
|
260
|
+
from += span;
|
|
261
|
+
}
|
|
250
262
|
const head = toks.folded[from];
|
|
251
263
|
if (head === undefined)
|
|
252
264
|
return false;
|
|
253
265
|
if (toks.raw[from] !== head)
|
|
254
266
|
return true;
|
|
267
|
+
if (head.startsWith("-") && head !== "-")
|
|
268
|
+
return true;
|
|
255
269
|
if (head === "cd")
|
|
256
270
|
return true;
|
|
257
271
|
if (!COMMAND_LAUNCHERS.has(head))
|
|
@@ -259,7 +273,7 @@ function tokensMoveBase(toks, allow) {
|
|
|
259
273
|
const unwrapped = unwrapLauncher(toks.folded.slice(from), allow);
|
|
260
274
|
return unwrapped.kind === "refused" || (unwrapped.kind === "reader" && unwrapped.head === "cd");
|
|
261
275
|
}
|
|
262
|
-
const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi)?sh|eval|source|\.|parallel)$/;
|
|
276
|
+
const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
|
|
263
277
|
const ARGV_REENTRY_PROGRAM_RE = /^xargs$/;
|
|
264
278
|
export function refusedCommandMayRead(command, allow, boundary) {
|
|
265
279
|
return !refusedCommandExempt(command, allow, boundary, 0);
|
|
@@ -466,11 +480,15 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
466
480
|
.filter((t) => t !== "");
|
|
467
481
|
});
|
|
468
482
|
const headIndex = (toks) => {
|
|
483
|
+
const folded = toks.map(fold);
|
|
469
484
|
let i = 0;
|
|
470
485
|
while (i < toks.length) {
|
|
471
|
-
const w =
|
|
486
|
+
const w = folded[i];
|
|
472
487
|
if (ASSIGNMENT_WORD.test(w) || SHELL_CONTROL_PREFIXES.has(w) || SHELL_BLOCK_WORDS.has(w) || /^[{}]$/.test(w)) {
|
|
473
488
|
i++;
|
|
489
|
+
const controlWord = programBasename(w);
|
|
490
|
+
for (let span = launcherOptionSpan(controlWord, folded, i); span > 0; span = launcherOptionSpan(controlWord, folded, i))
|
|
491
|
+
i += span;
|
|
474
492
|
continue;
|
|
475
493
|
}
|
|
476
494
|
if (/^(?:<<<|<<|<|>>?|&>>?)$/.test(w)) {
|
|
@@ -572,14 +590,21 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
572
590
|
const headWord = fold(toks[i]);
|
|
573
591
|
if (headWord.includes(SUBSTITUTION_PLACEHOLDER) || /[$`]/.test(headWord))
|
|
574
592
|
return false;
|
|
593
|
+
if (headWord.startsWith("-") && headWord !== "-")
|
|
594
|
+
return false;
|
|
575
595
|
let head = programOf(headWord);
|
|
576
|
-
|
|
596
|
+
const launcherUnwrap = COMMAND_LAUNCHERS.has(head) ? unwrapLauncher(toks.slice(i).map(fold), allow) : undefined;
|
|
597
|
+
if (launcherUnwrap?.kind === "refused")
|
|
598
|
+
return false;
|
|
599
|
+
const runs = launcherUnwrap === undefined ? { head, index: 0 } : { head: launcherUnwrap.head, index: launcherUnwrap.index };
|
|
600
|
+
if (runs.head !== undefined && runs.index !== undefined &&
|
|
601
|
+
argumentAsProgramRefusal(runs.head, runs.head, toks.slice(i + runs.index + 1).map(fold)) !== undefined)
|
|
602
|
+
return false;
|
|
603
|
+
if (launcherUnwrap !== undefined) {
|
|
577
604
|
const wrapped = runProgramOf(toks, i);
|
|
578
605
|
if (wrapped === undefined || /[$`]/.test(wrapped) || SHELL_REENTRY_PROGRAM_RE.test(wrapped))
|
|
579
606
|
return false;
|
|
580
|
-
const unwrapped =
|
|
581
|
-
if (unwrapped.kind === "refused")
|
|
582
|
-
return false;
|
|
607
|
+
const unwrapped = launcherUnwrap;
|
|
583
608
|
if (argvReentryUnjudged(unwrapped))
|
|
584
609
|
return false;
|
|
585
610
|
if (unwrapped.kind === "unlisted") {
|
|
@@ -643,10 +668,25 @@ function unwrapLauncher(folded, allow) {
|
|
|
643
668
|
const head = wordBasename;
|
|
644
669
|
if (SHELL_CONTROL_PREFIXES.has(head) || SHELL_BLOCK_WORDS.has(head))
|
|
645
670
|
return { kind: "refused", reason: `"${folded[0]}" is given shell control syntax ("${head}") where the program it runs would be ${NOT_AUTO_ALLOWED}` };
|
|
646
|
-
return allow.has(head) || head === "cd" ? { kind: "reader", index: i, head, argvFromStdin } : { kind: "unlisted", head, argvFromStdin };
|
|
671
|
+
return allow.has(head) || head === "cd" ? { kind: "reader", index: i, head, argvFromStdin } : { kind: "unlisted", head, index: i, argvFromStdin };
|
|
647
672
|
}
|
|
648
673
|
return { kind: "unlisted", argvFromStdin, runsDefaultProgram: lastReentry === folded.length - 1 };
|
|
649
674
|
}
|
|
675
|
+
const LAUNCHER_OPTION_GRAMMAR = new Map([
|
|
676
|
+
["time", {
|
|
677
|
+
plain: new Set(["-p", "-a", "-v", "-q", "--portability", "--append", "--verbose", "--quiet", "--help", "--version"]),
|
|
678
|
+
valued: new Set(["-f", "-o", "--format", "--output"]),
|
|
679
|
+
}],
|
|
680
|
+
]);
|
|
681
|
+
function launcherOptionSpan(launcher, folded, i) {
|
|
682
|
+
const row = launcher === undefined ? undefined : LAUNCHER_OPTION_GRAMMAR.get(launcher);
|
|
683
|
+
const tok = folded[i];
|
|
684
|
+
if (row === undefined || tok === undefined)
|
|
685
|
+
return 0;
|
|
686
|
+
if (row.plain.has(tok))
|
|
687
|
+
return 1;
|
|
688
|
+
return row.valued.has(tok) && folded[i + 1] !== undefined ? 2 : 0;
|
|
689
|
+
}
|
|
650
690
|
const ARGV_REENTRY_DEFAULT_PROGRAM = "echo";
|
|
651
691
|
function argvReentryUnjudged(unwrapped) {
|
|
652
692
|
if (!unwrapped.argvFromStdin)
|
|
@@ -846,6 +886,564 @@ const SED_READONLY_FLAGS = new Set([
|
|
|
846
886
|
]);
|
|
847
887
|
const SED_READONLY_CLUSTER_LETTERS = new Set(["n", "E", "r", "z"]);
|
|
848
888
|
const SED_READONLY_SCRIPT_PIECE = /^(?:\d+|\d+,\d+)?p$/;
|
|
889
|
+
const SED_FILE_OR_EXEC_COMMANDS = new Set(["r", "R", "w", "W", "e"]);
|
|
890
|
+
const SED_PLAIN_COMMANDS = new Set(["=", "d", "D", "F", "g", "G", "h", "H", "n", "N", "p", "P", "x", "z"]);
|
|
891
|
+
const SED_NUMERIC_COMMANDS = new Set(["q", "Q", "l", "L"]);
|
|
892
|
+
const SED_LABEL_COMMANDS = new Set(["b", "t", "T", ":", "v"]);
|
|
893
|
+
const SED_TEXT_COMMANDS = new Set(["a", "i", "c"]);
|
|
894
|
+
const SED_SUBSTITUTE_FLAGS = /[gpiImM0-9]/;
|
|
895
|
+
const SED_SCRIPT_LINE_LIMIT = 1024;
|
|
896
|
+
function utf8Length(text) {
|
|
897
|
+
let bytes = 0;
|
|
898
|
+
for (const ch of text) {
|
|
899
|
+
const code = ch.codePointAt(0);
|
|
900
|
+
bytes += code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4;
|
|
901
|
+
}
|
|
902
|
+
return bytes;
|
|
903
|
+
}
|
|
904
|
+
function readSedScript(script) {
|
|
905
|
+
if (script.split("\n").some((line) => utf8Length(line) >= SED_SCRIPT_LINE_LIMIT))
|
|
906
|
+
return "unseen";
|
|
907
|
+
let i = 0;
|
|
908
|
+
let depth = 0;
|
|
909
|
+
const len = script.length;
|
|
910
|
+
const isBlank = (ch) => ch === " " || ch === "\t" || ch === "\v" || ch === "\f";
|
|
911
|
+
const skipBlank = () => {
|
|
912
|
+
while (i < len && isBlank(script[i]))
|
|
913
|
+
i++;
|
|
914
|
+
};
|
|
915
|
+
const digits = () => {
|
|
916
|
+
while (i < len && script[i] >= "0" && script[i] <= "9")
|
|
917
|
+
i++;
|
|
918
|
+
};
|
|
919
|
+
const consumeDelimited = (delim, regex) => {
|
|
920
|
+
while (i < len) {
|
|
921
|
+
const ch = script[i];
|
|
922
|
+
if (ch === "\\") {
|
|
923
|
+
const escaped = script[i + 1];
|
|
924
|
+
if (escaped === undefined || escaped === "\n" || escaped === "\r")
|
|
925
|
+
return false;
|
|
926
|
+
i += 2;
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
if (ch === "\n" || ch === "\r")
|
|
930
|
+
return false;
|
|
931
|
+
if (ch === delim) {
|
|
932
|
+
i++;
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
if (regex && ch === "[") {
|
|
936
|
+
i++;
|
|
937
|
+
if (script[i] === "^")
|
|
938
|
+
i++;
|
|
939
|
+
if (script[i] === "]")
|
|
940
|
+
i++;
|
|
941
|
+
while (i < len && script[i] !== "]" && script[i] !== "\n" && script[i] !== "\r") {
|
|
942
|
+
const next = script[i + 1];
|
|
943
|
+
if (script[i] === "[" && (next === ":" || next === "." || next === "=")) {
|
|
944
|
+
const at = script.indexOf(`${next}]`, i + 2);
|
|
945
|
+
if (at === -1 || script.slice(i, at).includes("\n"))
|
|
946
|
+
return false;
|
|
947
|
+
i = at + 2;
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
i++;
|
|
951
|
+
}
|
|
952
|
+
if (i >= len || script[i] !== "]")
|
|
953
|
+
return false;
|
|
954
|
+
i++;
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
i++;
|
|
958
|
+
}
|
|
959
|
+
return false;
|
|
960
|
+
};
|
|
961
|
+
const readAddress = () => {
|
|
962
|
+
const ch = script[i];
|
|
963
|
+
if (ch === undefined)
|
|
964
|
+
return undefined;
|
|
965
|
+
if (ch === "$") {
|
|
966
|
+
i++;
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
if (ch >= "0" && ch <= "9") {
|
|
970
|
+
digits();
|
|
971
|
+
if (script[i] === "~") {
|
|
972
|
+
i++;
|
|
973
|
+
digits();
|
|
974
|
+
}
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
if (ch === "/" || ch === "\\") {
|
|
978
|
+
const delim = ch === "/" ? "/" : script[i + 1];
|
|
979
|
+
i += ch === "/" ? 1 : 2;
|
|
980
|
+
if (delim === undefined || !consumeDelimited(delim, true))
|
|
981
|
+
return false;
|
|
982
|
+
while (i < len && (script[i] === "I" || script[i] === "M"))
|
|
983
|
+
i++;
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
return undefined;
|
|
987
|
+
};
|
|
988
|
+
while (i < len) {
|
|
989
|
+
const ch = script[i];
|
|
990
|
+
if (isBlank(ch) || ch === "\n" || ch === "\r" || ch === ";") {
|
|
991
|
+
i++;
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
if (ch === "#") {
|
|
995
|
+
while (i < len && script[i] !== "\n")
|
|
996
|
+
i++;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (ch === "}") {
|
|
1000
|
+
if (depth === 0)
|
|
1001
|
+
return "unparsed";
|
|
1002
|
+
depth--;
|
|
1003
|
+
i++;
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
const first = readAddress();
|
|
1007
|
+
if (first === false)
|
|
1008
|
+
return "unparsed";
|
|
1009
|
+
if (first === true) {
|
|
1010
|
+
skipBlank();
|
|
1011
|
+
if (script[i] === ",") {
|
|
1012
|
+
i++;
|
|
1013
|
+
skipBlank();
|
|
1014
|
+
if (script[i] === "+" || script[i] === "~") {
|
|
1015
|
+
i++;
|
|
1016
|
+
if (!(script[i] >= "0" && script[i] <= "9"))
|
|
1017
|
+
return "unparsed";
|
|
1018
|
+
digits();
|
|
1019
|
+
}
|
|
1020
|
+
else if (readAddress() !== true)
|
|
1021
|
+
return "unparsed";
|
|
1022
|
+
}
|
|
1023
|
+
skipBlank();
|
|
1024
|
+
}
|
|
1025
|
+
while (script[i] === "!") {
|
|
1026
|
+
i++;
|
|
1027
|
+
skipBlank();
|
|
1028
|
+
}
|
|
1029
|
+
const cmd = script[i];
|
|
1030
|
+
if (cmd === undefined)
|
|
1031
|
+
return "unparsed";
|
|
1032
|
+
i++;
|
|
1033
|
+
if (SED_FILE_OR_EXEC_COMMANDS.has(cmd))
|
|
1034
|
+
return "names";
|
|
1035
|
+
if (cmd === "{") {
|
|
1036
|
+
depth++;
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if (cmd === "s" || cmd === "y") {
|
|
1040
|
+
const delim = script[i];
|
|
1041
|
+
if (delim === undefined || delim === "\\" || delim === "\n")
|
|
1042
|
+
return "unparsed";
|
|
1043
|
+
i++;
|
|
1044
|
+
if (!consumeDelimited(delim, cmd === "s"))
|
|
1045
|
+
return "unparsed";
|
|
1046
|
+
if (!consumeDelimited(delim, false))
|
|
1047
|
+
return "unparsed";
|
|
1048
|
+
if (cmd === "y")
|
|
1049
|
+
continue;
|
|
1050
|
+
while (i < len) {
|
|
1051
|
+
const flag = script[i];
|
|
1052
|
+
if (flag === "w" || flag === "W" || flag === "e")
|
|
1053
|
+
return "names";
|
|
1054
|
+
if (SED_SUBSTITUTE_FLAGS.test(flag) || isBlank(flag)) {
|
|
1055
|
+
i++;
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
if (flag === ";" || flag === "\n" || flag === "\r" || flag === "}" || flag === "#")
|
|
1059
|
+
break;
|
|
1060
|
+
return "unparsed";
|
|
1061
|
+
}
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
if (SED_TEXT_COMMANDS.has(cmd)) {
|
|
1065
|
+
while (i < len && script[i] !== "\n")
|
|
1066
|
+
i++;
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (SED_LABEL_COMMANDS.has(cmd)) {
|
|
1070
|
+
skipBlank();
|
|
1071
|
+
while (i < len && script[i] !== "\n" && script[i] !== ";" && script[i] !== "}" && !isBlank(script[i]))
|
|
1072
|
+
i++;
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
if (SED_NUMERIC_COMMANDS.has(cmd)) {
|
|
1076
|
+
skipBlank();
|
|
1077
|
+
digits();
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (!SED_PLAIN_COMMANDS.has(cmd))
|
|
1081
|
+
return "unparsed";
|
|
1082
|
+
}
|
|
1083
|
+
return depth === 0 ? "clean" : "unparsed";
|
|
1084
|
+
}
|
|
1085
|
+
const SED_OPTION_DIALECTS = [
|
|
1086
|
+
{ separated: "efl", suffix: "iI", stopsAtFirstOperand: false },
|
|
1087
|
+
{ separated: "ef", suffix: "iI", stopsAtFirstOperand: true },
|
|
1088
|
+
];
|
|
1089
|
+
function sedSeparatedSuffixTaken(tok) {
|
|
1090
|
+
return tok !== undefined && (tok === "" || tok.startsWith("."));
|
|
1091
|
+
}
|
|
1092
|
+
const SED_LONG_VALUE_OPTIONS = ["expression", "line-length"];
|
|
1093
|
+
const SED_LONG_PLAIN_OPTIONS = [
|
|
1094
|
+
"debug", "follow-symlinks", "help", "in-place", "null-data", "posix", "quiet", "regexp-extended",
|
|
1095
|
+
"sandbox", "separate", "silent", "unbuffered", "version", "zero-terminated",
|
|
1096
|
+
];
|
|
1097
|
+
function readSedInvocation(args, dialect) {
|
|
1098
|
+
const scripts = [];
|
|
1099
|
+
let scriptSupplied = false;
|
|
1100
|
+
let endOfOptions = false;
|
|
1101
|
+
let firstOperand;
|
|
1102
|
+
for (let k = 0; k < args.length; k++) {
|
|
1103
|
+
const tok = args[k];
|
|
1104
|
+
if (!endOfOptions && tok === "--") {
|
|
1105
|
+
endOfOptions = true;
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
if (endOfOptions || !tok.startsWith("-") || tok === "-") {
|
|
1109
|
+
if (firstOperand === undefined) {
|
|
1110
|
+
firstOperand = tok;
|
|
1111
|
+
if (dialect.stopsAtFirstOperand)
|
|
1112
|
+
endOfOptions = true;
|
|
1113
|
+
}
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
if (tok.startsWith("--")) {
|
|
1117
|
+
const long = longOptionNameOf(tok);
|
|
1118
|
+
if (long === undefined)
|
|
1119
|
+
return "unparsed";
|
|
1120
|
+
const eq = tok.indexOf("=");
|
|
1121
|
+
const attached = eq >= 0 ? tok.slice(eq + 1) : undefined;
|
|
1122
|
+
if (isLongOptionAbbrevOf(long, "file"))
|
|
1123
|
+
return "unseen";
|
|
1124
|
+
if (isLongOptionAbbrevOf(long, "expression")) {
|
|
1125
|
+
const value = attached ?? args[++k];
|
|
1126
|
+
if (value === undefined)
|
|
1127
|
+
return "unparsed";
|
|
1128
|
+
scripts.push(value);
|
|
1129
|
+
scriptSupplied = true;
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
if (SED_LONG_VALUE_OPTIONS.some((full) => isLongOptionAbbrevOf(long, full))) {
|
|
1133
|
+
if (attached === undefined)
|
|
1134
|
+
k++;
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (SED_LONG_PLAIN_OPTIONS.some((full) => isLongOptionAbbrevOf(long, full)))
|
|
1138
|
+
continue;
|
|
1139
|
+
return "unparsed";
|
|
1140
|
+
}
|
|
1141
|
+
for (let c = 1; c < tok.length; c++) {
|
|
1142
|
+
const letter = tok[c];
|
|
1143
|
+
if (dialect.suffix.includes(letter)) {
|
|
1144
|
+
if (c + 1 === tok.length && sedSeparatedSuffixTaken(args[k + 1]))
|
|
1145
|
+
k++;
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
if (dialect.separated.includes(letter)) {
|
|
1149
|
+
const rest = tok.slice(c + 1);
|
|
1150
|
+
const value = rest !== "" ? rest : args[++k];
|
|
1151
|
+
if (letter === "f")
|
|
1152
|
+
return "unseen";
|
|
1153
|
+
if (letter === "e") {
|
|
1154
|
+
if (value === undefined)
|
|
1155
|
+
return "unparsed";
|
|
1156
|
+
scripts.push(value);
|
|
1157
|
+
scriptSupplied = true;
|
|
1158
|
+
}
|
|
1159
|
+
break;
|
|
1160
|
+
}
|
|
1161
|
+
if (!SED_SHORT_PLAIN_LETTERS.includes(letter))
|
|
1162
|
+
return "unparsed";
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
const script = scriptSupplied ? scripts.join("\n") : firstOperand;
|
|
1166
|
+
if (script === undefined)
|
|
1167
|
+
return "clean";
|
|
1168
|
+
return readSedScript(script);
|
|
1169
|
+
}
|
|
1170
|
+
const SED_SHORT_PLAIN_LETTERS = "nsuzEraHl";
|
|
1171
|
+
const SCRIPT_SLOT_VERBS = new Map([
|
|
1172
|
+
["sed", (args) => {
|
|
1173
|
+
const readings = SED_OPTION_DIALECTS.map((dialect) => readSedInvocation(args, dialect));
|
|
1174
|
+
if (readings.includes("names") || readings.includes("unseen"))
|
|
1175
|
+
return true;
|
|
1176
|
+
return readings.every((reading) => reading === "unparsed");
|
|
1177
|
+
}],
|
|
1178
|
+
]);
|
|
1179
|
+
const INLINE_PROGRAM_TEXT = "runs its argument as an interpreted program";
|
|
1180
|
+
const INLINE_PROGRAM_STDIN = "reads the program it runs from its standard input";
|
|
1181
|
+
const INTERPRETER_BANNER_OPTIONS = new Set(["--version", "--help", "--usage", "-version", "-help"]);
|
|
1182
|
+
const BLANK_INLINE = {
|
|
1183
|
+
longText: new Set(), longFile: new Set(), longNamesProgram: new Set(), longSupplemental: new Set(),
|
|
1184
|
+
longValue: new Set(), longPlain: new Set(), fileOptionEndsOptions: false,
|
|
1185
|
+
textLetters: "", fileLetters: "", separatedValueLetters: "", attachedValueLetters: "",
|
|
1186
|
+
plainLetters: "", bannerLetters: "", interactiveLetters: "",
|
|
1187
|
+
programOperand: { at: 0, isText: false },
|
|
1188
|
+
};
|
|
1189
|
+
const PERL_INLINE = {
|
|
1190
|
+
...BLANK_INLINE, textLetters: "eEmM", separatedValueLetters: "I", attachedValueLetters: "iCFxDV",
|
|
1191
|
+
plainLetters: "nplawcsStTUWX", bannerLetters: "vVh", stdinWhenUnnamed: true,
|
|
1192
|
+
};
|
|
1193
|
+
const RUBY_INLINE = {
|
|
1194
|
+
...BLANK_INLINE, textLetters: "e", separatedValueLetters: "IrECFKT", plainLetters: "nplawcsSU",
|
|
1195
|
+
bannerLetters: "vh", stdinWhenUnnamed: true,
|
|
1196
|
+
};
|
|
1197
|
+
const PYTHON_INLINE = {
|
|
1198
|
+
...BLANK_INLINE, textLetters: "c", fileLetters: "m", separatedValueLetters: "WXQ",
|
|
1199
|
+
plainLetters: "uBEIsSqbdvxOtR", bannerLetters: "Vh?", interactiveLetters: "i",
|
|
1200
|
+
fileOptionEndsOptions: true, stdinWhenUnnamed: true,
|
|
1201
|
+
};
|
|
1202
|
+
const NODE_INLINE = {
|
|
1203
|
+
...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "ep",
|
|
1204
|
+
longValue: new Set(["--import", "--loader", "--experimental-loader", "--require", "--env-file", "--conditions", "--max-old-space-size"]),
|
|
1205
|
+
longNamesProgram: new Set(["--test"]),
|
|
1206
|
+
longPlain: new Set(["--inspect", "--inspect-brk", "--no-warnings", "--trace-warnings", "--watch", "--enable-source-maps", "--experimental-modules", "--no-deprecation", "--check"]),
|
|
1207
|
+
separatedValueLetters: "r", plainLetters: "c", bannerLetters: "vh", interactiveLetters: "i",
|
|
1208
|
+
stdinWhenUnnamed: true,
|
|
1209
|
+
};
|
|
1210
|
+
const PHP_INLINE = {
|
|
1211
|
+
...BLANK_INLINE, longText: new Set(["--run", "--process-begin", "--process-code", "--process-end"]),
|
|
1212
|
+
textLetters: "rRBE", fileLetters: "fF", separatedValueLetters: "dcz", plainLetters: "nqswl",
|
|
1213
|
+
bannerLetters: "vhi", interactiveLetters: "a", stdinWhenUnnamed: true,
|
|
1214
|
+
};
|
|
1215
|
+
const LUA_INLINE = {
|
|
1216
|
+
...BLANK_INLINE, textLetters: "e", separatedValueLetters: "l", bannerLetters: "vh",
|
|
1217
|
+
interactiveLetters: "i", stdinWhenUnnamed: true,
|
|
1218
|
+
};
|
|
1219
|
+
const TCL_INLINE = { ...BLANK_INLINE, stdinWhenUnnamed: true };
|
|
1220
|
+
const EXPECT_INLINE = {
|
|
1221
|
+
...BLANK_INLINE, textLetters: "c", separatedValueLetters: "Db", plainLetters: "dN",
|
|
1222
|
+
bannerLetters: "vh", interactiveLetters: "i", stdinWhenUnnamed: true,
|
|
1223
|
+
};
|
|
1224
|
+
const AWK_INLINE = {
|
|
1225
|
+
...BLANK_INLINE, longText: new Set(["--source"]), textLetters: "e",
|
|
1226
|
+
longFile: new Set(["--file", "--exec"]), fileLetters: "fE", separatedValueLetters: "vF",
|
|
1227
|
+
bannerLetters: "V", programOperand: { at: 0, isText: true }, stdinWhenUnnamed: false,
|
|
1228
|
+
};
|
|
1229
|
+
const JQ_INLINE = {
|
|
1230
|
+
...BLANK_INLINE, longFile: new Set(["--from-file"]), fileLetters: "f", separatedValueLetters: "L",
|
|
1231
|
+
plainLetters: "nrjcsSaCMRe", bannerLetters: "h", programOperand: { at: 0, isText: true },
|
|
1232
|
+
stdinWhenUnnamed: false,
|
|
1233
|
+
};
|
|
1234
|
+
const BC_INLINE = {
|
|
1235
|
+
...BLANK_INLINE, longText: new Set(["--expression"]), textLetters: "e", plainLetters: "lqsw",
|
|
1236
|
+
bannerLetters: "vh", interactiveLetters: "i", programOperand: undefined, stdinWhenUnnamed: true,
|
|
1237
|
+
};
|
|
1238
|
+
const DC_INLINE = {
|
|
1239
|
+
...BLANK_INLINE, longText: new Set(["--expression"]), textLetters: "e",
|
|
1240
|
+
longFile: new Set(["--file"]), fileLetters: "f", bannerLetters: "hV",
|
|
1241
|
+
programOperand: undefined, stdinWhenUnnamed: true,
|
|
1242
|
+
};
|
|
1243
|
+
const SQLITE_INLINE = {
|
|
1244
|
+
...BLANK_INLINE, longText: new Set(["-cmd"]), longSupplemental: new Set(["-init"]),
|
|
1245
|
+
programOperand: { at: 1, isText: true }, stdinWhenUnnamed: true,
|
|
1246
|
+
};
|
|
1247
|
+
const PSQL_INLINE = {
|
|
1248
|
+
...BLANK_INLINE, longText: new Set(["--command"]), textLetters: "c",
|
|
1249
|
+
longFile: new Set(["--file"]), fileLetters: "f", separatedValueLetters: "hpUdoF",
|
|
1250
|
+
plainLetters: "aeEqtxXwW", bannerLetters: "lV?", programOperand: undefined, stdinWhenUnnamed: true,
|
|
1251
|
+
};
|
|
1252
|
+
const OSASCRIPT_INLINE = {
|
|
1253
|
+
...BLANK_INLINE, textLetters: "e", separatedValueLetters: "ls", plainLetters: "i", bannerLetters: "h",
|
|
1254
|
+
stdinWhenUnnamed: true,
|
|
1255
|
+
};
|
|
1256
|
+
const RSCRIPT_INLINE = {
|
|
1257
|
+
...BLANK_INLINE, textLetters: "e", bannerLetters: "h", stdinWhenUnnamed: false,
|
|
1258
|
+
};
|
|
1259
|
+
const JULIA_INLINE = {
|
|
1260
|
+
...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "eE",
|
|
1261
|
+
separatedValueLetters: "JLp", plainLetters: "q", bannerLetters: "vh", interactiveLetters: "i",
|
|
1262
|
+
stdinWhenUnnamed: true,
|
|
1263
|
+
};
|
|
1264
|
+
const GHCI_INLINE = {
|
|
1265
|
+
...BLANK_INLINE, textLetters: "e", separatedValueLetters: "io", programOperand: undefined,
|
|
1266
|
+
stdinWhenUnnamed: true,
|
|
1267
|
+
};
|
|
1268
|
+
const GROOVY_INLINE = {
|
|
1269
|
+
...BLANK_INLINE, textLetters: "e", separatedValueLetters: "Dcl", plainLetters: "n",
|
|
1270
|
+
bannerLetters: "vh", interactiveLetters: "i", stdinWhenUnnamed: false,
|
|
1271
|
+
};
|
|
1272
|
+
const BUN_INLINE = {
|
|
1273
|
+
...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "ep",
|
|
1274
|
+
separatedValueLetters: "r", bannerLetters: "vh", stdinWhenUnnamed: false,
|
|
1275
|
+
};
|
|
1276
|
+
const INTERPRETER_INLINE_PROGRAMS = new Map([
|
|
1277
|
+
["perl", PERL_INLINE],
|
|
1278
|
+
["ruby", RUBY_INLINE],
|
|
1279
|
+
["python", PYTHON_INLINE],
|
|
1280
|
+
["node", NODE_INLINE],
|
|
1281
|
+
["nodejs", NODE_INLINE],
|
|
1282
|
+
["php", PHP_INLINE],
|
|
1283
|
+
["lua", LUA_INLINE],
|
|
1284
|
+
["luajit", LUA_INLINE],
|
|
1285
|
+
["tclsh", TCL_INLINE],
|
|
1286
|
+
["wish", TCL_INLINE],
|
|
1287
|
+
["expect", EXPECT_INLINE],
|
|
1288
|
+
["awk", AWK_INLINE],
|
|
1289
|
+
["gawk", AWK_INLINE],
|
|
1290
|
+
["mawk", AWK_INLINE],
|
|
1291
|
+
["nawk", AWK_INLINE],
|
|
1292
|
+
["jq", JQ_INLINE],
|
|
1293
|
+
["bc", BC_INLINE],
|
|
1294
|
+
["dc", DC_INLINE],
|
|
1295
|
+
["sqlite", SQLITE_INLINE],
|
|
1296
|
+
["psql", PSQL_INLINE],
|
|
1297
|
+
["osascript", OSASCRIPT_INLINE],
|
|
1298
|
+
["Rscript", RSCRIPT_INLINE],
|
|
1299
|
+
["julia", JULIA_INLINE],
|
|
1300
|
+
["ghci", GHCI_INLINE],
|
|
1301
|
+
["groovy", GROOVY_INLINE],
|
|
1302
|
+
["bun", BUN_INLINE],
|
|
1303
|
+
]);
|
|
1304
|
+
function interpreterKey(name) {
|
|
1305
|
+
return name.replace(/\d+(?:\.\d+)*$/, "");
|
|
1306
|
+
}
|
|
1307
|
+
function withoutRedirectionWords(args) {
|
|
1308
|
+
const out = [];
|
|
1309
|
+
for (let i = 0; i < args.length; i++) {
|
|
1310
|
+
const t = args[i];
|
|
1311
|
+
if (/^(?:<<<|<<|<|>>?|&>>?)$/.test(t)) {
|
|
1312
|
+
i++;
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
if (/^(?:>>?|<)&\d*$/.test(t))
|
|
1316
|
+
continue;
|
|
1317
|
+
out.push(t);
|
|
1318
|
+
}
|
|
1319
|
+
return out;
|
|
1320
|
+
}
|
|
1321
|
+
function inlineProgramClause(model, args) {
|
|
1322
|
+
let endOfOptions = false;
|
|
1323
|
+
let banner = false;
|
|
1324
|
+
let interactive = false;
|
|
1325
|
+
let named = false;
|
|
1326
|
+
let stdin = false;
|
|
1327
|
+
let explained = true;
|
|
1328
|
+
let operands = 0;
|
|
1329
|
+
scan: for (let i = 0; i < args.length; i++) {
|
|
1330
|
+
const arg = args[i];
|
|
1331
|
+
if (!endOfOptions && arg === "--") {
|
|
1332
|
+
endOfOptions = true;
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
if (arg === "-") {
|
|
1336
|
+
if (model.programOperand?.isText !== true)
|
|
1337
|
+
stdin = true;
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
if (!endOfOptions && arg.startsWith("-")) {
|
|
1341
|
+
const eq = arg.indexOf("=");
|
|
1342
|
+
const whole = eq < 0 ? arg : arg.slice(0, eq);
|
|
1343
|
+
const valueOf = () => (eq < 0 ? args[++i] : arg.slice(eq + 1));
|
|
1344
|
+
if (model.longText.has(whole))
|
|
1345
|
+
return INLINE_PROGRAM_TEXT;
|
|
1346
|
+
if (model.longFile.has(whole)) {
|
|
1347
|
+
if (valueOf() === "-") {
|
|
1348
|
+
stdin = true;
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
named = true;
|
|
1352
|
+
if (explained && model.fileOptionEndsOptions)
|
|
1353
|
+
break scan;
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
if (model.longNamesProgram.has(whole)) {
|
|
1357
|
+
named = true;
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
if (model.longSupplemental.has(whole)) {
|
|
1361
|
+
valueOf();
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
if (INTERPRETER_BANNER_OPTIONS.has(whole)) {
|
|
1365
|
+
banner = true;
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
if (model.longValue.has(whole)) {
|
|
1369
|
+
valueOf();
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
if (model.longPlain.has(whole))
|
|
1373
|
+
continue;
|
|
1374
|
+
if (arg.startsWith("--")) {
|
|
1375
|
+
if (eq < 0)
|
|
1376
|
+
explained = false;
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
for (let k = 1; k < arg.length; k++) {
|
|
1380
|
+
const ch = arg[k];
|
|
1381
|
+
if (ch >= "0" && ch <= "9")
|
|
1382
|
+
continue;
|
|
1383
|
+
if (model.textLetters.includes(ch))
|
|
1384
|
+
return INLINE_PROGRAM_TEXT;
|
|
1385
|
+
if (model.fileLetters.includes(ch)) {
|
|
1386
|
+
const value = k + 1 < arg.length ? arg.slice(k + 1) : args[++i];
|
|
1387
|
+
if (value === "-") {
|
|
1388
|
+
stdin = true;
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
named = true;
|
|
1392
|
+
if (explained && model.fileOptionEndsOptions)
|
|
1393
|
+
break scan;
|
|
1394
|
+
break;
|
|
1395
|
+
}
|
|
1396
|
+
if (model.attachedValueLetters.includes(ch)) {
|
|
1397
|
+
if (model.bannerLetters.includes(ch))
|
|
1398
|
+
banner = true;
|
|
1399
|
+
break;
|
|
1400
|
+
}
|
|
1401
|
+
if (model.bannerLetters.includes(ch)) {
|
|
1402
|
+
banner = true;
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
if (model.interactiveLetters.includes(ch)) {
|
|
1406
|
+
interactive = true;
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
if (model.separatedValueLetters.includes(ch)) {
|
|
1410
|
+
if (k + 1 >= arg.length)
|
|
1411
|
+
i++;
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1414
|
+
if (!model.plainLetters.includes(ch))
|
|
1415
|
+
explained = false;
|
|
1416
|
+
}
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
operands++;
|
|
1420
|
+
if (!named && explained && model.programOperand !== undefined && operands === model.programOperand.at + 1) {
|
|
1421
|
+
if (model.programOperand.isText)
|
|
1422
|
+
return INLINE_PROGRAM_TEXT;
|
|
1423
|
+
named = true;
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (stdin || interactive)
|
|
1428
|
+
return INLINE_PROGRAM_STDIN;
|
|
1429
|
+
if (named || banner)
|
|
1430
|
+
return undefined;
|
|
1431
|
+
return model.stdinWhenUnnamed ? INLINE_PROGRAM_STDIN : undefined;
|
|
1432
|
+
}
|
|
1433
|
+
function unjudgedProgramTextRefusal(written, clause) {
|
|
1434
|
+
return `command "${written}" ${clause}, which this walk does not judge ${NOT_AUTO_ALLOWED}`;
|
|
1435
|
+
}
|
|
1436
|
+
function argumentAsProgramRefusal(written, name, args) {
|
|
1437
|
+
const slot = SCRIPT_SLOT_VERBS.get(name);
|
|
1438
|
+
if (slot !== undefined && slot(args)) {
|
|
1439
|
+
return unjudgedProgramTextRefusal(written, "names files or runs commands inside its script text");
|
|
1440
|
+
}
|
|
1441
|
+
const interpreter = INTERPRETER_INLINE_PROGRAMS.get(interpreterKey(name));
|
|
1442
|
+
if (interpreter === undefined)
|
|
1443
|
+
return undefined;
|
|
1444
|
+
const clause = inlineProgramClause(interpreter, withoutRedirectionWords(args));
|
|
1445
|
+
return clause === undefined ? undefined : unjudgedProgramTextRefusal(written, clause);
|
|
1446
|
+
}
|
|
849
1447
|
function guardedVerbFormReason(name, toks, raw) {
|
|
850
1448
|
if (name === "find") {
|
|
851
1449
|
for (const t of toks) {
|
|
@@ -1130,12 +1728,12 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
1130
1728
|
for (let k = 0; k < args.length; k++) {
|
|
1131
1729
|
if (!argGlobs(k) || expandingArgs.has(k))
|
|
1132
1730
|
continue;
|
|
1133
|
-
const
|
|
1134
|
-
findings.push({ kind: "undecided", path:
|
|
1731
|
+
const r = resolveOperandSpellings(boundary.cwd ?? boundary.roots[0], args[k], boundary.homeDir);
|
|
1732
|
+
findings.push({ kind: "undecided", path: r?.folded ?? args[k], ...(r?.spelled !== undefined ? { spelled: r.spelled } : {}) });
|
|
1135
1733
|
}
|
|
1136
1734
|
for (const { text: candidate, globbed } of candidates) {
|
|
1137
|
-
const
|
|
1138
|
-
if (
|
|
1735
|
+
const spellings = resolveOperandSpellings(boundary.cwd ?? boundary.roots[0], candidate, boundary.homeDir);
|
|
1736
|
+
if (spellings === undefined) {
|
|
1139
1737
|
findings.push({
|
|
1140
1738
|
kind: "unresolvable",
|
|
1141
1739
|
operand: candidate,
|
|
@@ -1143,19 +1741,20 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
1143
1741
|
});
|
|
1144
1742
|
continue;
|
|
1145
1743
|
}
|
|
1744
|
+
const resolved = spellings.folded;
|
|
1146
1745
|
if (withinAnyRoot(boundary.roots, resolved)) {
|
|
1147
1746
|
if (!globbed)
|
|
1148
|
-
findings.push({ kind: "inside", path: resolved });
|
|
1747
|
+
findings.push({ kind: "inside", path: resolved, ...(spellings.spelled !== undefined ? { spelled: spellings.spelled } : {}) });
|
|
1149
1748
|
}
|
|
1150
1749
|
else
|
|
1151
|
-
findings.push({ kind: "outside", command: name, path: resolved });
|
|
1750
|
+
findings.push({ kind: "outside", command: name, path: resolved, ...(spellings.spelled !== undefined ? { spelled: spellings.spelled } : {}) });
|
|
1152
1751
|
}
|
|
1153
1752
|
for (const { text: operand, globbed } of bareWordOperands) {
|
|
1154
|
-
const
|
|
1155
|
-
if (
|
|
1753
|
+
const spellings = resolveOperandSpellings(boundary.cwd ?? boundary.roots[0], operand, boundary.homeDir);
|
|
1754
|
+
if (spellings === undefined)
|
|
1156
1755
|
continue;
|
|
1157
|
-
if (!globbed && withinAnyRoot(boundary.roots,
|
|
1158
|
-
findings.push({ kind: "inside", path:
|
|
1756
|
+
if (!globbed && withinAnyRoot(boundary.roots, spellings.folded))
|
|
1757
|
+
findings.push({ kind: "inside", path: spellings.folded, ...(spellings.spelled !== undefined ? { spelled: spellings.spelled } : {}) });
|
|
1159
1758
|
}
|
|
1160
1759
|
if (boundary.denyMatch !== undefined && segmentSelectsRecursiveRead(name, args)) {
|
|
1161
1760
|
const roots = positionalOperands.length > 0 ? positionalOperands : stdinOperand ? [] : ["."];
|
|
@@ -1236,11 +1835,17 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1236
1835
|
const toks = tokenizeSegment(segments[si]);
|
|
1237
1836
|
const folded = toks.folded;
|
|
1238
1837
|
let from = 0;
|
|
1239
|
-
while (from < folded.length && isBareControlWord(toks, from, SHELL_CONTROL_PREFIXES))
|
|
1838
|
+
while (from < folded.length && isBareControlWord(toks, from, SHELL_CONTROL_PREFIXES)) {
|
|
1839
|
+
const controlWord = programBasename(folded[from] ?? "");
|
|
1240
1840
|
from++;
|
|
1841
|
+
for (let span = launcherOptionSpan(controlWord, folded, from); span > 0; span = launcherOptionSpan(controlWord, folded, from))
|
|
1842
|
+
from += span;
|
|
1843
|
+
}
|
|
1241
1844
|
const written = folded[from];
|
|
1242
1845
|
if (written === undefined)
|
|
1243
1846
|
return { reason: "empty command" };
|
|
1847
|
+
if (written === "coproc")
|
|
1848
|
+
return { reason: "`coproc` backgrounds a command — a backgrounded process outlives the command", refusedWhole: true };
|
|
1244
1849
|
if (isBareControlWord(toks, from, SHELL_BLOCK_WORDS)) {
|
|
1245
1850
|
unlistedSegments.add(si);
|
|
1246
1851
|
nameRefusal ??= `command "${written}" is not in the read-only allowlist`;
|
|
@@ -1253,6 +1858,18 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1253
1858
|
const name = pathPrefixed ? parsed.name.slice(parsed.name.lastIndexOf("/") + 1) : parsed.name;
|
|
1254
1859
|
if (pathPrefixed)
|
|
1255
1860
|
nameRefusal ??= "the command must be a bare name resolved via PATH (no path prefix)";
|
|
1861
|
+
if (name.startsWith("-") && name !== "-") {
|
|
1862
|
+
return { reason: `"${folded[0]}" is given an option where the program it runs would be — which program runs is not readable without its option table ${NOT_AUTO_ALLOWED}`, refusedWhole: true };
|
|
1863
|
+
}
|
|
1864
|
+
const launcherUnwrap = COMMAND_LAUNCHERS.has(name) ? unwrapLauncher(folded.slice(from), allow) : undefined;
|
|
1865
|
+
if (launcherUnwrap?.kind === "refused")
|
|
1866
|
+
return { reason: launcherUnwrap.reason, refusedWhole: true };
|
|
1867
|
+
const runs = launcherUnwrap === undefined ? { head: name, index: 0 } : { head: launcherUnwrap.head, index: launcherUnwrap.index };
|
|
1868
|
+
if (runs?.head !== undefined && runs.index !== undefined) {
|
|
1869
|
+
const familyRefusal = argumentAsProgramRefusal(runs.head, runs.head, folded.slice(from + runs.index + 1));
|
|
1870
|
+
if (familyRefusal !== undefined)
|
|
1871
|
+
return { reason: familyRefusal, refusedWhole: true };
|
|
1872
|
+
}
|
|
1256
1873
|
if (allow.has(name)) {
|
|
1257
1874
|
if (pathPrefixed || from > 0)
|
|
1258
1875
|
reheaded.set(si, { from, head: name });
|
|
@@ -1262,9 +1879,9 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1262
1879
|
}
|
|
1263
1880
|
nameRefusal ??= `command "${parsed.name}" is not in the read-only allowlist`;
|
|
1264
1881
|
if (SHELL_REENTRY_PROGRAM_RE.test(name))
|
|
1265
|
-
return { reason:
|
|
1882
|
+
return { reason: unjudgedProgramTextRefusal(parsed.name, "runs its argument as shell text"), refusedWhole: true };
|
|
1266
1883
|
if (COMMAND_LAUNCHERS.has(name)) {
|
|
1267
|
-
const unwrapped = unwrapLauncher(folded.slice(from), allow);
|
|
1884
|
+
const unwrapped = launcherUnwrap ?? unwrapLauncher(folded.slice(from), allow);
|
|
1268
1885
|
if (unwrapped.kind === "refused")
|
|
1269
1886
|
return { reason: unwrapped.reason, refusedWhole: true };
|
|
1270
1887
|
if (unwrapped.kind === "unlisted" && unwrapped.head !== undefined && SHELL_REENTRY_PROGRAM_RE.test(unwrapped.head)) {
|
|
@@ -1438,13 +2055,22 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1438
2055
|
}
|
|
1439
2056
|
const BASE_CANDIDATE_MAX = 256;
|
|
1440
2057
|
function evaluateReadBoundary(foldedSegments, boundary, reach) {
|
|
1441
|
-
const seen = { outside: new Set(), inside: new Set(), undecided: new Set(), recursive: new Set(), unresolved: new Set() };
|
|
2058
|
+
const seen = { outside: new Set(), inside: new Set(), undecided: new Set(), recursive: new Set(), unresolved: new Set(), checkedSpelled: new Set(), undecidedSpelled: new Set(), outsideSpelled: new Set() };
|
|
1442
2059
|
const outside = [];
|
|
1443
2060
|
const inside = [];
|
|
1444
2061
|
let unresolvable;
|
|
1445
2062
|
const unresolved = [];
|
|
1446
2063
|
let baseLost = false;
|
|
1447
2064
|
const undecided = [];
|
|
2065
|
+
const checkedSpellings = [];
|
|
2066
|
+
const undecidedSpellings = [];
|
|
2067
|
+
const outOfRootSpellings = [];
|
|
2068
|
+
const recordSpelling = (into, seenHere, finding) => {
|
|
2069
|
+
if (finding.spelled === undefined || seenHere.has(finding.spelled))
|
|
2070
|
+
return;
|
|
2071
|
+
seenHere.add(finding.spelled);
|
|
2072
|
+
into.push(finding.spelled);
|
|
2073
|
+
};
|
|
1448
2074
|
const recursive = [];
|
|
1449
2075
|
let candidates = [{ base: boundary.cwd ?? boundary.roots[0], skippedCd: -1 }];
|
|
1450
2076
|
let andRunStart = 0;
|
|
@@ -1498,6 +2124,7 @@ function evaluateReadBoundary(foldedSegments, boundary, reach) {
|
|
|
1498
2124
|
seen.inside.add(finding.path);
|
|
1499
2125
|
inside.push(finding.path);
|
|
1500
2126
|
}
|
|
2127
|
+
recordSpelling(checkedSpellings, seen.checkedSpelled, finding);
|
|
1501
2128
|
continue;
|
|
1502
2129
|
}
|
|
1503
2130
|
if (finding.kind === "undecided") {
|
|
@@ -1505,6 +2132,7 @@ function evaluateReadBoundary(foldedSegments, boundary, reach) {
|
|
|
1505
2132
|
seen.undecided.add(finding.path);
|
|
1506
2133
|
undecided.push(finding.path);
|
|
1507
2134
|
}
|
|
2135
|
+
recordSpelling(undecidedSpellings, seen.undecidedSpelled, finding);
|
|
1508
2136
|
continue;
|
|
1509
2137
|
}
|
|
1510
2138
|
if (finding.kind === "recursive") {
|
|
@@ -1525,6 +2153,7 @@ function evaluateReadBoundary(foldedSegments, boundary, reach) {
|
|
|
1525
2153
|
seen.outside.add(finding.path);
|
|
1526
2154
|
outside.push(finding);
|
|
1527
2155
|
}
|
|
2156
|
+
recordSpelling(outOfRootSpellings, seen.outsideSpelled, finding);
|
|
1528
2157
|
}
|
|
1529
2158
|
}
|
|
1530
2159
|
if (!baseLost && toks.folded[0] === "cd") {
|
|
@@ -1563,25 +2192,32 @@ function evaluateReadBoundary(foldedSegments, boundary, reach) {
|
|
|
1563
2192
|
const undecidedField = {
|
|
1564
2193
|
...(undecidedAll.length > 0 ? { undecidedPaths: undecidedAll } : {}),
|
|
1565
2194
|
...(recursive.length > 0 ? { recursiveReadPaths: recursive } : {}),
|
|
2195
|
+
...(undecidedSpellings.length > 0 ? { undecidedSpellings } : {}),
|
|
2196
|
+
};
|
|
2197
|
+
const checkedField = {
|
|
2198
|
+
...(inside.length > 0 ? { checkedPaths: inside } : {}),
|
|
2199
|
+
...(checkedSpellings.length > 0 ? { checkedSpellings } : {}),
|
|
1566
2200
|
};
|
|
2201
|
+
const outsideField = outOfRootSpellings.length > 0 ? { outOfRootSpellings } : {};
|
|
1567
2202
|
if (unresolvable !== undefined) {
|
|
1568
2203
|
return {
|
|
1569
2204
|
reason: unresolvable,
|
|
1570
2205
|
unresolvedOperands: unresolved,
|
|
1571
|
-
...(outside.length > 0 ? { outOfRootRead: true, outOfRootPaths: outside.map((o) => o.path) } : {}),
|
|
1572
|
-
...
|
|
2206
|
+
...(outside.length > 0 ? { outOfRootRead: true, outOfRootPaths: outside.map((o) => o.path), ...outsideField } : {}),
|
|
2207
|
+
...checkedField,
|
|
1573
2208
|
...undecidedField,
|
|
1574
2209
|
};
|
|
1575
2210
|
}
|
|
1576
2211
|
if (outside.length === 0)
|
|
1577
|
-
return
|
|
2212
|
+
return { ...checkedField, ...undecidedField };
|
|
1578
2213
|
const paths = outside.map((o) => `"${o.path}"`).join(", ");
|
|
1579
2214
|
const allowed = boundary.roots.length > 0 ? boundary.roots.join(", ") : "(none)";
|
|
1580
2215
|
return {
|
|
1581
2216
|
reason: `"${outside[0].command}" reads ${paths}, outside the allowed directories for this session: ${allowed} ${NOT_AUTO_ALLOWED}`,
|
|
1582
2217
|
outOfRootRead: true,
|
|
1583
2218
|
outOfRootPaths: outside.map((o) => o.path),
|
|
1584
|
-
...
|
|
2219
|
+
...outsideField,
|
|
2220
|
+
...checkedField,
|
|
1585
2221
|
...undecidedField,
|
|
1586
2222
|
};
|
|
1587
2223
|
}
|
|
@@ -1589,6 +2225,19 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
|
|
|
1589
2225
|
const toks = tokenizeSegment(command);
|
|
1590
2226
|
if (toks.folded.length === 0)
|
|
1591
2227
|
return {};
|
|
2228
|
+
const head = programBasename(toks.folded[0]);
|
|
2229
|
+
let past = 0;
|
|
2230
|
+
for (let span = launcherOptionSpan(head, toks.folded, past + 1); span > 0; span = launcherOptionSpan(head, toks.folded, past + 1))
|
|
2231
|
+
past += span;
|
|
2232
|
+
const chain = past === 0 ? toks.folded : [toks.folded[0], ...toks.folded.slice(past + 1)];
|
|
2233
|
+
const runs = COMMAND_LAUNCHERS.has(head) ? unwrapLauncher(chain, new Set()) : { kind: "reader", head, index: 0 };
|
|
2234
|
+
if (runs.kind === "refused")
|
|
2235
|
+
return { reason: runs.reason, refusedWhole: true };
|
|
2236
|
+
if (runs.head !== undefined && runs.index !== undefined) {
|
|
2237
|
+
const familyRefusal = argumentAsProgramRefusal(runs.head, runs.head, toks.folded.slice(runs.index + past + 1));
|
|
2238
|
+
if (familyRefusal !== undefined)
|
|
2239
|
+
return { reason: familyRefusal, refusedWhole: true };
|
|
2240
|
+
}
|
|
1592
2241
|
return evaluateReadBoundary([toks], boundary);
|
|
1593
2242
|
}
|
|
1594
2243
|
const NOT_GATED = { gated: false, paths: [] };
|
|
@@ -1603,7 +2252,7 @@ function whitelistReadsTheSameWords(command, outsideHeredocBodies) {
|
|
|
1603
2252
|
}
|
|
1604
2253
|
const UNMODELLED_BASE_MOVERS = new Set(["pushd", "popd"]);
|
|
1605
2254
|
const COMMAND_LAUNCHERS = new Set([
|
|
1606
|
-
"env", "command", "builtin", "exec", "noglob", "xargs",
|
|
2255
|
+
"env", "command", "builtin", "exec", "noglob", "xargs", "busybox",
|
|
1607
2256
|
"sudo", "doas", "pkexec", "su", "runuser", "chroot", "setpriv",
|
|
1608
2257
|
"nohup", "nice", "ionice", "chrt", "taskset", "stdbuf", "timeout", "time", "watch",
|
|
1609
2258
|
"setsid", "flock", "unshare", "nsenter", "script", "numactl", "prlimit", "systemd-run",
|