@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/README.md +36 -7
- package/dist/cli.js +1338 -477
- package/dist/cli.mjs +1324 -463
- package/dist/index.js +168 -75
- package/dist/index.mjs +168 -75
- package/package.json +1 -1
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
|
|
159
|
-
return ` \u2022 ${
|
|
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
|
|
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,
|
|
1507
|
+
function getNestedValue(obj, path14) {
|
|
1458
1508
|
if (!obj || typeof obj !== "object") return null;
|
|
1459
|
-
return
|
|
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 ${
|
|
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 (
|
|
1683
|
+
if (allTrusted) {
|
|
1624
1684
|
return {
|
|
1625
|
-
decision: "
|
|
1626
|
-
blockedByLabel: "Node9: Pipe-Chain
|
|
1627
|
-
reason: `Sensitive file piped to
|
|
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 &&
|
|
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
|
|
1740
|
-
import
|
|
1741
|
-
import
|
|
1742
|
-
var PAUSED_FILE =
|
|
1743
|
-
var TRUST_FILE =
|
|
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 (!
|
|
1747
|
-
const state = JSON.parse(
|
|
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
|
-
|
|
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 =
|
|
1762
|
-
if (!
|
|
1763
|
-
const tmpPath = `${filePath}.${
|
|
1764
|
-
|
|
1765
|
-
|
|
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 (!
|
|
1770
|
-
const trust = JSON.parse(
|
|
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
|
-
|
|
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 (
|
|
1786
|
-
trust = JSON.parse(
|
|
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 =
|
|
1803
|
-
if (!
|
|
1804
|
-
const decisions = JSON.parse(
|
|
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
|
|
1814
|
-
import
|
|
1815
|
-
import
|
|
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 =
|
|
1822
|
-
if (!
|
|
1823
|
-
const data = JSON.parse(
|
|
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 =
|
|
1832
|
-
if (
|
|
1897
|
+
const pidFile = path10.join(os7.homedir(), ".node9", "daemon.pid");
|
|
1898
|
+
if (fs8.existsSync(pidFile)) {
|
|
1833
1899
|
try {
|
|
1834
|
-
const { pid, port } = JSON.parse(
|
|
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
|
|
1931
|
-
import
|
|
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
|
|
2002
|
+
import path12 from "path";
|
|
1937
2003
|
|
|
1938
2004
|
// src/context-sniper.ts
|
|
1939
|
-
import
|
|
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 =
|
|
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 ?
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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(
|
|
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
|
|
2238
|
-
import
|
|
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:
|
|
2334
|
+
hostname: os8.hostname(),
|
|
2251
2335
|
cwd: process.cwd(),
|
|
2252
|
-
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:
|
|
2357
|
+
hostname: os8.hostname(),
|
|
2274
2358
|
cwd: process.cwd(),
|
|
2275
|
-
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
|
-
|
|
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
|
-
|
|
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" :
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
2648
|
-
resolveViaDaemon(
|
|
2649
|
-
|
|
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
|
}
|