@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.
@@ -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 normalizeAbsPathLexically(nativeUncSpellingOf(family, raw));
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 normalizeAbsPathLexically(joinForFamily(family, base, raw));
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 = fold(toks[i]);
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
- if (COMMAND_LAUNCHERS.has(head)) {
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 = unwrapLauncher(toks.slice(i).map(fold), allow);
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 resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], args[k], boundary.homeDir);
1134
- findings.push({ kind: "undecided", path: resolved ?? args[k] });
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 resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], candidate, boundary.homeDir);
1138
- if (resolved === undefined) {
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 resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], operand, boundary.homeDir);
1155
- if (resolved === undefined)
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, resolved))
1158
- findings.push({ kind: "inside", path: resolved });
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: `command "${parsed.name}" runs its argument as shell text, which this walk does not judge ${NOT_AUTO_ALLOWED}`, refusedWhole: true };
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
- ...(inside.length > 0 ? { checkedPaths: inside } : {}),
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 inside.length > 0 ? { checkedPaths: inside, ...undecidedField } : { ...undecidedField };
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
- ...(inside.length > 0 ? { checkedPaths: inside } : {}),
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",