@node9/proxy 2.14.0 → 2.14.2

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.
@@ -848,6 +848,19 @@ function parseShared(command) {
848
848
  astCache.set(command, parsed);
849
849
  return parsed;
850
850
  }
851
+ function byteOffsetToCharIndex(command) {
852
+ if (!/[^\u0000-\u007F]/.test(command)) return null;
853
+ const map = /* @__PURE__ */ new Map();
854
+ let byte = 0;
855
+ for (let i = 0; i < command.length; ) {
856
+ map.set(byte, i);
857
+ const cp = command.codePointAt(i);
858
+ byte += cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
859
+ i += cp > 65535 ? 2 : 1;
860
+ }
861
+ map.set(byte, command.length);
862
+ return (b) => map.get(b) ?? -1;
863
+ }
851
864
  function cachedNormalize(command, compute) {
852
865
  const hit = normalizeCache.get(command);
853
866
  if (hit !== void 0) {
@@ -877,6 +890,8 @@ function normalizeCommandForPolicyImpl(command) {
877
890
  const f = parseShared(command);
878
891
  if (f === PARSE_FAIL) return { posix: command, separator: command };
879
892
  try {
893
+ const toCharIndex = byteOffsetToCharIndex(command);
894
+ const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
880
895
  const strips = [];
881
896
  const rewrites = [];
882
897
  const quoteOnlyRewrites = [];
@@ -897,8 +912,9 @@ function normalizeCommandForPolicyImpl(command) {
897
912
  const quotedNode = nextParts[0];
898
913
  const nt = syntax.NodeType(quotedNode);
899
914
  const markStrip = () => {
900
- const s = next.Pos().Offset();
901
- const e = next.End().Offset();
915
+ const s = at(next.Pos().Offset());
916
+ const e = at(next.End().Offset());
917
+ if (s < 0 || e < 0) return;
902
918
  strips.push([s, e]);
903
919
  msgSpans.add(`${s}:${e}`);
904
920
  };
@@ -915,8 +931,9 @@ function normalizeCommandForPolicyImpl(command) {
915
931
  }
916
932
  }
917
933
  for (const arg of args) {
918
- const s = arg.Pos().Offset();
919
- const e = arg.End().Offset();
934
+ const s = at(arg.Pos().Offset());
935
+ const e = at(arg.End().Offset());
936
+ if (s < 0 || e < 0) continue;
920
937
  if (msgSpans.has(`${s}:${e}`)) continue;
921
938
  const resolved = resolveWordLiteral(arg);
922
939
  if (resolved === null) continue;
@@ -1178,7 +1195,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1178
1195
  return result?.verdict !== "block";
1179
1196
  }
1180
1197
  if (nodeType !== "CallExpr") return true;
1181
- const { name, flags, paths, words } = extractLiteralArgs(n);
1198
+ const { name, flags, paths, words, args } = extractLiteralArgs(n);
1182
1199
  if (!name) return true;
1183
1200
  if (name === "rm") {
1184
1201
  const flagStr = flags.join("").toLowerCase();
@@ -1218,7 +1235,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1218
1235
  return true;
1219
1236
  }
1220
1237
  }
1221
- const readPaths = FS_READ_TOOLS.has(name) ? paths : wrappedReadPaths(words, name);
1238
+ const readPaths = FS_READ_TOOLS.has(name) ? [...readTargets(name, args, flags, words, 1), ...flagOperandFiles(name, words, 1)] : wrappedReadPaths(words, name);
1222
1239
  if (readPaths) {
1223
1240
  for (const p of readPaths) {
1224
1241
  result = stricter(result, matchSensitivePath2(p));
@@ -1254,9 +1271,22 @@ function flagIs(w, names) {
1254
1271
  const f = flagInfo(w);
1255
1272
  return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
1256
1273
  }
1257
- function operandOf(a, names) {
1274
+ function operandOf(a, names, valueLetters) {
1258
1275
  if (!names || a.afterFlag === null) return false;
1259
- return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
1276
+ const w = a.afterFlag;
1277
+ if (!w.startsWith("--") && valueLetters) {
1278
+ const letters = w.slice(1);
1279
+ for (let i = 0; i < letters.length; i++) {
1280
+ if (!valueLetters.includes(letters[i])) continue;
1281
+ return i === letters.length - 1 && names.includes(letters[i]);
1282
+ }
1283
+ return false;
1284
+ }
1285
+ if (w.startsWith("--") && !w.includes("=")) {
1286
+ const longs = names.filter((n) => n.startsWith("--"));
1287
+ if (longs.some((n) => n === w || w.length >= 3 && n.startsWith(w))) return true;
1288
+ }
1289
+ return flagIs(w, names) && flagInfo(w).attached === null;
1260
1290
  }
1261
1291
  function resolveCopyShape(words, h) {
1262
1292
  const verb = baseWord(words[h]);
@@ -1280,7 +1310,7 @@ function findStartPoints(words, h) {
1280
1310
  const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
1281
1311
  if (k < 0) return { k, starts: [] };
1282
1312
  const firstPredicate = words.findIndex(
1283
- (w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
1313
+ (w, i) => i > h && w !== null && w !== "--" && w.startsWith("-") && !FIND_OPTIONS.has(w)
1284
1314
  );
1285
1315
  const end = firstPredicate > h ? firstPredicate : k;
1286
1316
  return { k, starts: positionalAfter(words, h + 1, end) };
@@ -1300,14 +1330,35 @@ function copySourcePaths(words) {
1300
1330
  const { shape, last } = r;
1301
1331
  const args = positionedArgs(words, last + 1);
1302
1332
  const tail = words.slice(last + 1);
1303
- const skipped = (a) => operandOf(a, shape.skipFlags);
1304
- const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && flagIs(w, ["t", "--target-directory"]));
1305
- const targetOperand = (a) => targetDir && operandOf(a, ["t", "--target-directory"]);
1333
+ const copyOptionsEnd = tail.findIndex((w) => w === "--");
1334
+ const copyPastOptions = (a) => copyOptionsEnd >= 0 && a.argv > last + 1 + copyOptionsEnd;
1335
+ const skipped = (a) => !copyPastOptions(a) && operandOf(a, shape.skipFlags, shape.valueLetters);
1336
+ const firstValueLetter = (w) => {
1337
+ const letters = w.slice(1);
1338
+ for (let i = 0; i < letters.length; i++) {
1339
+ if ((shape.valueLetters ?? []).includes(letters[i]))
1340
+ return { letter: letters[i], last: i === letters.length - 1 };
1341
+ }
1342
+ return null;
1343
+ };
1344
+ const isTargetDirFlag = (w) => {
1345
+ if (w.startsWith("--")) {
1346
+ const name = w.includes("=") ? w.slice(0, w.indexOf("=")) : w;
1347
+ return name.length >= 3 && "--target-directory".startsWith(name);
1348
+ }
1349
+ return firstValueLetter(w)?.letter === "t";
1350
+ };
1351
+ const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && isTargetDirFlag(w));
1352
+ const targetTakesNextWord = (w) => {
1353
+ if (w.startsWith("--"))
1354
+ return !w.includes("=") && w.length >= 3 && "--target-directory".startsWith(w);
1355
+ const f = firstValueLetter(w);
1356
+ return f !== null && f.letter === "t" && f.last;
1357
+ };
1358
+ const targetOperand = (a) => targetDir && a.afterFlag !== null && !copyPastOptions(a) && targetTakesNextWord(a.afterFlag);
1306
1359
  const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
1307
1360
  const dynamicDest = lastOperand === null;
1308
1361
  const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
1309
- if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
1310
- return [];
1311
1362
  let src;
1312
1363
  switch (shape.source) {
1313
1364
  case "all":
@@ -1317,11 +1368,17 @@ function copySourcePaths(words) {
1317
1368
  src = targetDir ? args : args.slice(0, 1);
1318
1369
  break;
1319
1370
  case "flagOperand": {
1320
- const inline = tail.filter((w) => w !== null && w.startsWith("--")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && (shape.sourceFlags ?? []).includes(f.long ?? "")).map((f) => f.attached);
1371
+ const namesSource = (f) => {
1372
+ const names = shape.sourceFlags ?? [];
1373
+ if (f.long !== null)
1374
+ return names.some(
1375
+ (n) => n.startsWith("--") && (n === f.long || f.long.length >= 3 && n.startsWith(f.long))
1376
+ );
1377
+ return f.letter !== null && names.includes(f.letter);
1378
+ };
1379
+ const inline = tail.filter((w) => w !== null && w.startsWith("-")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && namesSource(f)).map((f) => f.attached);
1321
1380
  return [
1322
- ...args.filter(
1323
- (a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
1324
- ).map((a) => a.value),
1381
+ ...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => a.value),
1325
1382
  ...inline
1326
1383
  ];
1327
1384
  }
@@ -1332,7 +1389,14 @@ function copySourcePaths(words) {
1332
1389
  src = targetDir || dynamicDest ? args : args.slice(0, -1);
1333
1390
  break;
1334
1391
  }
1335
- return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
1392
+ const sources = src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
1393
+ if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand)) {
1394
+ const jailedSources = sources.filter((p) => matchSensitivePath2(p));
1395
+ const dirOf = (p) => /[\\/]/.test(p) ? p.replace(/[\\/][^\\/]*$/, "") : "";
1396
+ if (jailedSources.length === 0) return [];
1397
+ if (jailedSources.every((p) => dirOf(p) === dirOf(lastOperand))) return [];
1398
+ }
1399
+ return sources;
1336
1400
  }
1337
1401
  function archiveInputs(kind, args, tail) {
1338
1402
  const first = args[0];
@@ -1344,13 +1408,17 @@ function archiveInputs(kind, args, tail) {
1344
1408
  const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
1345
1409
  if (extracting && !writing) return [];
1346
1410
  void mode;
1347
- let i = 0;
1348
- if (bareKey) {
1349
- i = 1;
1350
- const next = args[1];
1351
- if (first.value.includes("f") && next && next.afterFlag === null) i = 2;
1352
- }
1353
- return args.slice(i);
1411
+ if (!bareKey) return args;
1412
+ let i = 1;
1413
+ const fromDirs = [];
1414
+ for (const ch of first.value) {
1415
+ if (!TAR_VALUE_LETTERS.includes(ch)) continue;
1416
+ const operand = args[i];
1417
+ if (!operand || operand.afterFlag !== null) break;
1418
+ i += 1;
1419
+ if (ch === "C") fromDirs.push(operand);
1420
+ }
1421
+ return [...fromDirs, ...args.slice(i)];
1354
1422
  }
1355
1423
  if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
1356
1424
  if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
@@ -1377,6 +1445,138 @@ function matchSensitivePath2(p) {
1377
1445
  function baseWord(w) {
1378
1446
  return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
1379
1447
  }
1448
+ function namesFlag(name, candidates, known) {
1449
+ if (candidates.has(name)) return true;
1450
+ if (!name.startsWith("--") || name.length < 3) return false;
1451
+ if (known?.has(name)) return false;
1452
+ for (const c of candidates) if (c.startsWith(name)) return true;
1453
+ return false;
1454
+ }
1455
+ function knownLongFlags(verb) {
1456
+ const out = /* @__PURE__ */ new Set();
1457
+ const shape = PATTERN_VERBS[verb];
1458
+ if (shape) {
1459
+ for (const set of [shape.takesValue, shape.noValue, shape.patternFlags, shape.noPatternFlags])
1460
+ for (const f of set) if (f.startsWith("--")) out.add(f);
1461
+ }
1462
+ for (const f of FILE_OPERAND_FLAGS[verb] ?? []) if (f.startsWith("--")) out.add(f);
1463
+ return out;
1464
+ }
1465
+ function flagNamesOf(token, shape) {
1466
+ if (token.startsWith("--")) {
1467
+ const eq = token.indexOf("=");
1468
+ return [eq > 0 ? token.slice(0, eq) : token];
1469
+ }
1470
+ const out = [];
1471
+ for (const c of token.slice(1)) {
1472
+ const name = `-${c}`;
1473
+ out.push(name);
1474
+ if (shape?.takesValue.has(name)) break;
1475
+ }
1476
+ return out;
1477
+ }
1478
+ function flagEffect(token, shape, known) {
1479
+ if (token === "--") return NONE;
1480
+ if (/^-+$/.test(token)) return UNKNOWN;
1481
+ if (/^-\d+$/.test(token)) return NONE;
1482
+ if (token.startsWith("--")) {
1483
+ if (token.includes("=")) return NONE;
1484
+ const takes = namesFlag(token, shape.takesValue, known);
1485
+ const none = namesFlag(token, shape.noValue, known);
1486
+ if (takes && none) return UNKNOWN;
1487
+ if (takes) return { kind: "takes", flag: token };
1488
+ if (none) return NONE;
1489
+ return UNKNOWN;
1490
+ }
1491
+ const letters = token.slice(1);
1492
+ if (!letters) return NONE;
1493
+ for (let i = 0; i < letters.length; i++) {
1494
+ const name = `-${letters[i]}`;
1495
+ if (shape.takesValue.has(name)) {
1496
+ return i === letters.length - 1 ? { kind: "takes", flag: name } : NONE;
1497
+ }
1498
+ if (!shape.noValue.has(name)) return UNKNOWN;
1499
+ }
1500
+ return NONE;
1501
+ }
1502
+ function readTargets(verb, args, flags, words = [], from = 1) {
1503
+ const shape = PATTERN_VERBS[verb];
1504
+ if (!shape) return args.map((a) => a.value);
1505
+ const known = knownLongFlags(verb);
1506
+ const names = flags.flatMap((f) => flagNamesOf(f, shape));
1507
+ const patternElsewhere = names.some(
1508
+ (n) => namesFlag(n, shape.patternFlags, known) || namesFlag(n, shape.noPatternFlags, known)
1509
+ );
1510
+ const excused = /* @__PURE__ */ new Set();
1511
+ const fileFlags = FILE_OPERAND_FLAGS[verb];
1512
+ const optionsEnd = words.findIndex((w, i) => i >= from && w === "--");
1513
+ const pastOptions = (a) => optionsEnd >= 0 && a.argv > optionsEnd;
1514
+ for (const a of args) {
1515
+ if (a.afterFlag === null || pastOptions(a)) continue;
1516
+ const e = flagEffect(a.afterFlag, shape, known);
1517
+ if (e.kind !== "takes") continue;
1518
+ if (fileFlags && namesFlag(e.flag, fileFlags, known)) continue;
1519
+ excused.add(a);
1520
+ }
1521
+ const patternArgv = (() => {
1522
+ for (let i = from; i < words.length; i++) {
1523
+ const w = words[i];
1524
+ if (w === null) return -1;
1525
+ if (w === "--") return i + 1;
1526
+ if (w.startsWith("-")) {
1527
+ const e = flagEffect(w, shape, known);
1528
+ if (e.kind === "takes") i += 1;
1529
+ else if (e.kind === "unknown") return -1;
1530
+ continue;
1531
+ }
1532
+ return i;
1533
+ }
1534
+ return -1;
1535
+ })();
1536
+ if (!patternElsewhere && patternArgv >= 0) {
1537
+ const a = args.find((x) => x.argv === patternArgv);
1538
+ if (a) excused.add(a);
1539
+ }
1540
+ return args.filter((a) => !excused.has(a)).map((a) => a.value);
1541
+ }
1542
+ function flagOperandFiles(verb, words, from) {
1543
+ const flags = FILE_OPERAND_FLAGS[verb];
1544
+ if (!flags) return [];
1545
+ const known = knownLongFlags(verb);
1546
+ const out = [];
1547
+ for (let i = from; i < words.length; i++) {
1548
+ const w = words[i];
1549
+ if (w === null || !w.startsWith("-") || w === "--") continue;
1550
+ if (w.startsWith("--")) {
1551
+ const eq = w.indexOf("=");
1552
+ if (eq <= 0) continue;
1553
+ const name = w.slice(0, eq);
1554
+ const value = w.slice(eq + 1);
1555
+ if (!value) continue;
1556
+ if (namesFlag(name, flags, known)) {
1557
+ out.push(value);
1558
+ continue;
1559
+ }
1560
+ const shape2 = PATTERN_VERBS[verb];
1561
+ if (!shape2) continue;
1562
+ const recognised = namesFlag(name, shape2.takesValue, known) || namesFlag(name, shape2.noValue, known) || namesFlag(name, shape2.patternFlags, known) || namesFlag(name, shape2.noPatternFlags, known);
1563
+ if (!recognised) out.push(value);
1564
+ continue;
1565
+ }
1566
+ const shape = PATTERN_VERBS[verb];
1567
+ const letters = w.slice(1);
1568
+ for (let j = 0; j < letters.length; j++) {
1569
+ const name = `-${letters[j]}`;
1570
+ const isFileFlag = flags.has(name);
1571
+ const argTaking = isFileFlag || (shape?.takesValue.has(name) ?? false) || (READER_VALUE_LETTERS[verb] ?? []).includes(letters[j]);
1572
+ if (!argTaking) continue;
1573
+ const attached = letters.slice(j + 1);
1574
+ if (isFileFlag && attached) out.push(attached);
1575
+ break;
1576
+ }
1577
+ }
1578
+ return out;
1579
+ }
1380
1580
  function wrappedReadPaths(words, name) {
1381
1581
  if (name === "find") {
1382
1582
  const { k, starts } = findStartPoints(words, 0);
@@ -1384,7 +1584,14 @@ function wrappedReadPaths(words, name) {
1384
1584
  }
1385
1585
  if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
1386
1586
  const h = unwrapCommandHead(words);
1387
- return h > 0 && isReaderWord(words[h] ?? null) ? positionalAfter(words, h + 1) : null;
1587
+ if (h <= 0 || !isReaderWord(words[h] ?? null)) return null;
1588
+ const head = baseWord(words[h]);
1589
+ const rest = words.slice(h + 1);
1590
+ const restFlags = rest.filter((w) => w !== null && w.startsWith("-"));
1591
+ return [
1592
+ ...readTargets(head, positionedArgs(words, h + 1), restFlags, words, h + 1),
1593
+ ...flagOperandFiles(head, words, h + 1)
1594
+ ];
1388
1595
  }
1389
1596
  function literalShellPayload(words, name) {
1390
1597
  const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
@@ -1568,6 +1775,9 @@ function classifySsrf(host) {
1568
1775
  return null;
1569
1776
  }
1570
1777
  }
1778
+ function stripTerminalEscapes(s) {
1779
+ return s.replace(TERMINAL_ESCAPE_RE, "");
1780
+ }
1571
1781
  function isShieldVerdict(v) {
1572
1782
  return v === "allow" || v === "review" || v === "block";
1573
1783
  }
@@ -1729,7 +1939,7 @@ function matchCanaryArgs(args, values) {
1729
1939
  return null;
1730
1940
  }
1731
1941
  }
1732
- var B58, B58_INDEX, XPRV_VERSIONS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, SCP_VALUE_FLAGS, RSYNC_SKIP, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, SOURCE_COMMANDS, SSRF_MAX_HOST, METADATA_ADDRESSES, METADATA_HOSTNAMES, v4Octets, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES;
1942
+ var B58, B58_INDEX, XPRV_VERSIONS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, SOURCE_COMMANDS, SSRF_MAX_HOST, METADATA_ADDRESSES, METADATA_HOSTNAMES, v4Octets, TERMINAL_ESCAPE_RE, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES;
1733
1943
  var init_dist = __esm({
1734
1944
  "packages/policy-engine/dist/index.mjs"() {
1735
1945
  "use strict";
@@ -2330,7 +2540,345 @@ var init_dist = __esm({
2330
2540
  "nl",
2331
2541
  "dd"
2332
2542
  ]);
2543
+ GREP_SHAPE = {
2544
+ takesValue: /* @__PURE__ */ new Set([
2545
+ "-A",
2546
+ "-B",
2547
+ "-C",
2548
+ "-D",
2549
+ "-d",
2550
+ "-e",
2551
+ "-f",
2552
+ "-m",
2553
+ "--after-context",
2554
+ "--before-context",
2555
+ "--binary-files",
2556
+ "--context",
2557
+ "--devices",
2558
+ "--directories",
2559
+ "--exclude",
2560
+ "--exclude-dir",
2561
+ "--exclude-from",
2562
+ "--file",
2563
+ "--include",
2564
+ "--label",
2565
+ "--max-count",
2566
+ "--regexp"
2567
+ ]),
2568
+ noValue: /* @__PURE__ */ new Set([
2569
+ "-E",
2570
+ "-F",
2571
+ "-G",
2572
+ "-P",
2573
+ "-i",
2574
+ "-y",
2575
+ "-v",
2576
+ "-V",
2577
+ "-w",
2578
+ "-x",
2579
+ "-c",
2580
+ "-l",
2581
+ "-L",
2582
+ "-o",
2583
+ "-q",
2584
+ "-s",
2585
+ "-b",
2586
+ "-H",
2587
+ "-h",
2588
+ "-n",
2589
+ "-T",
2590
+ "-Z",
2591
+ "-z",
2592
+ "-R",
2593
+ "-r",
2594
+ "-U",
2595
+ "-u",
2596
+ "-I",
2597
+ "-a",
2598
+ "--basic-regexp",
2599
+ "--binary",
2600
+ "--byte-offset",
2601
+ "--color",
2602
+ "--colour",
2603
+ "--count",
2604
+ "--dereference-recursive",
2605
+ "--extended-regexp",
2606
+ "--files-with-matches",
2607
+ "--files-without-match",
2608
+ "--fixed-strings",
2609
+ "--help",
2610
+ "--ignore-case",
2611
+ "--initial-tab",
2612
+ "--invert-match",
2613
+ "--line-buffered",
2614
+ "--line-number",
2615
+ "--line-regexp",
2616
+ "--no-filename",
2617
+ "--no-group-separator",
2618
+ "--no-ignore-case",
2619
+ "--no-messages",
2620
+ "--null",
2621
+ "--null-data",
2622
+ "--only-matching",
2623
+ "--perl-regexp",
2624
+ "--quiet",
2625
+ "--recursive",
2626
+ "--silent",
2627
+ "--text",
2628
+ "--version",
2629
+ "--with-filename",
2630
+ "--word-regexp"
2631
+ ]),
2632
+ patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
2633
+ noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file"])
2634
+ };
2635
+ PATTERN_VERBS = {
2636
+ grep: GREP_SHAPE,
2637
+ // /usr/bin/egrep and /usr/bin/fgrep are 41-byte shell wrappers that exec
2638
+ // `grep -E` and `grep -F`. Read in full, not assumed.
2639
+ egrep: GREP_SHAPE,
2640
+ fgrep: GREP_SHAPE,
2641
+ // ripgrep 14.1.1, complete from its own --help. An earlier comment here said rg
2642
+ // was not installed on the measuring machine; it is, and that stale claim is why
2643
+ // this table shipped incomplete for two rounds, blocking ordinary searches like
2644
+ // `rg --sort-files .env src` (/code-review round 4).
2645
+ rg: {
2646
+ takesValue: /* @__PURE__ */ new Set([
2647
+ "-A",
2648
+ "-B",
2649
+ "-C",
2650
+ "-d",
2651
+ "-E",
2652
+ "-e",
2653
+ "-f",
2654
+ "-g",
2655
+ "-j",
2656
+ "-M",
2657
+ "-m",
2658
+ "-r",
2659
+ "-t",
2660
+ "-T",
2661
+ "--after-context",
2662
+ "--before-context",
2663
+ "--color",
2664
+ "--colors",
2665
+ "--context",
2666
+ "--context-separator",
2667
+ "--dfa-size-limit",
2668
+ "--encoding",
2669
+ "--engine",
2670
+ "--field-context-separator",
2671
+ "--field-match-separator",
2672
+ "--file",
2673
+ "--generate",
2674
+ "--glob",
2675
+ "--hostname-bin",
2676
+ "--hyperlink-format",
2677
+ "--iglob",
2678
+ "--ignore-file",
2679
+ "--max-columns",
2680
+ "--max-count",
2681
+ "--max-depth",
2682
+ // An ALIAS of --max-depth, and it takes a value. The round-5 extraction read
2683
+ // alias lines as plain switches and swept it into noValue, which hard-blocked
2684
+ // `rg --maxdepth 2 .env src` while `--max-depth 2` ran (/code-review round 7).
2685
+ "--maxdepth",
2686
+ "--max-filesize",
2687
+ "--path-separator",
2688
+ "--pre",
2689
+ "--pre-glob",
2690
+ "--regexp",
2691
+ "--regex-size-limit",
2692
+ "--replace",
2693
+ "--sort",
2694
+ "--sortr",
2695
+ "--threads",
2696
+ "--type",
2697
+ "--type-add",
2698
+ "--type-clear",
2699
+ "--type-not"
2700
+ ]),
2701
+ noValue: /* @__PURE__ */ new Set([
2702
+ "-.",
2703
+ "-0",
2704
+ "-a",
2705
+ "-b",
2706
+ "-c",
2707
+ "-F",
2708
+ "-h",
2709
+ "-H",
2710
+ "-i",
2711
+ "-I",
2712
+ "-l",
2713
+ "-L",
2714
+ "-n",
2715
+ "-N",
2716
+ "-o",
2717
+ "-p",
2718
+ "-P",
2719
+ "-q",
2720
+ "-s",
2721
+ "-S",
2722
+ "-u",
2723
+ "-U",
2724
+ "-v",
2725
+ "-V",
2726
+ "-w",
2727
+ "-x",
2728
+ "-z",
2729
+ "--auto-hybrid-regex",
2730
+ "--binary",
2731
+ "--block-buffered",
2732
+ "--byte-offset",
2733
+ "--case-sensitive",
2734
+ "--column",
2735
+ "--count",
2736
+ "--count-matches",
2737
+ "--crlf",
2738
+ "--debug",
2739
+ "--files",
2740
+ "--files-with-matches",
2741
+ "--files-without-match",
2742
+ "--fixed-strings",
2743
+ "--follow",
2744
+ "--glob-case-insensitive",
2745
+ "--heading",
2746
+ "--help",
2747
+ "--hidden",
2748
+ "--ignore-case",
2749
+ "--ignore-file-case-insensitive",
2750
+ "--include-zero",
2751
+ "--invert-match",
2752
+ "--json",
2753
+ "--line-buffered",
2754
+ "--line-number",
2755
+ "--line-regexp",
2756
+ "--max-columns-preview",
2757
+ "--mmap",
2758
+ "--multiline",
2759
+ "--multiline-dotall",
2760
+ "--no-column",
2761
+ "--no-config",
2762
+ "--no-context-separator",
2763
+ "--no-encoding",
2764
+ "--no-filename",
2765
+ "--no-ignore",
2766
+ "--no-ignore-dot",
2767
+ "--no-ignore-exclude",
2768
+ "--no-ignore-files",
2769
+ "--no-ignore-global",
2770
+ "--no-ignore-messages",
2771
+ "--no-ignore-parent",
2772
+ "--no-ignore-vcs",
2773
+ "--no-line-number",
2774
+ "--no-messages",
2775
+ "--no-pcre2-unicode",
2776
+ "--no-pre",
2777
+ "--no-require-git",
2778
+ "--no-unicode",
2779
+ "--null",
2780
+ "--null-data",
2781
+ "--one-file-system",
2782
+ "--only-matching",
2783
+ "--passthru",
2784
+ "--pcre2",
2785
+ "--pcre2-version",
2786
+ "--pretty",
2787
+ "--print0",
2788
+ "--quiet",
2789
+ "--search-zip",
2790
+ "--smart-case",
2791
+ "--sort-files",
2792
+ "--stats",
2793
+ "--stop-on-nonmatch",
2794
+ "--text",
2795
+ "--trace",
2796
+ "--trim",
2797
+ "--type-list",
2798
+ "--unrestricted",
2799
+ "--version",
2800
+ "--vimgrep",
2801
+ "--with-filename",
2802
+ "--word-regexp",
2803
+ // The COMPLETE remainder of `rg --help`, added after /code-review round 5
2804
+ // found 40 documented switches in neither set. The table is now the whole
2805
+ // option list: `rg --help` yields 149 long options, 35 of them value-taking.
2806
+ "--ignore",
2807
+ "--ignore-dot",
2808
+ "--ignore-exclude",
2809
+ "--ignore-files",
2810
+ "--ignore-global",
2811
+ "--ignore-messages",
2812
+ "--ignore-parent",
2813
+ "--ignore-vcs",
2814
+ "--messages",
2815
+ "--no-auto-hybrid-regex",
2816
+ "--no-binary",
2817
+ "--no-block-buffered",
2818
+ "--no-byte-offset",
2819
+ "--no-crlf",
2820
+ "--no-fixed-strings",
2821
+ "--no-follow",
2822
+ "--no-glob-case-insensitive",
2823
+ "--no-heading",
2824
+ "--no-hidden",
2825
+ "--no-ignore-file-case-insensitive",
2826
+ "--no-include-zero",
2827
+ "--no-invert-match",
2828
+ "--no-json",
2829
+ "--no-line-buffered",
2830
+ "--no-max-columns-preview",
2831
+ "--no-mmap",
2832
+ "--no-multiline",
2833
+ "--no-multiline-dotall",
2834
+ "--no-one-file-system",
2835
+ "--no-pcre2",
2836
+ "--no-search-zip",
2837
+ "--no-sort-files",
2838
+ "--no-stats",
2839
+ "--no-text",
2840
+ "--no-trim",
2841
+ "--passthrough",
2842
+ "--pcre2-unicode",
2843
+ "--require-git",
2844
+ "--unicode"
2845
+ ]),
2846
+ patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
2847
+ // `--files` and `--type-list` list or enumerate without a pattern. Founder
2848
+ // decision 2026-09-13: `rg --files ~/.ssh` stays BLOCKED, which this achieves
2849
+ // by leaving the directory in the judged list.
2850
+ noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file", "--files", "--type-list", "--pcre2-version"])
2851
+ }
2852
+ };
2853
+ PATTERN_VERB_NAMES = Object.keys(PATTERN_VERBS);
2854
+ GREP_FILE_OPERANDS = /* @__PURE__ */ new Set(["-f", "--file", "--exclude-from", "--include"]);
2855
+ READER_VALUE_LETTERS = {
2856
+ awk: ["F", "v", "f"],
2857
+ gawk: ["F", "v", "f", "e", "E", "i", "l", "o", "p", "D"],
2858
+ sed: ["e", "f", "i", "l"],
2859
+ sort: ["C", "k", "o", "S", "t", "T"]
2860
+ };
2861
+ FILE_OPERAND_FLAGS = {
2862
+ grep: GREP_FILE_OPERANDS,
2863
+ egrep: GREP_FILE_OPERANDS,
2864
+ fgrep: GREP_FILE_OPERANDS,
2865
+ // rg and gawk are NOT installed on the measuring machine: these two rows are
2866
+ // from the shipped documentation (ripgrep `-f/--file`, `--ignore-file`; gawk
2867
+ // `-f/--file`) and are marked as such in the spec.
2868
+ // `-g/--glob/--iglob` name which files ripgrep SEARCHES, so an operand naming
2869
+ // a credential makes rg open it -- the same reason grep's `--include` is here.
2870
+ // Measured (/code-review round 3): `rg --hidden -g .env AWS tree` opened .env
2871
+ // and printed its contents.
2872
+ rg: /* @__PURE__ */ new Set(["-f", "--file", "--ignore-file", "-g", "--glob", "--iglob"]),
2873
+ sed: /* @__PURE__ */ new Set(["-f", "--file"]),
2874
+ awk: /* @__PURE__ */ new Set(["-f", "--file"]),
2875
+ gawk: /* @__PURE__ */ new Set(["-f", "--file"]),
2876
+ sort: /* @__PURE__ */ new Set(["--files0-from"])
2877
+ };
2333
2878
  SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
2879
+ TAR_VALUE_LETTERS = ["b", "C", "f", "F", "g", "H", "I", "K", "L", "N", "T", "V", "X"];
2880
+ ZIP_VALUE_LETTERS = ["b", "n", "P", "t", "s", "O", "x", "i"];
2881
+ RSYNC_VALUE_LETTERS = ["e", "f", "T", "B", "M"];
2334
2882
  RSYNC_SKIP = [
2335
2883
  "e",
2336
2884
  "--rsh",
@@ -2342,21 +2890,35 @@ var init_dist = __esm({
2342
2890
  "f",
2343
2891
  "--filter"
2344
2892
  ];
2893
+ CP_VALUE_LETTERS = ["S", "t"];
2894
+ INSTALL_VALUE_LETTERS = ["S", "t", "g", "m", "o"];
2345
2895
  COPY_VERBS = {
2346
- cp: { source: "allButLast", targetDirFlag: true },
2347
- mv: { source: "allButLast", targetDirFlag: true },
2348
- install: { source: "allButLast", targetDirFlag: true },
2349
- ln: { source: "first", targetDirFlag: true },
2350
- scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
2351
- rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
2896
+ cp: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
2897
+ mv: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
2898
+ install: { source: "allButLast", targetDirFlag: true, valueLetters: INSTALL_VALUE_LETTERS },
2899
+ ln: { source: "first", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
2900
+ scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS, valueLetters: SCP_VALUE_FLAGS },
2901
+ rsync: { source: "allButLast", skipFlags: RSYNC_SKIP, valueLetters: RSYNC_VALUE_LETTERS },
2352
2902
  tar: {
2353
2903
  source: "archive",
2354
2904
  archive: "tar",
2355
- skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
2905
+ skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"],
2906
+ valueLetters: TAR_VALUE_LETTERS
2907
+ },
2908
+ zip: {
2909
+ source: "archive",
2910
+ archive: "zip",
2911
+ skipFlags: ["x", "i", "--exclude", "--include"],
2912
+ valueLetters: ZIP_VALUE_LETTERS
2356
2913
  },
2357
- zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
2358
2914
  ar: { source: "archive", archive: "ar" },
2359
- "7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
2915
+ // 7z switches are INLINE only (`-mx9`, `-px`, `-x!pat`), so no following word is
2916
+ // ever a switch's operand -- which is why the short `x` is NOT a skipFlag here
2917
+ // and valueLetters is empty. Keeping the short `x` made `7z a out.7z -mx KEY`
2918
+ // drop the credential as an exclusion operand (/code-review round 8), and the
2919
+ // derived "every skipped letter is a value letter" row in jail-copy.spec.ts is
2920
+ // what keeps the two tables honest about it.
2921
+ "7z": { source: "archive", archive: "7z", skipFlags: ["--exclude"], valueLetters: [] },
2360
2922
  gzip: { source: "all" },
2361
2923
  bzip2: { source: "all" },
2362
2924
  xz: { source: "all" },
@@ -2567,6 +3129,8 @@ var init_dist = __esm({
2567
3129
  };
2568
3130
  isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
2569
3131
  positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
3132
+ NONE = { kind: "none" };
3133
+ UNKNOWN = { kind: "unknown" };
2570
3134
  SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
2571
3135
  SSRF_MAX_HOST = 253;
2572
3136
  METADATA_ADDRESSES = /* @__PURE__ */ new Set([
@@ -2589,6 +3153,7 @@ var init_dist = __esm({
2589
3153
  const p = a.split(".");
2590
3154
  return p.length === 4 ? p.map(Number) : null;
2591
3155
  };
3156
+ TERMINAL_ESCAPE_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
2592
3157
  aws_default = {
2593
3158
  name: "aws",
2594
3159
  description: "Protects AWS infrastructure from destructive AI operations",
@@ -5117,6 +5682,14 @@ var init_session_files = __esm({
5117
5682
  }
5118
5683
  });
5119
5684
 
5685
+ // src/utils/safe-text.ts
5686
+ var init_safe_text = __esm({
5687
+ "src/utils/safe-text.ts"() {
5688
+ "use strict";
5689
+ init_dist();
5690
+ }
5691
+ });
5692
+
5120
5693
  // src/costSync.ts
5121
5694
  function decodeProjectDirName(dirName) {
5122
5695
  return dirName.replace(/-/g, "/");
@@ -5132,6 +5705,7 @@ var init_costSync = __esm({
5132
5705
  init_cost_gemini();
5133
5706
  init_cost_copilot();
5134
5707
  init_session_files();
5708
+ init_safe_text();
5135
5709
  SYNC_INTERVAL_MS = 10 * 60 * 1e3;
5136
5710
  }
5137
5711
  });
@@ -6105,9 +6679,6 @@ function isNode9SelfOutput(text) {
6105
6679
  }
6106
6680
  return false;
6107
6681
  }
6108
- function stripTerminalEscapes(s) {
6109
- return s.replace(TERMINAL_ESCAPE_RE, "");
6110
- }
6111
6682
  function preview(input, max) {
6112
6683
  const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
6113
6684
  const s = stripTerminalEscapes(String(cmd)).replace(/\s+/g, " ").trim();
@@ -7083,7 +7654,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
7083
7654
  }
7084
7655
  return result;
7085
7656
  }
7086
- var toolInspectionMap, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, TERMINAL_ESCAPE_RE, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS;
7657
+ var toolInspectionMap, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS;
7087
7658
  var init_scan = __esm({
7088
7659
  "src/cli/commands/scan.ts"() {
7089
7660
  "use strict";
@@ -7109,6 +7680,7 @@ var init_scan = __esm({
7109
7680
  init_scan_json();
7110
7681
  init_session_files();
7111
7682
  init_scan_history();
7683
+ init_safe_text();
7112
7684
  toolInspectionMap = DEFAULT_CONFIG.policy.toolInspection;
7113
7685
  CODE_EXTENSIONS = /* @__PURE__ */ new Set([
7114
7686
  ".ts",
@@ -7143,7 +7715,6 @@ var init_scan = __esm({
7143
7715
  /\bseverity:\s*['"](?:block|review|allow)['"]/,
7144
7716
  /NODE9 SECURITY ALERT/
7145
7717
  ];
7146
- TERMINAL_ESCAPE_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
7147
7718
  LOOP_TOOLS = /* @__PURE__ */ new Set([
7148
7719
  "bash",
7149
7720
  "execute_bash",