@sema-agent/core 7.17.5 → 7.17.6
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 +48 -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 +394 -21
- package/dist/tools/fs/fs-bash.js +87 -21
- package/package.json +1 -1
- package/wire-changes/core@7.17.5.json +2 -2
|
@@ -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)] };
|
|
@@ -643,10 +651,25 @@ function unwrapLauncher(folded, allow) {
|
|
|
643
651
|
const head = wordBasename;
|
|
644
652
|
if (SHELL_CONTROL_PREFIXES.has(head) || SHELL_BLOCK_WORDS.has(head))
|
|
645
653
|
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 };
|
|
654
|
+
return allow.has(head) || head === "cd" ? { kind: "reader", index: i, head, argvFromStdin } : { kind: "unlisted", head, index: i, argvFromStdin };
|
|
647
655
|
}
|
|
648
656
|
return { kind: "unlisted", argvFromStdin, runsDefaultProgram: lastReentry === folded.length - 1 };
|
|
649
657
|
}
|
|
658
|
+
const LAUNCHER_OPTION_GRAMMAR = new Map([
|
|
659
|
+
["time", {
|
|
660
|
+
plain: new Set(["-p", "-a", "-v", "-q", "--portability", "--append", "--verbose", "--quiet", "--help", "--version"]),
|
|
661
|
+
valued: new Set(["-f", "-o", "--format", "--output"]),
|
|
662
|
+
}],
|
|
663
|
+
]);
|
|
664
|
+
function launcherOptionSpan(launcher, folded, i) {
|
|
665
|
+
const row = launcher === undefined ? undefined : LAUNCHER_OPTION_GRAMMAR.get(launcher);
|
|
666
|
+
const tok = folded[i];
|
|
667
|
+
if (row === undefined || tok === undefined)
|
|
668
|
+
return 0;
|
|
669
|
+
if (row.plain.has(tok))
|
|
670
|
+
return 1;
|
|
671
|
+
return row.valued.has(tok) && folded[i + 1] !== undefined ? 2 : 0;
|
|
672
|
+
}
|
|
650
673
|
const ARGV_REENTRY_DEFAULT_PROGRAM = "echo";
|
|
651
674
|
function argvReentryUnjudged(unwrapped) {
|
|
652
675
|
if (!unwrapped.argvFromStdin)
|
|
@@ -846,6 +869,305 @@ const SED_READONLY_FLAGS = new Set([
|
|
|
846
869
|
]);
|
|
847
870
|
const SED_READONLY_CLUSTER_LETTERS = new Set(["n", "E", "r", "z"]);
|
|
848
871
|
const SED_READONLY_SCRIPT_PIECE = /^(?:\d+|\d+,\d+)?p$/;
|
|
872
|
+
const SED_FILE_OR_EXEC_COMMANDS = new Set(["r", "R", "w", "W", "e"]);
|
|
873
|
+
const SED_PLAIN_COMMANDS = new Set(["=", "d", "D", "F", "g", "G", "h", "H", "n", "N", "p", "P", "x", "z"]);
|
|
874
|
+
const SED_NUMERIC_COMMANDS = new Set(["q", "Q", "l", "L"]);
|
|
875
|
+
const SED_LABEL_COMMANDS = new Set(["b", "t", "T", ":", "v"]);
|
|
876
|
+
const SED_TEXT_COMMANDS = new Set(["a", "i", "c"]);
|
|
877
|
+
const SED_SUBSTITUTE_FLAGS = /[gpiImM0-9]/;
|
|
878
|
+
const SED_SCRIPT_LINE_LIMIT = 1024;
|
|
879
|
+
function utf8Length(text) {
|
|
880
|
+
let bytes = 0;
|
|
881
|
+
for (const ch of text) {
|
|
882
|
+
const code = ch.codePointAt(0);
|
|
883
|
+
bytes += code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4;
|
|
884
|
+
}
|
|
885
|
+
return bytes;
|
|
886
|
+
}
|
|
887
|
+
function readSedScript(script) {
|
|
888
|
+
if (script.split("\n").some((line) => utf8Length(line) >= SED_SCRIPT_LINE_LIMIT))
|
|
889
|
+
return "unseen";
|
|
890
|
+
let i = 0;
|
|
891
|
+
let depth = 0;
|
|
892
|
+
const len = script.length;
|
|
893
|
+
const isBlank = (ch) => ch === " " || ch === "\t" || ch === "\v" || ch === "\f";
|
|
894
|
+
const skipBlank = () => {
|
|
895
|
+
while (i < len && isBlank(script[i]))
|
|
896
|
+
i++;
|
|
897
|
+
};
|
|
898
|
+
const digits = () => {
|
|
899
|
+
while (i < len && script[i] >= "0" && script[i] <= "9")
|
|
900
|
+
i++;
|
|
901
|
+
};
|
|
902
|
+
const consumeDelimited = (delim, regex) => {
|
|
903
|
+
while (i < len) {
|
|
904
|
+
const ch = script[i];
|
|
905
|
+
if (ch === "\\") {
|
|
906
|
+
const escaped = script[i + 1];
|
|
907
|
+
if (escaped === undefined || escaped === "\n" || escaped === "\r")
|
|
908
|
+
return false;
|
|
909
|
+
i += 2;
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
if (ch === "\n" || ch === "\r")
|
|
913
|
+
return false;
|
|
914
|
+
if (ch === delim) {
|
|
915
|
+
i++;
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
if (regex && ch === "[") {
|
|
919
|
+
i++;
|
|
920
|
+
if (script[i] === "^")
|
|
921
|
+
i++;
|
|
922
|
+
if (script[i] === "]")
|
|
923
|
+
i++;
|
|
924
|
+
while (i < len && script[i] !== "]" && script[i] !== "\n" && script[i] !== "\r") {
|
|
925
|
+
const next = script[i + 1];
|
|
926
|
+
if (script[i] === "[" && (next === ":" || next === "." || next === "=")) {
|
|
927
|
+
const at = script.indexOf(`${next}]`, i + 2);
|
|
928
|
+
if (at === -1 || script.slice(i, at).includes("\n"))
|
|
929
|
+
return false;
|
|
930
|
+
i = at + 2;
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
i++;
|
|
934
|
+
}
|
|
935
|
+
if (i >= len || script[i] !== "]")
|
|
936
|
+
return false;
|
|
937
|
+
i++;
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
i++;
|
|
941
|
+
}
|
|
942
|
+
return false;
|
|
943
|
+
};
|
|
944
|
+
const readAddress = () => {
|
|
945
|
+
const ch = script[i];
|
|
946
|
+
if (ch === undefined)
|
|
947
|
+
return undefined;
|
|
948
|
+
if (ch === "$") {
|
|
949
|
+
i++;
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
952
|
+
if (ch >= "0" && ch <= "9") {
|
|
953
|
+
digits();
|
|
954
|
+
if (script[i] === "~") {
|
|
955
|
+
i++;
|
|
956
|
+
digits();
|
|
957
|
+
}
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
if (ch === "/" || ch === "\\") {
|
|
961
|
+
const delim = ch === "/" ? "/" : script[i + 1];
|
|
962
|
+
i += ch === "/" ? 1 : 2;
|
|
963
|
+
if (delim === undefined || !consumeDelimited(delim, true))
|
|
964
|
+
return false;
|
|
965
|
+
while (i < len && (script[i] === "I" || script[i] === "M"))
|
|
966
|
+
i++;
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
return undefined;
|
|
970
|
+
};
|
|
971
|
+
while (i < len) {
|
|
972
|
+
const ch = script[i];
|
|
973
|
+
if (isBlank(ch) || ch === "\n" || ch === "\r" || ch === ";") {
|
|
974
|
+
i++;
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
if (ch === "#") {
|
|
978
|
+
while (i < len && script[i] !== "\n")
|
|
979
|
+
i++;
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
if (ch === "}") {
|
|
983
|
+
if (depth === 0)
|
|
984
|
+
return "unparsed";
|
|
985
|
+
depth--;
|
|
986
|
+
i++;
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
const first = readAddress();
|
|
990
|
+
if (first === false)
|
|
991
|
+
return "unparsed";
|
|
992
|
+
if (first === true) {
|
|
993
|
+
skipBlank();
|
|
994
|
+
if (script[i] === ",") {
|
|
995
|
+
i++;
|
|
996
|
+
skipBlank();
|
|
997
|
+
if (script[i] === "+" || script[i] === "~") {
|
|
998
|
+
i++;
|
|
999
|
+
if (!(script[i] >= "0" && script[i] <= "9"))
|
|
1000
|
+
return "unparsed";
|
|
1001
|
+
digits();
|
|
1002
|
+
}
|
|
1003
|
+
else if (readAddress() !== true)
|
|
1004
|
+
return "unparsed";
|
|
1005
|
+
}
|
|
1006
|
+
skipBlank();
|
|
1007
|
+
}
|
|
1008
|
+
while (script[i] === "!") {
|
|
1009
|
+
i++;
|
|
1010
|
+
skipBlank();
|
|
1011
|
+
}
|
|
1012
|
+
const cmd = script[i];
|
|
1013
|
+
if (cmd === undefined)
|
|
1014
|
+
return "unparsed";
|
|
1015
|
+
i++;
|
|
1016
|
+
if (SED_FILE_OR_EXEC_COMMANDS.has(cmd))
|
|
1017
|
+
return "names";
|
|
1018
|
+
if (cmd === "{") {
|
|
1019
|
+
depth++;
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
if (cmd === "s" || cmd === "y") {
|
|
1023
|
+
const delim = script[i];
|
|
1024
|
+
if (delim === undefined || delim === "\\" || delim === "\n")
|
|
1025
|
+
return "unparsed";
|
|
1026
|
+
i++;
|
|
1027
|
+
if (!consumeDelimited(delim, cmd === "s"))
|
|
1028
|
+
return "unparsed";
|
|
1029
|
+
if (!consumeDelimited(delim, false))
|
|
1030
|
+
return "unparsed";
|
|
1031
|
+
if (cmd === "y")
|
|
1032
|
+
continue;
|
|
1033
|
+
while (i < len) {
|
|
1034
|
+
const flag = script[i];
|
|
1035
|
+
if (flag === "w" || flag === "W" || flag === "e")
|
|
1036
|
+
return "names";
|
|
1037
|
+
if (SED_SUBSTITUTE_FLAGS.test(flag) || isBlank(flag)) {
|
|
1038
|
+
i++;
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
if (flag === ";" || flag === "\n" || flag === "\r" || flag === "}" || flag === "#")
|
|
1042
|
+
break;
|
|
1043
|
+
return "unparsed";
|
|
1044
|
+
}
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
if (SED_TEXT_COMMANDS.has(cmd)) {
|
|
1048
|
+
while (i < len && script[i] !== "\n")
|
|
1049
|
+
i++;
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
if (SED_LABEL_COMMANDS.has(cmd)) {
|
|
1053
|
+
skipBlank();
|
|
1054
|
+
while (i < len && script[i] !== "\n" && script[i] !== ";" && script[i] !== "}" && !isBlank(script[i]))
|
|
1055
|
+
i++;
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
if (SED_NUMERIC_COMMANDS.has(cmd)) {
|
|
1059
|
+
skipBlank();
|
|
1060
|
+
digits();
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if (!SED_PLAIN_COMMANDS.has(cmd))
|
|
1064
|
+
return "unparsed";
|
|
1065
|
+
}
|
|
1066
|
+
return depth === 0 ? "clean" : "unparsed";
|
|
1067
|
+
}
|
|
1068
|
+
const SED_OPTION_DIALECTS = [
|
|
1069
|
+
{ separated: "efl", suffix: "iI", stopsAtFirstOperand: false },
|
|
1070
|
+
{ separated: "ef", suffix: "iI", stopsAtFirstOperand: true },
|
|
1071
|
+
];
|
|
1072
|
+
function sedSeparatedSuffixTaken(tok) {
|
|
1073
|
+
return tok !== undefined && (tok === "" || tok.startsWith("."));
|
|
1074
|
+
}
|
|
1075
|
+
const SED_LONG_VALUE_OPTIONS = ["expression", "line-length"];
|
|
1076
|
+
const SED_LONG_PLAIN_OPTIONS = [
|
|
1077
|
+
"debug", "follow-symlinks", "help", "in-place", "null-data", "posix", "quiet", "regexp-extended",
|
|
1078
|
+
"sandbox", "separate", "silent", "unbuffered", "version", "zero-terminated",
|
|
1079
|
+
];
|
|
1080
|
+
function readSedInvocation(args, dialect) {
|
|
1081
|
+
const scripts = [];
|
|
1082
|
+
let scriptSupplied = false;
|
|
1083
|
+
let endOfOptions = false;
|
|
1084
|
+
let firstOperand;
|
|
1085
|
+
for (let k = 0; k < args.length; k++) {
|
|
1086
|
+
const tok = args[k];
|
|
1087
|
+
if (!endOfOptions && tok === "--") {
|
|
1088
|
+
endOfOptions = true;
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
if (endOfOptions || !tok.startsWith("-") || tok === "-") {
|
|
1092
|
+
if (firstOperand === undefined) {
|
|
1093
|
+
firstOperand = tok;
|
|
1094
|
+
if (dialect.stopsAtFirstOperand)
|
|
1095
|
+
endOfOptions = true;
|
|
1096
|
+
}
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
if (tok.startsWith("--")) {
|
|
1100
|
+
const long = longOptionNameOf(tok);
|
|
1101
|
+
if (long === undefined)
|
|
1102
|
+
return "unparsed";
|
|
1103
|
+
const eq = tok.indexOf("=");
|
|
1104
|
+
const attached = eq >= 0 ? tok.slice(eq + 1) : undefined;
|
|
1105
|
+
if (isLongOptionAbbrevOf(long, "file"))
|
|
1106
|
+
return "unseen";
|
|
1107
|
+
if (isLongOptionAbbrevOf(long, "expression")) {
|
|
1108
|
+
const value = attached ?? args[++k];
|
|
1109
|
+
if (value === undefined)
|
|
1110
|
+
return "unparsed";
|
|
1111
|
+
scripts.push(value);
|
|
1112
|
+
scriptSupplied = true;
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
if (SED_LONG_VALUE_OPTIONS.some((full) => isLongOptionAbbrevOf(long, full))) {
|
|
1116
|
+
if (attached === undefined)
|
|
1117
|
+
k++;
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
if (SED_LONG_PLAIN_OPTIONS.some((full) => isLongOptionAbbrevOf(long, full)))
|
|
1121
|
+
continue;
|
|
1122
|
+
return "unparsed";
|
|
1123
|
+
}
|
|
1124
|
+
for (let c = 1; c < tok.length; c++) {
|
|
1125
|
+
const letter = tok[c];
|
|
1126
|
+
if (dialect.suffix.includes(letter)) {
|
|
1127
|
+
if (c + 1 === tok.length && sedSeparatedSuffixTaken(args[k + 1]))
|
|
1128
|
+
k++;
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
if (dialect.separated.includes(letter)) {
|
|
1132
|
+
const rest = tok.slice(c + 1);
|
|
1133
|
+
const value = rest !== "" ? rest : args[++k];
|
|
1134
|
+
if (letter === "f")
|
|
1135
|
+
return "unseen";
|
|
1136
|
+
if (letter === "e") {
|
|
1137
|
+
if (value === undefined)
|
|
1138
|
+
return "unparsed";
|
|
1139
|
+
scripts.push(value);
|
|
1140
|
+
scriptSupplied = true;
|
|
1141
|
+
}
|
|
1142
|
+
break;
|
|
1143
|
+
}
|
|
1144
|
+
if (!SED_SHORT_PLAIN_LETTERS.includes(letter))
|
|
1145
|
+
return "unparsed";
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
const script = scriptSupplied ? scripts.join("\n") : firstOperand;
|
|
1149
|
+
if (script === undefined)
|
|
1150
|
+
return "clean";
|
|
1151
|
+
return readSedScript(script);
|
|
1152
|
+
}
|
|
1153
|
+
const SED_SHORT_PLAIN_LETTERS = "nsuzEraHl";
|
|
1154
|
+
const SCRIPT_SLOT_VERBS = new Map([
|
|
1155
|
+
["sed", (args) => {
|
|
1156
|
+
const readings = SED_OPTION_DIALECTS.map((dialect) => readSedInvocation(args, dialect));
|
|
1157
|
+
if (readings.includes("names") || readings.includes("unseen"))
|
|
1158
|
+
return true;
|
|
1159
|
+
return readings.every((reading) => reading === "unparsed");
|
|
1160
|
+
}],
|
|
1161
|
+
]);
|
|
1162
|
+
function unjudgedProgramTextRefusal(written, clause) {
|
|
1163
|
+
return `command "${written}" ${clause}, which this walk does not judge ${NOT_AUTO_ALLOWED}`;
|
|
1164
|
+
}
|
|
1165
|
+
function scriptSlotRefusal(written, name, args) {
|
|
1166
|
+
const recognizer = SCRIPT_SLOT_VERBS.get(name);
|
|
1167
|
+
if (recognizer === undefined || !recognizer(args))
|
|
1168
|
+
return undefined;
|
|
1169
|
+
return unjudgedProgramTextRefusal(written, "names files or runs commands inside its script text");
|
|
1170
|
+
}
|
|
849
1171
|
function guardedVerbFormReason(name, toks, raw) {
|
|
850
1172
|
if (name === "find") {
|
|
851
1173
|
for (const t of toks) {
|
|
@@ -1130,12 +1452,12 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
1130
1452
|
for (let k = 0; k < args.length; k++) {
|
|
1131
1453
|
if (!argGlobs(k) || expandingArgs.has(k))
|
|
1132
1454
|
continue;
|
|
1133
|
-
const
|
|
1134
|
-
findings.push({ kind: "undecided", path:
|
|
1455
|
+
const r = resolveOperandSpellings(boundary.cwd ?? boundary.roots[0], args[k], boundary.homeDir);
|
|
1456
|
+
findings.push({ kind: "undecided", path: r?.folded ?? args[k], ...(r?.spelled !== undefined ? { spelled: r.spelled } : {}) });
|
|
1135
1457
|
}
|
|
1136
1458
|
for (const { text: candidate, globbed } of candidates) {
|
|
1137
|
-
const
|
|
1138
|
-
if (
|
|
1459
|
+
const spellings = resolveOperandSpellings(boundary.cwd ?? boundary.roots[0], candidate, boundary.homeDir);
|
|
1460
|
+
if (spellings === undefined) {
|
|
1139
1461
|
findings.push({
|
|
1140
1462
|
kind: "unresolvable",
|
|
1141
1463
|
operand: candidate,
|
|
@@ -1143,19 +1465,20 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
1143
1465
|
});
|
|
1144
1466
|
continue;
|
|
1145
1467
|
}
|
|
1468
|
+
const resolved = spellings.folded;
|
|
1146
1469
|
if (withinAnyRoot(boundary.roots, resolved)) {
|
|
1147
1470
|
if (!globbed)
|
|
1148
|
-
findings.push({ kind: "inside", path: resolved });
|
|
1471
|
+
findings.push({ kind: "inside", path: resolved, ...(spellings.spelled !== undefined ? { spelled: spellings.spelled } : {}) });
|
|
1149
1472
|
}
|
|
1150
1473
|
else
|
|
1151
|
-
findings.push({ kind: "outside", command: name, path: resolved });
|
|
1474
|
+
findings.push({ kind: "outside", command: name, path: resolved, ...(spellings.spelled !== undefined ? { spelled: spellings.spelled } : {}) });
|
|
1152
1475
|
}
|
|
1153
1476
|
for (const { text: operand, globbed } of bareWordOperands) {
|
|
1154
|
-
const
|
|
1155
|
-
if (
|
|
1477
|
+
const spellings = resolveOperandSpellings(boundary.cwd ?? boundary.roots[0], operand, boundary.homeDir);
|
|
1478
|
+
if (spellings === undefined)
|
|
1156
1479
|
continue;
|
|
1157
|
-
if (!globbed && withinAnyRoot(boundary.roots,
|
|
1158
|
-
findings.push({ kind: "inside", path:
|
|
1480
|
+
if (!globbed && withinAnyRoot(boundary.roots, spellings.folded))
|
|
1481
|
+
findings.push({ kind: "inside", path: spellings.folded, ...(spellings.spelled !== undefined ? { spelled: spellings.spelled } : {}) });
|
|
1159
1482
|
}
|
|
1160
1483
|
if (boundary.denyMatch !== undefined && segmentSelectsRecursiveRead(name, args)) {
|
|
1161
1484
|
const roots = positionalOperands.length > 0 ? positionalOperands : stdinOperand ? [] : ["."];
|
|
@@ -1236,11 +1559,17 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1236
1559
|
const toks = tokenizeSegment(segments[si]);
|
|
1237
1560
|
const folded = toks.folded;
|
|
1238
1561
|
let from = 0;
|
|
1239
|
-
while (from < folded.length && isBareControlWord(toks, from, SHELL_CONTROL_PREFIXES))
|
|
1562
|
+
while (from < folded.length && isBareControlWord(toks, from, SHELL_CONTROL_PREFIXES)) {
|
|
1563
|
+
const controlWord = programBasename(folded[from] ?? "");
|
|
1240
1564
|
from++;
|
|
1565
|
+
for (let span = launcherOptionSpan(controlWord, folded, from); span > 0; span = launcherOptionSpan(controlWord, folded, from))
|
|
1566
|
+
from += span;
|
|
1567
|
+
}
|
|
1241
1568
|
const written = folded[from];
|
|
1242
1569
|
if (written === undefined)
|
|
1243
1570
|
return { reason: "empty command" };
|
|
1571
|
+
if (written === "coproc")
|
|
1572
|
+
return { reason: "`coproc` backgrounds a command — a backgrounded process outlives the command", refusedWhole: true };
|
|
1244
1573
|
if (isBareControlWord(toks, from, SHELL_BLOCK_WORDS)) {
|
|
1245
1574
|
unlistedSegments.add(si);
|
|
1246
1575
|
nameRefusal ??= `command "${written}" is not in the read-only allowlist`;
|
|
@@ -1253,6 +1582,18 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1253
1582
|
const name = pathPrefixed ? parsed.name.slice(parsed.name.lastIndexOf("/") + 1) : parsed.name;
|
|
1254
1583
|
if (pathPrefixed)
|
|
1255
1584
|
nameRefusal ??= "the command must be a bare name resolved via PATH (no path prefix)";
|
|
1585
|
+
if (name.startsWith("-") && name !== "-") {
|
|
1586
|
+
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 };
|
|
1587
|
+
}
|
|
1588
|
+
const launcherUnwrap = COMMAND_LAUNCHERS.has(name) ? unwrapLauncher(folded.slice(from), allow) : undefined;
|
|
1589
|
+
if (launcherUnwrap?.kind === "refused")
|
|
1590
|
+
return { reason: launcherUnwrap.reason, refusedWhole: true };
|
|
1591
|
+
const runs = launcherUnwrap === undefined ? { head: name, index: 0 } : { head: launcherUnwrap.head, index: launcherUnwrap.index };
|
|
1592
|
+
if (runs?.head !== undefined && runs.index !== undefined) {
|
|
1593
|
+
const slotRefusal = scriptSlotRefusal(runs.head, runs.head, folded.slice(from + runs.index + 1));
|
|
1594
|
+
if (slotRefusal !== undefined)
|
|
1595
|
+
return { reason: slotRefusal, refusedWhole: true };
|
|
1596
|
+
}
|
|
1256
1597
|
if (allow.has(name)) {
|
|
1257
1598
|
if (pathPrefixed || from > 0)
|
|
1258
1599
|
reheaded.set(si, { from, head: name });
|
|
@@ -1262,9 +1603,9 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1262
1603
|
}
|
|
1263
1604
|
nameRefusal ??= `command "${parsed.name}" is not in the read-only allowlist`;
|
|
1264
1605
|
if (SHELL_REENTRY_PROGRAM_RE.test(name))
|
|
1265
|
-
return { reason:
|
|
1606
|
+
return { reason: unjudgedProgramTextRefusal(parsed.name, "runs its argument as shell text"), refusedWhole: true };
|
|
1266
1607
|
if (COMMAND_LAUNCHERS.has(name)) {
|
|
1267
|
-
const unwrapped = unwrapLauncher(folded.slice(from), allow);
|
|
1608
|
+
const unwrapped = launcherUnwrap ?? unwrapLauncher(folded.slice(from), allow);
|
|
1268
1609
|
if (unwrapped.kind === "refused")
|
|
1269
1610
|
return { reason: unwrapped.reason, refusedWhole: true };
|
|
1270
1611
|
if (unwrapped.kind === "unlisted" && unwrapped.head !== undefined && SHELL_REENTRY_PROGRAM_RE.test(unwrapped.head)) {
|
|
@@ -1438,13 +1779,22 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1438
1779
|
}
|
|
1439
1780
|
const BASE_CANDIDATE_MAX = 256;
|
|
1440
1781
|
function evaluateReadBoundary(foldedSegments, boundary, reach) {
|
|
1441
|
-
const seen = { outside: new Set(), inside: new Set(), undecided: new Set(), recursive: new Set(), unresolved: new Set() };
|
|
1782
|
+
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
1783
|
const outside = [];
|
|
1443
1784
|
const inside = [];
|
|
1444
1785
|
let unresolvable;
|
|
1445
1786
|
const unresolved = [];
|
|
1446
1787
|
let baseLost = false;
|
|
1447
1788
|
const undecided = [];
|
|
1789
|
+
const checkedSpellings = [];
|
|
1790
|
+
const undecidedSpellings = [];
|
|
1791
|
+
const outOfRootSpellings = [];
|
|
1792
|
+
const recordSpelling = (into, seenHere, finding) => {
|
|
1793
|
+
if (finding.spelled === undefined || seenHere.has(finding.spelled))
|
|
1794
|
+
return;
|
|
1795
|
+
seenHere.add(finding.spelled);
|
|
1796
|
+
into.push(finding.spelled);
|
|
1797
|
+
};
|
|
1448
1798
|
const recursive = [];
|
|
1449
1799
|
let candidates = [{ base: boundary.cwd ?? boundary.roots[0], skippedCd: -1 }];
|
|
1450
1800
|
let andRunStart = 0;
|
|
@@ -1498,6 +1848,7 @@ function evaluateReadBoundary(foldedSegments, boundary, reach) {
|
|
|
1498
1848
|
seen.inside.add(finding.path);
|
|
1499
1849
|
inside.push(finding.path);
|
|
1500
1850
|
}
|
|
1851
|
+
recordSpelling(checkedSpellings, seen.checkedSpelled, finding);
|
|
1501
1852
|
continue;
|
|
1502
1853
|
}
|
|
1503
1854
|
if (finding.kind === "undecided") {
|
|
@@ -1505,6 +1856,7 @@ function evaluateReadBoundary(foldedSegments, boundary, reach) {
|
|
|
1505
1856
|
seen.undecided.add(finding.path);
|
|
1506
1857
|
undecided.push(finding.path);
|
|
1507
1858
|
}
|
|
1859
|
+
recordSpelling(undecidedSpellings, seen.undecidedSpelled, finding);
|
|
1508
1860
|
continue;
|
|
1509
1861
|
}
|
|
1510
1862
|
if (finding.kind === "recursive") {
|
|
@@ -1525,6 +1877,7 @@ function evaluateReadBoundary(foldedSegments, boundary, reach) {
|
|
|
1525
1877
|
seen.outside.add(finding.path);
|
|
1526
1878
|
outside.push(finding);
|
|
1527
1879
|
}
|
|
1880
|
+
recordSpelling(outOfRootSpellings, seen.outsideSpelled, finding);
|
|
1528
1881
|
}
|
|
1529
1882
|
}
|
|
1530
1883
|
if (!baseLost && toks.folded[0] === "cd") {
|
|
@@ -1563,25 +1916,32 @@ function evaluateReadBoundary(foldedSegments, boundary, reach) {
|
|
|
1563
1916
|
const undecidedField = {
|
|
1564
1917
|
...(undecidedAll.length > 0 ? { undecidedPaths: undecidedAll } : {}),
|
|
1565
1918
|
...(recursive.length > 0 ? { recursiveReadPaths: recursive } : {}),
|
|
1919
|
+
...(undecidedSpellings.length > 0 ? { undecidedSpellings } : {}),
|
|
1566
1920
|
};
|
|
1921
|
+
const checkedField = {
|
|
1922
|
+
...(inside.length > 0 ? { checkedPaths: inside } : {}),
|
|
1923
|
+
...(checkedSpellings.length > 0 ? { checkedSpellings } : {}),
|
|
1924
|
+
};
|
|
1925
|
+
const outsideField = outOfRootSpellings.length > 0 ? { outOfRootSpellings } : {};
|
|
1567
1926
|
if (unresolvable !== undefined) {
|
|
1568
1927
|
return {
|
|
1569
1928
|
reason: unresolvable,
|
|
1570
1929
|
unresolvedOperands: unresolved,
|
|
1571
|
-
...(outside.length > 0 ? { outOfRootRead: true, outOfRootPaths: outside.map((o) => o.path) } : {}),
|
|
1572
|
-
...
|
|
1930
|
+
...(outside.length > 0 ? { outOfRootRead: true, outOfRootPaths: outside.map((o) => o.path), ...outsideField } : {}),
|
|
1931
|
+
...checkedField,
|
|
1573
1932
|
...undecidedField,
|
|
1574
1933
|
};
|
|
1575
1934
|
}
|
|
1576
1935
|
if (outside.length === 0)
|
|
1577
|
-
return
|
|
1936
|
+
return { ...checkedField, ...undecidedField };
|
|
1578
1937
|
const paths = outside.map((o) => `"${o.path}"`).join(", ");
|
|
1579
1938
|
const allowed = boundary.roots.length > 0 ? boundary.roots.join(", ") : "(none)";
|
|
1580
1939
|
return {
|
|
1581
1940
|
reason: `"${outside[0].command}" reads ${paths}, outside the allowed directories for this session: ${allowed} ${NOT_AUTO_ALLOWED}`,
|
|
1582
1941
|
outOfRootRead: true,
|
|
1583
1942
|
outOfRootPaths: outside.map((o) => o.path),
|
|
1584
|
-
...
|
|
1943
|
+
...outsideField,
|
|
1944
|
+
...checkedField,
|
|
1585
1945
|
...undecidedField,
|
|
1586
1946
|
};
|
|
1587
1947
|
}
|
|
@@ -1589,6 +1949,19 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
|
|
|
1589
1949
|
const toks = tokenizeSegment(command);
|
|
1590
1950
|
if (toks.folded.length === 0)
|
|
1591
1951
|
return {};
|
|
1952
|
+
const head = programBasename(toks.folded[0]);
|
|
1953
|
+
let past = 0;
|
|
1954
|
+
for (let span = launcherOptionSpan(head, toks.folded, past + 1); span > 0; span = launcherOptionSpan(head, toks.folded, past + 1))
|
|
1955
|
+
past += span;
|
|
1956
|
+
const chain = past === 0 ? toks.folded : [toks.folded[0], ...toks.folded.slice(past + 1)];
|
|
1957
|
+
const runs = COMMAND_LAUNCHERS.has(head) ? unwrapLauncher(chain, new Set()) : { kind: "reader", head, index: 0 };
|
|
1958
|
+
if (runs.kind === "refused")
|
|
1959
|
+
return { reason: runs.reason, refusedWhole: true };
|
|
1960
|
+
if (runs.head !== undefined && runs.index !== undefined) {
|
|
1961
|
+
const slotRefusal = scriptSlotRefusal(runs.head, runs.head, toks.folded.slice(runs.index + past + 1));
|
|
1962
|
+
if (slotRefusal !== undefined)
|
|
1963
|
+
return { reason: slotRefusal, refusedWhole: true };
|
|
1964
|
+
}
|
|
1592
1965
|
return evaluateReadBoundary([toks], boundary);
|
|
1593
1966
|
}
|
|
1594
1967
|
const NOT_GATED = { gated: false, paths: [] };
|