@node9/proxy 1.1.5 → 1.1.7
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 +1 -1
- package/dist/cli.js +769 -515
- package/dist/cli.mjs +773 -519
- package/dist/index.js +107 -201
- package/dist/index.mjs +107 -201
- package/package.json +5 -4
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
// src/core.ts
|
|
2
|
-
import chalk2 from "chalk";
|
|
3
|
-
import { confirm } from "@inquirer/prompts";
|
|
4
2
|
import fs3 from "fs";
|
|
5
3
|
import path5 from "path";
|
|
6
4
|
import os2 from "os";
|
|
@@ -14,7 +12,6 @@ import { parse } from "sh-syntax";
|
|
|
14
12
|
// src/ui/native.ts
|
|
15
13
|
import { spawn } from "child_process";
|
|
16
14
|
import path2 from "path";
|
|
17
|
-
import chalk from "chalk";
|
|
18
15
|
|
|
19
16
|
// src/context-sniper.ts
|
|
20
17
|
import path from "path";
|
|
@@ -200,21 +197,6 @@ ${smartTruncate(str, 500)}`
|
|
|
200
197
|
}
|
|
201
198
|
return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
|
|
202
199
|
}
|
|
203
|
-
function sendDesktopNotification(title, body) {
|
|
204
|
-
if (isTestEnv()) return;
|
|
205
|
-
try {
|
|
206
|
-
if (process.platform === "darwin") {
|
|
207
|
-
const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
|
|
208
|
-
spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
|
|
209
|
-
} else if (process.platform === "linux") {
|
|
210
|
-
spawn("notify-send", [title, body, "--icon=dialog-warning"], {
|
|
211
|
-
detached: true,
|
|
212
|
-
stdio: "ignore"
|
|
213
|
-
}).unref();
|
|
214
|
-
}
|
|
215
|
-
} catch {
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
200
|
function escapePango(text) {
|
|
219
201
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
220
202
|
}
|
|
@@ -257,9 +239,6 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
257
239
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
258
240
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
259
241
|
const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
|
|
260
|
-
process.stderr.write(chalk.yellow(`
|
|
261
|
-
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
262
|
-
`));
|
|
263
242
|
return new Promise((resolve) => {
|
|
264
243
|
let childProcess = null;
|
|
265
244
|
const onAbort = () => {
|
|
@@ -383,6 +362,7 @@ var ConfigFileSchema = z.object({
|
|
|
383
362
|
enableUndo: z.boolean().optional(),
|
|
384
363
|
enableHookLogDebug: z.boolean().optional(),
|
|
385
364
|
approvalTimeoutMs: z.number().nonnegative().optional(),
|
|
365
|
+
approvalTimeoutSeconds: z.number().nonnegative().optional(),
|
|
386
366
|
flightRecorder: z.boolean().optional(),
|
|
387
367
|
approvers: z.object({
|
|
388
368
|
native: z.boolean().optional(),
|
|
@@ -716,9 +696,9 @@ var SENSITIVE_PATH_PATTERNS = [
|
|
|
716
696
|
/[/\\][^/\\]+\.key$/i,
|
|
717
697
|
/[/\\][^/\\]+\.p12$/i,
|
|
718
698
|
/[/\\][^/\\]+\.pfx$/i,
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
699
|
+
/^(?:[a-zA-Z]:)?\/etc\/passwd$/,
|
|
700
|
+
/^(?:[a-zA-Z]:)?\/etc\/shadow$/,
|
|
701
|
+
/^(?:[a-zA-Z]:)?\/etc\/sudoers$/,
|
|
722
702
|
/[/\\]credentials\.json$/i,
|
|
723
703
|
/[/\\]id_rsa$/i,
|
|
724
704
|
/[/\\]id_ed25519$/i,
|
|
@@ -1119,8 +1099,8 @@ var DEFAULT_CONFIG = {
|
|
|
1119
1099
|
enableUndo: true,
|
|
1120
1100
|
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
1121
1101
|
enableHookLogDebug: true,
|
|
1122
|
-
approvalTimeoutMs:
|
|
1123
|
-
//
|
|
1102
|
+
approvalTimeoutMs: 12e4,
|
|
1103
|
+
// 120-second auto-deny timeout
|
|
1124
1104
|
flightRecorder: true,
|
|
1125
1105
|
approvers: { native: true, browser: true, cloud: false, terminal: true }
|
|
1126
1106
|
},
|
|
@@ -1504,14 +1484,12 @@ function getPersistentDecision(toolName) {
|
|
|
1504
1484
|
}
|
|
1505
1485
|
return null;
|
|
1506
1486
|
}
|
|
1507
|
-
async function
|
|
1487
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
|
|
1508
1488
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1509
|
-
const
|
|
1510
|
-
const
|
|
1511
|
-
const onAbort = () => checkCtrl.abort();
|
|
1512
|
-
if (signal) signal.addEventListener("abort", onAbort);
|
|
1489
|
+
const ctrl = new AbortController();
|
|
1490
|
+
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
1513
1491
|
try {
|
|
1514
|
-
const
|
|
1492
|
+
const res = await fetch(`${base}/check`, {
|
|
1515
1493
|
method: "POST",
|
|
1516
1494
|
headers: { "Content-Type": "application/json" },
|
|
1517
1495
|
body: JSON.stringify({
|
|
@@ -1522,32 +1500,34 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId)
|
|
|
1522
1500
|
fromCLI: true,
|
|
1523
1501
|
// Pass the flight-recorder ID so the daemon uses the same UUID for
|
|
1524
1502
|
// activity-result as the CLI used for the pending activity event.
|
|
1525
|
-
// Without this, the two UUIDs never match and tail.ts never resolves
|
|
1526
|
-
// the pending item.
|
|
1527
1503
|
activityId,
|
|
1528
|
-
...riskMetadata && { riskMetadata }
|
|
1504
|
+
...riskMetadata && { riskMetadata },
|
|
1505
|
+
...cwd && { cwd }
|
|
1529
1506
|
}),
|
|
1530
|
-
signal:
|
|
1507
|
+
signal: ctrl.signal
|
|
1531
1508
|
});
|
|
1532
|
-
if (!
|
|
1533
|
-
const { id } = await
|
|
1534
|
-
|
|
1535
|
-
const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
|
|
1536
|
-
const onWaitAbort = () => waitCtrl.abort();
|
|
1537
|
-
if (signal) signal.addEventListener("abort", onWaitAbort);
|
|
1538
|
-
try {
|
|
1539
|
-
const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
|
|
1540
|
-
if (!waitRes.ok) return "deny";
|
|
1541
|
-
const { decision } = await waitRes.json();
|
|
1542
|
-
if (decision === "allow") return "allow";
|
|
1543
|
-
if (decision === "abandoned") return "abandoned";
|
|
1544
|
-
return "deny";
|
|
1545
|
-
} finally {
|
|
1546
|
-
clearTimeout(waitTimer);
|
|
1547
|
-
if (signal) signal.removeEventListener("abort", onWaitAbort);
|
|
1548
|
-
}
|
|
1509
|
+
if (!res.ok) throw new Error("Daemon fail");
|
|
1510
|
+
const { id } = await res.json();
|
|
1511
|
+
return id;
|
|
1549
1512
|
} finally {
|
|
1550
|
-
clearTimeout(
|
|
1513
|
+
clearTimeout(timer);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
async function waitForDaemonDecision(id, signal) {
|
|
1517
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1518
|
+
const waitCtrl = new AbortController();
|
|
1519
|
+
const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
|
|
1520
|
+
const onAbort = () => waitCtrl.abort();
|
|
1521
|
+
if (signal) signal.addEventListener("abort", onAbort);
|
|
1522
|
+
try {
|
|
1523
|
+
const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
|
|
1524
|
+
if (!waitRes.ok) return { decision: "deny" };
|
|
1525
|
+
const { decision, source } = await waitRes.json();
|
|
1526
|
+
if (decision === "allow") return { decision: "allow", source };
|
|
1527
|
+
if (decision === "abandoned") return { decision: "abandoned", source };
|
|
1528
|
+
return { decision: "deny", source };
|
|
1529
|
+
} finally {
|
|
1530
|
+
clearTimeout(waitTimer);
|
|
1551
1531
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1552
1532
|
}
|
|
1553
1533
|
}
|
|
@@ -1595,12 +1575,12 @@ function notifyActivity(data) {
|
|
|
1595
1575
|
}
|
|
1596
1576
|
});
|
|
1597
1577
|
}
|
|
1598
|
-
async function authorizeHeadless(toolName, args,
|
|
1578
|
+
async function authorizeHeadless(toolName, args, meta, options) {
|
|
1599
1579
|
if (!options?.calledFromDaemon) {
|
|
1600
1580
|
const actId = randomUUID();
|
|
1601
1581
|
const actTs = Date.now();
|
|
1602
1582
|
await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
|
|
1603
|
-
const result = await _authorizeHeadlessCore(toolName, args,
|
|
1583
|
+
const result = await _authorizeHeadlessCore(toolName, args, meta, {
|
|
1604
1584
|
...options,
|
|
1605
1585
|
activityId: actId
|
|
1606
1586
|
});
|
|
@@ -1615,14 +1595,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1615
1595
|
}
|
|
1616
1596
|
return result;
|
|
1617
1597
|
}
|
|
1618
|
-
return _authorizeHeadlessCore(toolName, args,
|
|
1598
|
+
return _authorizeHeadlessCore(toolName, args, meta, options);
|
|
1619
1599
|
}
|
|
1620
|
-
async function _authorizeHeadlessCore(toolName, args,
|
|
1600
|
+
async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
1621
1601
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
1622
1602
|
const pauseState = checkPause();
|
|
1623
1603
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
1624
1604
|
const creds = getCredentials();
|
|
1625
|
-
const config = getConfig();
|
|
1605
|
+
const config = getConfig(options?.cwd);
|
|
1626
1606
|
const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
|
|
1627
1607
|
const approvers = {
|
|
1628
1608
|
...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
|
|
@@ -1667,10 +1647,6 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1667
1647
|
if (approvers.cloud && creds?.apiKey) {
|
|
1668
1648
|
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
1669
1649
|
}
|
|
1670
|
-
sendDesktopNotification(
|
|
1671
|
-
"Node9 Audit Mode",
|
|
1672
|
-
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
1673
|
-
);
|
|
1674
1650
|
}
|
|
1675
1651
|
}
|
|
1676
1652
|
return { approved: true, checkedBy: "audit" };
|
|
@@ -1730,23 +1706,12 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1730
1706
|
return { approved: true };
|
|
1731
1707
|
}
|
|
1732
1708
|
let cloudRequestId = null;
|
|
1733
|
-
let isRemoteLocked = false;
|
|
1734
1709
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
1735
1710
|
if (cloudEnforced) {
|
|
1736
1711
|
try {
|
|
1737
1712
|
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
1738
1713
|
if (!initResult.pending) {
|
|
1739
1714
|
if (initResult.shadowMode) {
|
|
1740
|
-
console.error(
|
|
1741
|
-
chalk2.yellow(
|
|
1742
|
-
`
|
|
1743
|
-
\u26A0\uFE0F Node9 Shadow Mode: Action allowed, but would have been blocked by company policy.`
|
|
1744
|
-
)
|
|
1745
|
-
);
|
|
1746
|
-
if (initResult.shadowReason) {
|
|
1747
|
-
console.error(chalk2.dim(` Reason: ${initResult.shadowReason}
|
|
1748
|
-
`));
|
|
1749
|
-
}
|
|
1750
1715
|
return { approved: true, checkedBy: "cloud" };
|
|
1751
1716
|
}
|
|
1752
1717
|
return {
|
|
@@ -1758,36 +1723,8 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1758
1723
|
};
|
|
1759
1724
|
}
|
|
1760
1725
|
cloudRequestId = initResult.requestId || null;
|
|
1761
|
-
isRemoteLocked = !!initResult.remoteApprovalOnly;
|
|
1762
1726
|
explainableLabel = "Organization Policy (SaaS)";
|
|
1763
|
-
} catch
|
|
1764
|
-
const error = err;
|
|
1765
|
-
const isAuthError = error.message.includes("401") || error.message.includes("403");
|
|
1766
|
-
const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
|
|
1767
|
-
const reason = isAuthError ? "Invalid or missing API key. Run `node9 login` to generate a key (must start with n9_live_)." : isNetworkError ? "Could not reach the Node9 cloud. Check your network or API URL." : error.message;
|
|
1768
|
-
console.error(
|
|
1769
|
-
chalk2.yellow(`
|
|
1770
|
-
\u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + chalk2.dim(`
|
|
1771
|
-
Falling back to local rules...
|
|
1772
|
-
`)
|
|
1773
|
-
);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
if (!options?.calledFromDaemon) {
|
|
1777
|
-
if (cloudEnforced && cloudRequestId) {
|
|
1778
|
-
console.error(
|
|
1779
|
-
chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
|
|
1780
|
-
);
|
|
1781
|
-
console.error(
|
|
1782
|
-
chalk2.cyan(" Dashboard \u2192 ") + chalk2.bold("Mission Control > Activity Feed\n")
|
|
1783
|
-
);
|
|
1784
|
-
} else if (!cloudEnforced) {
|
|
1785
|
-
const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
|
|
1786
|
-
console.error(
|
|
1787
|
-
chalk2.dim(`
|
|
1788
|
-
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
1789
|
-
`)
|
|
1790
|
-
);
|
|
1727
|
+
} catch {
|
|
1791
1728
|
}
|
|
1792
1729
|
}
|
|
1793
1730
|
const abortController = new AbortController();
|
|
@@ -1814,15 +1751,29 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1814
1751
|
}
|
|
1815
1752
|
let viewerId = null;
|
|
1816
1753
|
const internalToken = getInternalToken();
|
|
1754
|
+
let daemonEntryId = null;
|
|
1755
|
+
if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
1756
|
+
if (cloudEnforced && cloudRequestId) {
|
|
1757
|
+
viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
|
|
1758
|
+
daemonEntryId = viewerId;
|
|
1759
|
+
} else {
|
|
1760
|
+
try {
|
|
1761
|
+
daemonEntryId = await registerDaemonEntry(
|
|
1762
|
+
toolName,
|
|
1763
|
+
args,
|
|
1764
|
+
meta,
|
|
1765
|
+
riskMetadata,
|
|
1766
|
+
options?.activityId,
|
|
1767
|
+
options?.cwd
|
|
1768
|
+
);
|
|
1769
|
+
} catch {
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1817
1773
|
if (cloudEnforced && cloudRequestId) {
|
|
1818
1774
|
racePromises.push(
|
|
1819
1775
|
(async () => {
|
|
1820
1776
|
try {
|
|
1821
|
-
if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
|
|
1822
|
-
viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
|
|
1823
|
-
() => null
|
|
1824
|
-
);
|
|
1825
|
-
}
|
|
1826
1777
|
const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
|
|
1827
1778
|
return {
|
|
1828
1779
|
approved: cloudResult.approved,
|
|
@@ -1847,7 +1798,7 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1847
1798
|
args,
|
|
1848
1799
|
meta?.agent,
|
|
1849
1800
|
explainableLabel,
|
|
1850
|
-
|
|
1801
|
+
false,
|
|
1851
1802
|
signal,
|
|
1852
1803
|
policyMatchedField,
|
|
1853
1804
|
policyMatchedWord
|
|
@@ -1862,96 +1813,31 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1862
1813
|
reason: isApproved ? void 0 : "The human user clicked 'Block' on the system dialog window.",
|
|
1863
1814
|
checkedBy: isApproved ? "daemon" : void 0,
|
|
1864
1815
|
blockedBy: isApproved ? void 0 : "local-decision",
|
|
1865
|
-
blockedByLabel: "User Decision (Native)"
|
|
1816
|
+
blockedByLabel: "User Decision (Native)",
|
|
1817
|
+
decisionSource: "native"
|
|
1866
1818
|
};
|
|
1867
1819
|
})()
|
|
1868
1820
|
);
|
|
1869
1821
|
}
|
|
1870
|
-
if (
|
|
1822
|
+
if (daemonEntryId && (approvers.browser || approvers.terminal)) {
|
|
1871
1823
|
racePromises.push(
|
|
1872
1824
|
(async () => {
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1890
|
-
const isApproved = daemonDecision === "allow";
|
|
1891
|
-
return {
|
|
1892
|
-
approved: isApproved,
|
|
1893
|
-
reason: isApproved ? void 0 : "The human user rejected this action via the Node9 Browser Dashboard.",
|
|
1894
|
-
checkedBy: isApproved ? "daemon" : void 0,
|
|
1895
|
-
blockedBy: isApproved ? void 0 : "local-decision",
|
|
1896
|
-
blockedByLabel: "User Decision (Browser)"
|
|
1897
|
-
};
|
|
1898
|
-
} catch (err) {
|
|
1899
|
-
throw err;
|
|
1900
|
-
}
|
|
1901
|
-
})()
|
|
1902
|
-
);
|
|
1903
|
-
}
|
|
1904
|
-
if (approvers.terminal && allowTerminalFallback && process.stdout.isTTY) {
|
|
1905
|
-
racePromises.push(
|
|
1906
|
-
(async () => {
|
|
1907
|
-
try {
|
|
1908
|
-
if (explainableLabel.includes("DLP")) {
|
|
1909
|
-
console.log(chalk2.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
1910
|
-
console.log(
|
|
1911
|
-
chalk2.red.bold(` A sensitive secret was detected in the tool arguments!`)
|
|
1912
|
-
);
|
|
1913
|
-
} else {
|
|
1914
|
-
console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
|
|
1915
|
-
}
|
|
1916
|
-
console.log(`${chalk2.bold("Action:")} ${chalk2.red(toolName)}`);
|
|
1917
|
-
console.log(`${chalk2.bold("Flagged By:")} ${chalk2.yellow(explainableLabel)}`);
|
|
1918
|
-
if (isRemoteLocked) {
|
|
1919
|
-
console.log(chalk2.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
|
|
1920
|
-
`));
|
|
1921
|
-
await new Promise((_, reject) => {
|
|
1922
|
-
signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
|
|
1923
|
-
});
|
|
1924
|
-
}
|
|
1925
|
-
const TIMEOUT_MS = 6e4;
|
|
1926
|
-
let timer;
|
|
1927
|
-
const result = await new Promise((resolve, reject) => {
|
|
1928
|
-
timer = setTimeout(() => reject(new Error("Terminal Timeout")), TIMEOUT_MS);
|
|
1929
|
-
confirm(
|
|
1930
|
-
{ message: `Authorize? (auto-deny in ${TIMEOUT_MS / 1e3}s)`, default: false },
|
|
1931
|
-
{ signal }
|
|
1932
|
-
).then(resolve).catch(reject);
|
|
1933
|
-
});
|
|
1934
|
-
clearTimeout(timer);
|
|
1935
|
-
return {
|
|
1936
|
-
approved: result,
|
|
1937
|
-
reason: result ? void 0 : "The human user typed 'N' in the terminal to reject this action.",
|
|
1938
|
-
checkedBy: result ? "terminal" : void 0,
|
|
1939
|
-
blockedBy: result ? void 0 : "local-decision",
|
|
1940
|
-
blockedByLabel: "User Decision (Terminal)"
|
|
1941
|
-
};
|
|
1942
|
-
} catch (err) {
|
|
1943
|
-
const error = err;
|
|
1944
|
-
if (error.name === "AbortError" || error.message?.includes("Prompt was canceled") || error.message?.includes("Aborted by SaaS"))
|
|
1945
|
-
throw err;
|
|
1946
|
-
if (error.message === "Terminal Timeout") {
|
|
1947
|
-
return {
|
|
1948
|
-
approved: false,
|
|
1949
|
-
reason: "The terminal prompt timed out without a human response.",
|
|
1950
|
-
blockedBy: "local-decision"
|
|
1951
|
-
};
|
|
1952
|
-
}
|
|
1953
|
-
throw err;
|
|
1954
|
-
}
|
|
1825
|
+
const { decision: daemonDecision, source: decisionSource } = await waitForDaemonDecision(
|
|
1826
|
+
daemonEntryId,
|
|
1827
|
+
signal
|
|
1828
|
+
);
|
|
1829
|
+
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1830
|
+
const isApproved = daemonDecision === "allow";
|
|
1831
|
+
const src = decisionSource === "terminal" || decisionSource === "browser" ? decisionSource : approvers.browser ? "browser" : "terminal";
|
|
1832
|
+
const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
|
|
1833
|
+
return {
|
|
1834
|
+
approved: isApproved,
|
|
1835
|
+
reason: isApproved ? void 0 : `The human user rejected this action via the Node9 ${via}.`,
|
|
1836
|
+
checkedBy: isApproved ? "daemon" : void 0,
|
|
1837
|
+
blockedBy: isApproved ? void 0 : "local-decision",
|
|
1838
|
+
blockedByLabel: `User Decision (${via})`,
|
|
1839
|
+
decisionSource: src
|
|
1840
|
+
};
|
|
1955
1841
|
})()
|
|
1956
1842
|
);
|
|
1957
1843
|
}
|
|
@@ -2002,7 +1888,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
2002
1888
|
}
|
|
2003
1889
|
});
|
|
2004
1890
|
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
2005
|
-
await resolveNode9SaaS(
|
|
1891
|
+
await resolveNode9SaaS(
|
|
1892
|
+
cloudRequestId,
|
|
1893
|
+
creds,
|
|
1894
|
+
finalResult.approved,
|
|
1895
|
+
finalResult.decisionSource ?? finalResult.checkedBy ?? "local"
|
|
1896
|
+
);
|
|
2006
1897
|
}
|
|
2007
1898
|
if (!isManual) {
|
|
2008
1899
|
appendLocalAudit(
|
|
@@ -2050,6 +1941,8 @@ function getConfig(cwd) {
|
|
|
2050
1941
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
2051
1942
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
2052
1943
|
if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
|
|
1944
|
+
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
1945
|
+
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
2053
1946
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
2054
1947
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
2055
1948
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
@@ -2209,7 +2102,7 @@ function getCredentials() {
|
|
|
2209
2102
|
return null;
|
|
2210
2103
|
}
|
|
2211
2104
|
async function authorizeAction(toolName, args) {
|
|
2212
|
-
const result = await authorizeHeadless(toolName, args
|
|
2105
|
+
const result = await authorizeHeadless(toolName, args);
|
|
2213
2106
|
return result.approved;
|
|
2214
2107
|
}
|
|
2215
2108
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
@@ -2278,11 +2171,9 @@ async function pollNode9SaaS(requestId, creds, signal) {
|
|
|
2278
2171
|
if (!statusRes.ok) continue;
|
|
2279
2172
|
const { status, reason } = await statusRes.json();
|
|
2280
2173
|
if (status === "APPROVED") {
|
|
2281
|
-
console.error(chalk2.green("\u2705 Approved via Cloud.\n"));
|
|
2282
2174
|
return { approved: true, reason };
|
|
2283
2175
|
}
|
|
2284
2176
|
if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
|
|
2285
|
-
console.error(chalk2.red("\u274C Denied via Cloud.\n"));
|
|
2286
2177
|
return { approved: false, reason };
|
|
2287
2178
|
}
|
|
2288
2179
|
} catch {
|
|
@@ -2290,19 +2181,34 @@ async function pollNode9SaaS(requestId, creds, signal) {
|
|
|
2290
2181
|
}
|
|
2291
2182
|
return { approved: false, reason: "Cloud approval timed out after 10 minutes." };
|
|
2292
2183
|
}
|
|
2293
|
-
async function resolveNode9SaaS(requestId, creds, approved) {
|
|
2184
|
+
async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
2294
2185
|
try {
|
|
2295
2186
|
const resolveUrl = `${creds.apiUrl}/requests/${requestId}`;
|
|
2296
2187
|
const ctrl = new AbortController();
|
|
2297
2188
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
2298
|
-
await fetch(resolveUrl, {
|
|
2189
|
+
const res = await fetch(resolveUrl, {
|
|
2299
2190
|
method: "PATCH",
|
|
2300
2191
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
2301
|
-
body: JSON.stringify({
|
|
2192
|
+
body: JSON.stringify({
|
|
2193
|
+
decision: approved ? "APPROVED" : "DENIED",
|
|
2194
|
+
...decidedBy && { decidedBy }
|
|
2195
|
+
}),
|
|
2302
2196
|
signal: ctrl.signal
|
|
2303
2197
|
});
|
|
2304
2198
|
clearTimeout(timer);
|
|
2305
|
-
|
|
2199
|
+
if (!res.ok) {
|
|
2200
|
+
fs3.appendFileSync(
|
|
2201
|
+
HOOK_DEBUG_LOG,
|
|
2202
|
+
`[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
|
|
2203
|
+
`
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
} catch (err) {
|
|
2207
|
+
fs3.appendFileSync(
|
|
2208
|
+
HOOK_DEBUG_LOG,
|
|
2209
|
+
`[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
|
|
2210
|
+
`
|
|
2211
|
+
);
|
|
2306
2212
|
}
|
|
2307
2213
|
}
|
|
2308
2214
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
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",
|
|
@@ -61,9 +61,10 @@
|
|
|
61
61
|
"test:e2e": "NODE9_TESTING=1 bash scripts/e2e.sh",
|
|
62
62
|
"preuninstall": "node9 uninstall || echo 'node9 uninstall failed — remove hooks manually from ~/.claude/settings.json'",
|
|
63
63
|
"prepublishOnly": "npm run validate",
|
|
64
|
-
"test": "
|
|
65
|
-
"test:watch": "
|
|
66
|
-
"test:ui": "
|
|
64
|
+
"test": "vitest --run",
|
|
65
|
+
"test:watch": "vitest",
|
|
66
|
+
"test:ui": "vitest --ui",
|
|
67
|
+
"dev:tail": "node -e \"try{const d=JSON.parse(require('fs').readFileSync(require('os').homedir()+'/.node9/daemon.pid','utf8'));process.kill(d.pid)}catch(e){if(e.code!=='ESRCH'&&e.code!=='ENOENT')process.stderr.write(e.message+'\\n')}\" && npm run build && node dist/cli.js tail"
|
|
67
68
|
},
|
|
68
69
|
"dependencies": {
|
|
69
70
|
"@inquirer/prompts": "^8.3.0",
|