@sema-agent/core 7.17.4 → 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 +77 -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 +10 -8
- package/dist/core/tool-catalog-entries.js +1 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +46 -5
- package/dist/tools/fs/bash-readonly-classifier.js +572 -80
- package/dist/tools/fs/fs-bash.d.ts +28 -0
- package/dist/tools/fs/fs-bash.js +135 -27
- package/dist/tools/fs/index.d.ts +13 -0
- package/dist/tools/fs/index.js +1 -0
- package/package.json +3 -2
- package/wire-changes/core@7.10.0.json +739 -0
- package/wire-changes/core@7.11.0.json +685 -0
- package/wire-changes/core@7.11.1.json +112 -0
- package/wire-changes/core@7.11.2.json +73 -0
- package/wire-changes/core@7.12.0.json +356 -0
- package/wire-changes/core@7.13.0.json +199 -0
- package/wire-changes/core@7.14.0.json +578 -0
- package/wire-changes/core@7.15.0.json +174 -0
- package/wire-changes/core@7.16.0.json +132 -0
- package/wire-changes/core@7.17.0.json +1621 -0
- package/wire-changes/core@7.17.1.json +139 -0
- package/wire-changes/core@7.17.2.json +65 -0
- package/wire-changes/core@7.17.3.json +44 -0
- package/wire-changes/core@7.17.4.json +52 -0
- package/wire-changes/core@7.17.5.json +67 -0
- package/wire-changes/core@7.5.0.json +1063 -0
- package/wire-changes/core@7.5.1.json +252 -0
- package/wire-changes/core@7.5.2.json +418 -0
- package/wire-changes/core@7.6.0.json +1243 -0
- package/wire-changes/core@7.6.1.json +1130 -0
- package/wire-changes/core@7.7.0.json +509 -0
- package/wire-changes/core@7.8.0.json +1293 -0
- package/wire-changes/core@7.9.0.json +691 -0
- package/wire-changes/core@7.9.1.json +518 -0
- package/wire-changes/core@7.9.2.json +46 -0
|
@@ -109,7 +109,7 @@ export function splitShellCompoundSegments(source, options) {
|
|
|
109
109
|
}
|
|
110
110
|
const scanned = options?.trailingTerminator !== "keep" ? source.replace(/;\s*$/, "") : source;
|
|
111
111
|
const segments = [];
|
|
112
|
-
const
|
|
112
|
+
const connectors = ["start"];
|
|
113
113
|
const mask = quoteMask(scanned);
|
|
114
114
|
const quoted = (i) => mask.balanced && mask.quoted[i] === true;
|
|
115
115
|
let cur = "";
|
|
@@ -122,7 +122,7 @@ export function splitShellCompoundSegments(source, options) {
|
|
|
122
122
|
if (scanned[i + 1] === "&") {
|
|
123
123
|
segments.push(cur);
|
|
124
124
|
cur = "";
|
|
125
|
-
|
|
125
|
+
connectors.push("&&");
|
|
126
126
|
i++;
|
|
127
127
|
}
|
|
128
128
|
else
|
|
@@ -132,25 +132,25 @@ export function splitShellCompoundSegments(source, options) {
|
|
|
132
132
|
if (scanned[i + 1] === "|") {
|
|
133
133
|
segments.push(cur);
|
|
134
134
|
cur = "";
|
|
135
|
-
|
|
135
|
+
connectors.push("||");
|
|
136
136
|
i++;
|
|
137
137
|
}
|
|
138
138
|
else {
|
|
139
139
|
segments.push(cur);
|
|
140
140
|
cur = "";
|
|
141
|
-
|
|
141
|
+
connectors.push("|");
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
else if (c === ";") {
|
|
145
145
|
segments.push(cur);
|
|
146
146
|
cur = "";
|
|
147
|
-
|
|
147
|
+
connectors.push(";");
|
|
148
148
|
}
|
|
149
149
|
else
|
|
150
150
|
cur += c;
|
|
151
151
|
}
|
|
152
152
|
segments.push(cur);
|
|
153
|
-
return { segments, pipeFed };
|
|
153
|
+
return { segments, pipeFed: connectors.map((k) => k === "|"), connectors };
|
|
154
154
|
}
|
|
155
155
|
const EXEMPT_REDIRECTION = /^(?:2>\/dev\/null|[12]?>&[12])$/;
|
|
156
156
|
function stripExemptRedirections(command) {
|
|
@@ -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)] };
|
|
@@ -259,7 +267,8 @@ function tokensMoveBase(toks, allow) {
|
|
|
259
267
|
const unwrapped = unwrapLauncher(toks.folded.slice(from), allow);
|
|
260
268
|
return unwrapped.kind === "refused" || (unwrapped.kind === "reader" && unwrapped.head === "cd");
|
|
261
269
|
}
|
|
262
|
-
const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi)?sh|eval|source
|
|
270
|
+
const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi)?sh|eval|source|\.|parallel)$/;
|
|
271
|
+
const ARGV_REENTRY_PROGRAM_RE = /^xargs$/;
|
|
263
272
|
export function refusedCommandMayRead(command, allow, boundary) {
|
|
264
273
|
return !refusedCommandExempt(command, allow, boundary, 0);
|
|
265
274
|
}
|
|
@@ -579,6 +588,8 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
579
588
|
const unwrapped = unwrapLauncher(toks.slice(i).map(fold), allow);
|
|
580
589
|
if (unwrapped.kind === "refused")
|
|
581
590
|
return false;
|
|
591
|
+
if (argvReentryUnjudged(unwrapped))
|
|
592
|
+
return false;
|
|
582
593
|
if (unwrapped.kind === "unlisted") {
|
|
583
594
|
if (!unlistedSegmentJudgedInside(toks, i))
|
|
584
595
|
return false;
|
|
@@ -621,18 +632,50 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
621
632
|
return true;
|
|
622
633
|
}
|
|
623
634
|
function unwrapLauncher(folded, allow) {
|
|
635
|
+
let argvFromStdin = ARGV_REENTRY_PROGRAM_RE.test(programBasename(folded[0] ?? ""));
|
|
636
|
+
let lastReentry = argvFromStdin ? 0 : -1;
|
|
624
637
|
for (let i = 1; i < folded.length; i++) {
|
|
625
638
|
const t = folded[i];
|
|
626
|
-
if (ASSIGNMENT_WORD.test(t) || /^\d+[smhd]?$/.test(t)
|
|
639
|
+
if (ASSIGNMENT_WORD.test(t) || /^\d+[smhd]?$/.test(t))
|
|
640
|
+
continue;
|
|
641
|
+
const wordBasename = programBasename(t);
|
|
642
|
+
if (ARGV_REENTRY_PROGRAM_RE.test(wordBasename)) {
|
|
643
|
+
argvFromStdin = true;
|
|
644
|
+
lastReentry = i;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
if (COMMAND_LAUNCHERS.has(wordBasename))
|
|
627
648
|
continue;
|
|
628
649
|
if (t.startsWith("-"))
|
|
629
650
|
return { kind: "refused", reason: `"${folded[0]}" is given an option before the program it runs — which program runs is not readable without the launcher's option table ${NOT_AUTO_ALLOWED}` };
|
|
630
|
-
const head =
|
|
651
|
+
const head = wordBasename;
|
|
631
652
|
if (SHELL_CONTROL_PREFIXES.has(head) || SHELL_BLOCK_WORDS.has(head))
|
|
632
653
|
return { kind: "refused", reason: `"${folded[0]}" is given shell control syntax ("${head}") where the program it runs would be ${NOT_AUTO_ALLOWED}` };
|
|
633
|
-
return allow.has(head) || head === "cd" ? { kind: "reader", index: i, head } : { kind: "unlisted", head };
|
|
654
|
+
return allow.has(head) || head === "cd" ? { kind: "reader", index: i, head, argvFromStdin } : { kind: "unlisted", head, index: i, argvFromStdin };
|
|
634
655
|
}
|
|
635
|
-
return { kind: "unlisted" };
|
|
656
|
+
return { kind: "unlisted", argvFromStdin, runsDefaultProgram: lastReentry === folded.length - 1 };
|
|
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
|
+
}
|
|
673
|
+
const ARGV_REENTRY_DEFAULT_PROGRAM = "echo";
|
|
674
|
+
function argvReentryUnjudged(unwrapped) {
|
|
675
|
+
if (!unwrapped.argvFromStdin)
|
|
676
|
+
return false;
|
|
677
|
+
const runs = unwrapped.head ?? (unwrapped.runsDefaultProgram === true ? ARGV_REENTRY_DEFAULT_PROGRAM : undefined);
|
|
678
|
+
return runs === undefined || !NO_PATH_OPERAND_COMMANDS.has(runs);
|
|
636
679
|
}
|
|
637
680
|
function tokenizeSegment(segment) {
|
|
638
681
|
const raw = splitWordsQuoteAware(segment);
|
|
@@ -826,6 +869,305 @@ const SED_READONLY_FLAGS = new Set([
|
|
|
826
869
|
]);
|
|
827
870
|
const SED_READONLY_CLUSTER_LETTERS = new Set(["n", "E", "r", "z"]);
|
|
828
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
|
+
}
|
|
829
1171
|
function guardedVerbFormReason(name, toks, raw) {
|
|
830
1172
|
if (name === "find") {
|
|
831
1173
|
for (const t of toks) {
|
|
@@ -1110,12 +1452,12 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
1110
1452
|
for (let k = 0; k < args.length; k++) {
|
|
1111
1453
|
if (!argGlobs(k) || expandingArgs.has(k))
|
|
1112
1454
|
continue;
|
|
1113
|
-
const
|
|
1114
|
-
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 } : {}) });
|
|
1115
1457
|
}
|
|
1116
1458
|
for (const { text: candidate, globbed } of candidates) {
|
|
1117
|
-
const
|
|
1118
|
-
if (
|
|
1459
|
+
const spellings = resolveOperandSpellings(boundary.cwd ?? boundary.roots[0], candidate, boundary.homeDir);
|
|
1460
|
+
if (spellings === undefined) {
|
|
1119
1461
|
findings.push({
|
|
1120
1462
|
kind: "unresolvable",
|
|
1121
1463
|
operand: candidate,
|
|
@@ -1123,19 +1465,20 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
1123
1465
|
});
|
|
1124
1466
|
continue;
|
|
1125
1467
|
}
|
|
1468
|
+
const resolved = spellings.folded;
|
|
1126
1469
|
if (withinAnyRoot(boundary.roots, resolved)) {
|
|
1127
1470
|
if (!globbed)
|
|
1128
|
-
findings.push({ kind: "inside", path: resolved });
|
|
1471
|
+
findings.push({ kind: "inside", path: resolved, ...(spellings.spelled !== undefined ? { spelled: spellings.spelled } : {}) });
|
|
1129
1472
|
}
|
|
1130
1473
|
else
|
|
1131
|
-
findings.push({ kind: "outside", command: name, path: resolved });
|
|
1474
|
+
findings.push({ kind: "outside", command: name, path: resolved, ...(spellings.spelled !== undefined ? { spelled: spellings.spelled } : {}) });
|
|
1132
1475
|
}
|
|
1133
1476
|
for (const { text: operand, globbed } of bareWordOperands) {
|
|
1134
|
-
const
|
|
1135
|
-
if (
|
|
1477
|
+
const spellings = resolveOperandSpellings(boundary.cwd ?? boundary.roots[0], operand, boundary.homeDir);
|
|
1478
|
+
if (spellings === undefined)
|
|
1136
1479
|
continue;
|
|
1137
|
-
if (!globbed && withinAnyRoot(boundary.roots,
|
|
1138
|
-
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 } : {}) });
|
|
1139
1482
|
}
|
|
1140
1483
|
if (boundary.denyMatch !== undefined && segmentSelectsRecursiveRead(name, args)) {
|
|
1141
1484
|
const roots = positionalOperands.length > 0 ? positionalOperands : stdinOperand ? [] : ["."];
|
|
@@ -1208,7 +1551,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1208
1551
|
const split = segmentCompoundForReadonly(command, boundary?.homeDir);
|
|
1209
1552
|
if ("reject" in split)
|
|
1210
1553
|
return { reason: split.reject, refusedWhole: true };
|
|
1211
|
-
const { segments, pipeFed } = split;
|
|
1554
|
+
const { segments, pipeFed, connectors } = split;
|
|
1212
1555
|
let nameRefusal;
|
|
1213
1556
|
const unlistedSegments = new Set();
|
|
1214
1557
|
const reheaded = new Map();
|
|
@@ -1216,11 +1559,17 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1216
1559
|
const toks = tokenizeSegment(segments[si]);
|
|
1217
1560
|
const folded = toks.folded;
|
|
1218
1561
|
let from = 0;
|
|
1219
|
-
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] ?? "");
|
|
1220
1564
|
from++;
|
|
1565
|
+
for (let span = launcherOptionSpan(controlWord, folded, from); span > 0; span = launcherOptionSpan(controlWord, folded, from))
|
|
1566
|
+
from += span;
|
|
1567
|
+
}
|
|
1221
1568
|
const written = folded[from];
|
|
1222
1569
|
if (written === undefined)
|
|
1223
1570
|
return { reason: "empty command" };
|
|
1571
|
+
if (written === "coproc")
|
|
1572
|
+
return { reason: "`coproc` backgrounds a command — a backgrounded process outlives the command", refusedWhole: true };
|
|
1224
1573
|
if (isBareControlWord(toks, from, SHELL_BLOCK_WORDS)) {
|
|
1225
1574
|
unlistedSegments.add(si);
|
|
1226
1575
|
nameRefusal ??= `command "${written}" is not in the read-only allowlist`;
|
|
@@ -1233,6 +1582,18 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1233
1582
|
const name = pathPrefixed ? parsed.name.slice(parsed.name.lastIndexOf("/") + 1) : parsed.name;
|
|
1234
1583
|
if (pathPrefixed)
|
|
1235
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
|
+
}
|
|
1236
1597
|
if (allow.has(name)) {
|
|
1237
1598
|
if (pathPrefixed || from > 0)
|
|
1238
1599
|
reheaded.set(si, { from, head: name });
|
|
@@ -1242,14 +1603,17 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1242
1603
|
}
|
|
1243
1604
|
nameRefusal ??= `command "${parsed.name}" is not in the read-only allowlist`;
|
|
1244
1605
|
if (SHELL_REENTRY_PROGRAM_RE.test(name))
|
|
1245
|
-
return { reason:
|
|
1606
|
+
return { reason: unjudgedProgramTextRefusal(parsed.name, "runs its argument as shell text"), refusedWhole: true };
|
|
1246
1607
|
if (COMMAND_LAUNCHERS.has(name)) {
|
|
1247
|
-
const unwrapped = unwrapLauncher(folded.slice(from), allow);
|
|
1608
|
+
const unwrapped = launcherUnwrap ?? unwrapLauncher(folded.slice(from), allow);
|
|
1248
1609
|
if (unwrapped.kind === "refused")
|
|
1249
1610
|
return { reason: unwrapped.reason, refusedWhole: true };
|
|
1250
1611
|
if (unwrapped.kind === "unlisted" && unwrapped.head !== undefined && SHELL_REENTRY_PROGRAM_RE.test(unwrapped.head)) {
|
|
1251
1612
|
return { reason: `"${written}" runs "${unwrapped.head}", which runs its argument as shell text this walk does not judge ${NOT_AUTO_ALLOWED}`, refusedWhole: true };
|
|
1252
1613
|
}
|
|
1614
|
+
if (argvReentryUnjudged(unwrapped)) {
|
|
1615
|
+
return { reason: `"${written}" takes the operands of the program it runs from its standard input, which this walk does not judge ${NOT_AUTO_ALLOWED}`, refusedWhole: true };
|
|
1616
|
+
}
|
|
1253
1617
|
if (unwrapped.kind === "unlisted")
|
|
1254
1618
|
unlistedSegments.add(si);
|
|
1255
1619
|
else
|
|
@@ -1288,13 +1652,29 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1288
1652
|
};
|
|
1289
1653
|
const STDIN_FILE_FLOOR = { cat: 1, grep: 2, head: 1, tail: 1, wc: 1, cut: 1, tr: Infinity, diff: 2, cmp: 2, comm: 2, sed: 2 };
|
|
1290
1654
|
const foldedSegments = [];
|
|
1655
|
+
const foldedReach = [];
|
|
1656
|
+
let crossedAllAnd = true;
|
|
1657
|
+
let crossedAnyOr = false;
|
|
1291
1658
|
let shapeRefusal = nameRefusal;
|
|
1292
1659
|
for (let si = 0; si < segments.length; si++) {
|
|
1660
|
+
if (si > 0) {
|
|
1661
|
+
if (connectors[si] !== "&&")
|
|
1662
|
+
crossedAllAnd = false;
|
|
1663
|
+
if (connectors[si] === "||")
|
|
1664
|
+
crossedAnyOr = true;
|
|
1665
|
+
}
|
|
1666
|
+
const moveProvable = pipeFed[si] !== true && pipeFed[si + 1] !== true && reheaded.get(si) === undefined;
|
|
1667
|
+
const pushReach = () => {
|
|
1668
|
+
foldedReach.push({ andJoined: si > 0 && crossedAllAnd, orBroken: !crossedAllAnd && crossedAnyOr, moveProvable });
|
|
1669
|
+
crossedAllAnd = true;
|
|
1670
|
+
crossedAnyOr = false;
|
|
1671
|
+
};
|
|
1293
1672
|
if (unlistedSegments.has(si)) {
|
|
1294
1673
|
const unlisted = tokenizeSegment(segments[si]);
|
|
1295
1674
|
if (unlisted.folded.length === 0)
|
|
1296
1675
|
continue;
|
|
1297
1676
|
foldedSegments.push({ folded: [LAUNCHER_SCAN_HEAD, ...unlisted.folded], raw: [LAUNCHER_SCAN_HEAD, ...unlisted.raw] });
|
|
1677
|
+
pushReach();
|
|
1298
1678
|
continue;
|
|
1299
1679
|
}
|
|
1300
1680
|
const rehead = reheaded.get(si);
|
|
@@ -1303,6 +1683,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1303
1683
|
if (toks.length === 0)
|
|
1304
1684
|
continue;
|
|
1305
1685
|
foldedSegments.push(segmentTokens);
|
|
1686
|
+
pushReach();
|
|
1306
1687
|
const name = toks[0];
|
|
1307
1688
|
const guardReason = guardedVerbFormReason(name, toks, segmentTokens.raw);
|
|
1308
1689
|
if (guardReason !== undefined) {
|
|
@@ -1383,7 +1764,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1383
1764
|
shapeRefusal ??= `multiple directory changes in one command require approval for clarity ${NOT_AUTO_ALLOWED}`;
|
|
1384
1765
|
}
|
|
1385
1766
|
if (boundary !== undefined) {
|
|
1386
|
-
const walk = evaluateReadBoundary(foldedSegments, boundary);
|
|
1767
|
+
const walk = evaluateReadBoundary(foldedSegments, boundary, foldedReach);
|
|
1387
1768
|
if (shapeRefusal === undefined)
|
|
1388
1769
|
return walk;
|
|
1389
1770
|
const { reason: _walkSentence, ...structure } = walk;
|
|
@@ -1396,90 +1777,171 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1396
1777
|
}
|
|
1397
1778
|
return {};
|
|
1398
1779
|
}
|
|
1399
|
-
|
|
1780
|
+
const BASE_CANDIDATE_MAX = 256;
|
|
1781
|
+
function evaluateReadBoundary(foldedSegments, boundary, reach) {
|
|
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() };
|
|
1400
1783
|
const outside = [];
|
|
1401
1784
|
const inside = [];
|
|
1402
1785
|
let unresolvable;
|
|
1403
1786
|
const unresolved = [];
|
|
1404
1787
|
let baseLost = false;
|
|
1405
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
|
+
};
|
|
1406
1798
|
const recursive = [];
|
|
1407
|
-
let
|
|
1408
|
-
|
|
1799
|
+
let candidates = [{ base: boundary.cwd ?? boundary.roots[0], skippedCd: -1 }];
|
|
1800
|
+
let andRunStart = 0;
|
|
1801
|
+
const findingsByWords = new Map();
|
|
1802
|
+
const findingsRowFor = (wordsKey) => {
|
|
1803
|
+
const row = findingsByWords.get(wordsKey);
|
|
1804
|
+
if (row !== undefined)
|
|
1805
|
+
return row;
|
|
1806
|
+
const fresh = new Map();
|
|
1807
|
+
findingsByWords.set(wordsKey, fresh);
|
|
1808
|
+
return fresh;
|
|
1809
|
+
};
|
|
1810
|
+
const findingsFor = (row, toks, base) => {
|
|
1811
|
+
const hit = row.get(base);
|
|
1812
|
+
if (hit !== undefined)
|
|
1813
|
+
return hit;
|
|
1814
|
+
const segBoundary = base === (boundary.cwd ?? boundary.roots[0]) ? boundary : { ...boundary, cwd: base };
|
|
1815
|
+
const computed = collectSegmentBoundaryFindings(toks, segBoundary);
|
|
1816
|
+
row.set(base, computed);
|
|
1817
|
+
return computed;
|
|
1818
|
+
};
|
|
1819
|
+
for (let si = 0; si < foldedSegments.length; si++) {
|
|
1820
|
+
const toks = foldedSegments[si];
|
|
1409
1821
|
if (baseLost)
|
|
1410
1822
|
break;
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
if (
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
if (
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1823
|
+
if (si > 0 && reach?.[si]?.andJoined !== true)
|
|
1824
|
+
andRunStart = reach?.[si]?.orBroken === true ? si + 1 : si;
|
|
1825
|
+
const reachable = candidates.filter((c) => c.skippedCd < andRunStart);
|
|
1826
|
+
const row = findingsRowFor(JSON.stringify([toks.folded, toks.raw]));
|
|
1827
|
+
for (const { base: currentBase } of reachable) {
|
|
1828
|
+
for (const finding of findingsFor(row, toks, currentBase)) {
|
|
1829
|
+
if (finding.kind === "unresolvable") {
|
|
1830
|
+
unresolvable ??= finding.reason;
|
|
1831
|
+
if (!seen.unresolved.has(finding.operand)) {
|
|
1832
|
+
seen.unresolved.add(finding.operand);
|
|
1833
|
+
unresolved.push(finding.operand);
|
|
1834
|
+
}
|
|
1835
|
+
if (toks.folded[0] === "cd")
|
|
1836
|
+
baseLost = true;
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1839
|
+
const denied = boundary.denyMatch?.(finding.path);
|
|
1840
|
+
if (denied != null) {
|
|
1841
|
+
return {
|
|
1842
|
+
reason: `a command operand resolves to "${finding.path}", which matches the sensitive-path read deny list (pattern "${denied}") ${NOT_AUTO_ALLOWED}`,
|
|
1843
|
+
readDenied: true,
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
if (finding.kind === "inside") {
|
|
1847
|
+
if (!seen.inside.has(finding.path)) {
|
|
1848
|
+
seen.inside.add(finding.path);
|
|
1849
|
+
inside.push(finding.path);
|
|
1850
|
+
}
|
|
1851
|
+
recordSpelling(checkedSpellings, seen.checkedSpelled, finding);
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
if (finding.kind === "undecided") {
|
|
1855
|
+
if (!seen.undecided.has(finding.path)) {
|
|
1856
|
+
seen.undecided.add(finding.path);
|
|
1857
|
+
undecided.push(finding.path);
|
|
1858
|
+
}
|
|
1859
|
+
recordSpelling(undecidedSpellings, seen.undecidedSpelled, finding);
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
if (finding.kind === "recursive") {
|
|
1863
|
+
if (!seen.recursive.has(finding.path)) {
|
|
1864
|
+
seen.recursive.add(finding.path);
|
|
1865
|
+
recursive.push(finding.path);
|
|
1866
|
+
}
|
|
1867
|
+
continue;
|
|
1868
|
+
}
|
|
1869
|
+
if (boundary.face === "open") {
|
|
1870
|
+
if (!seen.inside.has(finding.path)) {
|
|
1871
|
+
seen.inside.add(finding.path);
|
|
1872
|
+
inside.push(finding.path);
|
|
1873
|
+
}
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
if (!seen.outside.has(finding.path)) {
|
|
1877
|
+
seen.outside.add(finding.path);
|
|
1878
|
+
outside.push(finding);
|
|
1879
|
+
}
|
|
1880
|
+
recordSpelling(outOfRootSpellings, seen.outsideSpelled, finding);
|
|
1447
1881
|
}
|
|
1448
|
-
if (!outside.some((o) => o.path === finding.path))
|
|
1449
|
-
outside.push(finding);
|
|
1450
1882
|
}
|
|
1451
1883
|
if (!baseLost && toks.folded[0] === "cd") {
|
|
1452
1884
|
const cdArgs = toks.folded.slice(1);
|
|
1453
1885
|
const cdTargetIdx = cdArgs.findIndex((t) => !t.startsWith("-") || t === "-");
|
|
1454
1886
|
const cdTarget = cdTargetIdx === -1 ? undefined : cdArgs[cdTargetIdx];
|
|
1455
1887
|
if (cdTarget !== undefined && cdTarget !== "-") {
|
|
1456
|
-
|
|
1888
|
+
const provable = reach?.[si]?.moveProvable === true;
|
|
1889
|
+
const branched = [];
|
|
1890
|
+
for (const c of candidates) {
|
|
1891
|
+
if (c.skippedCd < andRunStart)
|
|
1892
|
+
branched.push({ base: resolveOperandLexically(c.base, cdTarget, boundary.homeDir) ?? c.base, skippedCd: c.skippedCd });
|
|
1893
|
+
branched.push({ base: c.base, skippedCd: provable ? si : c.skippedCd });
|
|
1894
|
+
}
|
|
1895
|
+
const byBase = new Map();
|
|
1896
|
+
for (const c of branched) {
|
|
1897
|
+
const prior = byBase.get(c.base);
|
|
1898
|
+
if (prior === undefined || c.skippedCd < prior.skippedCd)
|
|
1899
|
+
byBase.set(c.base, c);
|
|
1900
|
+
}
|
|
1901
|
+
const next = [...byBase.values()];
|
|
1902
|
+
if (next.length > BASE_CANDIDATE_MAX) {
|
|
1903
|
+
unresolvable ??= `too many directory changes to track where "${cdTarget}" leaves the working directory ${NOT_AUTO_ALLOWED}`;
|
|
1904
|
+
if (!seen.unresolved.has(cdTarget)) {
|
|
1905
|
+
seen.unresolved.add(cdTarget);
|
|
1906
|
+
unresolved.push(cdTarget);
|
|
1907
|
+
}
|
|
1908
|
+
baseLost = true;
|
|
1909
|
+
}
|
|
1910
|
+
else
|
|
1911
|
+
candidates = next;
|
|
1457
1912
|
}
|
|
1458
1913
|
}
|
|
1459
1914
|
}
|
|
1460
|
-
const undecidedAll = [...undecided, ...recursive.filter((p) => !undecided.
|
|
1915
|
+
const undecidedAll = [...undecided, ...recursive.filter((p) => !seen.undecided.has(p))];
|
|
1461
1916
|
const undecidedField = {
|
|
1462
1917
|
...(undecidedAll.length > 0 ? { undecidedPaths: undecidedAll } : {}),
|
|
1463
1918
|
...(recursive.length > 0 ? { recursiveReadPaths: recursive } : {}),
|
|
1919
|
+
...(undecidedSpellings.length > 0 ? { undecidedSpellings } : {}),
|
|
1464
1920
|
};
|
|
1921
|
+
const checkedField = {
|
|
1922
|
+
...(inside.length > 0 ? { checkedPaths: inside } : {}),
|
|
1923
|
+
...(checkedSpellings.length > 0 ? { checkedSpellings } : {}),
|
|
1924
|
+
};
|
|
1925
|
+
const outsideField = outOfRootSpellings.length > 0 ? { outOfRootSpellings } : {};
|
|
1465
1926
|
if (unresolvable !== undefined) {
|
|
1466
1927
|
return {
|
|
1467
1928
|
reason: unresolvable,
|
|
1468
1929
|
unresolvedOperands: unresolved,
|
|
1469
|
-
...(outside.length > 0 ? { outOfRootRead: true, outOfRootPaths: outside.map((o) => o.path) } : {}),
|
|
1470
|
-
...
|
|
1930
|
+
...(outside.length > 0 ? { outOfRootRead: true, outOfRootPaths: outside.map((o) => o.path), ...outsideField } : {}),
|
|
1931
|
+
...checkedField,
|
|
1471
1932
|
...undecidedField,
|
|
1472
1933
|
};
|
|
1473
1934
|
}
|
|
1474
1935
|
if (outside.length === 0)
|
|
1475
|
-
return
|
|
1936
|
+
return { ...checkedField, ...undecidedField };
|
|
1476
1937
|
const paths = outside.map((o) => `"${o.path}"`).join(", ");
|
|
1477
1938
|
const allowed = boundary.roots.length > 0 ? boundary.roots.join(", ") : "(none)";
|
|
1478
1939
|
return {
|
|
1479
1940
|
reason: `"${outside[0].command}" reads ${paths}, outside the allowed directories for this session: ${allowed} ${NOT_AUTO_ALLOWED}`,
|
|
1480
1941
|
outOfRootRead: true,
|
|
1481
1942
|
outOfRootPaths: outside.map((o) => o.path),
|
|
1482
|
-
...
|
|
1943
|
+
...outsideField,
|
|
1944
|
+
...checkedField,
|
|
1483
1945
|
...undecidedField,
|
|
1484
1946
|
};
|
|
1485
1947
|
}
|
|
@@ -1487,6 +1949,19 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
|
|
|
1487
1949
|
const toks = tokenizeSegment(command);
|
|
1488
1950
|
if (toks.folded.length === 0)
|
|
1489
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
|
+
}
|
|
1490
1965
|
return evaluateReadBoundary([toks], boundary);
|
|
1491
1966
|
}
|
|
1492
1967
|
const NOT_GATED = { gated: false, paths: [] };
|
|
@@ -1504,7 +1979,7 @@ const COMMAND_LAUNCHERS = new Set([
|
|
|
1504
1979
|
"env", "command", "builtin", "exec", "noglob", "xargs",
|
|
1505
1980
|
"sudo", "doas", "pkexec", "su", "runuser", "chroot", "setpriv",
|
|
1506
1981
|
"nohup", "nice", "ionice", "chrt", "taskset", "stdbuf", "timeout", "time", "watch",
|
|
1507
|
-
"setsid", "flock", "unshare", "nsenter", "script", "numactl", "prlimit", "systemd-run",
|
|
1982
|
+
"setsid", "flock", "unshare", "nsenter", "script", "numactl", "prlimit", "systemd-run",
|
|
1508
1983
|
"strace", "ltrace", "valgrind", "firejail", "bwrap",
|
|
1509
1984
|
]);
|
|
1510
1985
|
export const COMMAND_LAUNCHERS_FOR_TEST = COMMAND_LAUNCHERS;
|
|
@@ -1524,8 +1999,25 @@ export function classifyOutOfRootReadGate(command, allow, boundary) {
|
|
|
1524
1999
|
if ("reject" in split)
|
|
1525
2000
|
return NOT_GATED;
|
|
1526
2001
|
const readers = [];
|
|
2002
|
+
const readerReach = [];
|
|
2003
|
+
let crossedAllAnd = true;
|
|
2004
|
+
let crossedAnyOr = false;
|
|
1527
2005
|
let baseMoved = false;
|
|
1528
|
-
for (
|
|
2006
|
+
for (let si = 0; si < split.segments.length; si++) {
|
|
2007
|
+
const segment = split.segments[si];
|
|
2008
|
+
if (si > 0) {
|
|
2009
|
+
if (split.connectors[si] !== "&&")
|
|
2010
|
+
crossedAllAnd = false;
|
|
2011
|
+
if (split.connectors[si] === "||")
|
|
2012
|
+
crossedAnyOr = true;
|
|
2013
|
+
}
|
|
2014
|
+
const moveProvable = split.pipeFed[si] !== true && split.pipeFed[si + 1] !== true;
|
|
2015
|
+
const pushReader = (st) => {
|
|
2016
|
+
readers.push(st);
|
|
2017
|
+
readerReach.push({ andJoined: si > 0 && crossedAllAnd, orBroken: !crossedAllAnd && crossedAnyOr, moveProvable });
|
|
2018
|
+
crossedAllAnd = true;
|
|
2019
|
+
crossedAnyOr = false;
|
|
2020
|
+
};
|
|
1529
2021
|
const toks = tokenizeSegment(segment);
|
|
1530
2022
|
if (toks.folded.length === 0)
|
|
1531
2023
|
continue;
|
|
@@ -1539,16 +2031,16 @@ export function classifyOutOfRootReadGate(command, allow, boundary) {
|
|
|
1539
2031
|
if (UNMODELLED_BASE_MOVERS.has(name))
|
|
1540
2032
|
baseMoved = true;
|
|
1541
2033
|
if (!allow.has(name) && name !== "cd") {
|
|
1542
|
-
|
|
2034
|
+
pushReader({ folded: [LAUNCHER_SCAN_HEAD, ...toks.folded.slice(i)], raw: [LAUNCHER_SCAN_HEAD, ...toks.raw.slice(i)] });
|
|
1543
2035
|
continue;
|
|
1544
2036
|
}
|
|
1545
|
-
|
|
2037
|
+
pushReader(i === 0 && name === written ? toks : { folded: [name, ...toks.folded.slice(i + 1)], raw: [toks.raw[i] ?? name, ...toks.raw.slice(i + 1)] });
|
|
1546
2038
|
}
|
|
1547
2039
|
if (readers.length === 0)
|
|
1548
2040
|
return NOT_GATED;
|
|
1549
2041
|
if (baseMoved)
|
|
1550
2042
|
return { gated: true, paths: [] };
|
|
1551
|
-
const verdict = evaluateReadBoundary(readers, containmentOnly);
|
|
2043
|
+
const verdict = evaluateReadBoundary(readers, containmentOnly, readerReach);
|
|
1552
2044
|
return verdict.outOfRootRead === true ? { gated: true, paths: verdict.outOfRootPaths ?? [] } : NOT_GATED;
|
|
1553
2045
|
};
|
|
1554
2046
|
const direct = judge(command);
|