@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/index.js CHANGED
@@ -1700,12 +1700,56 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
1700
1700
  "nl",
1701
1701
  "dd"
1702
1702
  ]);
1703
+ var SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
1704
+ var RSYNC_SKIP = [
1705
+ "e",
1706
+ "--rsh",
1707
+ "--exclude",
1708
+ "--exclude-from",
1709
+ "--include",
1710
+ "--include-from",
1711
+ "--files-from",
1712
+ "f",
1713
+ "--filter"
1714
+ ];
1715
+ var COPY_VERBS = {
1716
+ cp: { source: "allButLast", targetDirFlag: true },
1717
+ mv: { source: "allButLast", targetDirFlag: true },
1718
+ install: { source: "allButLast", targetDirFlag: true },
1719
+ ln: { source: "first", targetDirFlag: true },
1720
+ scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
1721
+ rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
1722
+ tar: {
1723
+ source: "archive",
1724
+ archive: "tar",
1725
+ skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
1726
+ },
1727
+ zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
1728
+ ar: { source: "archive", archive: "ar" },
1729
+ "7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
1730
+ gzip: { source: "all" },
1731
+ bzip2: { source: "all" },
1732
+ xz: { source: "all" },
1733
+ "docker cp": { source: "allButLast" },
1734
+ "kubectl cp": { source: "allButLast" },
1735
+ "gsutil cp": { source: "allButLast" },
1736
+ "gsutil rsync": { source: "allButLast" },
1737
+ "rclone copy": { source: "allButLast" },
1738
+ "rclone sync": { source: "allButLast" },
1739
+ "aws s3 cp": { source: "allButLast" },
1740
+ "aws s3 mv": { source: "allButLast" },
1741
+ "aws s3 sync": { source: "allButLast" },
1742
+ "gcloud storage cp": { source: "allButLast" },
1743
+ "az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
1744
+ };
1745
+ var TAR_MODE_WORD = /^[a-zA-Z]+$/;
1746
+ var COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
1703
1747
  var FS_OP_PRESCREEN_RE = new RegExp(
1704
1748
  // A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
1705
1749
  // reader right after `"` / `'`, and without these two characters the
1706
1750
  // prescreen rejected every string-wrapped read before the parser ran.
1707
1751
  // Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
1708
- `(?:^|[\\s|;&("'\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
1752
+ `(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
1709
1753
  );
1710
1754
  var HOME_CACHE_ALLOWLIST = [
1711
1755
  ".cache",
@@ -2140,20 +2184,32 @@ function isProtectedHomePath(rawPath) {
2140
2184
  }
2141
2185
  return true;
2142
2186
  }
2143
- function extractLiteralArgs(callExpr) {
2144
- const args = callExpr.Args || [];
2145
- if (args.length === 0) return { name: "", flags: [], paths: [], words: [] };
2146
- const words = args.map((a) => resolveWordLiteral(a));
2147
- const name = (words[0] ?? "").toLowerCase();
2148
- const flags = [];
2149
- const paths = [];
2150
- for (let i = 1; i < words.length; i++) {
2187
+ function positionedArgs(words, from = 1, to = words.length) {
2188
+ const out = [];
2189
+ let afterFlag = null;
2190
+ for (let i = from; i < to; i++) {
2151
2191
  const v = words[i];
2152
- if (v === null) continue;
2153
- if (v.startsWith("-")) flags.push(v);
2154
- else paths.push(v);
2192
+ if (v === null) {
2193
+ afterFlag = null;
2194
+ continue;
2195
+ }
2196
+ if (v.startsWith("-")) {
2197
+ afterFlag = v;
2198
+ continue;
2199
+ }
2200
+ out.push({ value: v, index: out.length, argv: i, afterFlag });
2201
+ afterFlag = null;
2155
2202
  }
2156
- return { name, flags, paths, words };
2203
+ return out;
2204
+ }
2205
+ function extractLiteralArgs(callExpr) {
2206
+ const rawArgs = callExpr.Args || [];
2207
+ if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
2208
+ const words = rawArgs.map((a) => resolveWordLiteral(a));
2209
+ const name = baseWord(words[0]);
2210
+ const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
2211
+ const args = positionedArgs(words);
2212
+ return { name, flags, paths: args.map((a) => a.value), words, args };
2157
2213
  }
2158
2214
  var NET_BINARIES = /* @__PURE__ */ new Set([
2159
2215
  "curl",
@@ -2577,6 +2633,9 @@ function analyzeFsOperationImpl(command, depth = 0) {
2577
2633
  if (result?.verdict === "block") return false;
2578
2634
  }
2579
2635
  }
2636
+ for (const p of copySourcePaths(words)) {
2637
+ result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
2638
+ }
2580
2639
  return true;
2581
2640
  });
2582
2641
  return result;
@@ -2589,6 +2648,140 @@ function stricter(a, b) {
2589
2648
  if (!b) return a;
2590
2649
  return b.verdict === "block" && a.verdict !== "block" ? b : a;
2591
2650
  }
2651
+ function flagInfo(w) {
2652
+ if (w.startsWith("--")) {
2653
+ const eq = w.indexOf("=");
2654
+ return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
2655
+ }
2656
+ const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
2657
+ if (!m) return { letter: null, long: null, attached: null };
2658
+ return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
2659
+ }
2660
+ function flagIs(w, names) {
2661
+ if (w === null) return false;
2662
+ const f = flagInfo(w);
2663
+ return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
2664
+ }
2665
+ function operandOf(a, names) {
2666
+ if (!names || a.afterFlag === null) return false;
2667
+ return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
2668
+ }
2669
+ function resolveCopyShape(words, h) {
2670
+ const verb = baseWord(words[h]);
2671
+ if (!verb) return null;
2672
+ const direct = COPY_VERBS[verb];
2673
+ if (direct) return { shape: direct, last: h };
2674
+ const slots = positionedArgs(words, h + 1);
2675
+ for (let i = 0; i < slots.length; i++) {
2676
+ for (let n = 3; n >= 1; n--) {
2677
+ const part = slots.slice(i, i + n);
2678
+ if (part.length < n) continue;
2679
+ const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
2680
+ const shape = COPY_VERBS[key];
2681
+ if (shape) return { shape, last: part[n - 1].argv };
2682
+ }
2683
+ if (slots[i].afterFlag === null) return null;
2684
+ }
2685
+ return null;
2686
+ }
2687
+ var FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
2688
+ function findStartPoints(words, h) {
2689
+ const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
2690
+ if (k < 0) return { k, starts: [] };
2691
+ const firstPredicate = words.findIndex(
2692
+ (w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
2693
+ );
2694
+ const end = firstPredicate > h ? firstPredicate : k;
2695
+ return { k, starts: positionalAfter(words, h + 1, end) };
2696
+ }
2697
+ function copySourcePaths(words) {
2698
+ const h = unwrapCommandHead(words);
2699
+ const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
2700
+ if (fi >= 0) {
2701
+ const { k, starts } = findStartPoints(words, fi);
2702
+ if (k < 0) return [];
2703
+ const action = unwrapCommandHead(words.slice(k + 1));
2704
+ return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
2705
+ }
2706
+ if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
2707
+ const r = resolveCopyShape(words, h);
2708
+ if (!r) return [];
2709
+ const { shape, last } = r;
2710
+ const args = positionedArgs(words, last + 1);
2711
+ const tail = words.slice(last + 1);
2712
+ const skipped = (a) => operandOf(a, shape.skipFlags);
2713
+ const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && flagIs(w, ["t", "--target-directory"]));
2714
+ const targetOperand = (a) => targetDir && operandOf(a, ["t", "--target-directory"]);
2715
+ const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
2716
+ const dynamicDest = lastOperand === null;
2717
+ const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
2718
+ if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
2719
+ return [];
2720
+ let src;
2721
+ switch (shape.source) {
2722
+ case "all":
2723
+ src = args;
2724
+ break;
2725
+ case "first":
2726
+ src = targetDir ? args : args.slice(0, 1);
2727
+ break;
2728
+ case "flagOperand": {
2729
+ 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);
2730
+ return [
2731
+ ...args.filter(
2732
+ (a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
2733
+ ).map((a) => a.value),
2734
+ ...inline
2735
+ ];
2736
+ }
2737
+ case "archive":
2738
+ src = archiveInputs(shape.archive, args, tail);
2739
+ break;
2740
+ case "allButLast":
2741
+ src = targetDir || dynamicDest ? args : args.slice(0, -1);
2742
+ break;
2743
+ }
2744
+ return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
2745
+ }
2746
+ function archiveInputs(kind, args, tail) {
2747
+ const first = args[0];
2748
+ const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
2749
+ if (kind === "tar") {
2750
+ const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
2751
+ const mode = (bareKey ? first.value : "") + flagsText;
2752
+ const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
2753
+ const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
2754
+ if (extracting && !writing) return [];
2755
+ void mode;
2756
+ let i = 0;
2757
+ if (bareKey) {
2758
+ i = 1;
2759
+ const next = args[1];
2760
+ if (first.value.includes("f") && next && next.afterFlag === null) i = 2;
2761
+ }
2762
+ return args.slice(i);
2763
+ }
2764
+ if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
2765
+ if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
2766
+ return args.slice(2);
2767
+ }
2768
+ var COPY_RULE_OF = {
2769
+ "shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
2770
+ "shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
2771
+ "shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
2772
+ "shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
2773
+ };
2774
+ function copyVerdictOf(hit) {
2775
+ if (!hit) return null;
2776
+ const ruleName = COPY_RULE_OF[hit.ruleName];
2777
+ if (!ruleName) return null;
2778
+ return {
2779
+ ruleName,
2780
+ verdict: "review",
2781
+ reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
2782
+ path: hit.path
2783
+ };
2784
+ }
2592
2785
  function matchSensitivePath2(p) {
2593
2786
  for (const sp of SENSITIVE_PATH_RULES) {
2594
2787
  if (sp.match(p))
@@ -2596,14 +2789,15 @@ function matchSensitivePath2(p) {
2596
2789
  }
2597
2790
  return null;
2598
2791
  }
2599
- var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(w.split("/").pop()?.toLowerCase() ?? "");
2600
- var positionalAfter = (words, from, to = words.length) => words.slice(from, to).filter((w) => w !== null && !w.startsWith("-"));
2792
+ function baseWord(w) {
2793
+ return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
2794
+ }
2795
+ var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
2796
+ var positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
2601
2797
  function wrappedReadPaths(words, name) {
2602
2798
  if (name === "find") {
2603
- const k = words.findIndex((w) => w !== null && FIND_EXEC_FLAGS.has(w));
2604
- if (k < 1 || !isReaderWord(words[k + 1] ?? null)) return null;
2605
- const firstPredicate = words.findIndex((w, i) => i > 0 && w !== null && w.startsWith("-"));
2606
- return positionalAfter(words, 1, firstPredicate > 0 ? firstPredicate : k);
2799
+ const { k, starts } = findStartPoints(words, 0);
2800
+ return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
2607
2801
  }
2608
2802
  if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
2609
2803
  const h = unwrapCommandHead(words);
@@ -3464,6 +3658,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3464
3658
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
3465
3659
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
3466
3660
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
3661
+ let pendingAstReview;
3467
3662
  if (bashCommand !== null) {
3468
3663
  const pipeVerdict = pipeChainVerdict(
3469
3664
  bashCommand,
@@ -3475,7 +3670,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3475
3670
  if (fsVerdict) {
3476
3671
  const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
3477
3672
  const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
3478
- return {
3673
+ const astVerdict = {
3479
3674
  decision: fsVerdict.verdict,
3480
3675
  blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
3481
3676
  reason: fsVerdict.reason,
@@ -3483,6 +3678,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3483
3678
  ruleName: fsVerdict.ruleName,
3484
3679
  ruleDescription: fsVerdict.reason
3485
3680
  };
3681
+ if (fsVerdict.verdict === "block") return astVerdict;
3682
+ pendingAstReview = astVerdict;
3486
3683
  }
3487
3684
  const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
3488
3685
  const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
@@ -3525,7 +3722,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3525
3722
  const matchedRule = resolvePinned(matches);
3526
3723
  if (matchedRule) {
3527
3724
  if (matchedRule.verdict === "allow")
3528
- return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
3725
+ return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
3529
3726
  return {
3530
3727
  decision: matchedRule.verdict,
3531
3728
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
@@ -3552,6 +3749,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3552
3749
  allTokens = analyzed.allTokens;
3553
3750
  pathTokens = analyzed.paths;
3554
3751
  const candidates = [];
3752
+ if (pendingAstReview) candidates.push(pendingAstReview);
3555
3753
  const evalVerdict = detectDangerousShellExec(shellCommand);
3556
3754
  if (evalVerdict === "block") {
3557
3755
  return {
package/dist/index.mjs CHANGED
@@ -1670,12 +1670,56 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
1670
1670
  "nl",
1671
1671
  "dd"
1672
1672
  ]);
1673
+ var SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
1674
+ var RSYNC_SKIP = [
1675
+ "e",
1676
+ "--rsh",
1677
+ "--exclude",
1678
+ "--exclude-from",
1679
+ "--include",
1680
+ "--include-from",
1681
+ "--files-from",
1682
+ "f",
1683
+ "--filter"
1684
+ ];
1685
+ var COPY_VERBS = {
1686
+ cp: { source: "allButLast", targetDirFlag: true },
1687
+ mv: { source: "allButLast", targetDirFlag: true },
1688
+ install: { source: "allButLast", targetDirFlag: true },
1689
+ ln: { source: "first", targetDirFlag: true },
1690
+ scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
1691
+ rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
1692
+ tar: {
1693
+ source: "archive",
1694
+ archive: "tar",
1695
+ skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
1696
+ },
1697
+ zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
1698
+ ar: { source: "archive", archive: "ar" },
1699
+ "7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
1700
+ gzip: { source: "all" },
1701
+ bzip2: { source: "all" },
1702
+ xz: { source: "all" },
1703
+ "docker cp": { source: "allButLast" },
1704
+ "kubectl cp": { source: "allButLast" },
1705
+ "gsutil cp": { source: "allButLast" },
1706
+ "gsutil rsync": { source: "allButLast" },
1707
+ "rclone copy": { source: "allButLast" },
1708
+ "rclone sync": { source: "allButLast" },
1709
+ "aws s3 cp": { source: "allButLast" },
1710
+ "aws s3 mv": { source: "allButLast" },
1711
+ "aws s3 sync": { source: "allButLast" },
1712
+ "gcloud storage cp": { source: "allButLast" },
1713
+ "az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
1714
+ };
1715
+ var TAR_MODE_WORD = /^[a-zA-Z]+$/;
1716
+ var COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
1673
1717
  var FS_OP_PRESCREEN_RE = new RegExp(
1674
1718
  // A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
1675
1719
  // reader right after `"` / `'`, and without these two characters the
1676
1720
  // prescreen rejected every string-wrapped read before the parser ran.
1677
1721
  // Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
1678
- `(?:^|[\\s|;&("'\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
1722
+ `(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
1679
1723
  );
1680
1724
  var HOME_CACHE_ALLOWLIST = [
1681
1725
  ".cache",
@@ -2110,20 +2154,32 @@ function isProtectedHomePath(rawPath) {
2110
2154
  }
2111
2155
  return true;
2112
2156
  }
2113
- function extractLiteralArgs(callExpr) {
2114
- const args = callExpr.Args || [];
2115
- if (args.length === 0) return { name: "", flags: [], paths: [], words: [] };
2116
- const words = args.map((a) => resolveWordLiteral(a));
2117
- const name = (words[0] ?? "").toLowerCase();
2118
- const flags = [];
2119
- const paths = [];
2120
- for (let i = 1; i < words.length; i++) {
2157
+ function positionedArgs(words, from = 1, to = words.length) {
2158
+ const out = [];
2159
+ let afterFlag = null;
2160
+ for (let i = from; i < to; i++) {
2121
2161
  const v = words[i];
2122
- if (v === null) continue;
2123
- if (v.startsWith("-")) flags.push(v);
2124
- else paths.push(v);
2162
+ if (v === null) {
2163
+ afterFlag = null;
2164
+ continue;
2165
+ }
2166
+ if (v.startsWith("-")) {
2167
+ afterFlag = v;
2168
+ continue;
2169
+ }
2170
+ out.push({ value: v, index: out.length, argv: i, afterFlag });
2171
+ afterFlag = null;
2125
2172
  }
2126
- return { name, flags, paths, words };
2173
+ return out;
2174
+ }
2175
+ function extractLiteralArgs(callExpr) {
2176
+ const rawArgs = callExpr.Args || [];
2177
+ if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
2178
+ const words = rawArgs.map((a) => resolveWordLiteral(a));
2179
+ const name = baseWord(words[0]);
2180
+ const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
2181
+ const args = positionedArgs(words);
2182
+ return { name, flags, paths: args.map((a) => a.value), words, args };
2127
2183
  }
2128
2184
  var NET_BINARIES = /* @__PURE__ */ new Set([
2129
2185
  "curl",
@@ -2547,6 +2603,9 @@ function analyzeFsOperationImpl(command, depth = 0) {
2547
2603
  if (result?.verdict === "block") return false;
2548
2604
  }
2549
2605
  }
2606
+ for (const p of copySourcePaths(words)) {
2607
+ result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
2608
+ }
2550
2609
  return true;
2551
2610
  });
2552
2611
  return result;
@@ -2559,6 +2618,140 @@ function stricter(a, b) {
2559
2618
  if (!b) return a;
2560
2619
  return b.verdict === "block" && a.verdict !== "block" ? b : a;
2561
2620
  }
2621
+ function flagInfo(w) {
2622
+ if (w.startsWith("--")) {
2623
+ const eq = w.indexOf("=");
2624
+ return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
2625
+ }
2626
+ const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
2627
+ if (!m) return { letter: null, long: null, attached: null };
2628
+ return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
2629
+ }
2630
+ function flagIs(w, names) {
2631
+ if (w === null) return false;
2632
+ const f = flagInfo(w);
2633
+ return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
2634
+ }
2635
+ function operandOf(a, names) {
2636
+ if (!names || a.afterFlag === null) return false;
2637
+ return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
2638
+ }
2639
+ function resolveCopyShape(words, h) {
2640
+ const verb = baseWord(words[h]);
2641
+ if (!verb) return null;
2642
+ const direct = COPY_VERBS[verb];
2643
+ if (direct) return { shape: direct, last: h };
2644
+ const slots = positionedArgs(words, h + 1);
2645
+ for (let i = 0; i < slots.length; i++) {
2646
+ for (let n = 3; n >= 1; n--) {
2647
+ const part = slots.slice(i, i + n);
2648
+ if (part.length < n) continue;
2649
+ const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
2650
+ const shape = COPY_VERBS[key];
2651
+ if (shape) return { shape, last: part[n - 1].argv };
2652
+ }
2653
+ if (slots[i].afterFlag === null) return null;
2654
+ }
2655
+ return null;
2656
+ }
2657
+ var FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
2658
+ function findStartPoints(words, h) {
2659
+ const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
2660
+ if (k < 0) return { k, starts: [] };
2661
+ const firstPredicate = words.findIndex(
2662
+ (w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
2663
+ );
2664
+ const end = firstPredicate > h ? firstPredicate : k;
2665
+ return { k, starts: positionalAfter(words, h + 1, end) };
2666
+ }
2667
+ function copySourcePaths(words) {
2668
+ const h = unwrapCommandHead(words);
2669
+ const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
2670
+ if (fi >= 0) {
2671
+ const { k, starts } = findStartPoints(words, fi);
2672
+ if (k < 0) return [];
2673
+ const action = unwrapCommandHead(words.slice(k + 1));
2674
+ return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
2675
+ }
2676
+ if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
2677
+ const r = resolveCopyShape(words, h);
2678
+ if (!r) return [];
2679
+ const { shape, last } = r;
2680
+ const args = positionedArgs(words, last + 1);
2681
+ const tail = words.slice(last + 1);
2682
+ const skipped = (a) => operandOf(a, shape.skipFlags);
2683
+ const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && flagIs(w, ["t", "--target-directory"]));
2684
+ const targetOperand = (a) => targetDir && operandOf(a, ["t", "--target-directory"]);
2685
+ const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
2686
+ const dynamicDest = lastOperand === null;
2687
+ const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
2688
+ if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
2689
+ return [];
2690
+ let src;
2691
+ switch (shape.source) {
2692
+ case "all":
2693
+ src = args;
2694
+ break;
2695
+ case "first":
2696
+ src = targetDir ? args : args.slice(0, 1);
2697
+ break;
2698
+ case "flagOperand": {
2699
+ 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);
2700
+ return [
2701
+ ...args.filter(
2702
+ (a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
2703
+ ).map((a) => a.value),
2704
+ ...inline
2705
+ ];
2706
+ }
2707
+ case "archive":
2708
+ src = archiveInputs(shape.archive, args, tail);
2709
+ break;
2710
+ case "allButLast":
2711
+ src = targetDir || dynamicDest ? args : args.slice(0, -1);
2712
+ break;
2713
+ }
2714
+ return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
2715
+ }
2716
+ function archiveInputs(kind, args, tail) {
2717
+ const first = args[0];
2718
+ const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
2719
+ if (kind === "tar") {
2720
+ const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
2721
+ const mode = (bareKey ? first.value : "") + flagsText;
2722
+ const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
2723
+ const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
2724
+ if (extracting && !writing) return [];
2725
+ void mode;
2726
+ let i = 0;
2727
+ if (bareKey) {
2728
+ i = 1;
2729
+ const next = args[1];
2730
+ if (first.value.includes("f") && next && next.afterFlag === null) i = 2;
2731
+ }
2732
+ return args.slice(i);
2733
+ }
2734
+ if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
2735
+ if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
2736
+ return args.slice(2);
2737
+ }
2738
+ var COPY_RULE_OF = {
2739
+ "shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
2740
+ "shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
2741
+ "shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
2742
+ "shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
2743
+ };
2744
+ function copyVerdictOf(hit) {
2745
+ if (!hit) return null;
2746
+ const ruleName = COPY_RULE_OF[hit.ruleName];
2747
+ if (!ruleName) return null;
2748
+ return {
2749
+ ruleName,
2750
+ verdict: "review",
2751
+ reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
2752
+ path: hit.path
2753
+ };
2754
+ }
2562
2755
  function matchSensitivePath2(p) {
2563
2756
  for (const sp of SENSITIVE_PATH_RULES) {
2564
2757
  if (sp.match(p))
@@ -2566,14 +2759,15 @@ function matchSensitivePath2(p) {
2566
2759
  }
2567
2760
  return null;
2568
2761
  }
2569
- var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(w.split("/").pop()?.toLowerCase() ?? "");
2570
- var positionalAfter = (words, from, to = words.length) => words.slice(from, to).filter((w) => w !== null && !w.startsWith("-"));
2762
+ function baseWord(w) {
2763
+ return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
2764
+ }
2765
+ var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
2766
+ var positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
2571
2767
  function wrappedReadPaths(words, name) {
2572
2768
  if (name === "find") {
2573
- const k = words.findIndex((w) => w !== null && FIND_EXEC_FLAGS.has(w));
2574
- if (k < 1 || !isReaderWord(words[k + 1] ?? null)) return null;
2575
- const firstPredicate = words.findIndex((w, i) => i > 0 && w !== null && w.startsWith("-"));
2576
- return positionalAfter(words, 1, firstPredicate > 0 ? firstPredicate : k);
2769
+ const { k, starts } = findStartPoints(words, 0);
2770
+ return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
2577
2771
  }
2578
2772
  if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
2579
2773
  const h = unwrapCommandHead(words);
@@ -3434,6 +3628,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3434
3628
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
3435
3629
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
3436
3630
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
3631
+ let pendingAstReview;
3437
3632
  if (bashCommand !== null) {
3438
3633
  const pipeVerdict = pipeChainVerdict(
3439
3634
  bashCommand,
@@ -3445,7 +3640,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3445
3640
  if (fsVerdict) {
3446
3641
  const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
3447
3642
  const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
3448
- return {
3643
+ const astVerdict = {
3449
3644
  decision: fsVerdict.verdict,
3450
3645
  blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
3451
3646
  reason: fsVerdict.reason,
@@ -3453,6 +3648,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3453
3648
  ruleName: fsVerdict.ruleName,
3454
3649
  ruleDescription: fsVerdict.reason
3455
3650
  };
3651
+ if (fsVerdict.verdict === "block") return astVerdict;
3652
+ pendingAstReview = astVerdict;
3456
3653
  }
3457
3654
  const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
3458
3655
  const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
@@ -3495,7 +3692,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3495
3692
  const matchedRule = resolvePinned(matches);
3496
3693
  if (matchedRule) {
3497
3694
  if (matchedRule.verdict === "allow")
3498
- return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
3695
+ return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
3499
3696
  return {
3500
3697
  decision: matchedRule.verdict,
3501
3698
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
@@ -3522,6 +3719,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3522
3719
  allTokens = analyzed.allTokens;
3523
3720
  pathTokens = analyzed.paths;
3524
3721
  const candidates = [];
3722
+ if (pendingAstReview) candidates.push(pendingAstReview);
3525
3723
  const evalVerdict = detectDangerousShellExec(shellCommand);
3526
3724
  if (evalVerdict === "block") {
3527
3725
  return {