@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.js CHANGED
@@ -1291,20 +1291,32 @@ function isProtectedHomePath(rawPath) {
1291
1291
  }
1292
1292
  return true;
1293
1293
  }
1294
- function extractLiteralArgs(callExpr) {
1295
- const args = callExpr.Args || [];
1296
- if (args.length === 0) return { name: "", flags: [], paths: [], words: [] };
1297
- const words = args.map((a) => resolveWordLiteral(a));
1298
- const name = (words[0] ?? "").toLowerCase();
1299
- const flags = [];
1300
- const paths = [];
1301
- for (let i = 1; i < words.length; i++) {
1294
+ function positionedArgs(words, from = 1, to = words.length) {
1295
+ const out = [];
1296
+ let afterFlag = null;
1297
+ for (let i = from; i < to; i++) {
1302
1298
  const v = words[i];
1303
- if (v === null) continue;
1304
- if (v.startsWith("-")) flags.push(v);
1305
- else paths.push(v);
1299
+ if (v === null) {
1300
+ afterFlag = null;
1301
+ continue;
1302
+ }
1303
+ if (v.startsWith("-")) {
1304
+ afterFlag = v;
1305
+ continue;
1306
+ }
1307
+ out.push({ value: v, index: out.length, argv: i, afterFlag });
1308
+ afterFlag = null;
1306
1309
  }
1307
- return { name, flags, paths, words };
1310
+ return out;
1311
+ }
1312
+ function extractLiteralArgs(callExpr) {
1313
+ const rawArgs = callExpr.Args || [];
1314
+ if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
1315
+ const words = rawArgs.map((a) => resolveWordLiteral(a));
1316
+ const name = baseWord(words[0]);
1317
+ const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
1318
+ const args = positionedArgs(words);
1319
+ return { name, flags, paths: args.map((a) => a.value), words, args };
1308
1320
  }
1309
1321
  function resolveWordLiteral(w) {
1310
1322
  const parts = w?.Parts || [];
@@ -1623,6 +1635,9 @@ function analyzeFsOperationImpl(command, depth = 0) {
1623
1635
  if (result?.verdict === "block") return false;
1624
1636
  }
1625
1637
  }
1638
+ for (const p of copySourcePaths(words)) {
1639
+ result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
1640
+ }
1626
1641
  return true;
1627
1642
  });
1628
1643
  return result;
@@ -1635,6 +1650,133 @@ function stricter(a, b) {
1635
1650
  if (!b) return a;
1636
1651
  return b.verdict === "block" && a.verdict !== "block" ? b : a;
1637
1652
  }
1653
+ function flagInfo(w) {
1654
+ if (w.startsWith("--")) {
1655
+ const eq = w.indexOf("=");
1656
+ return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
1657
+ }
1658
+ const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
1659
+ if (!m) return { letter: null, long: null, attached: null };
1660
+ return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
1661
+ }
1662
+ function flagIs(w, names) {
1663
+ if (w === null) return false;
1664
+ const f = flagInfo(w);
1665
+ return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
1666
+ }
1667
+ function operandOf(a, names) {
1668
+ if (!names || a.afterFlag === null) return false;
1669
+ return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
1670
+ }
1671
+ function resolveCopyShape(words, h) {
1672
+ const verb = baseWord(words[h]);
1673
+ if (!verb) return null;
1674
+ const direct = COPY_VERBS[verb];
1675
+ if (direct) return { shape: direct, last: h };
1676
+ const slots = positionedArgs(words, h + 1);
1677
+ for (let i = 0; i < slots.length; i++) {
1678
+ for (let n = 3; n >= 1; n--) {
1679
+ const part = slots.slice(i, i + n);
1680
+ if (part.length < n) continue;
1681
+ const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
1682
+ const shape = COPY_VERBS[key];
1683
+ if (shape) return { shape, last: part[n - 1].argv };
1684
+ }
1685
+ if (slots[i].afterFlag === null) return null;
1686
+ }
1687
+ return null;
1688
+ }
1689
+ function findStartPoints(words, h) {
1690
+ const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
1691
+ if (k < 0) return { k, starts: [] };
1692
+ const firstPredicate = words.findIndex(
1693
+ (w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
1694
+ );
1695
+ const end = firstPredicate > h ? firstPredicate : k;
1696
+ return { k, starts: positionalAfter(words, h + 1, end) };
1697
+ }
1698
+ function copySourcePaths(words) {
1699
+ const h = unwrapCommandHead(words);
1700
+ const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
1701
+ if (fi >= 0) {
1702
+ const { k, starts } = findStartPoints(words, fi);
1703
+ if (k < 0) return [];
1704
+ const action = unwrapCommandHead(words.slice(k + 1));
1705
+ return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
1706
+ }
1707
+ if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
1708
+ const r = resolveCopyShape(words, h);
1709
+ if (!r) return [];
1710
+ const { shape, last } = r;
1711
+ const args = positionedArgs(words, last + 1);
1712
+ const tail = words.slice(last + 1);
1713
+ const skipped = (a) => operandOf(a, shape.skipFlags);
1714
+ const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && flagIs(w, ["t", "--target-directory"]));
1715
+ const targetOperand = (a) => targetDir && operandOf(a, ["t", "--target-directory"]);
1716
+ const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
1717
+ const dynamicDest = lastOperand === null;
1718
+ const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
1719
+ if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
1720
+ return [];
1721
+ let src;
1722
+ switch (shape.source) {
1723
+ case "all":
1724
+ src = args;
1725
+ break;
1726
+ case "first":
1727
+ src = targetDir ? args : args.slice(0, 1);
1728
+ break;
1729
+ case "flagOperand": {
1730
+ 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);
1731
+ return [
1732
+ ...args.filter(
1733
+ (a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
1734
+ ).map((a) => a.value),
1735
+ ...inline
1736
+ ];
1737
+ }
1738
+ case "archive":
1739
+ src = archiveInputs(shape.archive, args, tail);
1740
+ break;
1741
+ case "allButLast":
1742
+ src = targetDir || dynamicDest ? args : args.slice(0, -1);
1743
+ break;
1744
+ }
1745
+ return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
1746
+ }
1747
+ function archiveInputs(kind, args, tail) {
1748
+ const first = args[0];
1749
+ const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
1750
+ if (kind === "tar") {
1751
+ const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
1752
+ const mode = (bareKey ? first.value : "") + flagsText;
1753
+ const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
1754
+ const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
1755
+ if (extracting && !writing) return [];
1756
+ void mode;
1757
+ let i = 0;
1758
+ if (bareKey) {
1759
+ i = 1;
1760
+ const next = args[1];
1761
+ if (first.value.includes("f") && next && next.afterFlag === null) i = 2;
1762
+ }
1763
+ return args.slice(i);
1764
+ }
1765
+ if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
1766
+ if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
1767
+ return args.slice(2);
1768
+ }
1769
+ function copyVerdictOf(hit) {
1770
+ if (!hit) return null;
1771
+ const ruleName = COPY_RULE_OF[hit.ruleName];
1772
+ if (!ruleName) return null;
1773
+ return {
1774
+ ruleName,
1775
+ verdict: "review",
1776
+ reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
1777
+ path: hit.path
1778
+ };
1779
+ }
1638
1780
  function matchSensitivePath2(p) {
1639
1781
  for (const sp of SENSITIVE_PATH_RULES) {
1640
1782
  if (sp.match(p))
@@ -1642,12 +1784,13 @@ function matchSensitivePath2(p) {
1642
1784
  }
1643
1785
  return null;
1644
1786
  }
1787
+ function baseWord(w) {
1788
+ return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
1789
+ }
1645
1790
  function wrappedReadPaths(words, name) {
1646
1791
  if (name === "find") {
1647
- const k = words.findIndex((w) => w !== null && FIND_EXEC_FLAGS.has(w));
1648
- if (k < 1 || !isReaderWord(words[k + 1] ?? null)) return null;
1649
- const firstPredicate = words.findIndex((w, i) => i > 0 && w !== null && w.startsWith("-"));
1650
- return positionalAfter(words, 1, firstPredicate > 0 ? firstPredicate : k);
1792
+ const { k, starts } = findStartPoints(words, 0);
1793
+ return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
1651
1794
  }
1652
1795
  if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
1653
1796
  const h = unwrapCommandHead(words);
@@ -2319,6 +2462,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2319
2462
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
2320
2463
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
2321
2464
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
2465
+ let pendingAstReview;
2322
2466
  if (bashCommand !== null) {
2323
2467
  const pipeVerdict = pipeChainVerdict(
2324
2468
  bashCommand,
@@ -2330,7 +2474,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2330
2474
  if (fsVerdict) {
2331
2475
  const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
2332
2476
  const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
2333
- return {
2477
+ const astVerdict = {
2334
2478
  decision: fsVerdict.verdict,
2335
2479
  blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
2336
2480
  reason: fsVerdict.reason,
@@ -2338,6 +2482,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2338
2482
  ruleName: fsVerdict.ruleName,
2339
2483
  ruleDescription: fsVerdict.reason
2340
2484
  };
2485
+ if (fsVerdict.verdict === "block") return astVerdict;
2486
+ pendingAstReview = astVerdict;
2341
2487
  }
2342
2488
  const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
2343
2489
  const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
@@ -2380,7 +2526,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2380
2526
  const matchedRule = resolvePinned(matches);
2381
2527
  if (matchedRule) {
2382
2528
  if (matchedRule.verdict === "allow")
2383
- return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
2529
+ return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
2384
2530
  return {
2385
2531
  decision: matchedRule.verdict,
2386
2532
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
@@ -2407,6 +2553,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2407
2553
  allTokens = analyzed.allTokens;
2408
2554
  pathTokens = analyzed.paths;
2409
2555
  const candidates2 = [];
2556
+ if (pendingAstReview) candidates2.push(pendingAstReview);
2410
2557
  const evalVerdict = detectDangerousShellExec(shellCommand);
2411
2558
  if (evalVerdict === "block") {
2412
2559
  return {
@@ -2703,6 +2850,12 @@ function classifyRuleSeverity(name, verdict) {
2703
2850
  "read-ssh",
2704
2851
  "read-gcp",
2705
2852
  "read-cred",
2853
+ // Stage 4 (2026-09-11): a copy of a credential file scores like a READ of it --
2854
+ // read-ssh/aws/cred are critical, so copy-ssh/aws/cred are; read-env is high
2855
+ // (below), so copy-env joins the high list, not this one (/code-review).
2856
+ "copy-ssh",
2857
+ "copy-aws",
2858
+ "copy-cred",
2706
2859
  "delete-repo",
2707
2860
  "helm-uninstall",
2708
2861
  "drop-table",
@@ -2715,6 +2868,7 @@ function classifyRuleSeverity(name, verdict) {
2715
2868
  ];
2716
2869
  if (criticalPatterns.some((p) => n.includes(p))) return "critical";
2717
2870
  const highPatterns = [
2871
+ "copy-env",
2718
2872
  "force-push",
2719
2873
  "force_push",
2720
2874
  "git-destructive",
@@ -2732,6 +2886,11 @@ function narrativeRuleLabel(name) {
2732
2886
  const map = {
2733
2887
  "read-aws": "AWS credentials read",
2734
2888
  "read-ssh": "SSH private key read",
2889
+ // Stage 4 copy twins, so `scan --narrative` prints a label, not a raw slug.
2890
+ "copy-ssh": "SSH private key copied out",
2891
+ "copy-aws": "AWS credentials copied out",
2892
+ "copy-env": ".env file copied out",
2893
+ "copy-cred": "credential file copied out",
2735
2894
  "read-gcp": "GCP credentials read",
2736
2895
  "read-cred": "credential file read",
2737
2896
  "delete-repo": "GitHub repository deletion",
@@ -3321,7 +3480,7 @@ function* stringValues(obj, depth = 0) {
3321
3480
  }
3322
3481
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
3323
3482
  }
3324
- var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, 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;
3483
+ var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, 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;
3325
3484
  var init_dist = __esm({
3326
3485
  "packages/policy-engine/dist/index.mjs"() {
3327
3486
  "use strict";
@@ -4064,12 +4223,56 @@ var init_dist = __esm({
4064
4223
  "nl",
4065
4224
  "dd"
4066
4225
  ]);
4226
+ SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
4227
+ RSYNC_SKIP = [
4228
+ "e",
4229
+ "--rsh",
4230
+ "--exclude",
4231
+ "--exclude-from",
4232
+ "--include",
4233
+ "--include-from",
4234
+ "--files-from",
4235
+ "f",
4236
+ "--filter"
4237
+ ];
4238
+ COPY_VERBS = {
4239
+ cp: { source: "allButLast", targetDirFlag: true },
4240
+ mv: { source: "allButLast", targetDirFlag: true },
4241
+ install: { source: "allButLast", targetDirFlag: true },
4242
+ ln: { source: "first", targetDirFlag: true },
4243
+ scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
4244
+ rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
4245
+ tar: {
4246
+ source: "archive",
4247
+ archive: "tar",
4248
+ skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
4249
+ },
4250
+ zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
4251
+ ar: { source: "archive", archive: "ar" },
4252
+ "7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
4253
+ gzip: { source: "all" },
4254
+ bzip2: { source: "all" },
4255
+ xz: { source: "all" },
4256
+ "docker cp": { source: "allButLast" },
4257
+ "kubectl cp": { source: "allButLast" },
4258
+ "gsutil cp": { source: "allButLast" },
4259
+ "gsutil rsync": { source: "allButLast" },
4260
+ "rclone copy": { source: "allButLast" },
4261
+ "rclone sync": { source: "allButLast" },
4262
+ "aws s3 cp": { source: "allButLast" },
4263
+ "aws s3 mv": { source: "allButLast" },
4264
+ "aws s3 sync": { source: "allButLast" },
4265
+ "gcloud storage cp": { source: "allButLast" },
4266
+ "az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
4267
+ };
4268
+ TAR_MODE_WORD = /^[a-zA-Z]+$/;
4269
+ COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
4067
4270
  FS_OP_PRESCREEN_RE = new RegExp(
4068
4271
  // A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
4069
4272
  // reader right after `"` / `'`, and without these two characters the
4070
4273
  // prescreen rejected every string-wrapped read before the parser ran.
4071
4274
  // Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
4072
- `(?:^|[\\s|;&("'\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
4275
+ `(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
4073
4276
  );
4074
4277
  HOME_CACHE_ALLOWLIST = [
4075
4278
  ".cache",
@@ -4359,8 +4562,15 @@ var init_dist = __esm({
4359
4562
  deriveRedirOp("cat <<X\nX"),
4360
4563
  deriveRedirOp("cat <<-X\nX")
4361
4564
  ]);
4362
- isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(w.split("/").pop()?.toLowerCase() ?? "");
4363
- positionalAfter = (words, from, to = words.length) => words.slice(from, to).filter((w) => w !== null && !w.startsWith("-"));
4565
+ FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
4566
+ COPY_RULE_OF = {
4567
+ "shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
4568
+ "shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
4569
+ "shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
4570
+ "shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
4571
+ };
4572
+ isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
4573
+ positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
4364
4574
  DEFAULT_EGRESS_ALLOWLIST = [
4365
4575
  // node9's own control plane (api, app, dev-api, staging and the apex).
4366
4576
  // Without it, turning egress on asks the user to approve node9 itself.
@@ -5405,7 +5615,7 @@ var init_dist = __esm({
5405
5615
  { view: "separators-stripped", decoder: "separators", stripped: true }
5406
5616
  ];
5407
5617
  LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
5408
- CANONICAL_EXTRACTOR_VERSION = "canonical-v13";
5618
+ CANONICAL_EXTRACTOR_VERSION = "canonical-v14";
5409
5619
  DEDUPE_PREVIEW_LEN = 120;
5410
5620
  TERMINAL_ESCAPE_RE = // eslint-disable-next-line no-control-regex
5411
5621
  /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
@@ -12975,6 +13185,28 @@ function codexSessionCost(model, tokens, request2) {
12975
13185
  const tierMultiplier = request2?.serviceTier === "flex" ? 0.5 : codexModel(model) === "gpt-6-astra" && (request2?.serviceTier === "fast" || request2?.serviceTier === "priority") ? 2 : 1;
12976
13186
  return (((input - cached - written) * pin + cached * pcr + written * (pcw || pin)) * inputMultiplier + tokenNumber(tokens.output) * pout * outputMultiplier) * tierMultiplier;
12977
13187
  }
13188
+ function statAndFirstLine(file) {
13189
+ const CAP = 4 * 1024 * 1024;
13190
+ const CHUNK = 64 * 1024;
13191
+ const fd = import_fs20.default.openSync(file, "r");
13192
+ try {
13193
+ const stat = import_fs20.default.fstatSync(fd);
13194
+ const limit = Math.min(stat.size, CAP);
13195
+ const parts = [];
13196
+ for (let pos = 0; pos < limit; pos += CHUNK) {
13197
+ const buf = Buffer.alloc(Math.min(CHUNK, limit - pos));
13198
+ const read2 = import_fs20.default.readSync(fd, buf, 0, buf.length, pos);
13199
+ if (read2 <= 0) break;
13200
+ const slice = buf.subarray(0, read2);
13201
+ const nl = slice.indexOf(10);
13202
+ parts.push(nl >= 0 ? slice.subarray(0, nl) : slice);
13203
+ if (nl >= 0) break;
13204
+ }
13205
+ return { stat, first: Buffer.concat(parts).toString("utf8") };
13206
+ } finally {
13207
+ import_fs20.default.closeSync(fd);
13208
+ }
13209
+ }
12978
13210
  function listCodexSessionFiles(base = codexSessionsDir()) {
12979
13211
  const files = [];
12980
13212
  const walk = (dir) => {
@@ -12992,10 +13224,10 @@ function listCodexSessionFiles(base = codexSessionsDir()) {
12992
13224
  const sessions = /* @__PURE__ */ new Map();
12993
13225
  for (const file of files.sort()) {
12994
13226
  try {
12995
- const stat = import_fs20.default.statSync(file);
13227
+ const { stat, first: head } = statAndFirstLine(file);
12996
13228
  let id = "";
12997
13229
  try {
12998
- const first = JSON.parse(import_fs20.default.readFileSync(file, "utf8").split("\n", 1)[0]);
13230
+ const first = JSON.parse(head);
12999
13231
  if (first?.type === "session_meta" && typeof first.payload?.id === "string")
13000
13232
  id = first.payload.id;
13001
13233
  } catch {
@@ -59460,7 +59692,11 @@ var BUILTIN_JAIL = [
59460
59692
  "~/.ssh \u2014 SSH private keys",
59461
59693
  "~/.aws \u2014 AWS credentials",
59462
59694
  ".env files",
59463
- "credential files \u2014 credentials.json, .netrc, .npmrc, .docker, .kube, gcloud"
59695
+ "credential files \u2014 credentials.json, .netrc, .npmrc, .docker, .kube, gcloud",
59696
+ // Stage 4 (2026-09-11): reads are blocked; a COPY out of the jail (cp, tar,
59697
+ // scp, rsync, aws s3 cp, ...) is reviewed, because a backup and a theft are
59698
+ // the same command shape.
59699
+ "copies out of the jail (cp, tar, scp, rsync, cloud upload) \u2014 review"
59464
59700
  ];
59465
59701
  function registerJailCommand(program2) {
59466
59702
  const jail = program2.command("jail").description("Grow the credential jail \u2014 block/review reads of your sensitive paths");