@node9/proxy 2.13.1 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1302,20 +1302,32 @@ function isProtectedHomePath(rawPath) {
1302
1302
  }
1303
1303
  return true;
1304
1304
  }
1305
- function extractLiteralArgs(callExpr) {
1306
- const args = callExpr.Args || [];
1307
- if (args.length === 0) return { name: "", flags: [], paths: [], words: [] };
1308
- const words = args.map((a) => resolveWordLiteral(a));
1309
- const name = (words[0] ?? "").toLowerCase();
1310
- const flags = [];
1311
- const paths = [];
1312
- for (let i = 1; i < words.length; i++) {
1305
+ function positionedArgs(words, from = 1, to = words.length) {
1306
+ const out = [];
1307
+ let afterFlag = null;
1308
+ for (let i = from; i < to; i++) {
1313
1309
  const v = words[i];
1314
- if (v === null) continue;
1315
- if (v.startsWith("-")) flags.push(v);
1316
- else paths.push(v);
1310
+ if (v === null) {
1311
+ afterFlag = null;
1312
+ continue;
1313
+ }
1314
+ if (v.startsWith("-")) {
1315
+ afterFlag = v;
1316
+ continue;
1317
+ }
1318
+ out.push({ value: v, index: out.length, argv: i, afterFlag });
1319
+ afterFlag = null;
1317
1320
  }
1318
- return { name, flags, paths, words };
1321
+ return out;
1322
+ }
1323
+ function extractLiteralArgs(callExpr) {
1324
+ const rawArgs = callExpr.Args || [];
1325
+ if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
1326
+ const words = rawArgs.map((a) => resolveWordLiteral(a));
1327
+ const name = baseWord(words[0]);
1328
+ const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
1329
+ const args = positionedArgs(words);
1330
+ return { name, flags, paths: args.map((a) => a.value), words, args };
1319
1331
  }
1320
1332
  function resolveWordLiteral(w) {
1321
1333
  const parts = w?.Parts || [];
@@ -1634,6 +1646,9 @@ function analyzeFsOperationImpl(command, depth = 0) {
1634
1646
  if (result?.verdict === "block") return false;
1635
1647
  }
1636
1648
  }
1649
+ for (const p of copySourcePaths(words)) {
1650
+ result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
1651
+ }
1637
1652
  return true;
1638
1653
  });
1639
1654
  return result;
@@ -1646,6 +1661,133 @@ function stricter(a, b) {
1646
1661
  if (!b) return a;
1647
1662
  return b.verdict === "block" && a.verdict !== "block" ? b : a;
1648
1663
  }
1664
+ function flagInfo(w) {
1665
+ if (w.startsWith("--")) {
1666
+ const eq = w.indexOf("=");
1667
+ return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
1668
+ }
1669
+ const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
1670
+ if (!m) return { letter: null, long: null, attached: null };
1671
+ return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
1672
+ }
1673
+ function flagIs(w, names) {
1674
+ if (w === null) return false;
1675
+ const f = flagInfo(w);
1676
+ return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
1677
+ }
1678
+ function operandOf(a, names) {
1679
+ if (!names || a.afterFlag === null) return false;
1680
+ return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
1681
+ }
1682
+ function resolveCopyShape(words, h) {
1683
+ const verb = baseWord(words[h]);
1684
+ if (!verb) return null;
1685
+ const direct = COPY_VERBS[verb];
1686
+ if (direct) return { shape: direct, last: h };
1687
+ const slots = positionedArgs(words, h + 1);
1688
+ for (let i = 0; i < slots.length; i++) {
1689
+ for (let n = 3; n >= 1; n--) {
1690
+ const part = slots.slice(i, i + n);
1691
+ if (part.length < n) continue;
1692
+ const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
1693
+ const shape = COPY_VERBS[key];
1694
+ if (shape) return { shape, last: part[n - 1].argv };
1695
+ }
1696
+ if (slots[i].afterFlag === null) return null;
1697
+ }
1698
+ return null;
1699
+ }
1700
+ function findStartPoints(words, h) {
1701
+ const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
1702
+ if (k < 0) return { k, starts: [] };
1703
+ const firstPredicate = words.findIndex(
1704
+ (w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
1705
+ );
1706
+ const end = firstPredicate > h ? firstPredicate : k;
1707
+ return { k, starts: positionalAfter(words, h + 1, end) };
1708
+ }
1709
+ function copySourcePaths(words) {
1710
+ const h = unwrapCommandHead(words);
1711
+ const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
1712
+ if (fi >= 0) {
1713
+ const { k, starts } = findStartPoints(words, fi);
1714
+ if (k < 0) return [];
1715
+ const action = unwrapCommandHead(words.slice(k + 1));
1716
+ return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
1717
+ }
1718
+ if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
1719
+ const r = resolveCopyShape(words, h);
1720
+ if (!r) return [];
1721
+ const { shape, last } = r;
1722
+ const args = positionedArgs(words, last + 1);
1723
+ const tail = words.slice(last + 1);
1724
+ const skipped = (a) => operandOf(a, shape.skipFlags);
1725
+ const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && flagIs(w, ["t", "--target-directory"]));
1726
+ const targetOperand = (a) => targetDir && operandOf(a, ["t", "--target-directory"]);
1727
+ const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
1728
+ const dynamicDest = lastOperand === null;
1729
+ const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
1730
+ if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
1731
+ return [];
1732
+ let src;
1733
+ switch (shape.source) {
1734
+ case "all":
1735
+ src = args;
1736
+ break;
1737
+ case "first":
1738
+ src = targetDir ? args : args.slice(0, 1);
1739
+ break;
1740
+ case "flagOperand": {
1741
+ 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);
1742
+ return [
1743
+ ...args.filter(
1744
+ (a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
1745
+ ).map((a) => a.value),
1746
+ ...inline
1747
+ ];
1748
+ }
1749
+ case "archive":
1750
+ src = archiveInputs(shape.archive, args, tail);
1751
+ break;
1752
+ case "allButLast":
1753
+ src = targetDir || dynamicDest ? args : args.slice(0, -1);
1754
+ break;
1755
+ }
1756
+ return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
1757
+ }
1758
+ function archiveInputs(kind, args, tail) {
1759
+ const first = args[0];
1760
+ const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
1761
+ if (kind === "tar") {
1762
+ const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
1763
+ const mode = (bareKey ? first.value : "") + flagsText;
1764
+ const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
1765
+ const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
1766
+ if (extracting && !writing) return [];
1767
+ void mode;
1768
+ let i = 0;
1769
+ if (bareKey) {
1770
+ i = 1;
1771
+ const next = args[1];
1772
+ if (first.value.includes("f") && next && next.afterFlag === null) i = 2;
1773
+ }
1774
+ return args.slice(i);
1775
+ }
1776
+ if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
1777
+ if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
1778
+ return args.slice(2);
1779
+ }
1780
+ function copyVerdictOf(hit) {
1781
+ if (!hit) return null;
1782
+ const ruleName = COPY_RULE_OF[hit.ruleName];
1783
+ if (!ruleName) return null;
1784
+ return {
1785
+ ruleName,
1786
+ verdict: "review",
1787
+ reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
1788
+ path: hit.path
1789
+ };
1790
+ }
1649
1791
  function matchSensitivePath2(p) {
1650
1792
  for (const sp of SENSITIVE_PATH_RULES) {
1651
1793
  if (sp.match(p))
@@ -1653,12 +1795,13 @@ function matchSensitivePath2(p) {
1653
1795
  }
1654
1796
  return null;
1655
1797
  }
1798
+ function baseWord(w) {
1799
+ return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
1800
+ }
1656
1801
  function wrappedReadPaths(words, name) {
1657
1802
  if (name === "find") {
1658
- const k = words.findIndex((w) => w !== null && FIND_EXEC_FLAGS.has(w));
1659
- if (k < 1 || !isReaderWord(words[k + 1] ?? null)) return null;
1660
- const firstPredicate = words.findIndex((w, i) => i > 0 && w !== null && w.startsWith("-"));
1661
- return positionalAfter(words, 1, firstPredicate > 0 ? firstPredicate : k);
1803
+ const { k, starts } = findStartPoints(words, 0);
1804
+ return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
1662
1805
  }
1663
1806
  if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
1664
1807
  const h = unwrapCommandHead(words);
@@ -2330,6 +2473,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2330
2473
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
2331
2474
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
2332
2475
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
2476
+ let pendingAstReview;
2333
2477
  if (bashCommand !== null) {
2334
2478
  const pipeVerdict = pipeChainVerdict(
2335
2479
  bashCommand,
@@ -2341,7 +2485,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2341
2485
  if (fsVerdict) {
2342
2486
  const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
2343
2487
  const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
2344
- return {
2488
+ const astVerdict = {
2345
2489
  decision: fsVerdict.verdict,
2346
2490
  blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
2347
2491
  reason: fsVerdict.reason,
@@ -2349,6 +2493,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2349
2493
  ruleName: fsVerdict.ruleName,
2350
2494
  ruleDescription: fsVerdict.reason
2351
2495
  };
2496
+ if (fsVerdict.verdict === "block") return astVerdict;
2497
+ pendingAstReview = astVerdict;
2352
2498
  }
2353
2499
  const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
2354
2500
  const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
@@ -2391,7 +2537,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2391
2537
  const matchedRule = resolvePinned(matches);
2392
2538
  if (matchedRule) {
2393
2539
  if (matchedRule.verdict === "allow")
2394
- return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
2540
+ return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
2395
2541
  return {
2396
2542
  decision: matchedRule.verdict,
2397
2543
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
@@ -2418,6 +2564,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2418
2564
  allTokens = analyzed.allTokens;
2419
2565
  pathTokens = analyzed.paths;
2420
2566
  const candidates2 = [];
2567
+ if (pendingAstReview) candidates2.push(pendingAstReview);
2421
2568
  const evalVerdict = detectDangerousShellExec(shellCommand);
2422
2569
  if (evalVerdict === "block") {
2423
2570
  return {
@@ -2714,6 +2861,12 @@ function classifyRuleSeverity(name, verdict) {
2714
2861
  "read-ssh",
2715
2862
  "read-gcp",
2716
2863
  "read-cred",
2864
+ // Stage 4 (2026-09-11): a copy of a credential file scores like a READ of it --
2865
+ // read-ssh/aws/cred are critical, so copy-ssh/aws/cred are; read-env is high
2866
+ // (below), so copy-env joins the high list, not this one (/code-review).
2867
+ "copy-ssh",
2868
+ "copy-aws",
2869
+ "copy-cred",
2717
2870
  "delete-repo",
2718
2871
  "helm-uninstall",
2719
2872
  "drop-table",
@@ -2726,6 +2879,7 @@ function classifyRuleSeverity(name, verdict) {
2726
2879
  ];
2727
2880
  if (criticalPatterns.some((p) => n.includes(p))) return "critical";
2728
2881
  const highPatterns = [
2882
+ "copy-env",
2729
2883
  "force-push",
2730
2884
  "force_push",
2731
2885
  "git-destructive",
@@ -2743,6 +2897,11 @@ function narrativeRuleLabel(name) {
2743
2897
  const map = {
2744
2898
  "read-aws": "AWS credentials read",
2745
2899
  "read-ssh": "SSH private key read",
2900
+ // Stage 4 copy twins, so `scan --narrative` prints a label, not a raw slug.
2901
+ "copy-ssh": "SSH private key copied out",
2902
+ "copy-aws": "AWS credentials copied out",
2903
+ "copy-env": ".env file copied out",
2904
+ "copy-cred": "credential file copied out",
2746
2905
  "read-gcp": "GCP credentials read",
2747
2906
  "read-cred": "credential file read",
2748
2907
  "delete-repo": "GitHub repository deletion",
@@ -3332,7 +3491,7 @@ function* stringValues(obj, depth = 0) {
3332
3491
  }
3333
3492
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
3334
3493
  }
3335
- var IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, 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, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, isReaderWord, positionalAfter, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, DESTINATION_ARGS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, 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, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
3494
+ var IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, 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, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, DESTINATION_ARGS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, 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, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
3336
3495
  var init_dist = __esm({
3337
3496
  "packages/policy-engine/dist/index.mjs"() {
3338
3497
  "use strict";
@@ -4068,12 +4227,56 @@ var init_dist = __esm({
4068
4227
  "nl",
4069
4228
  "dd"
4070
4229
  ]);
4230
+ SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
4231
+ RSYNC_SKIP = [
4232
+ "e",
4233
+ "--rsh",
4234
+ "--exclude",
4235
+ "--exclude-from",
4236
+ "--include",
4237
+ "--include-from",
4238
+ "--files-from",
4239
+ "f",
4240
+ "--filter"
4241
+ ];
4242
+ COPY_VERBS = {
4243
+ cp: { source: "allButLast", targetDirFlag: true },
4244
+ mv: { source: "allButLast", targetDirFlag: true },
4245
+ install: { source: "allButLast", targetDirFlag: true },
4246
+ ln: { source: "first", targetDirFlag: true },
4247
+ scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
4248
+ rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
4249
+ tar: {
4250
+ source: "archive",
4251
+ archive: "tar",
4252
+ skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
4253
+ },
4254
+ zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
4255
+ ar: { source: "archive", archive: "ar" },
4256
+ "7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
4257
+ gzip: { source: "all" },
4258
+ bzip2: { source: "all" },
4259
+ xz: { source: "all" },
4260
+ "docker cp": { source: "allButLast" },
4261
+ "kubectl cp": { source: "allButLast" },
4262
+ "gsutil cp": { source: "allButLast" },
4263
+ "gsutil rsync": { source: "allButLast" },
4264
+ "rclone copy": { source: "allButLast" },
4265
+ "rclone sync": { source: "allButLast" },
4266
+ "aws s3 cp": { source: "allButLast" },
4267
+ "aws s3 mv": { source: "allButLast" },
4268
+ "aws s3 sync": { source: "allButLast" },
4269
+ "gcloud storage cp": { source: "allButLast" },
4270
+ "az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
4271
+ };
4272
+ TAR_MODE_WORD = /^[a-zA-Z]+$/;
4273
+ COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
4071
4274
  FS_OP_PRESCREEN_RE = new RegExp(
4072
4275
  // A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
4073
4276
  // reader right after `"` / `'`, and without these two characters the
4074
4277
  // prescreen rejected every string-wrapped read before the parser ran.
4075
4278
  // Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
4076
- `(?:^|[\\s|;&("'\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
4279
+ `(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
4077
4280
  );
4078
4281
  HOME_CACHE_ALLOWLIST = [
4079
4282
  ".cache",
@@ -4363,8 +4566,15 @@ var init_dist = __esm({
4363
4566
  deriveRedirOp("cat <<X\nX"),
4364
4567
  deriveRedirOp("cat <<-X\nX")
4365
4568
  ]);
4366
- isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(w.split("/").pop()?.toLowerCase() ?? "");
4367
- positionalAfter = (words, from, to = words.length) => words.slice(from, to).filter((w) => w !== null && !w.startsWith("-"));
4569
+ FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
4570
+ COPY_RULE_OF = {
4571
+ "shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
4572
+ "shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
4573
+ "shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
4574
+ "shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
4575
+ };
4576
+ isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
4577
+ positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
4368
4578
  DEFAULT_EGRESS_ALLOWLIST = [
4369
4579
  // node9's own control plane (api, app, dev-api, staging and the apex).
4370
4580
  // Without it, turning egress on asks the user to approve node9 itself.
@@ -5409,7 +5619,7 @@ var init_dist = __esm({
5409
5619
  { view: "separators-stripped", decoder: "separators", stripped: true }
5410
5620
  ];
5411
5621
  LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
5412
- CANONICAL_EXTRACTOR_VERSION = "canonical-v13";
5622
+ CANONICAL_EXTRACTOR_VERSION = "canonical-v14";
5413
5623
  DEDUPE_PREVIEW_LEN = 120;
5414
5624
  TERMINAL_ESCAPE_RE = // eslint-disable-next-line no-control-regex
5415
5625
  /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
@@ -12979,6 +13189,28 @@ function codexSessionCost(model, tokens, request2) {
12979
13189
  const tierMultiplier = request2?.serviceTier === "flex" ? 0.5 : codexModel(model) === "gpt-6-astra" && (request2?.serviceTier === "fast" || request2?.serviceTier === "priority") ? 2 : 1;
12980
13190
  return (((input - cached - written) * pin + cached * pcr + written * (pcw || pin)) * inputMultiplier + tokenNumber(tokens.output) * pout * outputMultiplier) * tierMultiplier;
12981
13191
  }
13192
+ function statAndFirstLine(file) {
13193
+ const CAP = 4 * 1024 * 1024;
13194
+ const CHUNK = 64 * 1024;
13195
+ const fd = fs21.openSync(file, "r");
13196
+ try {
13197
+ const stat = fs21.fstatSync(fd);
13198
+ const limit = Math.min(stat.size, CAP);
13199
+ const parts = [];
13200
+ for (let pos = 0; pos < limit; pos += CHUNK) {
13201
+ const buf = Buffer.alloc(Math.min(CHUNK, limit - pos));
13202
+ const read2 = fs21.readSync(fd, buf, 0, buf.length, pos);
13203
+ if (read2 <= 0) break;
13204
+ const slice = buf.subarray(0, read2);
13205
+ const nl = slice.indexOf(10);
13206
+ parts.push(nl >= 0 ? slice.subarray(0, nl) : slice);
13207
+ if (nl >= 0) break;
13208
+ }
13209
+ return { stat, first: Buffer.concat(parts).toString("utf8") };
13210
+ } finally {
13211
+ fs21.closeSync(fd);
13212
+ }
13213
+ }
12982
13214
  function listCodexSessionFiles(base = codexSessionsDir()) {
12983
13215
  const files = [];
12984
13216
  const walk = (dir) => {
@@ -12996,10 +13228,10 @@ function listCodexSessionFiles(base = codexSessionsDir()) {
12996
13228
  const sessions = /* @__PURE__ */ new Map();
12997
13229
  for (const file of files.sort()) {
12998
13230
  try {
12999
- const stat = fs21.statSync(file);
13231
+ const { stat, first: head } = statAndFirstLine(file);
13000
13232
  let id = "";
13001
13233
  try {
13002
- const first = JSON.parse(fs21.readFileSync(file, "utf8").split("\n", 1)[0]);
13234
+ const first = JSON.parse(head);
13003
13235
  if (first?.type === "session_meta" && typeof first.payload?.id === "string")
13004
13236
  id = first.payload.id;
13005
13237
  } catch {
@@ -59452,7 +59684,11 @@ var BUILTIN_JAIL = [
59452
59684
  "~/.ssh \u2014 SSH private keys",
59453
59685
  "~/.aws \u2014 AWS credentials",
59454
59686
  ".env files",
59455
- "credential files \u2014 credentials.json, .netrc, .npmrc, .docker, .kube, gcloud"
59687
+ "credential files \u2014 credentials.json, .netrc, .npmrc, .docker, .kube, gcloud",
59688
+ // Stage 4 (2026-09-11): reads are blocked; a COPY out of the jail (cp, tar,
59689
+ // scp, rsync, aws s3 cp, ...) is reviewed, because a backup and a theft are
59690
+ // the same command shape.
59691
+ "copies out of the jail (cp, tar, scp, rsync, cloud upload) \u2014 review"
59456
59692
  ];
59457
59693
  function registerJailCommand(program2) {
59458
59694
  const jail = program2.command("jail").description("Grow the credential jail \u2014 block/review reads of your sensitive paths");