@node9/proxy 2.14.0 → 2.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1480,6 +1480,19 @@ function parseShared(command) {
1480
1480
  astCache.set(command, parsed);
1481
1481
  return parsed;
1482
1482
  }
1483
+ function byteOffsetToCharIndex(command) {
1484
+ if (!/[^\u0000-\u007F]/.test(command)) return null;
1485
+ const map = /* @__PURE__ */ new Map();
1486
+ let byte = 0;
1487
+ for (let i = 0; i < command.length; ) {
1488
+ map.set(byte, i);
1489
+ const cp = command.codePointAt(i);
1490
+ byte += cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
1491
+ i += cp > 65535 ? 2 : 1;
1492
+ }
1493
+ map.set(byte, command.length);
1494
+ return (b) => map.get(b) ?? -1;
1495
+ }
1483
1496
  function cachedNormalize(command, compute) {
1484
1497
  const hit = normalizeCache.get(command);
1485
1498
  if (hit !== void 0) {
@@ -1509,6 +1522,8 @@ function normalizeCommandForPolicyImpl(command) {
1509
1522
  const f = parseShared(command);
1510
1523
  if (f === PARSE_FAIL) return { posix: command, separator: command };
1511
1524
  try {
1525
+ const toCharIndex = byteOffsetToCharIndex(command);
1526
+ const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
1512
1527
  const strips = [];
1513
1528
  const rewrites = [];
1514
1529
  const quoteOnlyRewrites = [];
@@ -1529,8 +1544,9 @@ function normalizeCommandForPolicyImpl(command) {
1529
1544
  const quotedNode = nextParts[0];
1530
1545
  const nt = syntax.NodeType(quotedNode);
1531
1546
  const markStrip = () => {
1532
- const s = next.Pos().Offset();
1533
- const e = next.End().Offset();
1547
+ const s = at(next.Pos().Offset());
1548
+ const e = at(next.End().Offset());
1549
+ if (s < 0 || e < 0) return;
1534
1550
  strips.push([s, e]);
1535
1551
  msgSpans.add(`${s}:${e}`);
1536
1552
  };
@@ -1547,8 +1563,9 @@ function normalizeCommandForPolicyImpl(command) {
1547
1563
  }
1548
1564
  }
1549
1565
  for (const arg of args) {
1550
- const s = arg.Pos().Offset();
1551
- const e = arg.End().Offset();
1566
+ const s = at(arg.Pos().Offset());
1567
+ const e = at(arg.End().Offset());
1568
+ if (s < 0 || e < 0) continue;
1552
1569
  if (msgSpans.has(`${s}:${e}`)) continue;
1553
1570
  const resolved = resolveWordLiteral(arg);
1554
1571
  if (resolved === null) continue;
@@ -1670,7 +1687,345 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
1670
1687
  "nl",
1671
1688
  "dd"
1672
1689
  ]);
1690
+ var GREP_SHAPE = {
1691
+ takesValue: /* @__PURE__ */ new Set([
1692
+ "-A",
1693
+ "-B",
1694
+ "-C",
1695
+ "-D",
1696
+ "-d",
1697
+ "-e",
1698
+ "-f",
1699
+ "-m",
1700
+ "--after-context",
1701
+ "--before-context",
1702
+ "--binary-files",
1703
+ "--context",
1704
+ "--devices",
1705
+ "--directories",
1706
+ "--exclude",
1707
+ "--exclude-dir",
1708
+ "--exclude-from",
1709
+ "--file",
1710
+ "--include",
1711
+ "--label",
1712
+ "--max-count",
1713
+ "--regexp"
1714
+ ]),
1715
+ noValue: /* @__PURE__ */ new Set([
1716
+ "-E",
1717
+ "-F",
1718
+ "-G",
1719
+ "-P",
1720
+ "-i",
1721
+ "-y",
1722
+ "-v",
1723
+ "-V",
1724
+ "-w",
1725
+ "-x",
1726
+ "-c",
1727
+ "-l",
1728
+ "-L",
1729
+ "-o",
1730
+ "-q",
1731
+ "-s",
1732
+ "-b",
1733
+ "-H",
1734
+ "-h",
1735
+ "-n",
1736
+ "-T",
1737
+ "-Z",
1738
+ "-z",
1739
+ "-R",
1740
+ "-r",
1741
+ "-U",
1742
+ "-u",
1743
+ "-I",
1744
+ "-a",
1745
+ "--basic-regexp",
1746
+ "--binary",
1747
+ "--byte-offset",
1748
+ "--color",
1749
+ "--colour",
1750
+ "--count",
1751
+ "--dereference-recursive",
1752
+ "--extended-regexp",
1753
+ "--files-with-matches",
1754
+ "--files-without-match",
1755
+ "--fixed-strings",
1756
+ "--help",
1757
+ "--ignore-case",
1758
+ "--initial-tab",
1759
+ "--invert-match",
1760
+ "--line-buffered",
1761
+ "--line-number",
1762
+ "--line-regexp",
1763
+ "--no-filename",
1764
+ "--no-group-separator",
1765
+ "--no-ignore-case",
1766
+ "--no-messages",
1767
+ "--null",
1768
+ "--null-data",
1769
+ "--only-matching",
1770
+ "--perl-regexp",
1771
+ "--quiet",
1772
+ "--recursive",
1773
+ "--silent",
1774
+ "--text",
1775
+ "--version",
1776
+ "--with-filename",
1777
+ "--word-regexp"
1778
+ ]),
1779
+ patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
1780
+ noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file"])
1781
+ };
1782
+ var PATTERN_VERBS = {
1783
+ grep: GREP_SHAPE,
1784
+ // /usr/bin/egrep and /usr/bin/fgrep are 41-byte shell wrappers that exec
1785
+ // `grep -E` and `grep -F`. Read in full, not assumed.
1786
+ egrep: GREP_SHAPE,
1787
+ fgrep: GREP_SHAPE,
1788
+ // ripgrep 14.1.1, complete from its own --help. An earlier comment here said rg
1789
+ // was not installed on the measuring machine; it is, and that stale claim is why
1790
+ // this table shipped incomplete for two rounds, blocking ordinary searches like
1791
+ // `rg --sort-files .env src` (/code-review round 4).
1792
+ rg: {
1793
+ takesValue: /* @__PURE__ */ new Set([
1794
+ "-A",
1795
+ "-B",
1796
+ "-C",
1797
+ "-d",
1798
+ "-E",
1799
+ "-e",
1800
+ "-f",
1801
+ "-g",
1802
+ "-j",
1803
+ "-M",
1804
+ "-m",
1805
+ "-r",
1806
+ "-t",
1807
+ "-T",
1808
+ "--after-context",
1809
+ "--before-context",
1810
+ "--color",
1811
+ "--colors",
1812
+ "--context",
1813
+ "--context-separator",
1814
+ "--dfa-size-limit",
1815
+ "--encoding",
1816
+ "--engine",
1817
+ "--field-context-separator",
1818
+ "--field-match-separator",
1819
+ "--file",
1820
+ "--generate",
1821
+ "--glob",
1822
+ "--hostname-bin",
1823
+ "--hyperlink-format",
1824
+ "--iglob",
1825
+ "--ignore-file",
1826
+ "--max-columns",
1827
+ "--max-count",
1828
+ "--max-depth",
1829
+ // An ALIAS of --max-depth, and it takes a value. The round-5 extraction read
1830
+ // alias lines as plain switches and swept it into noValue, which hard-blocked
1831
+ // `rg --maxdepth 2 .env src` while `--max-depth 2` ran (/code-review round 7).
1832
+ "--maxdepth",
1833
+ "--max-filesize",
1834
+ "--path-separator",
1835
+ "--pre",
1836
+ "--pre-glob",
1837
+ "--regexp",
1838
+ "--regex-size-limit",
1839
+ "--replace",
1840
+ "--sort",
1841
+ "--sortr",
1842
+ "--threads",
1843
+ "--type",
1844
+ "--type-add",
1845
+ "--type-clear",
1846
+ "--type-not"
1847
+ ]),
1848
+ noValue: /* @__PURE__ */ new Set([
1849
+ "-.",
1850
+ "-0",
1851
+ "-a",
1852
+ "-b",
1853
+ "-c",
1854
+ "-F",
1855
+ "-h",
1856
+ "-H",
1857
+ "-i",
1858
+ "-I",
1859
+ "-l",
1860
+ "-L",
1861
+ "-n",
1862
+ "-N",
1863
+ "-o",
1864
+ "-p",
1865
+ "-P",
1866
+ "-q",
1867
+ "-s",
1868
+ "-S",
1869
+ "-u",
1870
+ "-U",
1871
+ "-v",
1872
+ "-V",
1873
+ "-w",
1874
+ "-x",
1875
+ "-z",
1876
+ "--auto-hybrid-regex",
1877
+ "--binary",
1878
+ "--block-buffered",
1879
+ "--byte-offset",
1880
+ "--case-sensitive",
1881
+ "--column",
1882
+ "--count",
1883
+ "--count-matches",
1884
+ "--crlf",
1885
+ "--debug",
1886
+ "--files",
1887
+ "--files-with-matches",
1888
+ "--files-without-match",
1889
+ "--fixed-strings",
1890
+ "--follow",
1891
+ "--glob-case-insensitive",
1892
+ "--heading",
1893
+ "--help",
1894
+ "--hidden",
1895
+ "--ignore-case",
1896
+ "--ignore-file-case-insensitive",
1897
+ "--include-zero",
1898
+ "--invert-match",
1899
+ "--json",
1900
+ "--line-buffered",
1901
+ "--line-number",
1902
+ "--line-regexp",
1903
+ "--max-columns-preview",
1904
+ "--mmap",
1905
+ "--multiline",
1906
+ "--multiline-dotall",
1907
+ "--no-column",
1908
+ "--no-config",
1909
+ "--no-context-separator",
1910
+ "--no-encoding",
1911
+ "--no-filename",
1912
+ "--no-ignore",
1913
+ "--no-ignore-dot",
1914
+ "--no-ignore-exclude",
1915
+ "--no-ignore-files",
1916
+ "--no-ignore-global",
1917
+ "--no-ignore-messages",
1918
+ "--no-ignore-parent",
1919
+ "--no-ignore-vcs",
1920
+ "--no-line-number",
1921
+ "--no-messages",
1922
+ "--no-pcre2-unicode",
1923
+ "--no-pre",
1924
+ "--no-require-git",
1925
+ "--no-unicode",
1926
+ "--null",
1927
+ "--null-data",
1928
+ "--one-file-system",
1929
+ "--only-matching",
1930
+ "--passthru",
1931
+ "--pcre2",
1932
+ "--pcre2-version",
1933
+ "--pretty",
1934
+ "--print0",
1935
+ "--quiet",
1936
+ "--search-zip",
1937
+ "--smart-case",
1938
+ "--sort-files",
1939
+ "--stats",
1940
+ "--stop-on-nonmatch",
1941
+ "--text",
1942
+ "--trace",
1943
+ "--trim",
1944
+ "--type-list",
1945
+ "--unrestricted",
1946
+ "--version",
1947
+ "--vimgrep",
1948
+ "--with-filename",
1949
+ "--word-regexp",
1950
+ // The COMPLETE remainder of `rg --help`, added after /code-review round 5
1951
+ // found 40 documented switches in neither set. The table is now the whole
1952
+ // option list: `rg --help` yields 149 long options, 35 of them value-taking.
1953
+ "--ignore",
1954
+ "--ignore-dot",
1955
+ "--ignore-exclude",
1956
+ "--ignore-files",
1957
+ "--ignore-global",
1958
+ "--ignore-messages",
1959
+ "--ignore-parent",
1960
+ "--ignore-vcs",
1961
+ "--messages",
1962
+ "--no-auto-hybrid-regex",
1963
+ "--no-binary",
1964
+ "--no-block-buffered",
1965
+ "--no-byte-offset",
1966
+ "--no-crlf",
1967
+ "--no-fixed-strings",
1968
+ "--no-follow",
1969
+ "--no-glob-case-insensitive",
1970
+ "--no-heading",
1971
+ "--no-hidden",
1972
+ "--no-ignore-file-case-insensitive",
1973
+ "--no-include-zero",
1974
+ "--no-invert-match",
1975
+ "--no-json",
1976
+ "--no-line-buffered",
1977
+ "--no-max-columns-preview",
1978
+ "--no-mmap",
1979
+ "--no-multiline",
1980
+ "--no-multiline-dotall",
1981
+ "--no-one-file-system",
1982
+ "--no-pcre2",
1983
+ "--no-search-zip",
1984
+ "--no-sort-files",
1985
+ "--no-stats",
1986
+ "--no-text",
1987
+ "--no-trim",
1988
+ "--passthrough",
1989
+ "--pcre2-unicode",
1990
+ "--require-git",
1991
+ "--unicode"
1992
+ ]),
1993
+ patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
1994
+ // `--files` and `--type-list` list or enumerate without a pattern. Founder
1995
+ // decision 2026-09-13: `rg --files ~/.ssh` stays BLOCKED, which this achieves
1996
+ // by leaving the directory in the judged list.
1997
+ noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file", "--files", "--type-list", "--pcre2-version"])
1998
+ }
1999
+ };
2000
+ var PATTERN_VERB_NAMES = Object.keys(PATTERN_VERBS);
2001
+ var GREP_FILE_OPERANDS = /* @__PURE__ */ new Set(["-f", "--file", "--exclude-from", "--include"]);
2002
+ var READER_VALUE_LETTERS = {
2003
+ awk: ["F", "v", "f"],
2004
+ gawk: ["F", "v", "f", "e", "E", "i", "l", "o", "p", "D"],
2005
+ sed: ["e", "f", "i", "l"],
2006
+ sort: ["C", "k", "o", "S", "t", "T"]
2007
+ };
2008
+ var FILE_OPERAND_FLAGS = {
2009
+ grep: GREP_FILE_OPERANDS,
2010
+ egrep: GREP_FILE_OPERANDS,
2011
+ fgrep: GREP_FILE_OPERANDS,
2012
+ // rg and gawk are NOT installed on the measuring machine: these two rows are
2013
+ // from the shipped documentation (ripgrep `-f/--file`, `--ignore-file`; gawk
2014
+ // `-f/--file`) and are marked as such in the spec.
2015
+ // `-g/--glob/--iglob` name which files ripgrep SEARCHES, so an operand naming
2016
+ // a credential makes rg open it -- the same reason grep's `--include` is here.
2017
+ // Measured (/code-review round 3): `rg --hidden -g .env AWS tree` opened .env
2018
+ // and printed its contents.
2019
+ rg: /* @__PURE__ */ new Set(["-f", "--file", "--ignore-file", "-g", "--glob", "--iglob"]),
2020
+ sed: /* @__PURE__ */ new Set(["-f", "--file"]),
2021
+ awk: /* @__PURE__ */ new Set(["-f", "--file"]),
2022
+ gawk: /* @__PURE__ */ new Set(["-f", "--file"]),
2023
+ sort: /* @__PURE__ */ new Set(["--files0-from"])
2024
+ };
1673
2025
  var SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
2026
+ var TAR_VALUE_LETTERS = ["b", "C", "f", "F", "g", "H", "I", "K", "L", "N", "T", "V", "X"];
2027
+ var ZIP_VALUE_LETTERS = ["b", "n", "P", "t", "s", "O", "x", "i"];
2028
+ var RSYNC_VALUE_LETTERS = ["e", "f", "T", "B", "M"];
1674
2029
  var RSYNC_SKIP = [
1675
2030
  "e",
1676
2031
  "--rsh",
@@ -1682,21 +2037,35 @@ var RSYNC_SKIP = [
1682
2037
  "f",
1683
2038
  "--filter"
1684
2039
  ];
2040
+ var CP_VALUE_LETTERS = ["S", "t"];
2041
+ var INSTALL_VALUE_LETTERS = ["S", "t", "g", "m", "o"];
1685
2042
  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 },
2043
+ cp: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
2044
+ mv: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
2045
+ install: { source: "allButLast", targetDirFlag: true, valueLetters: INSTALL_VALUE_LETTERS },
2046
+ ln: { source: "first", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
2047
+ scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS, valueLetters: SCP_VALUE_FLAGS },
2048
+ rsync: { source: "allButLast", skipFlags: RSYNC_SKIP, valueLetters: RSYNC_VALUE_LETTERS },
1692
2049
  tar: {
1693
2050
  source: "archive",
1694
2051
  archive: "tar",
1695
- skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
2052
+ skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"],
2053
+ valueLetters: TAR_VALUE_LETTERS
2054
+ },
2055
+ zip: {
2056
+ source: "archive",
2057
+ archive: "zip",
2058
+ skipFlags: ["x", "i", "--exclude", "--include"],
2059
+ valueLetters: ZIP_VALUE_LETTERS
1696
2060
  },
1697
- zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
1698
2061
  ar: { source: "archive", archive: "ar" },
1699
- "7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
2062
+ // 7z switches are INLINE only (`-mx9`, `-px`, `-x!pat`), so no following word is
2063
+ // ever a switch's operand -- which is why the short `x` is NOT a skipFlag here
2064
+ // and valueLetters is empty. Keeping the short `x` made `7z a out.7z -mx KEY`
2065
+ // drop the credential as an exclusion operand (/code-review round 8), and the
2066
+ // derived "every skipped letter is a value letter" row in jail-copy.spec.ts is
2067
+ // what keeps the two tables honest about it.
2068
+ "7z": { source: "archive", archive: "7z", skipFlags: ["--exclude"], valueLetters: [] },
1700
2069
  gzip: { source: "all" },
1701
2070
  bzip2: { source: "all" },
1702
2071
  xz: { source: "all" },
@@ -2556,7 +2925,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
2556
2925
  return result?.verdict !== "block";
2557
2926
  }
2558
2927
  if (nodeType !== "CallExpr") return true;
2559
- const { name, flags, paths, words } = extractLiteralArgs(n);
2928
+ const { name, flags, paths, words, args } = extractLiteralArgs(n);
2560
2929
  if (!name) return true;
2561
2930
  if (name === "rm") {
2562
2931
  const flagStr = flags.join("").toLowerCase();
@@ -2596,7 +2965,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
2596
2965
  return true;
2597
2966
  }
2598
2967
  }
2599
- const readPaths = FS_READ_TOOLS.has(name) ? paths : wrappedReadPaths(words, name);
2968
+ const readPaths = FS_READ_TOOLS.has(name) ? [...readTargets(name, args, flags, words, 1), ...flagOperandFiles(name, words, 1)] : wrappedReadPaths(words, name);
2600
2969
  if (readPaths) {
2601
2970
  for (const p of readPaths) {
2602
2971
  result = stricter(result, matchSensitivePath2(p));
@@ -2632,9 +3001,22 @@ function flagIs(w, names) {
2632
3001
  const f = flagInfo(w);
2633
3002
  return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
2634
3003
  }
2635
- function operandOf(a, names) {
3004
+ function operandOf(a, names, valueLetters) {
2636
3005
  if (!names || a.afterFlag === null) return false;
2637
- return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
3006
+ const w = a.afterFlag;
3007
+ if (!w.startsWith("--") && valueLetters) {
3008
+ const letters = w.slice(1);
3009
+ for (let i = 0; i < letters.length; i++) {
3010
+ if (!valueLetters.includes(letters[i])) continue;
3011
+ return i === letters.length - 1 && names.includes(letters[i]);
3012
+ }
3013
+ return false;
3014
+ }
3015
+ if (w.startsWith("--") && !w.includes("=")) {
3016
+ const longs = names.filter((n) => n.startsWith("--"));
3017
+ if (longs.some((n) => n === w || w.length >= 3 && n.startsWith(w))) return true;
3018
+ }
3019
+ return flagIs(w, names) && flagInfo(w).attached === null;
2638
3020
  }
2639
3021
  function resolveCopyShape(words, h) {
2640
3022
  const verb = baseWord(words[h]);
@@ -2659,7 +3041,7 @@ function findStartPoints(words, h) {
2659
3041
  const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
2660
3042
  if (k < 0) return { k, starts: [] };
2661
3043
  const firstPredicate = words.findIndex(
2662
- (w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
3044
+ (w, i) => i > h && w !== null && w !== "--" && w.startsWith("-") && !FIND_OPTIONS.has(w)
2663
3045
  );
2664
3046
  const end = firstPredicate > h ? firstPredicate : k;
2665
3047
  return { k, starts: positionalAfter(words, h + 1, end) };
@@ -2679,14 +3061,35 @@ function copySourcePaths(words) {
2679
3061
  const { shape, last } = r;
2680
3062
  const args = positionedArgs(words, last + 1);
2681
3063
  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"]);
3064
+ const copyOptionsEnd = tail.findIndex((w) => w === "--");
3065
+ const copyPastOptions = (a) => copyOptionsEnd >= 0 && a.argv > last + 1 + copyOptionsEnd;
3066
+ const skipped = (a) => !copyPastOptions(a) && operandOf(a, shape.skipFlags, shape.valueLetters);
3067
+ const firstValueLetter = (w) => {
3068
+ const letters = w.slice(1);
3069
+ for (let i = 0; i < letters.length; i++) {
3070
+ if ((shape.valueLetters ?? []).includes(letters[i]))
3071
+ return { letter: letters[i], last: i === letters.length - 1 };
3072
+ }
3073
+ return null;
3074
+ };
3075
+ const isTargetDirFlag = (w) => {
3076
+ if (w.startsWith("--")) {
3077
+ const name = w.includes("=") ? w.slice(0, w.indexOf("=")) : w;
3078
+ return name.length >= 3 && "--target-directory".startsWith(name);
3079
+ }
3080
+ return firstValueLetter(w)?.letter === "t";
3081
+ };
3082
+ const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && isTargetDirFlag(w));
3083
+ const targetTakesNextWord = (w) => {
3084
+ if (w.startsWith("--"))
3085
+ return !w.includes("=") && w.length >= 3 && "--target-directory".startsWith(w);
3086
+ const f = firstValueLetter(w);
3087
+ return f !== null && f.letter === "t" && f.last;
3088
+ };
3089
+ const targetOperand = (a) => targetDir && a.afterFlag !== null && !copyPastOptions(a) && targetTakesNextWord(a.afterFlag);
2685
3090
  const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
2686
3091
  const dynamicDest = lastOperand === null;
2687
3092
  const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
2688
- if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
2689
- return [];
2690
3093
  let src;
2691
3094
  switch (shape.source) {
2692
3095
  case "all":
@@ -2696,11 +3099,17 @@ function copySourcePaths(words) {
2696
3099
  src = targetDir ? args : args.slice(0, 1);
2697
3100
  break;
2698
3101
  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);
3102
+ const namesSource = (f) => {
3103
+ const names = shape.sourceFlags ?? [];
3104
+ if (f.long !== null)
3105
+ return names.some(
3106
+ (n) => n.startsWith("--") && (n === f.long || f.long.length >= 3 && n.startsWith(f.long))
3107
+ );
3108
+ return f.letter !== null && names.includes(f.letter);
3109
+ };
3110
+ const inline = tail.filter((w) => w !== null && w.startsWith("-")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && namesSource(f)).map((f) => f.attached);
2700
3111
  return [
2701
- ...args.filter(
2702
- (a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
2703
- ).map((a) => a.value),
3112
+ ...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => a.value),
2704
3113
  ...inline
2705
3114
  ];
2706
3115
  }
@@ -2711,7 +3120,14 @@ function copySourcePaths(words) {
2711
3120
  src = targetDir || dynamicDest ? args : args.slice(0, -1);
2712
3121
  break;
2713
3122
  }
2714
- return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
3123
+ const sources = src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
3124
+ if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand)) {
3125
+ const jailedSources = sources.filter((p) => matchSensitivePath2(p));
3126
+ const dirOf = (p) => /[\\/]/.test(p) ? p.replace(/[\\/][^\\/]*$/, "") : "";
3127
+ if (jailedSources.length === 0) return [];
3128
+ if (jailedSources.every((p) => dirOf(p) === dirOf(lastOperand))) return [];
3129
+ }
3130
+ return sources;
2715
3131
  }
2716
3132
  function archiveInputs(kind, args, tail) {
2717
3133
  const first = args[0];
@@ -2723,13 +3139,17 @@ function archiveInputs(kind, args, tail) {
2723
3139
  const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
2724
3140
  if (extracting && !writing) return [];
2725
3141
  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);
3142
+ if (!bareKey) return args;
3143
+ let i = 1;
3144
+ const fromDirs = [];
3145
+ for (const ch of first.value) {
3146
+ if (!TAR_VALUE_LETTERS.includes(ch)) continue;
3147
+ const operand = args[i];
3148
+ if (!operand || operand.afterFlag !== null) break;
3149
+ i += 1;
3150
+ if (ch === "C") fromDirs.push(operand);
3151
+ }
3152
+ return [...fromDirs, ...args.slice(i)];
2733
3153
  }
2734
3154
  if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
2735
3155
  if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
@@ -2764,6 +3184,140 @@ function baseWord(w) {
2764
3184
  }
2765
3185
  var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
2766
3186
  var positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
3187
+ function namesFlag(name, candidates, known) {
3188
+ if (candidates.has(name)) return true;
3189
+ if (!name.startsWith("--") || name.length < 3) return false;
3190
+ if (known?.has(name)) return false;
3191
+ for (const c of candidates) if (c.startsWith(name)) return true;
3192
+ return false;
3193
+ }
3194
+ function knownLongFlags(verb) {
3195
+ const out = /* @__PURE__ */ new Set();
3196
+ const shape = PATTERN_VERBS[verb];
3197
+ if (shape) {
3198
+ for (const set of [shape.takesValue, shape.noValue, shape.patternFlags, shape.noPatternFlags])
3199
+ for (const f of set) if (f.startsWith("--")) out.add(f);
3200
+ }
3201
+ for (const f of FILE_OPERAND_FLAGS[verb] ?? []) if (f.startsWith("--")) out.add(f);
3202
+ return out;
3203
+ }
3204
+ function flagNamesOf(token, shape) {
3205
+ if (token.startsWith("--")) {
3206
+ const eq = token.indexOf("=");
3207
+ return [eq > 0 ? token.slice(0, eq) : token];
3208
+ }
3209
+ const out = [];
3210
+ for (const c of token.slice(1)) {
3211
+ const name = `-${c}`;
3212
+ out.push(name);
3213
+ if (shape?.takesValue.has(name)) break;
3214
+ }
3215
+ return out;
3216
+ }
3217
+ var NONE = { kind: "none" };
3218
+ var UNKNOWN = { kind: "unknown" };
3219
+ function flagEffect(token, shape, known) {
3220
+ if (token === "--") return NONE;
3221
+ if (/^-+$/.test(token)) return UNKNOWN;
3222
+ if (/^-\d+$/.test(token)) return NONE;
3223
+ if (token.startsWith("--")) {
3224
+ if (token.includes("=")) return NONE;
3225
+ const takes = namesFlag(token, shape.takesValue, known);
3226
+ const none = namesFlag(token, shape.noValue, known);
3227
+ if (takes && none) return UNKNOWN;
3228
+ if (takes) return { kind: "takes", flag: token };
3229
+ if (none) return NONE;
3230
+ return UNKNOWN;
3231
+ }
3232
+ const letters = token.slice(1);
3233
+ if (!letters) return NONE;
3234
+ for (let i = 0; i < letters.length; i++) {
3235
+ const name = `-${letters[i]}`;
3236
+ if (shape.takesValue.has(name)) {
3237
+ return i === letters.length - 1 ? { kind: "takes", flag: name } : NONE;
3238
+ }
3239
+ if (!shape.noValue.has(name)) return UNKNOWN;
3240
+ }
3241
+ return NONE;
3242
+ }
3243
+ function readTargets(verb, args, flags, words = [], from = 1) {
3244
+ const shape = PATTERN_VERBS[verb];
3245
+ if (!shape) return args.map((a) => a.value);
3246
+ const known = knownLongFlags(verb);
3247
+ const names = flags.flatMap((f) => flagNamesOf(f, shape));
3248
+ const patternElsewhere = names.some(
3249
+ (n) => namesFlag(n, shape.patternFlags, known) || namesFlag(n, shape.noPatternFlags, known)
3250
+ );
3251
+ const excused = /* @__PURE__ */ new Set();
3252
+ const fileFlags = FILE_OPERAND_FLAGS[verb];
3253
+ const optionsEnd = words.findIndex((w, i) => i >= from && w === "--");
3254
+ const pastOptions = (a) => optionsEnd >= 0 && a.argv > optionsEnd;
3255
+ for (const a of args) {
3256
+ if (a.afterFlag === null || pastOptions(a)) continue;
3257
+ const e = flagEffect(a.afterFlag, shape, known);
3258
+ if (e.kind !== "takes") continue;
3259
+ if (fileFlags && namesFlag(e.flag, fileFlags, known)) continue;
3260
+ excused.add(a);
3261
+ }
3262
+ const patternArgv = (() => {
3263
+ for (let i = from; i < words.length; i++) {
3264
+ const w = words[i];
3265
+ if (w === null) return -1;
3266
+ if (w === "--") return i + 1;
3267
+ if (w.startsWith("-")) {
3268
+ const e = flagEffect(w, shape, known);
3269
+ if (e.kind === "takes") i += 1;
3270
+ else if (e.kind === "unknown") return -1;
3271
+ continue;
3272
+ }
3273
+ return i;
3274
+ }
3275
+ return -1;
3276
+ })();
3277
+ if (!patternElsewhere && patternArgv >= 0) {
3278
+ const a = args.find((x) => x.argv === patternArgv);
3279
+ if (a) excused.add(a);
3280
+ }
3281
+ return args.filter((a) => !excused.has(a)).map((a) => a.value);
3282
+ }
3283
+ function flagOperandFiles(verb, words, from) {
3284
+ const flags = FILE_OPERAND_FLAGS[verb];
3285
+ if (!flags) return [];
3286
+ const known = knownLongFlags(verb);
3287
+ const out = [];
3288
+ for (let i = from; i < words.length; i++) {
3289
+ const w = words[i];
3290
+ if (w === null || !w.startsWith("-") || w === "--") continue;
3291
+ if (w.startsWith("--")) {
3292
+ const eq = w.indexOf("=");
3293
+ if (eq <= 0) continue;
3294
+ const name = w.slice(0, eq);
3295
+ const value = w.slice(eq + 1);
3296
+ if (!value) continue;
3297
+ if (namesFlag(name, flags, known)) {
3298
+ out.push(value);
3299
+ continue;
3300
+ }
3301
+ const shape2 = PATTERN_VERBS[verb];
3302
+ if (!shape2) continue;
3303
+ const recognised = namesFlag(name, shape2.takesValue, known) || namesFlag(name, shape2.noValue, known) || namesFlag(name, shape2.patternFlags, known) || namesFlag(name, shape2.noPatternFlags, known);
3304
+ if (!recognised) out.push(value);
3305
+ continue;
3306
+ }
3307
+ const shape = PATTERN_VERBS[verb];
3308
+ const letters = w.slice(1);
3309
+ for (let j = 0; j < letters.length; j++) {
3310
+ const name = `-${letters[j]}`;
3311
+ const isFileFlag = flags.has(name);
3312
+ const argTaking = isFileFlag || (shape?.takesValue.has(name) ?? false) || (READER_VALUE_LETTERS[verb] ?? []).includes(letters[j]);
3313
+ if (!argTaking) continue;
3314
+ const attached = letters.slice(j + 1);
3315
+ if (isFileFlag && attached) out.push(attached);
3316
+ break;
3317
+ }
3318
+ }
3319
+ return out;
3320
+ }
2767
3321
  function wrappedReadPaths(words, name) {
2768
3322
  if (name === "find") {
2769
3323
  const { k, starts } = findStartPoints(words, 0);
@@ -2771,7 +3325,14 @@ function wrappedReadPaths(words, name) {
2771
3325
  }
2772
3326
  if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
2773
3327
  const h = unwrapCommandHead(words);
2774
- return h > 0 && isReaderWord(words[h] ?? null) ? positionalAfter(words, h + 1) : null;
3328
+ if (h <= 0 || !isReaderWord(words[h] ?? null)) return null;
3329
+ const head = baseWord(words[h]);
3330
+ const rest = words.slice(h + 1);
3331
+ const restFlags = rest.filter((w) => w !== null && w.startsWith("-"));
3332
+ return [
3333
+ ...readTargets(head, positionedArgs(words, h + 1), restFlags, words, h + 1),
3334
+ ...flagOperandFiles(head, words, h + 1)
3335
+ ];
2775
3336
  }
2776
3337
  function literalShellPayload(words, name) {
2777
3338
  const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
@@ -3893,6 +4454,15 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3893
4454
  function isIgnoredTool(toolName, config) {
3894
4455
  return matchesPattern(toolName, config.policy.ignoredTools);
3895
4456
  }
4457
+ var TERMINAL_ESCAPE_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
4458
+ function stripTerminalEscapes(s) {
4459
+ return s.replace(TERMINAL_ESCAPE_RE, "");
4460
+ }
4461
+ function safeMessage(value, max = 300) {
4462
+ const raw = typeof value === "string" ? value : value instanceof Error ? value.message : String(value ?? "");
4463
+ const s = stripTerminalEscapes(raw).replace(/\s+/g, " ").trim();
4464
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
4465
+ }
3896
4466
  var aws_default = {
3897
4467
  name: "aws",
3898
4468
  description: "Protects AWS infrastructure from destructive AI operations",
@@ -6892,7 +7462,7 @@ function escapePango(text) {
6892
7462
  function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
6893
7463
  const lines = [];
6894
7464
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
6895
- const safeAgent = (agent ?? "AI Agent").replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80);
7465
+ const safeAgent = safeMessage(agent ?? "AI Agent", 80);
6896
7466
  lines.push(`\u{1F916} ${safeAgent} | \u{1F527} ${toolName}`);
6897
7467
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
6898
7468
  if (ruleDescription) lines.push(`\u2139 ${ruleDescription}`);
@@ -7220,14 +7790,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
7220
7790
  if (!res.ok) {
7221
7791
  fs11.appendFileSync(
7222
7792
  HOOK_DEBUG_LOG,
7223
- `[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
7793
+ `[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
7224
7794
  `
7225
7795
  );
7226
7796
  }
7227
7797
  } catch (err) {
7228
7798
  fs11.appendFileSync(
7229
7799
  HOOK_DEBUG_LOG,
7230
- `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
7800
+ `[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
7231
7801
  `
7232
7802
  );
7233
7803
  }
@@ -7328,9 +7898,8 @@ async function authorizeHeadless(toolName, args, meta, options) {
7328
7898
  if (!options?.calledFromDaemon) {
7329
7899
  const actId = randomUUID();
7330
7900
  const actTs = Date.now();
7331
- const stripAnsi = (s) => s.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "");
7332
- const sanitizedAgent = meta?.agent ? stripAnsi(meta.agent).slice(0, 80) : void 0;
7333
- const sanitizedMcpServer = meta?.mcpServer ? stripAnsi(meta.mcpServer).slice(0, 40) : void 0;
7901
+ const sanitizedAgent = meta?.agent ? safeMessage(meta.agent, 80) : void 0;
7902
+ const sanitizedMcpServer = meta?.mcpServer ? safeMessage(meta.mcpServer, 40) : void 0;
7334
7903
  const socketOk = await notifyActivity({
7335
7904
  id: actId,
7336
7905
  ts: actTs,