@node9/proxy 1.3.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -155,8 +155,8 @@ function sanitizeConfig(raw) {
155
155
  }
156
156
  }
157
157
  const lines = result.error.issues.map((issue) => {
158
- const path13 = issue.path.length > 0 ? issue.path.join(".") : "root";
159
- return ` \u2022 ${path13}: ${issue.message}`;
158
+ const path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
159
+ return ` \u2022 ${path14}: ${issue.message}`;
160
160
  });
161
161
  return {
162
162
  sanitized,
@@ -886,7 +886,7 @@ function getCompiledRegex(pattern, flags = "") {
886
886
  }
887
887
 
888
888
  // src/policy/index.ts
889
- import path7 from "path";
889
+ import path8 from "path";
890
890
  import pm from "picomatch";
891
891
  import { parse } from "sh-syntax";
892
892
 
@@ -1440,6 +1440,56 @@ function extractAllSshHosts(tokens) {
1440
1440
  return [...hosts].filter(Boolean);
1441
1441
  }
1442
1442
 
1443
+ // src/auth/trusted-hosts.ts
1444
+ import fs6 from "fs";
1445
+ import path7 from "path";
1446
+ import os5 from "os";
1447
+ function getTrustedHostsPath() {
1448
+ return path7.join(os5.homedir(), ".node9", "trusted-hosts.json");
1449
+ }
1450
+ function readTrustedHosts() {
1451
+ try {
1452
+ const raw = fs6.readFileSync(getTrustedHostsPath(), "utf8");
1453
+ const parsed = JSON.parse(raw);
1454
+ return Array.isArray(parsed.hosts) ? parsed.hosts : [];
1455
+ } catch {
1456
+ return [];
1457
+ }
1458
+ }
1459
+ var _cache = null;
1460
+ var CACHE_TTL_MS = 5e3;
1461
+ function getFileMtime() {
1462
+ try {
1463
+ return fs6.statSync(getTrustedHostsPath()).mtimeMs;
1464
+ } catch {
1465
+ return 0;
1466
+ }
1467
+ }
1468
+ function getCachedHosts() {
1469
+ const now = Date.now();
1470
+ if (_cache && now < _cache.expiry) {
1471
+ const mtime = getFileMtime();
1472
+ if (mtime === _cache.mtime) return _cache.hosts;
1473
+ }
1474
+ const hosts = readTrustedHosts();
1475
+ _cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
1476
+ return hosts;
1477
+ }
1478
+ function normalizeHost(raw) {
1479
+ return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
1480
+ }
1481
+ function isTrustedHost(host) {
1482
+ const normalized = normalizeHost(host);
1483
+ return getCachedHosts().some((entry) => {
1484
+ const entryHost = entry.host.toLowerCase();
1485
+ if (entryHost.startsWith("*.")) {
1486
+ const domain = entryHost.slice(2);
1487
+ return normalized.endsWith("." + domain);
1488
+ }
1489
+ return normalized === entryHost;
1490
+ });
1491
+ }
1492
+
1443
1493
  // src/policy/index.ts
1444
1494
  function tokenize2(toolName) {
1445
1495
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
@@ -1454,9 +1504,9 @@ function matchesPattern(text, patterns) {
1454
1504
  const withoutDotSlash = text.replace(/^\.\//, "");
1455
1505
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1456
1506
  }
1457
- function getNestedValue(obj, path13) {
1507
+ function getNestedValue(obj, path14) {
1458
1508
  if (!obj || typeof obj !== "object") return null;
1459
- return path13.split(".").reduce((prev, curr) => prev?.[curr], obj);
1509
+ return path14.split(".").reduce((prev, curr) => prev?.[curr], obj);
1460
1510
  }
1461
1511
  function evaluateSmartConditions(args, rule) {
1462
1512
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -1611,23 +1661,39 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1611
1661
  return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
1612
1662
  }
1613
1663
  const pipeAnalysis = analyzePipeChain(shellCommand);
1614
- if (pipeAnalysis.isPipeline) {
1664
+ if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
1665
+ const sinks = pipeAnalysis.sinkTargets;
1666
+ const allTrusted = sinks.length > 0 && sinks.every(isTrustedHost);
1615
1667
  if (pipeAnalysis.risk === "critical") {
1668
+ if (allTrusted) {
1669
+ return {
1670
+ decision: "review",
1671
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
1672
+ reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
1673
+ tier: 3
1674
+ };
1675
+ }
1616
1676
  return {
1617
1677
  decision: "block",
1618
1678
  blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1619
- reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${pipeAnalysis.sinkTargets.join(", ")}`,
1679
+ reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1620
1680
  tier: 3
1621
1681
  };
1622
1682
  }
1623
- if (pipeAnalysis.risk === "high") {
1683
+ if (allTrusted) {
1624
1684
  return {
1625
- decision: "review",
1626
- blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1627
- reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${pipeAnalysis.sinkTargets.join(", ")}`,
1685
+ decision: "allow",
1686
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1687
+ reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1628
1688
  tier: 3
1629
1689
  };
1630
1690
  }
1691
+ return {
1692
+ decision: "review",
1693
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1694
+ reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1695
+ tier: 3
1696
+ };
1631
1697
  }
1632
1698
  const firstToken = analyzed.actions[0] ?? "";
1633
1699
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
@@ -1635,7 +1701,7 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1635
1701
  const sshHosts = extractAllSshHosts(rawTokens.slice(1));
1636
1702
  allTokens.push(...sshHosts);
1637
1703
  }
1638
- if (firstToken && path7.posix.isAbsolute(firstToken)) {
1704
+ if (firstToken && path8.posix.isAbsolute(firstToken)) {
1639
1705
  const prov = checkProvenance(firstToken, cwd);
1640
1706
  if (prov.trustLevel === "suspect") {
1641
1707
  return {
@@ -1736,18 +1802,18 @@ function isIgnoredTool(toolName) {
1736
1802
  }
1737
1803
 
1738
1804
  // src/auth/state.ts
1739
- import fs6 from "fs";
1740
- import path8 from "path";
1741
- import os5 from "os";
1742
- var PAUSED_FILE = path8.join(os5.homedir(), ".node9", "PAUSED");
1743
- var TRUST_FILE = path8.join(os5.homedir(), ".node9", "trust.json");
1805
+ import fs7 from "fs";
1806
+ import path9 from "path";
1807
+ import os6 from "os";
1808
+ var PAUSED_FILE = path9.join(os6.homedir(), ".node9", "PAUSED");
1809
+ var TRUST_FILE = path9.join(os6.homedir(), ".node9", "trust.json");
1744
1810
  function checkPause() {
1745
1811
  try {
1746
- if (!fs6.existsSync(PAUSED_FILE)) return { paused: false };
1747
- const state = JSON.parse(fs6.readFileSync(PAUSED_FILE, "utf-8"));
1812
+ if (!fs7.existsSync(PAUSED_FILE)) return { paused: false };
1813
+ const state = JSON.parse(fs7.readFileSync(PAUSED_FILE, "utf-8"));
1748
1814
  if (state.expiry > 0 && Date.now() >= state.expiry) {
1749
1815
  try {
1750
- fs6.unlinkSync(PAUSED_FILE);
1816
+ fs7.unlinkSync(PAUSED_FILE);
1751
1817
  } catch {
1752
1818
  }
1753
1819
  return { paused: false };
@@ -1758,20 +1824,20 @@ function checkPause() {
1758
1824
  }
1759
1825
  }
1760
1826
  function atomicWriteSync(filePath, data, options) {
1761
- const dir = path8.dirname(filePath);
1762
- if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
1763
- const tmpPath = `${filePath}.${os5.hostname()}.${process.pid}.tmp`;
1764
- fs6.writeFileSync(tmpPath, data, options);
1765
- fs6.renameSync(tmpPath, filePath);
1827
+ const dir = path9.dirname(filePath);
1828
+ if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
1829
+ const tmpPath = `${filePath}.${os6.hostname()}.${process.pid}.tmp`;
1830
+ fs7.writeFileSync(tmpPath, data, options);
1831
+ fs7.renameSync(tmpPath, filePath);
1766
1832
  }
1767
1833
  function getActiveTrustSession(toolName) {
1768
1834
  try {
1769
- if (!fs6.existsSync(TRUST_FILE)) return false;
1770
- const trust = JSON.parse(fs6.readFileSync(TRUST_FILE, "utf-8"));
1835
+ if (!fs7.existsSync(TRUST_FILE)) return false;
1836
+ const trust = JSON.parse(fs7.readFileSync(TRUST_FILE, "utf-8"));
1771
1837
  const now = Date.now();
1772
1838
  const active = trust.entries.filter((e) => e.expiry > now);
1773
1839
  if (active.length !== trust.entries.length) {
1774
- fs6.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
1840
+ fs7.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
1775
1841
  }
1776
1842
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
1777
1843
  } catch {
@@ -1782,8 +1848,8 @@ function writeTrustSession(toolName, durationMs) {
1782
1848
  try {
1783
1849
  let trust = { entries: [] };
1784
1850
  try {
1785
- if (fs6.existsSync(TRUST_FILE)) {
1786
- trust = JSON.parse(fs6.readFileSync(TRUST_FILE, "utf-8"));
1851
+ if (fs7.existsSync(TRUST_FILE)) {
1852
+ trust = JSON.parse(fs7.readFileSync(TRUST_FILE, "utf-8"));
1787
1853
  }
1788
1854
  } catch {
1789
1855
  }
@@ -1799,9 +1865,9 @@ function writeTrustSession(toolName, durationMs) {
1799
1865
  }
1800
1866
  function getPersistentDecision(toolName) {
1801
1867
  try {
1802
- const file = path8.join(os5.homedir(), ".node9", "decisions.json");
1803
- if (!fs6.existsSync(file)) return null;
1804
- const decisions = JSON.parse(fs6.readFileSync(file, "utf-8"));
1868
+ const file = path9.join(os6.homedir(), ".node9", "decisions.json");
1869
+ if (!fs7.existsSync(file)) return null;
1870
+ const decisions = JSON.parse(fs7.readFileSync(file, "utf-8"));
1805
1871
  const d = decisions[toolName];
1806
1872
  if (d === "allow" || d === "deny") return d;
1807
1873
  } catch {
@@ -1810,17 +1876,17 @@ function getPersistentDecision(toolName) {
1810
1876
  }
1811
1877
 
1812
1878
  // src/auth/daemon.ts
1813
- import fs7 from "fs";
1814
- import path9 from "path";
1815
- import os6 from "os";
1879
+ import fs8 from "fs";
1880
+ import path10 from "path";
1881
+ import os7 from "os";
1816
1882
  import { spawnSync } from "child_process";
1817
1883
  var DAEMON_PORT = 7391;
1818
1884
  var DAEMON_HOST = "127.0.0.1";
1819
1885
  function getInternalToken() {
1820
1886
  try {
1821
- const pidFile = path9.join(os6.homedir(), ".node9", "daemon.pid");
1822
- if (!fs7.existsSync(pidFile)) return null;
1823
- const data = JSON.parse(fs7.readFileSync(pidFile, "utf-8"));
1887
+ const pidFile = path10.join(os7.homedir(), ".node9", "daemon.pid");
1888
+ if (!fs8.existsSync(pidFile)) return null;
1889
+ const data = JSON.parse(fs8.readFileSync(pidFile, "utf-8"));
1824
1890
  process.kill(data.pid, 0);
1825
1891
  return data.internalToken ?? null;
1826
1892
  } catch {
@@ -1828,10 +1894,10 @@ function getInternalToken() {
1828
1894
  }
1829
1895
  }
1830
1896
  function isDaemonRunning() {
1831
- const pidFile = path9.join(os6.homedir(), ".node9", "daemon.pid");
1832
- if (fs7.existsSync(pidFile)) {
1897
+ const pidFile = path10.join(os7.homedir(), ".node9", "daemon.pid");
1898
+ if (fs8.existsSync(pidFile)) {
1833
1899
  try {
1834
- const { pid, port } = JSON.parse(fs7.readFileSync(pidFile, "utf-8"));
1900
+ const { pid, port } = JSON.parse(fs8.readFileSync(pidFile, "utf-8"));
1835
1901
  if (port !== DAEMON_PORT) return false;
1836
1902
  process.kill(pid, 0);
1837
1903
  return true;
@@ -1872,8 +1938,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
1872
1938
  signal: ctrl.signal
1873
1939
  });
1874
1940
  if (!res.ok) throw new Error("Daemon fail");
1875
- const { id } = await res.json();
1876
- return id;
1941
+ const { id, allowCount } = await res.json();
1942
+ return { id, allowCount: allowCount ?? 1 };
1877
1943
  } finally {
1878
1944
  clearTimeout(timer);
1879
1945
  }
@@ -1912,31 +1978,31 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
1912
1978
  signal: AbortSignal.timeout(3e3)
1913
1979
  });
1914
1980
  if (!res.ok) throw new Error("Daemon unreachable");
1915
- const { id } = await res.json();
1916
- return id;
1981
+ const { id, allowCount } = await res.json();
1982
+ return { id, allowCount: allowCount ?? 1 };
1917
1983
  }
1918
- async function resolveViaDaemon(id, decision, internalToken) {
1984
+ async function resolveViaDaemon(id, decision, internalToken, source) {
1919
1985
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1920
1986
  await fetch(`${base}/resolve/${id}`, {
1921
1987
  method: "POST",
1922
1988
  headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
1923
- body: JSON.stringify({ decision }),
1989
+ body: JSON.stringify({ decision, ...source && { source } }),
1924
1990
  signal: AbortSignal.timeout(3e3)
1925
1991
  });
1926
1992
  }
1927
1993
 
1928
1994
  // src/auth/orchestrator.ts
1929
1995
  import net from "net";
1930
- import path12 from "path";
1931
- import os8 from "os";
1996
+ import path13 from "path";
1997
+ import os9 from "os";
1932
1998
  import { randomUUID } from "crypto";
1933
1999
 
1934
2000
  // src/ui/native.ts
1935
2001
  import { spawn } from "child_process";
1936
- import path11 from "path";
2002
+ import path12 from "path";
1937
2003
 
1938
2004
  // src/context-sniper.ts
1939
- import path10 from "path";
2005
+ import path11 from "path";
1940
2006
  function smartTruncate(str, maxLen = 500) {
1941
2007
  if (str.length <= maxLen) return str;
1942
2008
  const edge = Math.floor(maxLen / 2) - 3;
@@ -2004,7 +2070,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
2004
2070
  intent = "EDIT";
2005
2071
  if (obj.file_path) {
2006
2072
  editFilePath = String(obj.file_path);
2007
- editFileName = path10.basename(editFilePath);
2073
+ editFileName = path11.basename(editFilePath);
2008
2074
  }
2009
2075
  const result = extractContext(String(obj.new_string), matchedWord);
2010
2076
  contextSnippet = result.snippet;
@@ -2059,7 +2125,7 @@ function formatArgs(args, matchedField, matchedWord) {
2059
2125
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
2060
2126
  const obj = parsed;
2061
2127
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
2062
- const file = obj.file_path ? path11.basename(String(obj.file_path)) : "file";
2128
+ const file = obj.file_path ? path12.basename(String(obj.file_path)) : "file";
2063
2129
  const oldPreview = smartTruncate(String(obj.old_string), 120);
2064
2130
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
2065
2131
  return {
@@ -2122,20 +2188,24 @@ ${smartTruncate(str, 500)}`
2122
2188
  function escapePango(text) {
2123
2189
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2124
2190
  }
2125
- function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2191
+ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2126
2192
  const lines = [];
2127
2193
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
2128
2194
  lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
2129
2195
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
2130
2196
  lines.push("");
2131
2197
  lines.push(formattedArgs);
2198
+ if (allowCount >= 3) {
2199
+ lines.push("");
2200
+ lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
2201
+ }
2132
2202
  if (!locked) {
2133
2203
  lines.push("");
2134
2204
  lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
2135
2205
  }
2136
2206
  return lines.join("\n");
2137
2207
  }
2138
- function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2208
+ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2139
2209
  const lines = [];
2140
2210
  if (locked) {
2141
2211
  lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
@@ -2147,6 +2217,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2147
2217
  lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
2148
2218
  lines.push("");
2149
2219
  lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
2220
+ if (allowCount >= 3) {
2221
+ lines.push("");
2222
+ lines.push(
2223
+ `<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
2224
+ );
2225
+ }
2150
2226
  if (!locked) {
2151
2227
  lines.push("");
2152
2228
  lines.push(
@@ -2155,12 +2231,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2155
2231
  }
2156
2232
  return lines.join("\n");
2157
2233
  }
2158
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
2234
+ async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
2159
2235
  if (isTestEnv()) return "deny";
2160
2236
  const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
2161
2237
  const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
2162
2238
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
2163
- const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
2239
+ const message = buildPlainMessage(
2240
+ toolName,
2241
+ formattedArgs,
2242
+ agent,
2243
+ explainableLabel,
2244
+ locked,
2245
+ allowCount
2246
+ );
2164
2247
  return new Promise((resolve) => {
2165
2248
  let childProcess = null;
2166
2249
  const onAbort = () => {
@@ -2192,7 +2275,8 @@ end run`;
2192
2275
  formattedArgs,
2193
2276
  agent,
2194
2277
  explainableLabel,
2195
- locked
2278
+ locked,
2279
+ allowCount
2196
2280
  );
2197
2281
  const argsList = [
2198
2282
  locked ? "--info" : "--question",
@@ -2234,8 +2318,8 @@ end run`;
2234
2318
  }
2235
2319
 
2236
2320
  // src/auth/cloud.ts
2237
- import fs8 from "fs";
2238
- import os7 from "os";
2321
+ import fs9 from "fs";
2322
+ import os8 from "os";
2239
2323
  function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
2240
2324
  return fetch(`${creds.apiUrl}/audit`, {
2241
2325
  method: "POST",
@@ -2247,9 +2331,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
2247
2331
  context: {
2248
2332
  agent: meta?.agent,
2249
2333
  mcpServer: meta?.mcpServer,
2250
- hostname: os7.hostname(),
2334
+ hostname: os8.hostname(),
2251
2335
  cwd: process.cwd(),
2252
- platform: os7.platform()
2336
+ platform: os8.platform()
2253
2337
  }
2254
2338
  }),
2255
2339
  signal: AbortSignal.timeout(5e3)
@@ -2270,9 +2354,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
2270
2354
  context: {
2271
2355
  agent: meta?.agent,
2272
2356
  mcpServer: meta?.mcpServer,
2273
- hostname: os7.hostname(),
2357
+ hostname: os8.hostname(),
2274
2358
  cwd: process.cwd(),
2275
- platform: os7.platform()
2359
+ platform: os8.platform()
2276
2360
  },
2277
2361
  ...riskMetadata && { riskMetadata }
2278
2362
  }),
@@ -2328,14 +2412,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
2328
2412
  });
2329
2413
  clearTimeout(timer);
2330
2414
  if (!res.ok) {
2331
- fs8.appendFileSync(
2415
+ fs9.appendFileSync(
2332
2416
  HOOK_DEBUG_LOG,
2333
2417
  `[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
2334
2418
  `
2335
2419
  );
2336
2420
  }
2337
2421
  } catch (err) {
2338
- fs8.appendFileSync(
2422
+ fs9.appendFileSync(
2339
2423
  HOOK_DEBUG_LOG,
2340
2424
  `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
2341
2425
  `
@@ -2344,7 +2428,7 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
2344
2428
  }
2345
2429
 
2346
2430
  // src/auth/orchestrator.ts
2347
- var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path12.join(os8.tmpdir(), "node9-activity.sock");
2431
+ var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os9.tmpdir(), "node9-activity.sock");
2348
2432
  function notifyActivity(data) {
2349
2433
  return new Promise((resolve) => {
2350
2434
  try {
@@ -2537,13 +2621,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2537
2621
  let viewerId = null;
2538
2622
  const internalToken = getInternalToken();
2539
2623
  let daemonEntryId = null;
2624
+ let daemonAllowCount = 1;
2540
2625
  if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
2541
2626
  if (cloudEnforced && cloudRequestId) {
2542
- viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
2627
+ const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
2628
+ viewerId = viewer?.id ?? null;
2543
2629
  daemonEntryId = viewerId;
2630
+ if (viewer) daemonAllowCount = viewer.allowCount;
2544
2631
  } else {
2545
2632
  try {
2546
- daemonEntryId = await registerDaemonEntry(
2633
+ const entry = await registerDaemonEntry(
2547
2634
  toolName,
2548
2635
  args,
2549
2636
  meta,
@@ -2551,6 +2638,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2551
2638
  options?.activityId,
2552
2639
  options?.cwd
2553
2640
  );
2641
+ daemonEntryId = entry.id;
2642
+ daemonAllowCount = entry.allowCount;
2554
2643
  } catch {
2555
2644
  }
2556
2645
  }
@@ -2586,7 +2675,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2586
2675
  false,
2587
2676
  signal,
2588
2677
  policyMatchedField,
2589
- policyMatchedWord
2678
+ policyMatchedWord,
2679
+ daemonAllowCount
2590
2680
  );
2591
2681
  if (decision === "always_allow") {
2592
2682
  writeTrustSession(toolName, 36e5);
@@ -2644,10 +2734,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
2644
2734
  if (!resolved) {
2645
2735
  resolved = true;
2646
2736
  abortController.abort();
2647
- if (viewerId && internalToken) {
2648
- resolveViaDaemon(viewerId, res.approved ? "allow" : "deny", internalToken).catch(
2649
- () => null
2650
- );
2737
+ if (daemonEntryId && internalToken) {
2738
+ resolveViaDaemon(
2739
+ daemonEntryId,
2740
+ res.approved ? "allow" : "deny",
2741
+ internalToken,
2742
+ res.decisionSource
2743
+ ).catch(() => null);
2651
2744
  }
2652
2745
  resolve(res);
2653
2746
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.3.2",
3
+ "version": "1.5.0",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",