@node9/proxy 2.8.5 → 2.9.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/scan-ink.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/cli/render/ink/StaticScorecard.tsx
2
- import { Box as Box11, render } from "ink";
2
+ import { Box as Box12, render } from "ink";
3
3
 
4
4
  // src/cli/render/ink/SeverityBand.tsx
5
5
  import { Box, Text } from "ink";
@@ -177,7 +177,11 @@ var ConfigFileSchema = z.object({
177
177
  mode: z.enum(["off", "review", "block"]).optional(),
178
178
  allow: z.array(z.string()).optional(),
179
179
  deny: z.array(z.string()).optional(),
180
- allowPrivate: z.boolean().optional()
180
+ allowPrivate: z.boolean().optional(),
181
+ // SSRF floor. `ssrfAllow` exempts OVERRIDABLE tiers only; a tier-1
182
+ // entry is dropped with a warning at load, never silently honoured.
183
+ ssrfAllow: z.array(z.string()).optional(),
184
+ ssrfStrict: z.boolean().optional()
181
185
  }).optional(),
182
186
  loopDetection: z.object({
183
187
  enabled: z.boolean().optional(),
@@ -205,10 +209,52 @@ import os from "os";
205
209
 
206
210
  // packages/policy-engine/dist/index.mjs
207
211
  import safeRegex from "safe-regex2";
212
+ import { createHash } from "crypto";
208
213
  import mvdanSh from "mvdan-sh";
209
214
  import pm from "picomatch";
210
215
  import safeRegex2 from "safe-regex2";
211
216
  import safeRegex3 from "safe-regex2";
217
+ var B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
218
+ var B58_INDEX = Object.fromEntries(
219
+ [...B58].map((c, i) => [c, i])
220
+ );
221
+ function validateBase58Check(s) {
222
+ if (!s) return null;
223
+ let n = 0n;
224
+ for (const c of s) {
225
+ const v = B58_INDEX[c];
226
+ if (v === void 0) return null;
227
+ n = n * 58n + BigInt(v);
228
+ }
229
+ let hex = n.toString(16);
230
+ if (hex.length % 2) hex = "0" + hex;
231
+ let zeros = 0;
232
+ for (const c of s) {
233
+ if (c !== "1") break;
234
+ zeros++;
235
+ }
236
+ const bytes = Buffer.concat([
237
+ Buffer.alloc(zeros),
238
+ n === 0n ? Buffer.alloc(0) : Buffer.from(hex, "hex")
239
+ ]);
240
+ if (bytes.length < 5) return null;
241
+ const body = bytes.subarray(0, bytes.length - 4);
242
+ const check = bytes.subarray(bytes.length - 4);
243
+ const h = createHash("sha256").update(createHash("sha256").update(body).digest()).digest();
244
+ return h.subarray(0, 4).equals(check) ? body : null;
245
+ }
246
+ function validateWif(s) {
247
+ const p = validateBase58Check(s);
248
+ if (!p || p[0] !== 128) return false;
249
+ return p.length === 33 || p.length === 34 && p[33] === 1;
250
+ }
251
+ var XPRV_VERSIONS = /* @__PURE__ */ new Set([76066276, 77428856, 78791436]);
252
+ function validateXprv(s) {
253
+ const p = validateBase58Check(s);
254
+ if (!p || p.length !== 78) return false;
255
+ const version = p.readUInt32BE(0);
256
+ return XPRV_VERSIONS.has(version) && p[45] === 0;
257
+ }
212
258
  var DLP_PATTERNS = [
213
259
  // ── AWS ───────────────────────────────────────────────────────────────────
214
260
  {
@@ -360,6 +406,27 @@ var DLP_PATTERNS = [
360
406
  severity: "block",
361
407
  keywords: ["sg."]
362
408
  },
409
+ // ── Cryptocurrency private keys (base58check-validated) ───────────────────
410
+ // Both are anchored with \b on each side: unanchored, `[KL][base58]{51}`
411
+ // matches INSIDE any longer base58 blob (an xprv, a Solana keypair, a
412
+ // Monero address). Lookbehind fails safe-regex2; \b is the house style
413
+ // (see the card regexes). Mainnet only, matching validateWif / validateXprv;
414
+ // testnet (WIF 0xEF, tprv) is deferred. Cost was measured: the WIF regex
415
+ // runs on every string (first keyword-less pattern) at 0.024 ms per 100 KB
416
+ // of prose, so no prefilter is warranted.
417
+ {
418
+ name: "Bitcoin WIF Private Key",
419
+ regex: /\b(?:5[1-9A-HJ-NP-Za-km-z]{50}|[KL][1-9A-HJ-NP-Za-km-z]{51})\b/,
420
+ severity: "block",
421
+ validate: validateWif
422
+ },
423
+ {
424
+ name: "Extended Private Key",
425
+ regex: /\b[xyz]prv[1-9A-HJ-NP-Za-km-z]{107}\b/,
426
+ severity: "block",
427
+ keywords: ["xprv", "yprv", "zprv"],
428
+ validate: validateXprv
429
+ },
363
430
  // ── Private keys (PEM) ────────────────────────────────────────────────────
364
431
  {
365
432
  name: "Private Key (PEM)",
@@ -1743,10 +1810,52 @@ function LeaksPanel({ summary, width, now = /* @__PURE__ */ new Date() }) {
1743
1810
  ] });
1744
1811
  }
1745
1812
 
1746
- // src/cli/render/ink/panels/BlockedPanel.tsx
1813
+ // src/cli/render/ink/panels/DecoyPanel.tsx
1747
1814
  import { Box as Box6, Text as Text6 } from "ink";
1815
+ import os2 from "os";
1748
1816
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1749
- var ROW_LIMIT2 = 12;
1817
+ var ROW_LIMIT2 = 4;
1818
+ function shortPath(p) {
1819
+ const home = os2.homedir();
1820
+ return p.startsWith(home) ? "~" + p.slice(home.length) : p;
1821
+ }
1822
+ function decoySeamPhrase(toolName) {
1823
+ if (toolName === "tool-result") return "read into the agent context";
1824
+ if (toolName === "user-prompt") return "pasted into a prompt";
1825
+ return `appeared in a ${toolName} call`;
1826
+ }
1827
+ function DecoyPanel({ summary, width, now = /* @__PURE__ */ new Date() }) {
1828
+ const rows = summary.canaries;
1829
+ if (rows.length === 0) return null;
1830
+ return /* @__PURE__ */ jsxs6(Box6, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", width, children: [
1831
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "red", children: [
1832
+ "DECOY TRIPPED",
1833
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: topOf(Math.min(rows.length, ROW_LIMIT2), rows.length) })
1834
+ ] }),
1835
+ rows.slice(0, ROW_LIMIT2).map((c, i) => /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1836
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1837
+ /* @__PURE__ */ jsx6(Box6, { width: 5, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: relativeDate(c.timestamp, now).padStart(4) }) }),
1838
+ /* @__PURE__ */ jsx6(Box6, { width: 16, children: /* @__PURE__ */ jsxs6(Text6, { color: "red", bold: true, wrap: "truncate-end", children: [
1839
+ c.kind,
1840
+ c.retired ? " (retired)" : ""
1841
+ ] }) }),
1842
+ /* @__PURE__ */ jsx6(Box6, { width: 15, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: `[${c.toolName}]` }) }),
1843
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: c.agent })
1844
+ ] }),
1845
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: ` ${shortPath(c.path)} \u2014 ${decoySeamPhrase(c.toolName)}` })
1846
+ ] }, i)),
1847
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1848
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u2192 " }),
1849
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: "node9 canary status" }),
1850
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: " (something read that file)" })
1851
+ ] })
1852
+ ] });
1853
+ }
1854
+
1855
+ // src/cli/render/ink/panels/BlockedPanel.tsx
1856
+ import { Box as Box7, Text as Text7 } from "ink";
1857
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1858
+ var ROW_LIMIT3 = 12;
1750
1859
  function BlockedPanel({
1751
1860
  summary,
1752
1861
  width,
@@ -1754,30 +1863,30 @@ function BlockedPanel({
1754
1863
  }) {
1755
1864
  const enabled = new Set(enabledShields);
1756
1865
  const allRules = topRulesByVerdict(summary.sections, "block", Number.MAX_SAFE_INTEGER);
1757
- const rules = allRules.slice(0, ROW_LIMIT2);
1866
+ const rules = allRules.slice(0, ROW_LIMIT3);
1758
1867
  if (rules.length === 0) return null;
1759
1868
  const origins = rules.map((r) => originForRule(r.name, summary.sections, enabled));
1760
1869
  const anyNeeds = origins.some((o) => o.startsWith("needs shield:"));
1761
1870
  const footer = enabled.size === 0 ? "\u2192 install node9 + enable shields above" : anyNeeds ? "\u2192 \u2713 marks rules from enabled shields \xB7 enable the rest above" : "\u2192 \u2713 all these rules come from enabled shields";
1762
- return /* @__PURE__ */ jsxs6(Box6, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", width, children: [
1763
- /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "red", children: [
1871
+ return /* @__PURE__ */ jsxs7(Box7, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", width, children: [
1872
+ /* @__PURE__ */ jsxs7(Text7, { bold: true, color: "red", children: [
1764
1873
  "WOULD HAVE BLOCKED",
1765
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: topOf(rules.length, allRules.length) })
1874
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: topOf(rules.length, allRules.length) })
1766
1875
  ] }),
1767
- rules.map((rule, i) => /* @__PURE__ */ jsxs6(Box6, { children: [
1768
- /* @__PURE__ */ jsx6(Box6, { width: 3, children: /* @__PURE__ */ jsx6(Text6, { color: "red", children: "\u2717" }) }),
1769
- /* @__PURE__ */ jsx6(Box6, { width: 24, children: /* @__PURE__ */ jsx6(Text6, { bold: true, wrap: "truncate-end", children: rule.name }) }),
1770
- /* @__PURE__ */ jsx6(Box6, { width: 6, children: /* @__PURE__ */ jsx6(Text6, { bold: true, children: `\xD7${rule.count}` }) }),
1771
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections, enabled) })
1876
+ rules.map((rule, i) => /* @__PURE__ */ jsxs7(Box7, { children: [
1877
+ /* @__PURE__ */ jsx7(Box7, { width: 3, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: "\u2717" }) }),
1878
+ /* @__PURE__ */ jsx7(Box7, { width: 24, children: /* @__PURE__ */ jsx7(Text7, { bold: true, wrap: "truncate-end", children: rule.name }) }),
1879
+ /* @__PURE__ */ jsx7(Box7, { width: 6, children: /* @__PURE__ */ jsx7(Text7, { bold: true, children: `\xD7${rule.count}` }) }),
1880
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections, enabled) })
1772
1881
  ] }, i)),
1773
- /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: footer }) })
1882
+ /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, wrap: "truncate-end", children: footer }) })
1774
1883
  ] });
1775
1884
  }
1776
1885
 
1777
1886
  // src/cli/render/ink/panels/BlastRadiusPanel.tsx
1778
- import { Box as Box7, Text as Text7 } from "ink";
1779
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1780
- var ROW_LIMIT3 = 4;
1887
+ import { Box as Box8, Text as Text8 } from "ink";
1888
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1889
+ var ROW_LIMIT4 = 4;
1781
1890
  var ENV_ROW_LIMIT = 3;
1782
1891
  function BlastRadiusPanel({
1783
1892
  blast,
@@ -1785,62 +1894,62 @@ function BlastRadiusPanel({
1785
1894
  width
1786
1895
  }) {
1787
1896
  if (blastExposures === 0) return null;
1788
- const shown = Math.min(blast.reachable.length, ROW_LIMIT3) + Math.min(blast.envFindings.length, ENV_ROW_LIMIT);
1789
- return /* @__PURE__ */ jsxs7(Box7, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", width, children: [
1790
- /* @__PURE__ */ jsxs7(Text7, { bold: true, color: "yellow", children: [
1897
+ const shown = Math.min(blast.reachable.length, ROW_LIMIT4) + Math.min(blast.envFindings.length, ENV_ROW_LIMIT);
1898
+ return /* @__PURE__ */ jsxs8(Box8, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", width, children: [
1899
+ /* @__PURE__ */ jsxs8(Text8, { bold: true, color: "yellow", children: [
1791
1900
  "BLAST RADIUS",
1792
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: topOf(shown, blastExposures) })
1901
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: topOf(shown, blastExposures) })
1793
1902
  ] }),
1794
- blast.reachable.slice(0, ROW_LIMIT3).map((path2, i) => {
1903
+ blast.reachable.slice(0, ROW_LIMIT4).map((path2, i) => {
1795
1904
  const desc = path2.description.split(" \u2014 ")[0].split(/—|--/)[0].trim();
1796
- return /* @__PURE__ */ jsxs7(Box7, { children: [
1797
- /* @__PURE__ */ jsx7(Box7, { width: 3, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: "\u2717" }) }),
1798
- /* @__PURE__ */ jsx7(Box7, { width: 36, children: /* @__PURE__ */ jsx7(Text7, { children: path2.label }) }),
1799
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: desc })
1905
+ return /* @__PURE__ */ jsxs8(Box8, { children: [
1906
+ /* @__PURE__ */ jsx8(Box8, { width: 3, children: /* @__PURE__ */ jsx8(Text8, { color: "red", children: "\u2717" }) }),
1907
+ /* @__PURE__ */ jsx8(Box8, { width: 36, children: /* @__PURE__ */ jsx8(Text8, { children: path2.label }) }),
1908
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: desc })
1800
1909
  ] }, i);
1801
1910
  }),
1802
- blast.envFindings.slice(0, 3).map((env, i) => /* @__PURE__ */ jsxs7(Box7, { children: [
1803
- /* @__PURE__ */ jsx7(Box7, { width: 3, children: /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "\u26A0" }) }),
1804
- /* @__PURE__ */ jsx7(Text7, { children: env.key }),
1805
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` (${env.patternName})` })
1911
+ blast.envFindings.slice(0, 3).map((env, i) => /* @__PURE__ */ jsxs8(Box8, { children: [
1912
+ /* @__PURE__ */ jsx8(Box8, { width: 3, children: /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u26A0" }) }),
1913
+ /* @__PURE__ */ jsx8(Text8, { children: env.key }),
1914
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` (${env.patternName})` })
1806
1915
  ] }, `env-${i}`)),
1807
- /* @__PURE__ */ jsxs7(Box7, { children: [
1808
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2192 " }),
1809
- /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "project-jail" }),
1810
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " shield blocks agent reads of these paths" })
1916
+ /* @__PURE__ */ jsxs8(Box8, { children: [
1917
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2192 " }),
1918
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: "project-jail" }),
1919
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " shield blocks agent reads of these paths" })
1811
1920
  ] })
1812
1921
  ] });
1813
1922
  }
1814
1923
 
1815
1924
  // src/cli/render/ink/panels/ReviewQueuePanel.tsx
1816
- import { Box as Box8, Text as Text8 } from "ink";
1817
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1818
- var ROW_LIMIT4 = 5;
1925
+ import { Box as Box9, Text as Text9 } from "ink";
1926
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1927
+ var ROW_LIMIT5 = 5;
1819
1928
  function ReviewQueuePanel({
1820
1929
  summary,
1821
1930
  width,
1822
1931
  enabledShields = []
1823
1932
  }) {
1824
1933
  const enabled = new Set(enabledShields);
1825
- const rules = topRulesByVerdict(summary.sections, "review", ROW_LIMIT4);
1934
+ const rules = topRulesByVerdict(summary.sections, "review", ROW_LIMIT5);
1826
1935
  if (rules.length === 0) return null;
1827
- return /* @__PURE__ */ jsxs8(Box8, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
1828
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: "REVIEW QUEUE" }),
1829
- rules.map((rule, i) => /* @__PURE__ */ jsxs8(Box8, { children: [
1830
- /* @__PURE__ */ jsx8(Box8, { width: 22, children: /* @__PURE__ */ jsx8(Text8, { wrap: "truncate-end", children: rule.name }) }),
1831
- /* @__PURE__ */ jsx8(Box8, { width: 6, children: /* @__PURE__ */ jsx8(Text8, { bold: true, children: `\xD7${rule.count}` }) }),
1832
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections, enabled) })
1936
+ return /* @__PURE__ */ jsxs9(Box9, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
1937
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "REVIEW QUEUE" }),
1938
+ rules.map((rule, i) => /* @__PURE__ */ jsxs9(Box9, { children: [
1939
+ /* @__PURE__ */ jsx9(Box9, { width: 22, children: /* @__PURE__ */ jsx9(Text9, { wrap: "truncate-end", children: rule.name }) }),
1940
+ /* @__PURE__ */ jsx9(Box9, { width: 6, children: /* @__PURE__ */ jsx9(Text9, { bold: true, children: `\xD7${rule.count}` }) }),
1941
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-end", children: originForRule(rule.name, summary.sections, enabled) })
1833
1942
  ] }, i)),
1834
- /* @__PURE__ */ jsxs8(Box8, { children: [
1835
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: "\u2192 " }),
1836
- /* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", wrap: "truncate-end", children: "runtime approval" })
1943
+ /* @__PURE__ */ jsxs9(Box9, { children: [
1944
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-end", children: "\u2192 " }),
1945
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", wrap: "truncate-end", children: "runtime approval" })
1837
1946
  ] })
1838
1947
  ] });
1839
1948
  }
1840
1949
 
1841
1950
  // src/cli/render/ink/panels/AgentLoopsPanel.tsx
1842
- import { Box as Box9, Text as Text9 } from "ink";
1843
- import { Fragment, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1951
+ import { Box as Box10, Text as Text10 } from "ink";
1952
+ import { Fragment, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1844
1953
  var TOOL_ROW_LIMIT = 5;
1845
1954
  var STUCK_ROW_LIMIT = 1;
1846
1955
  function fmtNum2(n) {
@@ -1861,38 +1970,38 @@ function AgentLoopsPanel({ loopFindings, width }) {
1861
1970
  }
1862
1971
  const toolEntries = [...byTool.entries()].sort((a, b) => b[1] - a[1]).slice(0, TOOL_ROW_LIMIT);
1863
1972
  const topStuck = [...loopFindings].sort((a, b) => b.count - a.count).slice(0, STUCK_ROW_LIMIT);
1864
- return /* @__PURE__ */ jsxs9(Box9, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
1865
- /* @__PURE__ */ jsxs9(Text9, { bold: true, children: [
1973
+ return /* @__PURE__ */ jsxs10(Box10, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width, children: [
1974
+ /* @__PURE__ */ jsxs10(Text10, { bold: true, children: [
1866
1975
  "AGENT LOOPS",
1867
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: topOf(toolEntries.length, byTool.size) })
1976
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: topOf(toolEntries.length, byTool.size) })
1868
1977
  ] }),
1869
1978
  toolEntries.map(([tool, repeats]) => {
1870
1979
  const pct = totalRepeats > 0 ? Math.round(repeats / totalRepeats * 100) : 0;
1871
- return /* @__PURE__ */ jsxs9(Box9, { children: [
1872
- /* @__PURE__ */ jsx9(Box9, { width: 10, children: /* @__PURE__ */ jsx9(Text9, { bold: true, children: tool }) }),
1873
- /* @__PURE__ */ jsx9(Box9, { width: 14, children: /* @__PURE__ */ jsx9(Text9, { children: `\xD7${fmtNum2(repeats)}` }) }),
1874
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: `${pct}%` })
1980
+ return /* @__PURE__ */ jsxs10(Box10, { children: [
1981
+ /* @__PURE__ */ jsx10(Box10, { width: 10, children: /* @__PURE__ */ jsx10(Text10, { bold: true, children: tool }) }),
1982
+ /* @__PURE__ */ jsx10(Box10, { width: 14, children: /* @__PURE__ */ jsx10(Text10, { children: `\xD7${fmtNum2(repeats)}` }) }),
1983
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: `${pct}%` })
1875
1984
  ] }, tool);
1876
1985
  }),
1877
- topStuck.length > 0 ? /* @__PURE__ */ jsxs9(Fragment, { children: [
1878
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Top stuck:" }),
1986
+ topStuck.length > 0 ? /* @__PURE__ */ jsxs10(Fragment, { children: [
1987
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "Top stuck:" }),
1879
1988
  topStuck.map((f, i) => {
1880
1989
  const target = trimRight(f.commandPreview || f.toolName, 32);
1881
- return /* @__PURE__ */ jsxs9(Box9, { children: [
1882
- /* @__PURE__ */ jsx9(Box9, { width: 8, children: /* @__PURE__ */ jsx9(Text9, { bold: true, children: `\xD7${fmtNum2(f.count)}` }) }),
1883
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-end", children: target })
1990
+ return /* @__PURE__ */ jsxs10(Box10, { children: [
1991
+ /* @__PURE__ */ jsx10(Box10, { width: 8, children: /* @__PURE__ */ jsx10(Text10, { bold: true, children: `\xD7${fmtNum2(f.count)}` }) }),
1992
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, wrap: "truncate-end", children: target })
1884
1993
  ] }, `stuck-${i}`);
1885
1994
  })
1886
1995
  ] }) : null,
1887
- /* @__PURE__ */ jsxs9(Box9, { children: [
1888
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-end", children: "\u2192 " }),
1889
- /* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", wrap: "truncate-end", children: "live loop-detector" })
1996
+ /* @__PURE__ */ jsxs10(Box10, { children: [
1997
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, wrap: "truncate-end", children: "\u2192 " }),
1998
+ /* @__PURE__ */ jsx10(Text10, { bold: true, color: "cyan", wrap: "truncate-end", children: "live loop-detector" })
1890
1999
  ] })
1891
2000
  ] });
1892
2001
  }
1893
2002
 
1894
2003
  // src/cli/render/ink/panels/ShieldsPanel.tsx
1895
- import { Box as Box10, Text as Text10 } from "ink";
2004
+ import { Box as Box11, Text as Text11 } from "ink";
1896
2005
 
1897
2006
  // src/protection.ts
1898
2007
  var PROTECTIVE_SHIELD_DISCOUNTS = {
@@ -1900,7 +2009,7 @@ var PROTECTIVE_SHIELD_DISCOUNTS = {
1900
2009
  };
1901
2010
 
1902
2011
  // src/cli/render/ink/panels/ShieldsPanel.tsx
1903
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
2012
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1904
2013
  function ShieldsPanel({ summary, width, enabledShields = [] }) {
1905
2014
  const enabled = new Set(enabledShields);
1906
2015
  const impacts = rollupByShield(summary.sections);
@@ -1921,26 +2030,26 @@ function ShieldsPanel({ summary, width, enabledShields = [] }) {
1921
2030
  const allCovered = !topRec && hitShields.some(
1922
2031
  (r) => (PROTECTIVE_SHIELD_DISCOUNTS[r.shieldName] ?? 0) > 0 && enabled.has(r.shieldName)
1923
2032
  );
1924
- return /* @__PURE__ */ jsxs10(Box10, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", width, children: [
1925
- /* @__PURE__ */ jsx10(Text10, { bold: true, color: "cyan", children: "SHIELDS" }),
2033
+ return /* @__PURE__ */ jsxs11(Box11, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", width, children: [
2034
+ /* @__PURE__ */ jsx11(Text11, { bold: true, color: "cyan", children: "SHIELDS" }),
1926
2035
  hitShields.map((impact) => {
1927
2036
  const discount = PROTECTIVE_SHIELD_DISCOUNTS[impact.shieldName] ?? 0;
1928
2037
  const isOn = enabled.has(impact.shieldName);
1929
2038
  const noun = `op${impact.totalCatches !== 1 ? "s" : ""}`;
1930
- return /* @__PURE__ */ jsxs10(Box10, { children: [
1931
- /* @__PURE__ */ jsx10(Box10, { width: 16, children: /* @__PURE__ */ jsx10(Text10, { bold: true, color: discount > 0 ? "cyan" : void 0, dimColor: discount === 0, children: impact.shieldName }) }),
1932
- /* @__PURE__ */ jsx10(Box10, { width: 20, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: `catches ${impact.totalCatches} ${noun}` }) }),
1933
- isOn ? /* @__PURE__ */ jsx10(Text10, { bold: true, color: "green", children: `\u2713 enabled` }) : discount > 0 ? /* @__PURE__ */ jsx10(Text10, { bold: true, color: "green", children: `\u2192 blocks these reads in-path` }) : null
2039
+ return /* @__PURE__ */ jsxs11(Box11, { children: [
2040
+ /* @__PURE__ */ jsx11(Box11, { width: 16, children: /* @__PURE__ */ jsx11(Text11, { bold: true, color: discount > 0 ? "cyan" : void 0, dimColor: discount === 0, children: impact.shieldName }) }),
2041
+ /* @__PURE__ */ jsx11(Box11, { width: 20, children: /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: `catches ${impact.totalCatches} ${noun}` }) }),
2042
+ isOn ? /* @__PURE__ */ jsx11(Text11, { bold: true, color: "green", children: `\u2713 enabled` }) : discount > 0 ? /* @__PURE__ */ jsx11(Text11, { bold: true, color: "green", children: `\u2192 blocks these reads in-path` }) : null
1934
2043
  ] }, impact.shieldName);
1935
2044
  }),
1936
- zeroHitEnabled.length > 0 ? /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, wrap: "truncate-end", children: "\u2713 active: " + zeroHitEnabled.join(" \xB7 ") + " (no hits in this history)" }) }) : null,
1937
- zeroHitBuiltins.length > 0 ? /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, wrap: "truncate-end", children: zeroHitBuiltins.join(" \xB7 ") + " (no hits \u2014 install proactively)" }) }) : null,
1938
- topRec ? /* @__PURE__ */ jsx10(Box10, { children: /* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: `\u2192 node9 shield enable ${topRec.shieldName} (start here \u2014 widest coverage)` }) }) : allCovered ? /* @__PURE__ */ jsx10(Box10, { children: /* @__PURE__ */ jsx10(Text10, { color: "green", bold: true, children: "\u2713 recommended shields are enabled" }) }) : null
2045
+ zeroHitEnabled.length > 0 ? /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsx11(Text11, { dimColor: true, wrap: "truncate-end", children: "\u2713 active: " + zeroHitEnabled.join(" \xB7 ") + " (no hits in this history)" }) }) : null,
2046
+ zeroHitBuiltins.length > 0 ? /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsx11(Text11, { dimColor: true, wrap: "truncate-end", children: zeroHitBuiltins.join(" \xB7 ") + " (no hits \u2014 install proactively)" }) }) : null,
2047
+ topRec ? /* @__PURE__ */ jsx11(Box11, { children: /* @__PURE__ */ jsx11(Text11, { color: "cyan", bold: true, children: `\u2192 node9 shield enable ${topRec.shieldName} (start here \u2014 widest coverage)` }) }) : allCovered ? /* @__PURE__ */ jsx11(Box11, { children: /* @__PURE__ */ jsx11(Text11, { color: "green", bold: true, children: "\u2713 recommended shields are enabled" }) }) : null
1939
2048
  ] });
1940
2049
  }
1941
2050
 
1942
2051
  // src/cli/render/ink/StaticScorecard.tsx
1943
- import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2052
+ import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1944
2053
  var MAX_WIDTH = 90;
1945
2054
  function renderWidth() {
1946
2055
  const term = process.stdout.columns ?? MAX_WIDTH;
@@ -1951,9 +2060,13 @@ function StaticScorecard({ input, rangeLabel, now }) {
1951
2060
  const width = renderWidth();
1952
2061
  const halfWidth = Math.floor((width - 1) / 2);
1953
2062
  const leakCount = summary.leaks.length;
1954
- const hasCritical = leakCount > 0 || blockedCount > 0;
2063
+ const decoyCount = summary.canaries.length;
2064
+ const hasCritical = decoyCount > 0 || leakCount > 0 || blockedCount > 0;
1955
2065
  const criticalLabel = (() => {
1956
2066
  const parts = [];
2067
+ if (decoyCount > 0) {
2068
+ parts.push(`${decoyCount} decoy${decoyCount !== 1 ? "s" : ""} tripped`);
2069
+ }
1957
2070
  if (leakCount > 0) {
1958
2071
  parts.push(`${leakCount} secret${leakCount !== 1 ? "s" : ""} leaked`);
1959
2072
  }
@@ -1962,18 +2075,19 @@ function StaticScorecard({ input, rangeLabel, now }) {
1962
2075
  }
1963
2076
  return `Critical (${parts.join(" + ")})`;
1964
2077
  })();
1965
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingTop: 1, width, children: [
1966
- /* @__PURE__ */ jsx11(Header, { rangeLabel }),
1967
- /* @__PURE__ */ jsx11(SeverityBand, { label: "Spend & activity", width }),
1968
- /* @__PURE__ */ jsxs11(Box11, { flexDirection: "row", gap: 1, children: [
1969
- /* @__PURE__ */ jsx11(CostPanel, { summary, width: halfWidth, keyed: input.keyed }),
1970
- /* @__PURE__ */ jsx11(ActivityPanel, { summary, scan: input.scan, width: halfWidth })
2078
+ return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingTop: 1, width, children: [
2079
+ /* @__PURE__ */ jsx12(Header, { rangeLabel }),
2080
+ /* @__PURE__ */ jsx12(SeverityBand, { label: "Spend & activity", width }),
2081
+ /* @__PURE__ */ jsxs12(Box12, { flexDirection: "row", gap: 1, children: [
2082
+ /* @__PURE__ */ jsx12(CostPanel, { summary, width: halfWidth, keyed: input.keyed }),
2083
+ /* @__PURE__ */ jsx12(ActivityPanel, { summary, scan: input.scan, width: halfWidth })
1971
2084
  ] }),
1972
- hasCritical ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
1973
- /* @__PURE__ */ jsx11(SeverityBand, { label: criticalLabel, width }),
1974
- /* @__PURE__ */ jsxs11(Box11, { flexDirection: "row", gap: 1, children: [
1975
- /* @__PURE__ */ jsx11(LeaksPanel, { summary, width: halfWidth, now }),
1976
- /* @__PURE__ */ jsx11(
2085
+ hasCritical ? /* @__PURE__ */ jsxs12(Fragment2, { children: [
2086
+ /* @__PURE__ */ jsx12(SeverityBand, { label: criticalLabel, width }),
2087
+ decoyCount > 0 ? /* @__PURE__ */ jsx12(Box12, { flexDirection: "row", gap: 1, children: /* @__PURE__ */ jsx12(DecoyPanel, { summary, width: halfWidth, now }) }) : null,
2088
+ /* @__PURE__ */ jsxs12(Box12, { flexDirection: "row", gap: 1, children: [
2089
+ /* @__PURE__ */ jsx12(LeaksPanel, { summary, width: halfWidth, now }),
2090
+ /* @__PURE__ */ jsx12(
1977
2091
  BlockedPanel,
1978
2092
  {
1979
2093
  summary,
@@ -1983,8 +2097,8 @@ function StaticScorecard({ input, rangeLabel, now }) {
1983
2097
  )
1984
2098
  ] })
1985
2099
  ] }) : null,
1986
- input.blastExposures > 0 ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
1987
- /* @__PURE__ */ jsx11(
2100
+ input.blastExposures > 0 ? /* @__PURE__ */ jsxs12(Fragment2, { children: [
2101
+ /* @__PURE__ */ jsx12(
1988
2102
  SeverityBand,
1989
2103
  {
1990
2104
  label: (() => {
@@ -1997,7 +2111,7 @@ function StaticScorecard({ input, rangeLabel, now }) {
1997
2111
  width
1998
2112
  }
1999
2113
  ),
2000
- /* @__PURE__ */ jsx11(
2114
+ /* @__PURE__ */ jsx12(
2001
2115
  BlastRadiusPanel,
2002
2116
  {
2003
2117
  blast: input.blast,
@@ -2020,10 +2134,10 @@ function StaticScorecard({ input, rangeLabel, now }) {
2020
2134
  parts.push(
2021
2135
  `${loopCount} loop${loopCount !== 1 ? "s" : ""}${wastedCalls > 0 ? ` \xB7 ${wastedCalls} repeat calls (${wastePct}% of all calls)` : ""}`
2022
2136
  );
2023
- return /* @__PURE__ */ jsxs11(Fragment2, { children: [
2024
- /* @__PURE__ */ jsx11(SeverityBand, { label: `Medium (${parts.join(" \xB7 ")})`, width }),
2025
- /* @__PURE__ */ jsxs11(Box11, { flexDirection: "row", gap: 1, children: [
2026
- /* @__PURE__ */ jsx11(
2137
+ return /* @__PURE__ */ jsxs12(Fragment2, { children: [
2138
+ /* @__PURE__ */ jsx12(SeverityBand, { label: `Medium (${parts.join(" \xB7 ")})`, width }),
2139
+ /* @__PURE__ */ jsxs12(Box12, { flexDirection: "row", gap: 1, children: [
2140
+ /* @__PURE__ */ jsx12(
2027
2141
  ReviewQueuePanel,
2028
2142
  {
2029
2143
  summary: input.summary,
@@ -2031,16 +2145,16 @@ function StaticScorecard({ input, rangeLabel, now }) {
2031
2145
  enabledShields: input.enabledShields
2032
2146
  }
2033
2147
  ),
2034
- /* @__PURE__ */ jsx11(AgentLoopsPanel, { loopFindings: input.scan.loopFindings, width: halfWidth })
2148
+ /* @__PURE__ */ jsx12(AgentLoopsPanel, { loopFindings: input.scan.loopFindings, width: halfWidth })
2035
2149
  ] })
2036
2150
  ] });
2037
2151
  })(),
2038
- /* @__PURE__ */ jsx11(SeverityBand, { label: "Recommended action", width }),
2039
- /* @__PURE__ */ jsx11(ShieldsPanel, { summary: input.summary, width, enabledShields: input.enabledShields })
2152
+ /* @__PURE__ */ jsx12(SeverityBand, { label: "Recommended action", width }),
2153
+ /* @__PURE__ */ jsx12(ShieldsPanel, { summary: input.summary, width, enabledShields: input.enabledShields })
2040
2154
  ] });
2041
2155
  }
2042
2156
  function renderScanScorecardInk(input, rangeLabel) {
2043
- const { unmount } = render(/* @__PURE__ */ jsx11(StaticScorecard, { input, rangeLabel }), {
2157
+ const { unmount } = render(/* @__PURE__ */ jsx12(StaticScorecard, { input, rangeLabel }), {
2044
2158
  patchConsole: false
2045
2159
  });
2046
2160
  unmount();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "2.8.5",
3
+ "version": "2.9.0",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",