@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.js
CHANGED
|
@@ -191,8 +191,8 @@ function sanitizeConfig(raw) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
const lines = result.error.issues.map((issue) => {
|
|
194
|
-
const
|
|
195
|
-
return ` \u2022 ${
|
|
194
|
+
const path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
195
|
+
return ` \u2022 ${path14}: ${issue.message}`;
|
|
196
196
|
});
|
|
197
197
|
return {
|
|
198
198
|
sanitized,
|
|
@@ -922,7 +922,7 @@ function getCompiledRegex(pattern, flags = "") {
|
|
|
922
922
|
}
|
|
923
923
|
|
|
924
924
|
// src/policy/index.ts
|
|
925
|
-
var
|
|
925
|
+
var import_path8 = __toESM(require("path"));
|
|
926
926
|
var import_picomatch = __toESM(require("picomatch"));
|
|
927
927
|
var import_sh_syntax = require("sh-syntax");
|
|
928
928
|
|
|
@@ -1476,6 +1476,56 @@ function extractAllSshHosts(tokens) {
|
|
|
1476
1476
|
return [...hosts].filter(Boolean);
|
|
1477
1477
|
}
|
|
1478
1478
|
|
|
1479
|
+
// src/auth/trusted-hosts.ts
|
|
1480
|
+
var import_fs6 = __toESM(require("fs"));
|
|
1481
|
+
var import_path7 = __toESM(require("path"));
|
|
1482
|
+
var import_os5 = __toESM(require("os"));
|
|
1483
|
+
function getTrustedHostsPath() {
|
|
1484
|
+
return import_path7.default.join(import_os5.default.homedir(), ".node9", "trusted-hosts.json");
|
|
1485
|
+
}
|
|
1486
|
+
function readTrustedHosts() {
|
|
1487
|
+
try {
|
|
1488
|
+
const raw = import_fs6.default.readFileSync(getTrustedHostsPath(), "utf8");
|
|
1489
|
+
const parsed = JSON.parse(raw);
|
|
1490
|
+
return Array.isArray(parsed.hosts) ? parsed.hosts : [];
|
|
1491
|
+
} catch {
|
|
1492
|
+
return [];
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
var _cache = null;
|
|
1496
|
+
var CACHE_TTL_MS = 5e3;
|
|
1497
|
+
function getFileMtime() {
|
|
1498
|
+
try {
|
|
1499
|
+
return import_fs6.default.statSync(getTrustedHostsPath()).mtimeMs;
|
|
1500
|
+
} catch {
|
|
1501
|
+
return 0;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
function getCachedHosts() {
|
|
1505
|
+
const now = Date.now();
|
|
1506
|
+
if (_cache && now < _cache.expiry) {
|
|
1507
|
+
const mtime = getFileMtime();
|
|
1508
|
+
if (mtime === _cache.mtime) return _cache.hosts;
|
|
1509
|
+
}
|
|
1510
|
+
const hosts = readTrustedHosts();
|
|
1511
|
+
_cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
1512
|
+
return hosts;
|
|
1513
|
+
}
|
|
1514
|
+
function normalizeHost(raw) {
|
|
1515
|
+
return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
|
|
1516
|
+
}
|
|
1517
|
+
function isTrustedHost(host) {
|
|
1518
|
+
const normalized = normalizeHost(host);
|
|
1519
|
+
return getCachedHosts().some((entry) => {
|
|
1520
|
+
const entryHost = entry.host.toLowerCase();
|
|
1521
|
+
if (entryHost.startsWith("*.")) {
|
|
1522
|
+
const domain = entryHost.slice(2);
|
|
1523
|
+
return normalized.endsWith("." + domain);
|
|
1524
|
+
}
|
|
1525
|
+
return normalized === entryHost;
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1479
1529
|
// src/policy/index.ts
|
|
1480
1530
|
function tokenize2(toolName) {
|
|
1481
1531
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
@@ -1490,9 +1540,9 @@ function matchesPattern(text, patterns) {
|
|
|
1490
1540
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1491
1541
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1492
1542
|
}
|
|
1493
|
-
function getNestedValue(obj,
|
|
1543
|
+
function getNestedValue(obj, path14) {
|
|
1494
1544
|
if (!obj || typeof obj !== "object") return null;
|
|
1495
|
-
return
|
|
1545
|
+
return path14.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1496
1546
|
}
|
|
1497
1547
|
function evaluateSmartConditions(args, rule) {
|
|
1498
1548
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -1647,23 +1697,39 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1647
1697
|
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
1648
1698
|
}
|
|
1649
1699
|
const pipeAnalysis = analyzePipeChain(shellCommand);
|
|
1650
|
-
if (pipeAnalysis.isPipeline) {
|
|
1700
|
+
if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
|
|
1701
|
+
const sinks = pipeAnalysis.sinkTargets;
|
|
1702
|
+
const allTrusted = sinks.length > 0 && sinks.every(isTrustedHost);
|
|
1651
1703
|
if (pipeAnalysis.risk === "critical") {
|
|
1704
|
+
if (allTrusted) {
|
|
1705
|
+
return {
|
|
1706
|
+
decision: "review",
|
|
1707
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
|
|
1708
|
+
reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
|
|
1709
|
+
tier: 3
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1652
1712
|
return {
|
|
1653
1713
|
decision: "block",
|
|
1654
1714
|
blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
|
|
1655
|
-
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${
|
|
1715
|
+
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1656
1716
|
tier: 3
|
|
1657
1717
|
};
|
|
1658
1718
|
}
|
|
1659
|
-
if (
|
|
1719
|
+
if (allTrusted) {
|
|
1660
1720
|
return {
|
|
1661
|
-
decision: "
|
|
1662
|
-
blockedByLabel: "Node9: Pipe-Chain
|
|
1663
|
-
reason: `Sensitive file piped to
|
|
1721
|
+
decision: "allow",
|
|
1722
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
1723
|
+
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
1664
1724
|
tier: 3
|
|
1665
1725
|
};
|
|
1666
1726
|
}
|
|
1727
|
+
return {
|
|
1728
|
+
decision: "review",
|
|
1729
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
1730
|
+
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1731
|
+
tier: 3
|
|
1732
|
+
};
|
|
1667
1733
|
}
|
|
1668
1734
|
const firstToken = analyzed.actions[0] ?? "";
|
|
1669
1735
|
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
@@ -1671,7 +1737,7 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1671
1737
|
const sshHosts = extractAllSshHosts(rawTokens.slice(1));
|
|
1672
1738
|
allTokens.push(...sshHosts);
|
|
1673
1739
|
}
|
|
1674
|
-
if (firstToken &&
|
|
1740
|
+
if (firstToken && import_path8.default.posix.isAbsolute(firstToken)) {
|
|
1675
1741
|
const prov = checkProvenance(firstToken, cwd);
|
|
1676
1742
|
if (prov.trustLevel === "suspect") {
|
|
1677
1743
|
return {
|
|
@@ -1772,18 +1838,18 @@ function isIgnoredTool(toolName) {
|
|
|
1772
1838
|
}
|
|
1773
1839
|
|
|
1774
1840
|
// src/auth/state.ts
|
|
1775
|
-
var
|
|
1776
|
-
var
|
|
1777
|
-
var
|
|
1778
|
-
var PAUSED_FILE =
|
|
1779
|
-
var TRUST_FILE =
|
|
1841
|
+
var import_fs7 = __toESM(require("fs"));
|
|
1842
|
+
var import_path9 = __toESM(require("path"));
|
|
1843
|
+
var import_os6 = __toESM(require("os"));
|
|
1844
|
+
var PAUSED_FILE = import_path9.default.join(import_os6.default.homedir(), ".node9", "PAUSED");
|
|
1845
|
+
var TRUST_FILE = import_path9.default.join(import_os6.default.homedir(), ".node9", "trust.json");
|
|
1780
1846
|
function checkPause() {
|
|
1781
1847
|
try {
|
|
1782
|
-
if (!
|
|
1783
|
-
const state = JSON.parse(
|
|
1848
|
+
if (!import_fs7.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
1849
|
+
const state = JSON.parse(import_fs7.default.readFileSync(PAUSED_FILE, "utf-8"));
|
|
1784
1850
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
1785
1851
|
try {
|
|
1786
|
-
|
|
1852
|
+
import_fs7.default.unlinkSync(PAUSED_FILE);
|
|
1787
1853
|
} catch {
|
|
1788
1854
|
}
|
|
1789
1855
|
return { paused: false };
|
|
@@ -1794,20 +1860,20 @@ function checkPause() {
|
|
|
1794
1860
|
}
|
|
1795
1861
|
}
|
|
1796
1862
|
function atomicWriteSync(filePath, data, options) {
|
|
1797
|
-
const dir =
|
|
1798
|
-
if (!
|
|
1799
|
-
const tmpPath = `${filePath}.${
|
|
1800
|
-
|
|
1801
|
-
|
|
1863
|
+
const dir = import_path9.default.dirname(filePath);
|
|
1864
|
+
if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
|
|
1865
|
+
const tmpPath = `${filePath}.${import_os6.default.hostname()}.${process.pid}.tmp`;
|
|
1866
|
+
import_fs7.default.writeFileSync(tmpPath, data, options);
|
|
1867
|
+
import_fs7.default.renameSync(tmpPath, filePath);
|
|
1802
1868
|
}
|
|
1803
1869
|
function getActiveTrustSession(toolName) {
|
|
1804
1870
|
try {
|
|
1805
|
-
if (!
|
|
1806
|
-
const trust = JSON.parse(
|
|
1871
|
+
if (!import_fs7.default.existsSync(TRUST_FILE)) return false;
|
|
1872
|
+
const trust = JSON.parse(import_fs7.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
1807
1873
|
const now = Date.now();
|
|
1808
1874
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
1809
1875
|
if (active.length !== trust.entries.length) {
|
|
1810
|
-
|
|
1876
|
+
import_fs7.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
1811
1877
|
}
|
|
1812
1878
|
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
1813
1879
|
} catch {
|
|
@@ -1818,8 +1884,8 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
1818
1884
|
try {
|
|
1819
1885
|
let trust = { entries: [] };
|
|
1820
1886
|
try {
|
|
1821
|
-
if (
|
|
1822
|
-
trust = JSON.parse(
|
|
1887
|
+
if (import_fs7.default.existsSync(TRUST_FILE)) {
|
|
1888
|
+
trust = JSON.parse(import_fs7.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
1823
1889
|
}
|
|
1824
1890
|
} catch {
|
|
1825
1891
|
}
|
|
@@ -1835,9 +1901,9 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
1835
1901
|
}
|
|
1836
1902
|
function getPersistentDecision(toolName) {
|
|
1837
1903
|
try {
|
|
1838
|
-
const file =
|
|
1839
|
-
if (!
|
|
1840
|
-
const decisions = JSON.parse(
|
|
1904
|
+
const file = import_path9.default.join(import_os6.default.homedir(), ".node9", "decisions.json");
|
|
1905
|
+
if (!import_fs7.default.existsSync(file)) return null;
|
|
1906
|
+
const decisions = JSON.parse(import_fs7.default.readFileSync(file, "utf-8"));
|
|
1841
1907
|
const d = decisions[toolName];
|
|
1842
1908
|
if (d === "allow" || d === "deny") return d;
|
|
1843
1909
|
} catch {
|
|
@@ -1846,17 +1912,17 @@ function getPersistentDecision(toolName) {
|
|
|
1846
1912
|
}
|
|
1847
1913
|
|
|
1848
1914
|
// src/auth/daemon.ts
|
|
1849
|
-
var
|
|
1850
|
-
var
|
|
1851
|
-
var
|
|
1915
|
+
var import_fs8 = __toESM(require("fs"));
|
|
1916
|
+
var import_path10 = __toESM(require("path"));
|
|
1917
|
+
var import_os7 = __toESM(require("os"));
|
|
1852
1918
|
var import_child_process = require("child_process");
|
|
1853
1919
|
var DAEMON_PORT = 7391;
|
|
1854
1920
|
var DAEMON_HOST = "127.0.0.1";
|
|
1855
1921
|
function getInternalToken() {
|
|
1856
1922
|
try {
|
|
1857
|
-
const pidFile =
|
|
1858
|
-
if (!
|
|
1859
|
-
const data = JSON.parse(
|
|
1923
|
+
const pidFile = import_path10.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
|
|
1924
|
+
if (!import_fs8.default.existsSync(pidFile)) return null;
|
|
1925
|
+
const data = JSON.parse(import_fs8.default.readFileSync(pidFile, "utf-8"));
|
|
1860
1926
|
process.kill(data.pid, 0);
|
|
1861
1927
|
return data.internalToken ?? null;
|
|
1862
1928
|
} catch {
|
|
@@ -1864,10 +1930,10 @@ function getInternalToken() {
|
|
|
1864
1930
|
}
|
|
1865
1931
|
}
|
|
1866
1932
|
function isDaemonRunning() {
|
|
1867
|
-
const pidFile =
|
|
1868
|
-
if (
|
|
1933
|
+
const pidFile = import_path10.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
|
|
1934
|
+
if (import_fs8.default.existsSync(pidFile)) {
|
|
1869
1935
|
try {
|
|
1870
|
-
const { pid, port } = JSON.parse(
|
|
1936
|
+
const { pid, port } = JSON.parse(import_fs8.default.readFileSync(pidFile, "utf-8"));
|
|
1871
1937
|
if (port !== DAEMON_PORT) return false;
|
|
1872
1938
|
process.kill(pid, 0);
|
|
1873
1939
|
return true;
|
|
@@ -1908,8 +1974,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
1908
1974
|
signal: ctrl.signal
|
|
1909
1975
|
});
|
|
1910
1976
|
if (!res.ok) throw new Error("Daemon fail");
|
|
1911
|
-
const { id } = await res.json();
|
|
1912
|
-
return id;
|
|
1977
|
+
const { id, allowCount } = await res.json();
|
|
1978
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
1913
1979
|
} finally {
|
|
1914
1980
|
clearTimeout(timer);
|
|
1915
1981
|
}
|
|
@@ -1948,31 +2014,31 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
|
1948
2014
|
signal: AbortSignal.timeout(3e3)
|
|
1949
2015
|
});
|
|
1950
2016
|
if (!res.ok) throw new Error("Daemon unreachable");
|
|
1951
|
-
const { id } = await res.json();
|
|
1952
|
-
return id;
|
|
2017
|
+
const { id, allowCount } = await res.json();
|
|
2018
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
1953
2019
|
}
|
|
1954
|
-
async function resolveViaDaemon(id, decision, internalToken) {
|
|
2020
|
+
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
1955
2021
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1956
2022
|
await fetch(`${base}/resolve/${id}`, {
|
|
1957
2023
|
method: "POST",
|
|
1958
2024
|
headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
|
|
1959
|
-
body: JSON.stringify({ decision }),
|
|
2025
|
+
body: JSON.stringify({ decision, ...source && { source } }),
|
|
1960
2026
|
signal: AbortSignal.timeout(3e3)
|
|
1961
2027
|
});
|
|
1962
2028
|
}
|
|
1963
2029
|
|
|
1964
2030
|
// src/auth/orchestrator.ts
|
|
1965
2031
|
var import_net = __toESM(require("net"));
|
|
1966
|
-
var
|
|
1967
|
-
var
|
|
2032
|
+
var import_path13 = __toESM(require("path"));
|
|
2033
|
+
var import_os9 = __toESM(require("os"));
|
|
1968
2034
|
var import_crypto = require("crypto");
|
|
1969
2035
|
|
|
1970
2036
|
// src/ui/native.ts
|
|
1971
2037
|
var import_child_process2 = require("child_process");
|
|
1972
|
-
var
|
|
2038
|
+
var import_path12 = __toESM(require("path"));
|
|
1973
2039
|
|
|
1974
2040
|
// src/context-sniper.ts
|
|
1975
|
-
var
|
|
2041
|
+
var import_path11 = __toESM(require("path"));
|
|
1976
2042
|
function smartTruncate(str, maxLen = 500) {
|
|
1977
2043
|
if (str.length <= maxLen) return str;
|
|
1978
2044
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -2040,7 +2106,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
2040
2106
|
intent = "EDIT";
|
|
2041
2107
|
if (obj.file_path) {
|
|
2042
2108
|
editFilePath = String(obj.file_path);
|
|
2043
|
-
editFileName =
|
|
2109
|
+
editFileName = import_path11.default.basename(editFilePath);
|
|
2044
2110
|
}
|
|
2045
2111
|
const result = extractContext(String(obj.new_string), matchedWord);
|
|
2046
2112
|
contextSnippet = result.snippet;
|
|
@@ -2095,7 +2161,7 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
2095
2161
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2096
2162
|
const obj = parsed;
|
|
2097
2163
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
2098
|
-
const file = obj.file_path ?
|
|
2164
|
+
const file = obj.file_path ? import_path12.default.basename(String(obj.file_path)) : "file";
|
|
2099
2165
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
2100
2166
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
2101
2167
|
return {
|
|
@@ -2158,20 +2224,24 @@ ${smartTruncate(str, 500)}`
|
|
|
2158
2224
|
function escapePango(text) {
|
|
2159
2225
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2160
2226
|
}
|
|
2161
|
-
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2227
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2162
2228
|
const lines = [];
|
|
2163
2229
|
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
2164
2230
|
lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
|
|
2165
2231
|
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
2166
2232
|
lines.push("");
|
|
2167
2233
|
lines.push(formattedArgs);
|
|
2234
|
+
if (allowCount >= 3) {
|
|
2235
|
+
lines.push("");
|
|
2236
|
+
lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
|
|
2237
|
+
}
|
|
2168
2238
|
if (!locked) {
|
|
2169
2239
|
lines.push("");
|
|
2170
2240
|
lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
|
|
2171
2241
|
}
|
|
2172
2242
|
return lines.join("\n");
|
|
2173
2243
|
}
|
|
2174
|
-
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2244
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2175
2245
|
const lines = [];
|
|
2176
2246
|
if (locked) {
|
|
2177
2247
|
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
@@ -2183,6 +2253,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2183
2253
|
lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
|
|
2184
2254
|
lines.push("");
|
|
2185
2255
|
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
2256
|
+
if (allowCount >= 3) {
|
|
2257
|
+
lines.push("");
|
|
2258
|
+
lines.push(
|
|
2259
|
+
`<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2186
2262
|
if (!locked) {
|
|
2187
2263
|
lines.push("");
|
|
2188
2264
|
lines.push(
|
|
@@ -2191,12 +2267,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2191
2267
|
}
|
|
2192
2268
|
return lines.join("\n");
|
|
2193
2269
|
}
|
|
2194
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
|
|
2270
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
|
|
2195
2271
|
if (isTestEnv()) return "deny";
|
|
2196
2272
|
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
2197
2273
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
2198
2274
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
2199
|
-
const message = buildPlainMessage(
|
|
2275
|
+
const message = buildPlainMessage(
|
|
2276
|
+
toolName,
|
|
2277
|
+
formattedArgs,
|
|
2278
|
+
agent,
|
|
2279
|
+
explainableLabel,
|
|
2280
|
+
locked,
|
|
2281
|
+
allowCount
|
|
2282
|
+
);
|
|
2200
2283
|
return new Promise((resolve) => {
|
|
2201
2284
|
let childProcess = null;
|
|
2202
2285
|
const onAbort = () => {
|
|
@@ -2228,7 +2311,8 @@ end run`;
|
|
|
2228
2311
|
formattedArgs,
|
|
2229
2312
|
agent,
|
|
2230
2313
|
explainableLabel,
|
|
2231
|
-
locked
|
|
2314
|
+
locked,
|
|
2315
|
+
allowCount
|
|
2232
2316
|
);
|
|
2233
2317
|
const argsList = [
|
|
2234
2318
|
locked ? "--info" : "--question",
|
|
@@ -2270,8 +2354,8 @@ end run`;
|
|
|
2270
2354
|
}
|
|
2271
2355
|
|
|
2272
2356
|
// src/auth/cloud.ts
|
|
2273
|
-
var
|
|
2274
|
-
var
|
|
2357
|
+
var import_fs9 = __toESM(require("fs"));
|
|
2358
|
+
var import_os8 = __toESM(require("os"));
|
|
2275
2359
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
2276
2360
|
return fetch(`${creds.apiUrl}/audit`, {
|
|
2277
2361
|
method: "POST",
|
|
@@ -2283,9 +2367,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
2283
2367
|
context: {
|
|
2284
2368
|
agent: meta?.agent,
|
|
2285
2369
|
mcpServer: meta?.mcpServer,
|
|
2286
|
-
hostname:
|
|
2370
|
+
hostname: import_os8.default.hostname(),
|
|
2287
2371
|
cwd: process.cwd(),
|
|
2288
|
-
platform:
|
|
2372
|
+
platform: import_os8.default.platform()
|
|
2289
2373
|
}
|
|
2290
2374
|
}),
|
|
2291
2375
|
signal: AbortSignal.timeout(5e3)
|
|
@@ -2306,9 +2390,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
2306
2390
|
context: {
|
|
2307
2391
|
agent: meta?.agent,
|
|
2308
2392
|
mcpServer: meta?.mcpServer,
|
|
2309
|
-
hostname:
|
|
2393
|
+
hostname: import_os8.default.hostname(),
|
|
2310
2394
|
cwd: process.cwd(),
|
|
2311
|
-
platform:
|
|
2395
|
+
platform: import_os8.default.platform()
|
|
2312
2396
|
},
|
|
2313
2397
|
...riskMetadata && { riskMetadata }
|
|
2314
2398
|
}),
|
|
@@ -2364,14 +2448,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
2364
2448
|
});
|
|
2365
2449
|
clearTimeout(timer);
|
|
2366
2450
|
if (!res.ok) {
|
|
2367
|
-
|
|
2451
|
+
import_fs9.default.appendFileSync(
|
|
2368
2452
|
HOOK_DEBUG_LOG,
|
|
2369
2453
|
`[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
|
|
2370
2454
|
`
|
|
2371
2455
|
);
|
|
2372
2456
|
}
|
|
2373
2457
|
} catch (err) {
|
|
2374
|
-
|
|
2458
|
+
import_fs9.default.appendFileSync(
|
|
2375
2459
|
HOOK_DEBUG_LOG,
|
|
2376
2460
|
`[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
|
|
2377
2461
|
`
|
|
@@ -2380,7 +2464,7 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
2380
2464
|
}
|
|
2381
2465
|
|
|
2382
2466
|
// src/auth/orchestrator.ts
|
|
2383
|
-
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
2467
|
+
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path13.default.join(import_os9.default.tmpdir(), "node9-activity.sock");
|
|
2384
2468
|
function notifyActivity(data) {
|
|
2385
2469
|
return new Promise((resolve) => {
|
|
2386
2470
|
try {
|
|
@@ -2573,13 +2657,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2573
2657
|
let viewerId = null;
|
|
2574
2658
|
const internalToken = getInternalToken();
|
|
2575
2659
|
let daemonEntryId = null;
|
|
2660
|
+
let daemonAllowCount = 1;
|
|
2576
2661
|
if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
2577
2662
|
if (cloudEnforced && cloudRequestId) {
|
|
2578
|
-
|
|
2663
|
+
const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
|
|
2664
|
+
viewerId = viewer?.id ?? null;
|
|
2579
2665
|
daemonEntryId = viewerId;
|
|
2666
|
+
if (viewer) daemonAllowCount = viewer.allowCount;
|
|
2580
2667
|
} else {
|
|
2581
2668
|
try {
|
|
2582
|
-
|
|
2669
|
+
const entry = await registerDaemonEntry(
|
|
2583
2670
|
toolName,
|
|
2584
2671
|
args,
|
|
2585
2672
|
meta,
|
|
@@ -2587,6 +2674,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2587
2674
|
options?.activityId,
|
|
2588
2675
|
options?.cwd
|
|
2589
2676
|
);
|
|
2677
|
+
daemonEntryId = entry.id;
|
|
2678
|
+
daemonAllowCount = entry.allowCount;
|
|
2590
2679
|
} catch {
|
|
2591
2680
|
}
|
|
2592
2681
|
}
|
|
@@ -2622,7 +2711,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2622
2711
|
false,
|
|
2623
2712
|
signal,
|
|
2624
2713
|
policyMatchedField,
|
|
2625
|
-
policyMatchedWord
|
|
2714
|
+
policyMatchedWord,
|
|
2715
|
+
daemonAllowCount
|
|
2626
2716
|
);
|
|
2627
2717
|
if (decision === "always_allow") {
|
|
2628
2718
|
writeTrustSession(toolName, 36e5);
|
|
@@ -2680,10 +2770,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
2680
2770
|
if (!resolved) {
|
|
2681
2771
|
resolved = true;
|
|
2682
2772
|
abortController.abort();
|
|
2683
|
-
if (
|
|
2684
|
-
resolveViaDaemon(
|
|
2685
|
-
|
|
2686
|
-
|
|
2773
|
+
if (daemonEntryId && internalToken) {
|
|
2774
|
+
resolveViaDaemon(
|
|
2775
|
+
daemonEntryId,
|
|
2776
|
+
res.approved ? "allow" : "deny",
|
|
2777
|
+
internalToken,
|
|
2778
|
+
res.decisionSource
|
|
2779
|
+
).catch(() => null);
|
|
2687
2780
|
}
|
|
2688
2781
|
resolve(res);
|
|
2689
2782
|
}
|