@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.js CHANGED
@@ -1510,6 +1510,19 @@ function parseShared(command) {
1510
1510
  astCache.set(command, parsed);
1511
1511
  return parsed;
1512
1512
  }
1513
+ function byteOffsetToCharIndex(command) {
1514
+ if (!/[^\u0000-\u007F]/.test(command)) return null;
1515
+ const map = /* @__PURE__ */ new Map();
1516
+ let byte = 0;
1517
+ for (let i = 0; i < command.length; ) {
1518
+ map.set(byte, i);
1519
+ const cp = command.codePointAt(i);
1520
+ byte += cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
1521
+ i += cp > 65535 ? 2 : 1;
1522
+ }
1523
+ map.set(byte, command.length);
1524
+ return (b) => map.get(b) ?? -1;
1525
+ }
1513
1526
  function cachedNormalize(command, compute) {
1514
1527
  const hit = normalizeCache.get(command);
1515
1528
  if (hit !== void 0) {
@@ -1539,6 +1552,8 @@ function normalizeCommandForPolicyImpl(command) {
1539
1552
  const f = parseShared(command);
1540
1553
  if (f === PARSE_FAIL) return { posix: command, separator: command };
1541
1554
  try {
1555
+ const toCharIndex = byteOffsetToCharIndex(command);
1556
+ const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
1542
1557
  const strips = [];
1543
1558
  const rewrites = [];
1544
1559
  const quoteOnlyRewrites = [];
@@ -1559,8 +1574,9 @@ function normalizeCommandForPolicyImpl(command) {
1559
1574
  const quotedNode = nextParts[0];
1560
1575
  const nt = syntax.NodeType(quotedNode);
1561
1576
  const markStrip = () => {
1562
- const s = next.Pos().Offset();
1563
- const e = next.End().Offset();
1577
+ const s = at(next.Pos().Offset());
1578
+ const e = at(next.End().Offset());
1579
+ if (s < 0 || e < 0) return;
1564
1580
  strips.push([s, e]);
1565
1581
  msgSpans.add(`${s}:${e}`);
1566
1582
  };
@@ -1577,8 +1593,9 @@ function normalizeCommandForPolicyImpl(command) {
1577
1593
  }
1578
1594
  }
1579
1595
  for (const arg of args) {
1580
- const s = arg.Pos().Offset();
1581
- const e = arg.End().Offset();
1596
+ const s = at(arg.Pos().Offset());
1597
+ const e = at(arg.End().Offset());
1598
+ if (s < 0 || e < 0) continue;
1582
1599
  if (msgSpans.has(`${s}:${e}`)) continue;
1583
1600
  const resolved = resolveWordLiteral(arg);
1584
1601
  if (resolved === null) continue;
@@ -1700,7 +1717,345 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
1700
1717
  "nl",
1701
1718
  "dd"
1702
1719
  ]);
1720
+ var GREP_SHAPE = {
1721
+ takesValue: /* @__PURE__ */ new Set([
1722
+ "-A",
1723
+ "-B",
1724
+ "-C",
1725
+ "-D",
1726
+ "-d",
1727
+ "-e",
1728
+ "-f",
1729
+ "-m",
1730
+ "--after-context",
1731
+ "--before-context",
1732
+ "--binary-files",
1733
+ "--context",
1734
+ "--devices",
1735
+ "--directories",
1736
+ "--exclude",
1737
+ "--exclude-dir",
1738
+ "--exclude-from",
1739
+ "--file",
1740
+ "--include",
1741
+ "--label",
1742
+ "--max-count",
1743
+ "--regexp"
1744
+ ]),
1745
+ noValue: /* @__PURE__ */ new Set([
1746
+ "-E",
1747
+ "-F",
1748
+ "-G",
1749
+ "-P",
1750
+ "-i",
1751
+ "-y",
1752
+ "-v",
1753
+ "-V",
1754
+ "-w",
1755
+ "-x",
1756
+ "-c",
1757
+ "-l",
1758
+ "-L",
1759
+ "-o",
1760
+ "-q",
1761
+ "-s",
1762
+ "-b",
1763
+ "-H",
1764
+ "-h",
1765
+ "-n",
1766
+ "-T",
1767
+ "-Z",
1768
+ "-z",
1769
+ "-R",
1770
+ "-r",
1771
+ "-U",
1772
+ "-u",
1773
+ "-I",
1774
+ "-a",
1775
+ "--basic-regexp",
1776
+ "--binary",
1777
+ "--byte-offset",
1778
+ "--color",
1779
+ "--colour",
1780
+ "--count",
1781
+ "--dereference-recursive",
1782
+ "--extended-regexp",
1783
+ "--files-with-matches",
1784
+ "--files-without-match",
1785
+ "--fixed-strings",
1786
+ "--help",
1787
+ "--ignore-case",
1788
+ "--initial-tab",
1789
+ "--invert-match",
1790
+ "--line-buffered",
1791
+ "--line-number",
1792
+ "--line-regexp",
1793
+ "--no-filename",
1794
+ "--no-group-separator",
1795
+ "--no-ignore-case",
1796
+ "--no-messages",
1797
+ "--null",
1798
+ "--null-data",
1799
+ "--only-matching",
1800
+ "--perl-regexp",
1801
+ "--quiet",
1802
+ "--recursive",
1803
+ "--silent",
1804
+ "--text",
1805
+ "--version",
1806
+ "--with-filename",
1807
+ "--word-regexp"
1808
+ ]),
1809
+ patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
1810
+ noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file"])
1811
+ };
1812
+ var PATTERN_VERBS = {
1813
+ grep: GREP_SHAPE,
1814
+ // /usr/bin/egrep and /usr/bin/fgrep are 41-byte shell wrappers that exec
1815
+ // `grep -E` and `grep -F`. Read in full, not assumed.
1816
+ egrep: GREP_SHAPE,
1817
+ fgrep: GREP_SHAPE,
1818
+ // ripgrep 14.1.1, complete from its own --help. An earlier comment here said rg
1819
+ // was not installed on the measuring machine; it is, and that stale claim is why
1820
+ // this table shipped incomplete for two rounds, blocking ordinary searches like
1821
+ // `rg --sort-files .env src` (/code-review round 4).
1822
+ rg: {
1823
+ takesValue: /* @__PURE__ */ new Set([
1824
+ "-A",
1825
+ "-B",
1826
+ "-C",
1827
+ "-d",
1828
+ "-E",
1829
+ "-e",
1830
+ "-f",
1831
+ "-g",
1832
+ "-j",
1833
+ "-M",
1834
+ "-m",
1835
+ "-r",
1836
+ "-t",
1837
+ "-T",
1838
+ "--after-context",
1839
+ "--before-context",
1840
+ "--color",
1841
+ "--colors",
1842
+ "--context",
1843
+ "--context-separator",
1844
+ "--dfa-size-limit",
1845
+ "--encoding",
1846
+ "--engine",
1847
+ "--field-context-separator",
1848
+ "--field-match-separator",
1849
+ "--file",
1850
+ "--generate",
1851
+ "--glob",
1852
+ "--hostname-bin",
1853
+ "--hyperlink-format",
1854
+ "--iglob",
1855
+ "--ignore-file",
1856
+ "--max-columns",
1857
+ "--max-count",
1858
+ "--max-depth",
1859
+ // An ALIAS of --max-depth, and it takes a value. The round-5 extraction read
1860
+ // alias lines as plain switches and swept it into noValue, which hard-blocked
1861
+ // `rg --maxdepth 2 .env src` while `--max-depth 2` ran (/code-review round 7).
1862
+ "--maxdepth",
1863
+ "--max-filesize",
1864
+ "--path-separator",
1865
+ "--pre",
1866
+ "--pre-glob",
1867
+ "--regexp",
1868
+ "--regex-size-limit",
1869
+ "--replace",
1870
+ "--sort",
1871
+ "--sortr",
1872
+ "--threads",
1873
+ "--type",
1874
+ "--type-add",
1875
+ "--type-clear",
1876
+ "--type-not"
1877
+ ]),
1878
+ noValue: /* @__PURE__ */ new Set([
1879
+ "-.",
1880
+ "-0",
1881
+ "-a",
1882
+ "-b",
1883
+ "-c",
1884
+ "-F",
1885
+ "-h",
1886
+ "-H",
1887
+ "-i",
1888
+ "-I",
1889
+ "-l",
1890
+ "-L",
1891
+ "-n",
1892
+ "-N",
1893
+ "-o",
1894
+ "-p",
1895
+ "-P",
1896
+ "-q",
1897
+ "-s",
1898
+ "-S",
1899
+ "-u",
1900
+ "-U",
1901
+ "-v",
1902
+ "-V",
1903
+ "-w",
1904
+ "-x",
1905
+ "-z",
1906
+ "--auto-hybrid-regex",
1907
+ "--binary",
1908
+ "--block-buffered",
1909
+ "--byte-offset",
1910
+ "--case-sensitive",
1911
+ "--column",
1912
+ "--count",
1913
+ "--count-matches",
1914
+ "--crlf",
1915
+ "--debug",
1916
+ "--files",
1917
+ "--files-with-matches",
1918
+ "--files-without-match",
1919
+ "--fixed-strings",
1920
+ "--follow",
1921
+ "--glob-case-insensitive",
1922
+ "--heading",
1923
+ "--help",
1924
+ "--hidden",
1925
+ "--ignore-case",
1926
+ "--ignore-file-case-insensitive",
1927
+ "--include-zero",
1928
+ "--invert-match",
1929
+ "--json",
1930
+ "--line-buffered",
1931
+ "--line-number",
1932
+ "--line-regexp",
1933
+ "--max-columns-preview",
1934
+ "--mmap",
1935
+ "--multiline",
1936
+ "--multiline-dotall",
1937
+ "--no-column",
1938
+ "--no-config",
1939
+ "--no-context-separator",
1940
+ "--no-encoding",
1941
+ "--no-filename",
1942
+ "--no-ignore",
1943
+ "--no-ignore-dot",
1944
+ "--no-ignore-exclude",
1945
+ "--no-ignore-files",
1946
+ "--no-ignore-global",
1947
+ "--no-ignore-messages",
1948
+ "--no-ignore-parent",
1949
+ "--no-ignore-vcs",
1950
+ "--no-line-number",
1951
+ "--no-messages",
1952
+ "--no-pcre2-unicode",
1953
+ "--no-pre",
1954
+ "--no-require-git",
1955
+ "--no-unicode",
1956
+ "--null",
1957
+ "--null-data",
1958
+ "--one-file-system",
1959
+ "--only-matching",
1960
+ "--passthru",
1961
+ "--pcre2",
1962
+ "--pcre2-version",
1963
+ "--pretty",
1964
+ "--print0",
1965
+ "--quiet",
1966
+ "--search-zip",
1967
+ "--smart-case",
1968
+ "--sort-files",
1969
+ "--stats",
1970
+ "--stop-on-nonmatch",
1971
+ "--text",
1972
+ "--trace",
1973
+ "--trim",
1974
+ "--type-list",
1975
+ "--unrestricted",
1976
+ "--version",
1977
+ "--vimgrep",
1978
+ "--with-filename",
1979
+ "--word-regexp",
1980
+ // The COMPLETE remainder of `rg --help`, added after /code-review round 5
1981
+ // found 40 documented switches in neither set. The table is now the whole
1982
+ // option list: `rg --help` yields 149 long options, 35 of them value-taking.
1983
+ "--ignore",
1984
+ "--ignore-dot",
1985
+ "--ignore-exclude",
1986
+ "--ignore-files",
1987
+ "--ignore-global",
1988
+ "--ignore-messages",
1989
+ "--ignore-parent",
1990
+ "--ignore-vcs",
1991
+ "--messages",
1992
+ "--no-auto-hybrid-regex",
1993
+ "--no-binary",
1994
+ "--no-block-buffered",
1995
+ "--no-byte-offset",
1996
+ "--no-crlf",
1997
+ "--no-fixed-strings",
1998
+ "--no-follow",
1999
+ "--no-glob-case-insensitive",
2000
+ "--no-heading",
2001
+ "--no-hidden",
2002
+ "--no-ignore-file-case-insensitive",
2003
+ "--no-include-zero",
2004
+ "--no-invert-match",
2005
+ "--no-json",
2006
+ "--no-line-buffered",
2007
+ "--no-max-columns-preview",
2008
+ "--no-mmap",
2009
+ "--no-multiline",
2010
+ "--no-multiline-dotall",
2011
+ "--no-one-file-system",
2012
+ "--no-pcre2",
2013
+ "--no-search-zip",
2014
+ "--no-sort-files",
2015
+ "--no-stats",
2016
+ "--no-text",
2017
+ "--no-trim",
2018
+ "--passthrough",
2019
+ "--pcre2-unicode",
2020
+ "--require-git",
2021
+ "--unicode"
2022
+ ]),
2023
+ patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
2024
+ // `--files` and `--type-list` list or enumerate without a pattern. Founder
2025
+ // decision 2026-09-13: `rg --files ~/.ssh` stays BLOCKED, which this achieves
2026
+ // by leaving the directory in the judged list.
2027
+ noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file", "--files", "--type-list", "--pcre2-version"])
2028
+ }
2029
+ };
2030
+ var PATTERN_VERB_NAMES = Object.keys(PATTERN_VERBS);
2031
+ var GREP_FILE_OPERANDS = /* @__PURE__ */ new Set(["-f", "--file", "--exclude-from", "--include"]);
2032
+ var READER_VALUE_LETTERS = {
2033
+ awk: ["F", "v", "f"],
2034
+ gawk: ["F", "v", "f", "e", "E", "i", "l", "o", "p", "D"],
2035
+ sed: ["e", "f", "i", "l"],
2036
+ sort: ["C", "k", "o", "S", "t", "T"]
2037
+ };
2038
+ var FILE_OPERAND_FLAGS = {
2039
+ grep: GREP_FILE_OPERANDS,
2040
+ egrep: GREP_FILE_OPERANDS,
2041
+ fgrep: GREP_FILE_OPERANDS,
2042
+ // rg and gawk are NOT installed on the measuring machine: these two rows are
2043
+ // from the shipped documentation (ripgrep `-f/--file`, `--ignore-file`; gawk
2044
+ // `-f/--file`) and are marked as such in the spec.
2045
+ // `-g/--glob/--iglob` name which files ripgrep SEARCHES, so an operand naming
2046
+ // a credential makes rg open it -- the same reason grep's `--include` is here.
2047
+ // Measured (/code-review round 3): `rg --hidden -g .env AWS tree` opened .env
2048
+ // and printed its contents.
2049
+ rg: /* @__PURE__ */ new Set(["-f", "--file", "--ignore-file", "-g", "--glob", "--iglob"]),
2050
+ sed: /* @__PURE__ */ new Set(["-f", "--file"]),
2051
+ awk: /* @__PURE__ */ new Set(["-f", "--file"]),
2052
+ gawk: /* @__PURE__ */ new Set(["-f", "--file"]),
2053
+ sort: /* @__PURE__ */ new Set(["--files0-from"])
2054
+ };
1703
2055
  var SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
2056
+ var TAR_VALUE_LETTERS = ["b", "C", "f", "F", "g", "H", "I", "K", "L", "N", "T", "V", "X"];
2057
+ var ZIP_VALUE_LETTERS = ["b", "n", "P", "t", "s", "O", "x", "i"];
2058
+ var RSYNC_VALUE_LETTERS = ["e", "f", "T", "B", "M"];
1704
2059
  var RSYNC_SKIP = [
1705
2060
  "e",
1706
2061
  "--rsh",
@@ -1712,21 +2067,35 @@ var RSYNC_SKIP = [
1712
2067
  "f",
1713
2068
  "--filter"
1714
2069
  ];
2070
+ var CP_VALUE_LETTERS = ["S", "t"];
2071
+ var INSTALL_VALUE_LETTERS = ["S", "t", "g", "m", "o"];
1715
2072
  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 },
2073
+ cp: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
2074
+ mv: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
2075
+ install: { source: "allButLast", targetDirFlag: true, valueLetters: INSTALL_VALUE_LETTERS },
2076
+ ln: { source: "first", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
2077
+ scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS, valueLetters: SCP_VALUE_FLAGS },
2078
+ rsync: { source: "allButLast", skipFlags: RSYNC_SKIP, valueLetters: RSYNC_VALUE_LETTERS },
1722
2079
  tar: {
1723
2080
  source: "archive",
1724
2081
  archive: "tar",
1725
- skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
2082
+ skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"],
2083
+ valueLetters: TAR_VALUE_LETTERS
2084
+ },
2085
+ zip: {
2086
+ source: "archive",
2087
+ archive: "zip",
2088
+ skipFlags: ["x", "i", "--exclude", "--include"],
2089
+ valueLetters: ZIP_VALUE_LETTERS
1726
2090
  },
1727
- zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
1728
2091
  ar: { source: "archive", archive: "ar" },
1729
- "7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
2092
+ // 7z switches are INLINE only (`-mx9`, `-px`, `-x!pat`), so no following word is
2093
+ // ever a switch's operand -- which is why the short `x` is NOT a skipFlag here
2094
+ // and valueLetters is empty. Keeping the short `x` made `7z a out.7z -mx KEY`
2095
+ // drop the credential as an exclusion operand (/code-review round 8), and the
2096
+ // derived "every skipped letter is a value letter" row in jail-copy.spec.ts is
2097
+ // what keeps the two tables honest about it.
2098
+ "7z": { source: "archive", archive: "7z", skipFlags: ["--exclude"], valueLetters: [] },
1730
2099
  gzip: { source: "all" },
1731
2100
  bzip2: { source: "all" },
1732
2101
  xz: { source: "all" },
@@ -2586,7 +2955,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
2586
2955
  return result?.verdict !== "block";
2587
2956
  }
2588
2957
  if (nodeType !== "CallExpr") return true;
2589
- const { name, flags, paths, words } = extractLiteralArgs(n);
2958
+ const { name, flags, paths, words, args } = extractLiteralArgs(n);
2590
2959
  if (!name) return true;
2591
2960
  if (name === "rm") {
2592
2961
  const flagStr = flags.join("").toLowerCase();
@@ -2626,7 +2995,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
2626
2995
  return true;
2627
2996
  }
2628
2997
  }
2629
- const readPaths = FS_READ_TOOLS.has(name) ? paths : wrappedReadPaths(words, name);
2998
+ const readPaths = FS_READ_TOOLS.has(name) ? [...readTargets(name, args, flags, words, 1), ...flagOperandFiles(name, words, 1)] : wrappedReadPaths(words, name);
2630
2999
  if (readPaths) {
2631
3000
  for (const p of readPaths) {
2632
3001
  result = stricter(result, matchSensitivePath2(p));
@@ -2662,9 +3031,22 @@ function flagIs(w, names) {
2662
3031
  const f = flagInfo(w);
2663
3032
  return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
2664
3033
  }
2665
- function operandOf(a, names) {
3034
+ function operandOf(a, names, valueLetters) {
2666
3035
  if (!names || a.afterFlag === null) return false;
2667
- return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
3036
+ const w = a.afterFlag;
3037
+ if (!w.startsWith("--") && valueLetters) {
3038
+ const letters = w.slice(1);
3039
+ for (let i = 0; i < letters.length; i++) {
3040
+ if (!valueLetters.includes(letters[i])) continue;
3041
+ return i === letters.length - 1 && names.includes(letters[i]);
3042
+ }
3043
+ return false;
3044
+ }
3045
+ if (w.startsWith("--") && !w.includes("=")) {
3046
+ const longs = names.filter((n) => n.startsWith("--"));
3047
+ if (longs.some((n) => n === w || w.length >= 3 && n.startsWith(w))) return true;
3048
+ }
3049
+ return flagIs(w, names) && flagInfo(w).attached === null;
2668
3050
  }
2669
3051
  function resolveCopyShape(words, h) {
2670
3052
  const verb = baseWord(words[h]);
@@ -2689,7 +3071,7 @@ function findStartPoints(words, h) {
2689
3071
  const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
2690
3072
  if (k < 0) return { k, starts: [] };
2691
3073
  const firstPredicate = words.findIndex(
2692
- (w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
3074
+ (w, i) => i > h && w !== null && w !== "--" && w.startsWith("-") && !FIND_OPTIONS.has(w)
2693
3075
  );
2694
3076
  const end = firstPredicate > h ? firstPredicate : k;
2695
3077
  return { k, starts: positionalAfter(words, h + 1, end) };
@@ -2709,14 +3091,35 @@ function copySourcePaths(words) {
2709
3091
  const { shape, last } = r;
2710
3092
  const args = positionedArgs(words, last + 1);
2711
3093
  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"]);
3094
+ const copyOptionsEnd = tail.findIndex((w) => w === "--");
3095
+ const copyPastOptions = (a) => copyOptionsEnd >= 0 && a.argv > last + 1 + copyOptionsEnd;
3096
+ const skipped = (a) => !copyPastOptions(a) && operandOf(a, shape.skipFlags, shape.valueLetters);
3097
+ const firstValueLetter = (w) => {
3098
+ const letters = w.slice(1);
3099
+ for (let i = 0; i < letters.length; i++) {
3100
+ if ((shape.valueLetters ?? []).includes(letters[i]))
3101
+ return { letter: letters[i], last: i === letters.length - 1 };
3102
+ }
3103
+ return null;
3104
+ };
3105
+ const isTargetDirFlag = (w) => {
3106
+ if (w.startsWith("--")) {
3107
+ const name = w.includes("=") ? w.slice(0, w.indexOf("=")) : w;
3108
+ return name.length >= 3 && "--target-directory".startsWith(name);
3109
+ }
3110
+ return firstValueLetter(w)?.letter === "t";
3111
+ };
3112
+ const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && isTargetDirFlag(w));
3113
+ const targetTakesNextWord = (w) => {
3114
+ if (w.startsWith("--"))
3115
+ return !w.includes("=") && w.length >= 3 && "--target-directory".startsWith(w);
3116
+ const f = firstValueLetter(w);
3117
+ return f !== null && f.letter === "t" && f.last;
3118
+ };
3119
+ const targetOperand = (a) => targetDir && a.afterFlag !== null && !copyPastOptions(a) && targetTakesNextWord(a.afterFlag);
2715
3120
  const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
2716
3121
  const dynamicDest = lastOperand === null;
2717
3122
  const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
2718
- if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
2719
- return [];
2720
3123
  let src;
2721
3124
  switch (shape.source) {
2722
3125
  case "all":
@@ -2726,11 +3129,17 @@ function copySourcePaths(words) {
2726
3129
  src = targetDir ? args : args.slice(0, 1);
2727
3130
  break;
2728
3131
  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);
3132
+ const namesSource = (f) => {
3133
+ const names = shape.sourceFlags ?? [];
3134
+ if (f.long !== null)
3135
+ return names.some(
3136
+ (n) => n.startsWith("--") && (n === f.long || f.long.length >= 3 && n.startsWith(f.long))
3137
+ );
3138
+ return f.letter !== null && names.includes(f.letter);
3139
+ };
3140
+ const inline = tail.filter((w) => w !== null && w.startsWith("-")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && namesSource(f)).map((f) => f.attached);
2730
3141
  return [
2731
- ...args.filter(
2732
- (a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
2733
- ).map((a) => a.value),
3142
+ ...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => a.value),
2734
3143
  ...inline
2735
3144
  ];
2736
3145
  }
@@ -2741,7 +3150,14 @@ function copySourcePaths(words) {
2741
3150
  src = targetDir || dynamicDest ? args : args.slice(0, -1);
2742
3151
  break;
2743
3152
  }
2744
- return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
3153
+ const sources = src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
3154
+ if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand)) {
3155
+ const jailedSources = sources.filter((p) => matchSensitivePath2(p));
3156
+ const dirOf = (p) => /[\\/]/.test(p) ? p.replace(/[\\/][^\\/]*$/, "") : "";
3157
+ if (jailedSources.length === 0) return [];
3158
+ if (jailedSources.every((p) => dirOf(p) === dirOf(lastOperand))) return [];
3159
+ }
3160
+ return sources;
2745
3161
  }
2746
3162
  function archiveInputs(kind, args, tail) {
2747
3163
  const first = args[0];
@@ -2753,13 +3169,17 @@ function archiveInputs(kind, args, tail) {
2753
3169
  const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
2754
3170
  if (extracting && !writing) return [];
2755
3171
  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);
3172
+ if (!bareKey) return args;
3173
+ let i = 1;
3174
+ const fromDirs = [];
3175
+ for (const ch of first.value) {
3176
+ if (!TAR_VALUE_LETTERS.includes(ch)) continue;
3177
+ const operand = args[i];
3178
+ if (!operand || operand.afterFlag !== null) break;
3179
+ i += 1;
3180
+ if (ch === "C") fromDirs.push(operand);
3181
+ }
3182
+ return [...fromDirs, ...args.slice(i)];
2763
3183
  }
2764
3184
  if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
2765
3185
  if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
@@ -2794,6 +3214,140 @@ function baseWord(w) {
2794
3214
  }
2795
3215
  var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
2796
3216
  var positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
3217
+ function namesFlag(name, candidates, known) {
3218
+ if (candidates.has(name)) return true;
3219
+ if (!name.startsWith("--") || name.length < 3) return false;
3220
+ if (known?.has(name)) return false;
3221
+ for (const c of candidates) if (c.startsWith(name)) return true;
3222
+ return false;
3223
+ }
3224
+ function knownLongFlags(verb) {
3225
+ const out = /* @__PURE__ */ new Set();
3226
+ const shape = PATTERN_VERBS[verb];
3227
+ if (shape) {
3228
+ for (const set of [shape.takesValue, shape.noValue, shape.patternFlags, shape.noPatternFlags])
3229
+ for (const f of set) if (f.startsWith("--")) out.add(f);
3230
+ }
3231
+ for (const f of FILE_OPERAND_FLAGS[verb] ?? []) if (f.startsWith("--")) out.add(f);
3232
+ return out;
3233
+ }
3234
+ function flagNamesOf(token, shape) {
3235
+ if (token.startsWith("--")) {
3236
+ const eq = token.indexOf("=");
3237
+ return [eq > 0 ? token.slice(0, eq) : token];
3238
+ }
3239
+ const out = [];
3240
+ for (const c of token.slice(1)) {
3241
+ const name = `-${c}`;
3242
+ out.push(name);
3243
+ if (shape?.takesValue.has(name)) break;
3244
+ }
3245
+ return out;
3246
+ }
3247
+ var NONE = { kind: "none" };
3248
+ var UNKNOWN = { kind: "unknown" };
3249
+ function flagEffect(token, shape, known) {
3250
+ if (token === "--") return NONE;
3251
+ if (/^-+$/.test(token)) return UNKNOWN;
3252
+ if (/^-\d+$/.test(token)) return NONE;
3253
+ if (token.startsWith("--")) {
3254
+ if (token.includes("=")) return NONE;
3255
+ const takes = namesFlag(token, shape.takesValue, known);
3256
+ const none = namesFlag(token, shape.noValue, known);
3257
+ if (takes && none) return UNKNOWN;
3258
+ if (takes) return { kind: "takes", flag: token };
3259
+ if (none) return NONE;
3260
+ return UNKNOWN;
3261
+ }
3262
+ const letters = token.slice(1);
3263
+ if (!letters) return NONE;
3264
+ for (let i = 0; i < letters.length; i++) {
3265
+ const name = `-${letters[i]}`;
3266
+ if (shape.takesValue.has(name)) {
3267
+ return i === letters.length - 1 ? { kind: "takes", flag: name } : NONE;
3268
+ }
3269
+ if (!shape.noValue.has(name)) return UNKNOWN;
3270
+ }
3271
+ return NONE;
3272
+ }
3273
+ function readTargets(verb, args, flags, words = [], from = 1) {
3274
+ const shape = PATTERN_VERBS[verb];
3275
+ if (!shape) return args.map((a) => a.value);
3276
+ const known = knownLongFlags(verb);
3277
+ const names = flags.flatMap((f) => flagNamesOf(f, shape));
3278
+ const patternElsewhere = names.some(
3279
+ (n) => namesFlag(n, shape.patternFlags, known) || namesFlag(n, shape.noPatternFlags, known)
3280
+ );
3281
+ const excused = /* @__PURE__ */ new Set();
3282
+ const fileFlags = FILE_OPERAND_FLAGS[verb];
3283
+ const optionsEnd = words.findIndex((w, i) => i >= from && w === "--");
3284
+ const pastOptions = (a) => optionsEnd >= 0 && a.argv > optionsEnd;
3285
+ for (const a of args) {
3286
+ if (a.afterFlag === null || pastOptions(a)) continue;
3287
+ const e = flagEffect(a.afterFlag, shape, known);
3288
+ if (e.kind !== "takes") continue;
3289
+ if (fileFlags && namesFlag(e.flag, fileFlags, known)) continue;
3290
+ excused.add(a);
3291
+ }
3292
+ const patternArgv = (() => {
3293
+ for (let i = from; i < words.length; i++) {
3294
+ const w = words[i];
3295
+ if (w === null) return -1;
3296
+ if (w === "--") return i + 1;
3297
+ if (w.startsWith("-")) {
3298
+ const e = flagEffect(w, shape, known);
3299
+ if (e.kind === "takes") i += 1;
3300
+ else if (e.kind === "unknown") return -1;
3301
+ continue;
3302
+ }
3303
+ return i;
3304
+ }
3305
+ return -1;
3306
+ })();
3307
+ if (!patternElsewhere && patternArgv >= 0) {
3308
+ const a = args.find((x) => x.argv === patternArgv);
3309
+ if (a) excused.add(a);
3310
+ }
3311
+ return args.filter((a) => !excused.has(a)).map((a) => a.value);
3312
+ }
3313
+ function flagOperandFiles(verb, words, from) {
3314
+ const flags = FILE_OPERAND_FLAGS[verb];
3315
+ if (!flags) return [];
3316
+ const known = knownLongFlags(verb);
3317
+ const out = [];
3318
+ for (let i = from; i < words.length; i++) {
3319
+ const w = words[i];
3320
+ if (w === null || !w.startsWith("-") || w === "--") continue;
3321
+ if (w.startsWith("--")) {
3322
+ const eq = w.indexOf("=");
3323
+ if (eq <= 0) continue;
3324
+ const name = w.slice(0, eq);
3325
+ const value = w.slice(eq + 1);
3326
+ if (!value) continue;
3327
+ if (namesFlag(name, flags, known)) {
3328
+ out.push(value);
3329
+ continue;
3330
+ }
3331
+ const shape2 = PATTERN_VERBS[verb];
3332
+ if (!shape2) continue;
3333
+ const recognised = namesFlag(name, shape2.takesValue, known) || namesFlag(name, shape2.noValue, known) || namesFlag(name, shape2.patternFlags, known) || namesFlag(name, shape2.noPatternFlags, known);
3334
+ if (!recognised) out.push(value);
3335
+ continue;
3336
+ }
3337
+ const shape = PATTERN_VERBS[verb];
3338
+ const letters = w.slice(1);
3339
+ for (let j = 0; j < letters.length; j++) {
3340
+ const name = `-${letters[j]}`;
3341
+ const isFileFlag = flags.has(name);
3342
+ const argTaking = isFileFlag || (shape?.takesValue.has(name) ?? false) || (READER_VALUE_LETTERS[verb] ?? []).includes(letters[j]);
3343
+ if (!argTaking) continue;
3344
+ const attached = letters.slice(j + 1);
3345
+ if (isFileFlag && attached) out.push(attached);
3346
+ break;
3347
+ }
3348
+ }
3349
+ return out;
3350
+ }
2797
3351
  function wrappedReadPaths(words, name) {
2798
3352
  if (name === "find") {
2799
3353
  const { k, starts } = findStartPoints(words, 0);
@@ -2801,7 +3355,14 @@ function wrappedReadPaths(words, name) {
2801
3355
  }
2802
3356
  if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
2803
3357
  const h = unwrapCommandHead(words);
2804
- return h > 0 && isReaderWord(words[h] ?? null) ? positionalAfter(words, h + 1) : null;
3358
+ if (h <= 0 || !isReaderWord(words[h] ?? null)) return null;
3359
+ const head = baseWord(words[h]);
3360
+ const rest = words.slice(h + 1);
3361
+ const restFlags = rest.filter((w) => w !== null && w.startsWith("-"));
3362
+ return [
3363
+ ...readTargets(head, positionedArgs(words, h + 1), restFlags, words, h + 1),
3364
+ ...flagOperandFiles(head, words, h + 1)
3365
+ ];
2805
3366
  }
2806
3367
  function literalShellPayload(words, name) {
2807
3368
  const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
@@ -3923,6 +4484,15 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3923
4484
  function isIgnoredTool(toolName, config) {
3924
4485
  return matchesPattern(toolName, config.policy.ignoredTools);
3925
4486
  }
4487
+ var TERMINAL_ESCAPE_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
4488
+ function stripTerminalEscapes(s) {
4489
+ return s.replace(TERMINAL_ESCAPE_RE, "");
4490
+ }
4491
+ function safeMessage(value, max = 300) {
4492
+ const raw = typeof value === "string" ? value : value instanceof Error ? value.message : String(value ?? "");
4493
+ const s = stripTerminalEscapes(raw).replace(/\s+/g, " ").trim();
4494
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
4495
+ }
3926
4496
  var aws_default = {
3927
4497
  name: "aws",
3928
4498
  description: "Protects AWS infrastructure from destructive AI operations",
@@ -6922,7 +7492,7 @@ function escapePango(text) {
6922
7492
  function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
6923
7493
  const lines = [];
6924
7494
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
6925
- const safeAgent = (agent ?? "AI Agent").replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80);
7495
+ const safeAgent = safeMessage(agent ?? "AI Agent", 80);
6926
7496
  lines.push(`\u{1F916} ${safeAgent} | \u{1F527} ${toolName}`);
6927
7497
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
6928
7498
  if (ruleDescription) lines.push(`\u2139 ${ruleDescription}`);
@@ -7250,14 +7820,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
7250
7820
  if (!res.ok) {
7251
7821
  import_fs11.default.appendFileSync(
7252
7822
  HOOK_DEBUG_LOG,
7253
- `[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
7823
+ `[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
7254
7824
  `
7255
7825
  );
7256
7826
  }
7257
7827
  } catch (err) {
7258
7828
  import_fs11.default.appendFileSync(
7259
7829
  HOOK_DEBUG_LOG,
7260
- `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
7830
+ `[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
7261
7831
  `
7262
7832
  );
7263
7833
  }
@@ -7358,9 +7928,8 @@ async function authorizeHeadless(toolName, args, meta, options) {
7358
7928
  if (!options?.calledFromDaemon) {
7359
7929
  const actId = (0, import_crypto5.randomUUID)();
7360
7930
  const actTs = Date.now();
7361
- const stripAnsi = (s) => s.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "");
7362
- const sanitizedAgent = meta?.agent ? stripAnsi(meta.agent).slice(0, 80) : void 0;
7363
- const sanitizedMcpServer = meta?.mcpServer ? stripAnsi(meta.mcpServer).slice(0, 40) : void 0;
7931
+ const sanitizedAgent = meta?.agent ? safeMessage(meta.agent, 80) : void 0;
7932
+ const sanitizedMcpServer = meta?.mcpServer ? safeMessage(meta.mcpServer, 40) : void 0;
7364
7933
  const socketOk = await notifyActivity({
7365
7934
  id: actId,
7366
7935
  ts: actTs,