@node9/proxy 2.13.1 → 2.14.1

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.
@@ -343,6 +343,28 @@ function codexSessionCost(model, tokens, request) {
343
343
  const tierMultiplier = request?.serviceTier === "flex" ? 0.5 : codexModel(model) === "gpt-6-astra" && (request?.serviceTier === "fast" || request?.serviceTier === "priority") ? 2 : 1;
344
344
  return (((input - cached - written) * pin + cached * pcr + written * (pcw || pin)) * inputMultiplier + tokenNumber(tokens.output) * pout * outputMultiplier) * tierMultiplier;
345
345
  }
346
+ function statAndFirstLine(file) {
347
+ const CAP = 4 * 1024 * 1024;
348
+ const CHUNK = 64 * 1024;
349
+ const fd = fs2.openSync(file, "r");
350
+ try {
351
+ const stat = fs2.fstatSync(fd);
352
+ const limit = Math.min(stat.size, CAP);
353
+ const parts = [];
354
+ for (let pos = 0; pos < limit; pos += CHUNK) {
355
+ const buf = Buffer.alloc(Math.min(CHUNK, limit - pos));
356
+ const read = fs2.readSync(fd, buf, 0, buf.length, pos);
357
+ if (read <= 0) break;
358
+ const slice = buf.subarray(0, read);
359
+ const nl = slice.indexOf(10);
360
+ parts.push(nl >= 0 ? slice.subarray(0, nl) : slice);
361
+ if (nl >= 0) break;
362
+ }
363
+ return { stat, first: Buffer.concat(parts).toString("utf8") };
364
+ } finally {
365
+ fs2.closeSync(fd);
366
+ }
367
+ }
346
368
  function listCodexSessionFiles(base = codexSessionsDir()) {
347
369
  const files = [];
348
370
  const walk = (dir) => {
@@ -360,10 +382,10 @@ function listCodexSessionFiles(base = codexSessionsDir()) {
360
382
  const sessions = /* @__PURE__ */ new Map();
361
383
  for (const file of files.sort()) {
362
384
  try {
363
- const stat = fs2.statSync(file);
385
+ const { stat, first: head } = statAndFirstLine(file);
364
386
  let id = "";
365
387
  try {
366
- const first = JSON.parse(fs2.readFileSync(file, "utf8").split("\n", 1)[0]);
388
+ const first = JSON.parse(head);
367
389
  if (first?.type === "session_meta" && typeof first.payload?.id === "string")
368
390
  id = first.payload.id;
369
391
  } catch {
@@ -826,6 +848,19 @@ function parseShared(command) {
826
848
  astCache.set(command, parsed);
827
849
  return parsed;
828
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
+ }
829
864
  function cachedNormalize(command, compute) {
830
865
  const hit = normalizeCache.get(command);
831
866
  if (hit !== void 0) {
@@ -855,6 +890,8 @@ function normalizeCommandForPolicyImpl(command) {
855
890
  const f = parseShared(command);
856
891
  if (f === PARSE_FAIL) return { posix: command, separator: command };
857
892
  try {
893
+ const toCharIndex = byteOffsetToCharIndex(command);
894
+ const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
858
895
  const strips = [];
859
896
  const rewrites = [];
860
897
  const quoteOnlyRewrites = [];
@@ -875,8 +912,9 @@ function normalizeCommandForPolicyImpl(command) {
875
912
  const quotedNode = nextParts[0];
876
913
  const nt = syntax.NodeType(quotedNode);
877
914
  const markStrip = () => {
878
- const s = next.Pos().Offset();
879
- 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;
880
918
  strips.push([s, e]);
881
919
  msgSpans.add(`${s}:${e}`);
882
920
  };
@@ -893,8 +931,9 @@ function normalizeCommandForPolicyImpl(command) {
893
931
  }
894
932
  }
895
933
  for (const arg of args) {
896
- const s = arg.Pos().Offset();
897
- 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;
898
937
  if (msgSpans.has(`${s}:${e}`)) continue;
899
938
  const resolved = resolveWordLiteral(arg);
900
939
  if (resolved === null) continue;
@@ -1067,20 +1106,32 @@ function isProtectedHomePath(rawPath) {
1067
1106
  }
1068
1107
  return true;
1069
1108
  }
1070
- function extractLiteralArgs(callExpr) {
1071
- const args = callExpr.Args || [];
1072
- if (args.length === 0) return { name: "", flags: [], paths: [], words: [] };
1073
- const words = args.map((a) => resolveWordLiteral(a));
1074
- const name = (words[0] ?? "").toLowerCase();
1075
- const flags = [];
1076
- const paths = [];
1077
- for (let i = 1; i < words.length; i++) {
1109
+ function positionedArgs(words, from = 1, to = words.length) {
1110
+ const out = [];
1111
+ let afterFlag = null;
1112
+ for (let i = from; i < to; i++) {
1078
1113
  const v = words[i];
1079
- if (v === null) continue;
1080
- if (v.startsWith("-")) flags.push(v);
1081
- else paths.push(v);
1114
+ if (v === null) {
1115
+ afterFlag = null;
1116
+ continue;
1117
+ }
1118
+ if (v.startsWith("-")) {
1119
+ afterFlag = v;
1120
+ continue;
1121
+ }
1122
+ out.push({ value: v, index: out.length, argv: i, afterFlag });
1123
+ afterFlag = null;
1082
1124
  }
1083
- return { name, flags, paths, words };
1125
+ return out;
1126
+ }
1127
+ function extractLiteralArgs(callExpr) {
1128
+ const rawArgs = callExpr.Args || [];
1129
+ if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
1130
+ const words = rawArgs.map((a) => resolveWordLiteral(a));
1131
+ const name = baseWord(words[0]);
1132
+ const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
1133
+ const args = positionedArgs(words);
1134
+ return { name, flags, paths: args.map((a) => a.value), words, args };
1084
1135
  }
1085
1136
  function resolveWordLiteral(w) {
1086
1137
  const parts = w?.Parts || [];
@@ -1144,7 +1195,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1144
1195
  return result?.verdict !== "block";
1145
1196
  }
1146
1197
  if (nodeType !== "CallExpr") return true;
1147
- const { name, flags, paths, words } = extractLiteralArgs(n);
1198
+ const { name, flags, paths, words, args } = extractLiteralArgs(n);
1148
1199
  if (!name) return true;
1149
1200
  if (name === "rm") {
1150
1201
  const flagStr = flags.join("").toLowerCase();
@@ -1184,13 +1235,16 @@ function analyzeFsOperationImpl(command, depth = 0) {
1184
1235
  return true;
1185
1236
  }
1186
1237
  }
1187
- 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);
1188
1239
  if (readPaths) {
1189
1240
  for (const p of readPaths) {
1190
1241
  result = stricter(result, matchSensitivePath2(p));
1191
1242
  if (result?.verdict === "block") return false;
1192
1243
  }
1193
1244
  }
1245
+ for (const p of copySourcePaths(words)) {
1246
+ result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
1247
+ }
1194
1248
  return true;
1195
1249
  });
1196
1250
  return result;
@@ -1203,6 +1257,184 @@ function stricter(a, b) {
1203
1257
  if (!b) return a;
1204
1258
  return b.verdict === "block" && a.verdict !== "block" ? b : a;
1205
1259
  }
1260
+ function flagInfo(w) {
1261
+ if (w.startsWith("--")) {
1262
+ const eq = w.indexOf("=");
1263
+ return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
1264
+ }
1265
+ const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
1266
+ if (!m) return { letter: null, long: null, attached: null };
1267
+ return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
1268
+ }
1269
+ function flagIs(w, names) {
1270
+ if (w === null) return false;
1271
+ const f = flagInfo(w);
1272
+ return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
1273
+ }
1274
+ function operandOf(a, names, valueLetters) {
1275
+ if (!names || a.afterFlag === null) return false;
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;
1290
+ }
1291
+ function resolveCopyShape(words, h) {
1292
+ const verb = baseWord(words[h]);
1293
+ if (!verb) return null;
1294
+ const direct = COPY_VERBS[verb];
1295
+ if (direct) return { shape: direct, last: h };
1296
+ const slots = positionedArgs(words, h + 1);
1297
+ for (let i = 0; i < slots.length; i++) {
1298
+ for (let n = 3; n >= 1; n--) {
1299
+ const part = slots.slice(i, i + n);
1300
+ if (part.length < n) continue;
1301
+ const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
1302
+ const shape = COPY_VERBS[key];
1303
+ if (shape) return { shape, last: part[n - 1].argv };
1304
+ }
1305
+ if (slots[i].afterFlag === null) return null;
1306
+ }
1307
+ return null;
1308
+ }
1309
+ function findStartPoints(words, h) {
1310
+ const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
1311
+ if (k < 0) return { k, starts: [] };
1312
+ const firstPredicate = words.findIndex(
1313
+ (w, i) => i > h && w !== null && w !== "--" && w.startsWith("-") && !FIND_OPTIONS.has(w)
1314
+ );
1315
+ const end = firstPredicate > h ? firstPredicate : k;
1316
+ return { k, starts: positionalAfter(words, h + 1, end) };
1317
+ }
1318
+ function copySourcePaths(words) {
1319
+ const h = unwrapCommandHead(words);
1320
+ const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
1321
+ if (fi >= 0) {
1322
+ const { k, starts } = findStartPoints(words, fi);
1323
+ if (k < 0) return [];
1324
+ const action = unwrapCommandHead(words.slice(k + 1));
1325
+ return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
1326
+ }
1327
+ if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
1328
+ const r = resolveCopyShape(words, h);
1329
+ if (!r) return [];
1330
+ const { shape, last } = r;
1331
+ const args = positionedArgs(words, last + 1);
1332
+ const tail = words.slice(last + 1);
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);
1359
+ const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
1360
+ const dynamicDest = lastOperand === null;
1361
+ const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
1362
+ let src;
1363
+ switch (shape.source) {
1364
+ case "all":
1365
+ src = args;
1366
+ break;
1367
+ case "first":
1368
+ src = targetDir ? args : args.slice(0, 1);
1369
+ break;
1370
+ case "flagOperand": {
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);
1380
+ return [
1381
+ ...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => a.value),
1382
+ ...inline
1383
+ ];
1384
+ }
1385
+ case "archive":
1386
+ src = archiveInputs(shape.archive, args, tail);
1387
+ break;
1388
+ case "allButLast":
1389
+ src = targetDir || dynamicDest ? args : args.slice(0, -1);
1390
+ break;
1391
+ }
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;
1400
+ }
1401
+ function archiveInputs(kind, args, tail) {
1402
+ const first = args[0];
1403
+ const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
1404
+ if (kind === "tar") {
1405
+ const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
1406
+ const mode = (bareKey ? first.value : "") + flagsText;
1407
+ const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
1408
+ const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
1409
+ if (extracting && !writing) return [];
1410
+ void mode;
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)];
1422
+ }
1423
+ if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
1424
+ if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
1425
+ return args.slice(2);
1426
+ }
1427
+ function copyVerdictOf(hit) {
1428
+ if (!hit) return null;
1429
+ const ruleName = COPY_RULE_OF[hit.ruleName];
1430
+ if (!ruleName) return null;
1431
+ return {
1432
+ ruleName,
1433
+ verdict: "review",
1434
+ reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
1435
+ path: hit.path
1436
+ };
1437
+ }
1206
1438
  function matchSensitivePath2(p) {
1207
1439
  for (const sp of SENSITIVE_PATH_RULES) {
1208
1440
  if (sp.match(p))
@@ -1210,16 +1442,156 @@ function matchSensitivePath2(p) {
1210
1442
  }
1211
1443
  return null;
1212
1444
  }
1445
+ function baseWord(w) {
1446
+ return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
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
+ }
1213
1580
  function wrappedReadPaths(words, name) {
1214
1581
  if (name === "find") {
1215
- const k = words.findIndex((w) => w !== null && FIND_EXEC_FLAGS.has(w));
1216
- if (k < 1 || !isReaderWord(words[k + 1] ?? null)) return null;
1217
- const firstPredicate = words.findIndex((w, i) => i > 0 && w !== null && w.startsWith("-"));
1218
- return positionalAfter(words, 1, firstPredicate > 0 ? firstPredicate : k);
1582
+ const { k, starts } = findStartPoints(words, 0);
1583
+ return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
1219
1584
  }
1220
1585
  if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
1221
1586
  const h = unwrapCommandHead(words);
1222
- 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
+ ];
1223
1595
  }
1224
1596
  function literalShellPayload(words, name) {
1225
1597
  const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
@@ -1564,7 +1936,7 @@ function matchCanaryArgs(args, values) {
1564
1936
  return null;
1565
1937
  }
1566
1938
  }
1567
- 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, 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, 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;
1939
+ 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, 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;
1568
1940
  var init_dist = __esm({
1569
1941
  "packages/policy-engine/dist/index.mjs"() {
1570
1942
  "use strict";
@@ -2165,12 +2537,408 @@ var init_dist = __esm({
2165
2537
  "nl",
2166
2538
  "dd"
2167
2539
  ]);
2540
+ GREP_SHAPE = {
2541
+ takesValue: /* @__PURE__ */ new Set([
2542
+ "-A",
2543
+ "-B",
2544
+ "-C",
2545
+ "-D",
2546
+ "-d",
2547
+ "-e",
2548
+ "-f",
2549
+ "-m",
2550
+ "--after-context",
2551
+ "--before-context",
2552
+ "--binary-files",
2553
+ "--context",
2554
+ "--devices",
2555
+ "--directories",
2556
+ "--exclude",
2557
+ "--exclude-dir",
2558
+ "--exclude-from",
2559
+ "--file",
2560
+ "--include",
2561
+ "--label",
2562
+ "--max-count",
2563
+ "--regexp"
2564
+ ]),
2565
+ noValue: /* @__PURE__ */ new Set([
2566
+ "-E",
2567
+ "-F",
2568
+ "-G",
2569
+ "-P",
2570
+ "-i",
2571
+ "-y",
2572
+ "-v",
2573
+ "-V",
2574
+ "-w",
2575
+ "-x",
2576
+ "-c",
2577
+ "-l",
2578
+ "-L",
2579
+ "-o",
2580
+ "-q",
2581
+ "-s",
2582
+ "-b",
2583
+ "-H",
2584
+ "-h",
2585
+ "-n",
2586
+ "-T",
2587
+ "-Z",
2588
+ "-z",
2589
+ "-R",
2590
+ "-r",
2591
+ "-U",
2592
+ "-u",
2593
+ "-I",
2594
+ "-a",
2595
+ "--basic-regexp",
2596
+ "--binary",
2597
+ "--byte-offset",
2598
+ "--color",
2599
+ "--colour",
2600
+ "--count",
2601
+ "--dereference-recursive",
2602
+ "--extended-regexp",
2603
+ "--files-with-matches",
2604
+ "--files-without-match",
2605
+ "--fixed-strings",
2606
+ "--help",
2607
+ "--ignore-case",
2608
+ "--initial-tab",
2609
+ "--invert-match",
2610
+ "--line-buffered",
2611
+ "--line-number",
2612
+ "--line-regexp",
2613
+ "--no-filename",
2614
+ "--no-group-separator",
2615
+ "--no-ignore-case",
2616
+ "--no-messages",
2617
+ "--null",
2618
+ "--null-data",
2619
+ "--only-matching",
2620
+ "--perl-regexp",
2621
+ "--quiet",
2622
+ "--recursive",
2623
+ "--silent",
2624
+ "--text",
2625
+ "--version",
2626
+ "--with-filename",
2627
+ "--word-regexp"
2628
+ ]),
2629
+ patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
2630
+ noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file"])
2631
+ };
2632
+ PATTERN_VERBS = {
2633
+ grep: GREP_SHAPE,
2634
+ // /usr/bin/egrep and /usr/bin/fgrep are 41-byte shell wrappers that exec
2635
+ // `grep -E` and `grep -F`. Read in full, not assumed.
2636
+ egrep: GREP_SHAPE,
2637
+ fgrep: GREP_SHAPE,
2638
+ // ripgrep 14.1.1, complete from its own --help. An earlier comment here said rg
2639
+ // was not installed on the measuring machine; it is, and that stale claim is why
2640
+ // this table shipped incomplete for two rounds, blocking ordinary searches like
2641
+ // `rg --sort-files .env src` (/code-review round 4).
2642
+ rg: {
2643
+ takesValue: /* @__PURE__ */ new Set([
2644
+ "-A",
2645
+ "-B",
2646
+ "-C",
2647
+ "-d",
2648
+ "-E",
2649
+ "-e",
2650
+ "-f",
2651
+ "-g",
2652
+ "-j",
2653
+ "-M",
2654
+ "-m",
2655
+ "-r",
2656
+ "-t",
2657
+ "-T",
2658
+ "--after-context",
2659
+ "--before-context",
2660
+ "--color",
2661
+ "--colors",
2662
+ "--context",
2663
+ "--context-separator",
2664
+ "--dfa-size-limit",
2665
+ "--encoding",
2666
+ "--engine",
2667
+ "--field-context-separator",
2668
+ "--field-match-separator",
2669
+ "--file",
2670
+ "--generate",
2671
+ "--glob",
2672
+ "--hostname-bin",
2673
+ "--hyperlink-format",
2674
+ "--iglob",
2675
+ "--ignore-file",
2676
+ "--max-columns",
2677
+ "--max-count",
2678
+ "--max-depth",
2679
+ // An ALIAS of --max-depth, and it takes a value. The round-5 extraction read
2680
+ // alias lines as plain switches and swept it into noValue, which hard-blocked
2681
+ // `rg --maxdepth 2 .env src` while `--max-depth 2` ran (/code-review round 7).
2682
+ "--maxdepth",
2683
+ "--max-filesize",
2684
+ "--path-separator",
2685
+ "--pre",
2686
+ "--pre-glob",
2687
+ "--regexp",
2688
+ "--regex-size-limit",
2689
+ "--replace",
2690
+ "--sort",
2691
+ "--sortr",
2692
+ "--threads",
2693
+ "--type",
2694
+ "--type-add",
2695
+ "--type-clear",
2696
+ "--type-not"
2697
+ ]),
2698
+ noValue: /* @__PURE__ */ new Set([
2699
+ "-.",
2700
+ "-0",
2701
+ "-a",
2702
+ "-b",
2703
+ "-c",
2704
+ "-F",
2705
+ "-h",
2706
+ "-H",
2707
+ "-i",
2708
+ "-I",
2709
+ "-l",
2710
+ "-L",
2711
+ "-n",
2712
+ "-N",
2713
+ "-o",
2714
+ "-p",
2715
+ "-P",
2716
+ "-q",
2717
+ "-s",
2718
+ "-S",
2719
+ "-u",
2720
+ "-U",
2721
+ "-v",
2722
+ "-V",
2723
+ "-w",
2724
+ "-x",
2725
+ "-z",
2726
+ "--auto-hybrid-regex",
2727
+ "--binary",
2728
+ "--block-buffered",
2729
+ "--byte-offset",
2730
+ "--case-sensitive",
2731
+ "--column",
2732
+ "--count",
2733
+ "--count-matches",
2734
+ "--crlf",
2735
+ "--debug",
2736
+ "--files",
2737
+ "--files-with-matches",
2738
+ "--files-without-match",
2739
+ "--fixed-strings",
2740
+ "--follow",
2741
+ "--glob-case-insensitive",
2742
+ "--heading",
2743
+ "--help",
2744
+ "--hidden",
2745
+ "--ignore-case",
2746
+ "--ignore-file-case-insensitive",
2747
+ "--include-zero",
2748
+ "--invert-match",
2749
+ "--json",
2750
+ "--line-buffered",
2751
+ "--line-number",
2752
+ "--line-regexp",
2753
+ "--max-columns-preview",
2754
+ "--mmap",
2755
+ "--multiline",
2756
+ "--multiline-dotall",
2757
+ "--no-column",
2758
+ "--no-config",
2759
+ "--no-context-separator",
2760
+ "--no-encoding",
2761
+ "--no-filename",
2762
+ "--no-ignore",
2763
+ "--no-ignore-dot",
2764
+ "--no-ignore-exclude",
2765
+ "--no-ignore-files",
2766
+ "--no-ignore-global",
2767
+ "--no-ignore-messages",
2768
+ "--no-ignore-parent",
2769
+ "--no-ignore-vcs",
2770
+ "--no-line-number",
2771
+ "--no-messages",
2772
+ "--no-pcre2-unicode",
2773
+ "--no-pre",
2774
+ "--no-require-git",
2775
+ "--no-unicode",
2776
+ "--null",
2777
+ "--null-data",
2778
+ "--one-file-system",
2779
+ "--only-matching",
2780
+ "--passthru",
2781
+ "--pcre2",
2782
+ "--pcre2-version",
2783
+ "--pretty",
2784
+ "--print0",
2785
+ "--quiet",
2786
+ "--search-zip",
2787
+ "--smart-case",
2788
+ "--sort-files",
2789
+ "--stats",
2790
+ "--stop-on-nonmatch",
2791
+ "--text",
2792
+ "--trace",
2793
+ "--trim",
2794
+ "--type-list",
2795
+ "--unrestricted",
2796
+ "--version",
2797
+ "--vimgrep",
2798
+ "--with-filename",
2799
+ "--word-regexp",
2800
+ // The COMPLETE remainder of `rg --help`, added after /code-review round 5
2801
+ // found 40 documented switches in neither set. The table is now the whole
2802
+ // option list: `rg --help` yields 149 long options, 35 of them value-taking.
2803
+ "--ignore",
2804
+ "--ignore-dot",
2805
+ "--ignore-exclude",
2806
+ "--ignore-files",
2807
+ "--ignore-global",
2808
+ "--ignore-messages",
2809
+ "--ignore-parent",
2810
+ "--ignore-vcs",
2811
+ "--messages",
2812
+ "--no-auto-hybrid-regex",
2813
+ "--no-binary",
2814
+ "--no-block-buffered",
2815
+ "--no-byte-offset",
2816
+ "--no-crlf",
2817
+ "--no-fixed-strings",
2818
+ "--no-follow",
2819
+ "--no-glob-case-insensitive",
2820
+ "--no-heading",
2821
+ "--no-hidden",
2822
+ "--no-ignore-file-case-insensitive",
2823
+ "--no-include-zero",
2824
+ "--no-invert-match",
2825
+ "--no-json",
2826
+ "--no-line-buffered",
2827
+ "--no-max-columns-preview",
2828
+ "--no-mmap",
2829
+ "--no-multiline",
2830
+ "--no-multiline-dotall",
2831
+ "--no-one-file-system",
2832
+ "--no-pcre2",
2833
+ "--no-search-zip",
2834
+ "--no-sort-files",
2835
+ "--no-stats",
2836
+ "--no-text",
2837
+ "--no-trim",
2838
+ "--passthrough",
2839
+ "--pcre2-unicode",
2840
+ "--require-git",
2841
+ "--unicode"
2842
+ ]),
2843
+ patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
2844
+ // `--files` and `--type-list` list or enumerate without a pattern. Founder
2845
+ // decision 2026-09-13: `rg --files ~/.ssh` stays BLOCKED, which this achieves
2846
+ // by leaving the directory in the judged list.
2847
+ noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file", "--files", "--type-list", "--pcre2-version"])
2848
+ }
2849
+ };
2850
+ PATTERN_VERB_NAMES = Object.keys(PATTERN_VERBS);
2851
+ GREP_FILE_OPERANDS = /* @__PURE__ */ new Set(["-f", "--file", "--exclude-from", "--include"]);
2852
+ READER_VALUE_LETTERS = {
2853
+ awk: ["F", "v", "f"],
2854
+ gawk: ["F", "v", "f", "e", "E", "i", "l", "o", "p", "D"],
2855
+ sed: ["e", "f", "i", "l"],
2856
+ sort: ["C", "k", "o", "S", "t", "T"]
2857
+ };
2858
+ FILE_OPERAND_FLAGS = {
2859
+ grep: GREP_FILE_OPERANDS,
2860
+ egrep: GREP_FILE_OPERANDS,
2861
+ fgrep: GREP_FILE_OPERANDS,
2862
+ // rg and gawk are NOT installed on the measuring machine: these two rows are
2863
+ // from the shipped documentation (ripgrep `-f/--file`, `--ignore-file`; gawk
2864
+ // `-f/--file`) and are marked as such in the spec.
2865
+ // `-g/--glob/--iglob` name which files ripgrep SEARCHES, so an operand naming
2866
+ // a credential makes rg open it -- the same reason grep's `--include` is here.
2867
+ // Measured (/code-review round 3): `rg --hidden -g .env AWS tree` opened .env
2868
+ // and printed its contents.
2869
+ rg: /* @__PURE__ */ new Set(["-f", "--file", "--ignore-file", "-g", "--glob", "--iglob"]),
2870
+ sed: /* @__PURE__ */ new Set(["-f", "--file"]),
2871
+ awk: /* @__PURE__ */ new Set(["-f", "--file"]),
2872
+ gawk: /* @__PURE__ */ new Set(["-f", "--file"]),
2873
+ sort: /* @__PURE__ */ new Set(["--files0-from"])
2874
+ };
2875
+ SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
2876
+ TAR_VALUE_LETTERS = ["b", "C", "f", "F", "g", "H", "I", "K", "L", "N", "T", "V", "X"];
2877
+ ZIP_VALUE_LETTERS = ["b", "n", "P", "t", "s", "O", "x", "i"];
2878
+ RSYNC_VALUE_LETTERS = ["e", "f", "T", "B", "M"];
2879
+ RSYNC_SKIP = [
2880
+ "e",
2881
+ "--rsh",
2882
+ "--exclude",
2883
+ "--exclude-from",
2884
+ "--include",
2885
+ "--include-from",
2886
+ "--files-from",
2887
+ "f",
2888
+ "--filter"
2889
+ ];
2890
+ CP_VALUE_LETTERS = ["S", "t"];
2891
+ INSTALL_VALUE_LETTERS = ["S", "t", "g", "m", "o"];
2892
+ COPY_VERBS = {
2893
+ cp: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
2894
+ mv: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
2895
+ install: { source: "allButLast", targetDirFlag: true, valueLetters: INSTALL_VALUE_LETTERS },
2896
+ ln: { source: "first", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
2897
+ scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS, valueLetters: SCP_VALUE_FLAGS },
2898
+ rsync: { source: "allButLast", skipFlags: RSYNC_SKIP, valueLetters: RSYNC_VALUE_LETTERS },
2899
+ tar: {
2900
+ source: "archive",
2901
+ archive: "tar",
2902
+ skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"],
2903
+ valueLetters: TAR_VALUE_LETTERS
2904
+ },
2905
+ zip: {
2906
+ source: "archive",
2907
+ archive: "zip",
2908
+ skipFlags: ["x", "i", "--exclude", "--include"],
2909
+ valueLetters: ZIP_VALUE_LETTERS
2910
+ },
2911
+ ar: { source: "archive", archive: "ar" },
2912
+ // 7z switches are INLINE only (`-mx9`, `-px`, `-x!pat`), so no following word is
2913
+ // ever a switch's operand -- which is why the short `x` is NOT a skipFlag here
2914
+ // and valueLetters is empty. Keeping the short `x` made `7z a out.7z -mx KEY`
2915
+ // drop the credential as an exclusion operand (/code-review round 8), and the
2916
+ // derived "every skipped letter is a value letter" row in jail-copy.spec.ts is
2917
+ // what keeps the two tables honest about it.
2918
+ "7z": { source: "archive", archive: "7z", skipFlags: ["--exclude"], valueLetters: [] },
2919
+ gzip: { source: "all" },
2920
+ bzip2: { source: "all" },
2921
+ xz: { source: "all" },
2922
+ "docker cp": { source: "allButLast" },
2923
+ "kubectl cp": { source: "allButLast" },
2924
+ "gsutil cp": { source: "allButLast" },
2925
+ "gsutil rsync": { source: "allButLast" },
2926
+ "rclone copy": { source: "allButLast" },
2927
+ "rclone sync": { source: "allButLast" },
2928
+ "aws s3 cp": { source: "allButLast" },
2929
+ "aws s3 mv": { source: "allButLast" },
2930
+ "aws s3 sync": { source: "allButLast" },
2931
+ "gcloud storage cp": { source: "allButLast" },
2932
+ "az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
2933
+ };
2934
+ TAR_MODE_WORD = /^[a-zA-Z]+$/;
2935
+ COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
2168
2936
  FS_OP_PRESCREEN_RE = new RegExp(
2169
2937
  // A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
2170
2938
  // reader right after `"` / `'`, and without these two characters the
2171
2939
  // prescreen rejected every string-wrapped read before the parser ran.
2172
2940
  // Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
2173
- `(?:^|[\\s|;&("'\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
2941
+ `(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
2174
2942
  );
2175
2943
  HOME_CACHE_ALLOWLIST = [
2176
2944
  ".cache",
@@ -2349,8 +3117,17 @@ var init_dist = __esm({
2349
3117
  deriveRedirOp("cat <<X\nX"),
2350
3118
  deriveRedirOp("cat <<-X\nX")
2351
3119
  ]);
2352
- isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(w.split("/").pop()?.toLowerCase() ?? "");
2353
- positionalAfter = (words, from, to = words.length) => words.slice(from, to).filter((w) => w !== null && !w.startsWith("-"));
3120
+ FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
3121
+ COPY_RULE_OF = {
3122
+ "shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
3123
+ "shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
3124
+ "shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
3125
+ "shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
3126
+ };
3127
+ isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
3128
+ positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
3129
+ NONE = { kind: "none" };
3130
+ UNKNOWN = { kind: "unknown" };
2354
3131
  SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
2355
3132
  SSRF_MAX_HOST = 253;
2356
3133
  METADATA_ADDRESSES = /* @__PURE__ */ new Set([
@@ -7278,10 +8055,16 @@ function buildRuleToShieldMap() {
7278
8055
  }
7279
8056
  return map;
7280
8057
  }
8058
+ function shieldOfRule(map, rule) {
8059
+ const exact = map.get(rule);
8060
+ if (exact) return exact;
8061
+ const m = /^shield:([^:]+):/.exec(rule);
8062
+ return m && SHIELDS[m[1]] ? m[1] : void 0;
8063
+ }
7281
8064
  function applyActivityToShields(agg, e, ruleToShield) {
7282
8065
  if (e.kind !== "tool" || !e.checkedBy) return agg;
7283
8066
  if (e.verdict !== "block" && e.verdict !== "review") return agg;
7284
- const shieldName = ruleToShield.get(e.checkedBy);
8067
+ const shieldName = shieldOfRule(ruleToShield, e.checkedBy);
7285
8068
  if (!shieldName) return agg;
7286
8069
  const current = agg.byShield[shieldName] ?? { blocks: 0, reviews: 0 };
7287
8070
  const updated = {
@@ -8277,7 +9060,7 @@ function PeriodShields({
8277
9060
  const byShield = /* @__PURE__ */ new Map();
8278
9061
  if (data) {
8279
9062
  for (const [rule, count] of data.ruleMap) {
8280
- const shield = ruleToShield.get(rule);
9063
+ const shield = shieldOfRule(ruleToShield, rule);
8281
9064
  if (!shield) continue;
8282
9065
  byShield.set(shield, (byShield.get(shield) ?? 0) + count);
8283
9066
  }