@node9/proxy 1.1.6 → 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 +757 -509
- package/dist/cli.mjs +761 -513
- package/dist/index.js +104 -198
- package/dist/index.mjs +104 -198
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -35,8 +35,6 @@ __export(src_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(src_exports);
|
|
36
36
|
|
|
37
37
|
// src/core.ts
|
|
38
|
-
var import_chalk2 = __toESM(require("chalk"));
|
|
39
|
-
var import_prompts = require("@inquirer/prompts");
|
|
40
38
|
var import_fs3 = __toESM(require("fs"));
|
|
41
39
|
var import_path5 = __toESM(require("path"));
|
|
42
40
|
var import_os2 = __toESM(require("os"));
|
|
@@ -50,7 +48,6 @@ var import_sh_syntax = require("sh-syntax");
|
|
|
50
48
|
// src/ui/native.ts
|
|
51
49
|
var import_child_process = require("child_process");
|
|
52
50
|
var import_path2 = __toESM(require("path"));
|
|
53
|
-
var import_chalk = __toESM(require("chalk"));
|
|
54
51
|
|
|
55
52
|
// src/context-sniper.ts
|
|
56
53
|
var import_path = __toESM(require("path"));
|
|
@@ -236,21 +233,6 @@ ${smartTruncate(str, 500)}`
|
|
|
236
233
|
}
|
|
237
234
|
return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
|
|
238
235
|
}
|
|
239
|
-
function sendDesktopNotification(title, body) {
|
|
240
|
-
if (isTestEnv()) return;
|
|
241
|
-
try {
|
|
242
|
-
if (process.platform === "darwin") {
|
|
243
|
-
const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
|
|
244
|
-
(0, import_child_process.spawn)("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
|
|
245
|
-
} else if (process.platform === "linux") {
|
|
246
|
-
(0, import_child_process.spawn)("notify-send", [title, body, "--icon=dialog-warning"], {
|
|
247
|
-
detached: true,
|
|
248
|
-
stdio: "ignore"
|
|
249
|
-
}).unref();
|
|
250
|
-
}
|
|
251
|
-
} catch {
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
236
|
function escapePango(text) {
|
|
255
237
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
256
238
|
}
|
|
@@ -293,9 +275,6 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
293
275
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
294
276
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
295
277
|
const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
|
|
296
|
-
process.stderr.write(import_chalk.default.yellow(`
|
|
297
|
-
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
298
|
-
`));
|
|
299
278
|
return new Promise((resolve) => {
|
|
300
279
|
let childProcess = null;
|
|
301
280
|
const onAbort = () => {
|
|
@@ -419,6 +398,7 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
419
398
|
enableUndo: import_zod.z.boolean().optional(),
|
|
420
399
|
enableHookLogDebug: import_zod.z.boolean().optional(),
|
|
421
400
|
approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
|
|
401
|
+
approvalTimeoutSeconds: import_zod.z.number().nonnegative().optional(),
|
|
422
402
|
flightRecorder: import_zod.z.boolean().optional(),
|
|
423
403
|
approvers: import_zod.z.object({
|
|
424
404
|
native: import_zod.z.boolean().optional(),
|
|
@@ -1155,8 +1135,8 @@ var DEFAULT_CONFIG = {
|
|
|
1155
1135
|
enableUndo: true,
|
|
1156
1136
|
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
1157
1137
|
enableHookLogDebug: true,
|
|
1158
|
-
approvalTimeoutMs:
|
|
1159
|
-
//
|
|
1138
|
+
approvalTimeoutMs: 12e4,
|
|
1139
|
+
// 120-second auto-deny timeout
|
|
1160
1140
|
flightRecorder: true,
|
|
1161
1141
|
approvers: { native: true, browser: true, cloud: false, terminal: true }
|
|
1162
1142
|
},
|
|
@@ -1540,14 +1520,12 @@ function getPersistentDecision(toolName) {
|
|
|
1540
1520
|
}
|
|
1541
1521
|
return null;
|
|
1542
1522
|
}
|
|
1543
|
-
async function
|
|
1523
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
|
|
1544
1524
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1545
|
-
const
|
|
1546
|
-
const
|
|
1547
|
-
const onAbort = () => checkCtrl.abort();
|
|
1548
|
-
if (signal) signal.addEventListener("abort", onAbort);
|
|
1525
|
+
const ctrl = new AbortController();
|
|
1526
|
+
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
1549
1527
|
try {
|
|
1550
|
-
const
|
|
1528
|
+
const res = await fetch(`${base}/check`, {
|
|
1551
1529
|
method: "POST",
|
|
1552
1530
|
headers: { "Content-Type": "application/json" },
|
|
1553
1531
|
body: JSON.stringify({
|
|
@@ -1558,32 +1536,34 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId)
|
|
|
1558
1536
|
fromCLI: true,
|
|
1559
1537
|
// Pass the flight-recorder ID so the daemon uses the same UUID for
|
|
1560
1538
|
// activity-result as the CLI used for the pending activity event.
|
|
1561
|
-
// Without this, the two UUIDs never match and tail.ts never resolves
|
|
1562
|
-
// the pending item.
|
|
1563
1539
|
activityId,
|
|
1564
|
-
...riskMetadata && { riskMetadata }
|
|
1540
|
+
...riskMetadata && { riskMetadata },
|
|
1541
|
+
...cwd && { cwd }
|
|
1565
1542
|
}),
|
|
1566
|
-
signal:
|
|
1543
|
+
signal: ctrl.signal
|
|
1567
1544
|
});
|
|
1568
|
-
if (!
|
|
1569
|
-
const { id } = await
|
|
1570
|
-
|
|
1571
|
-
const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
|
|
1572
|
-
const onWaitAbort = () => waitCtrl.abort();
|
|
1573
|
-
if (signal) signal.addEventListener("abort", onWaitAbort);
|
|
1574
|
-
try {
|
|
1575
|
-
const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
|
|
1576
|
-
if (!waitRes.ok) return "deny";
|
|
1577
|
-
const { decision } = await waitRes.json();
|
|
1578
|
-
if (decision === "allow") return "allow";
|
|
1579
|
-
if (decision === "abandoned") return "abandoned";
|
|
1580
|
-
return "deny";
|
|
1581
|
-
} finally {
|
|
1582
|
-
clearTimeout(waitTimer);
|
|
1583
|
-
if (signal) signal.removeEventListener("abort", onWaitAbort);
|
|
1584
|
-
}
|
|
1545
|
+
if (!res.ok) throw new Error("Daemon fail");
|
|
1546
|
+
const { id } = await res.json();
|
|
1547
|
+
return id;
|
|
1585
1548
|
} finally {
|
|
1586
|
-
clearTimeout(
|
|
1549
|
+
clearTimeout(timer);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
async function waitForDaemonDecision(id, signal) {
|
|
1553
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1554
|
+
const waitCtrl = new AbortController();
|
|
1555
|
+
const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
|
|
1556
|
+
const onAbort = () => waitCtrl.abort();
|
|
1557
|
+
if (signal) signal.addEventListener("abort", onAbort);
|
|
1558
|
+
try {
|
|
1559
|
+
const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
|
|
1560
|
+
if (!waitRes.ok) return { decision: "deny" };
|
|
1561
|
+
const { decision, source } = await waitRes.json();
|
|
1562
|
+
if (decision === "allow") return { decision: "allow", source };
|
|
1563
|
+
if (decision === "abandoned") return { decision: "abandoned", source };
|
|
1564
|
+
return { decision: "deny", source };
|
|
1565
|
+
} finally {
|
|
1566
|
+
clearTimeout(waitTimer);
|
|
1587
1567
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1588
1568
|
}
|
|
1589
1569
|
}
|
|
@@ -1631,12 +1611,12 @@ function notifyActivity(data) {
|
|
|
1631
1611
|
}
|
|
1632
1612
|
});
|
|
1633
1613
|
}
|
|
1634
|
-
async function authorizeHeadless(toolName, args,
|
|
1614
|
+
async function authorizeHeadless(toolName, args, meta, options) {
|
|
1635
1615
|
if (!options?.calledFromDaemon) {
|
|
1636
1616
|
const actId = (0, import_crypto.randomUUID)();
|
|
1637
1617
|
const actTs = Date.now();
|
|
1638
1618
|
await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
|
|
1639
|
-
const result = await _authorizeHeadlessCore(toolName, args,
|
|
1619
|
+
const result = await _authorizeHeadlessCore(toolName, args, meta, {
|
|
1640
1620
|
...options,
|
|
1641
1621
|
activityId: actId
|
|
1642
1622
|
});
|
|
@@ -1651,14 +1631,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1651
1631
|
}
|
|
1652
1632
|
return result;
|
|
1653
1633
|
}
|
|
1654
|
-
return _authorizeHeadlessCore(toolName, args,
|
|
1634
|
+
return _authorizeHeadlessCore(toolName, args, meta, options);
|
|
1655
1635
|
}
|
|
1656
|
-
async function _authorizeHeadlessCore(toolName, args,
|
|
1636
|
+
async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
1657
1637
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
1658
1638
|
const pauseState = checkPause();
|
|
1659
1639
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
1660
1640
|
const creds = getCredentials();
|
|
1661
|
-
const config = getConfig();
|
|
1641
|
+
const config = getConfig(options?.cwd);
|
|
1662
1642
|
const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
|
|
1663
1643
|
const approvers = {
|
|
1664
1644
|
...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
|
|
@@ -1703,10 +1683,6 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1703
1683
|
if (approvers.cloud && creds?.apiKey) {
|
|
1704
1684
|
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
1705
1685
|
}
|
|
1706
|
-
sendDesktopNotification(
|
|
1707
|
-
"Node9 Audit Mode",
|
|
1708
|
-
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
1709
|
-
);
|
|
1710
1686
|
}
|
|
1711
1687
|
}
|
|
1712
1688
|
return { approved: true, checkedBy: "audit" };
|
|
@@ -1766,23 +1742,12 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1766
1742
|
return { approved: true };
|
|
1767
1743
|
}
|
|
1768
1744
|
let cloudRequestId = null;
|
|
1769
|
-
let isRemoteLocked = false;
|
|
1770
1745
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
1771
1746
|
if (cloudEnforced) {
|
|
1772
1747
|
try {
|
|
1773
1748
|
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
1774
1749
|
if (!initResult.pending) {
|
|
1775
1750
|
if (initResult.shadowMode) {
|
|
1776
|
-
console.error(
|
|
1777
|
-
import_chalk2.default.yellow(
|
|
1778
|
-
`
|
|
1779
|
-
\u26A0\uFE0F Node9 Shadow Mode: Action allowed, but would have been blocked by company policy.`
|
|
1780
|
-
)
|
|
1781
|
-
);
|
|
1782
|
-
if (initResult.shadowReason) {
|
|
1783
|
-
console.error(import_chalk2.default.dim(` Reason: ${initResult.shadowReason}
|
|
1784
|
-
`));
|
|
1785
|
-
}
|
|
1786
1751
|
return { approved: true, checkedBy: "cloud" };
|
|
1787
1752
|
}
|
|
1788
1753
|
return {
|
|
@@ -1794,36 +1759,8 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1794
1759
|
};
|
|
1795
1760
|
}
|
|
1796
1761
|
cloudRequestId = initResult.requestId || null;
|
|
1797
|
-
isRemoteLocked = !!initResult.remoteApprovalOnly;
|
|
1798
1762
|
explainableLabel = "Organization Policy (SaaS)";
|
|
1799
|
-
} catch
|
|
1800
|
-
const error = err;
|
|
1801
|
-
const isAuthError = error.message.includes("401") || error.message.includes("403");
|
|
1802
|
-
const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
|
|
1803
|
-
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;
|
|
1804
|
-
console.error(
|
|
1805
|
-
import_chalk2.default.yellow(`
|
|
1806
|
-
\u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + import_chalk2.default.dim(`
|
|
1807
|
-
Falling back to local rules...
|
|
1808
|
-
`)
|
|
1809
|
-
);
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
if (!options?.calledFromDaemon) {
|
|
1813
|
-
if (cloudEnforced && cloudRequestId) {
|
|
1814
|
-
console.error(
|
|
1815
|
-
import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
|
|
1816
|
-
);
|
|
1817
|
-
console.error(
|
|
1818
|
-
import_chalk2.default.cyan(" Dashboard \u2192 ") + import_chalk2.default.bold("Mission Control > Activity Feed\n")
|
|
1819
|
-
);
|
|
1820
|
-
} else if (!cloudEnforced) {
|
|
1821
|
-
const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
|
|
1822
|
-
console.error(
|
|
1823
|
-
import_chalk2.default.dim(`
|
|
1824
|
-
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
1825
|
-
`)
|
|
1826
|
-
);
|
|
1763
|
+
} catch {
|
|
1827
1764
|
}
|
|
1828
1765
|
}
|
|
1829
1766
|
const abortController = new AbortController();
|
|
@@ -1850,15 +1787,29 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1850
1787
|
}
|
|
1851
1788
|
let viewerId = null;
|
|
1852
1789
|
const internalToken = getInternalToken();
|
|
1790
|
+
let daemonEntryId = null;
|
|
1791
|
+
if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
1792
|
+
if (cloudEnforced && cloudRequestId) {
|
|
1793
|
+
viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
|
|
1794
|
+
daemonEntryId = viewerId;
|
|
1795
|
+
} else {
|
|
1796
|
+
try {
|
|
1797
|
+
daemonEntryId = await registerDaemonEntry(
|
|
1798
|
+
toolName,
|
|
1799
|
+
args,
|
|
1800
|
+
meta,
|
|
1801
|
+
riskMetadata,
|
|
1802
|
+
options?.activityId,
|
|
1803
|
+
options?.cwd
|
|
1804
|
+
);
|
|
1805
|
+
} catch {
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1853
1809
|
if (cloudEnforced && cloudRequestId) {
|
|
1854
1810
|
racePromises.push(
|
|
1855
1811
|
(async () => {
|
|
1856
1812
|
try {
|
|
1857
|
-
if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
|
|
1858
|
-
viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
|
|
1859
|
-
() => null
|
|
1860
|
-
);
|
|
1861
|
-
}
|
|
1862
1813
|
const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
|
|
1863
1814
|
return {
|
|
1864
1815
|
approved: cloudResult.approved,
|
|
@@ -1883,7 +1834,7 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1883
1834
|
args,
|
|
1884
1835
|
meta?.agent,
|
|
1885
1836
|
explainableLabel,
|
|
1886
|
-
|
|
1837
|
+
false,
|
|
1887
1838
|
signal,
|
|
1888
1839
|
policyMatchedField,
|
|
1889
1840
|
policyMatchedWord
|
|
@@ -1898,96 +1849,31 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1898
1849
|
reason: isApproved ? void 0 : "The human user clicked 'Block' on the system dialog window.",
|
|
1899
1850
|
checkedBy: isApproved ? "daemon" : void 0,
|
|
1900
1851
|
blockedBy: isApproved ? void 0 : "local-decision",
|
|
1901
|
-
blockedByLabel: "User Decision (Native)"
|
|
1852
|
+
blockedByLabel: "User Decision (Native)",
|
|
1853
|
+
decisionSource: "native"
|
|
1902
1854
|
};
|
|
1903
1855
|
})()
|
|
1904
1856
|
);
|
|
1905
1857
|
}
|
|
1906
|
-
if (
|
|
1858
|
+
if (daemonEntryId && (approvers.browser || approvers.terminal)) {
|
|
1907
1859
|
racePromises.push(
|
|
1908
1860
|
(async () => {
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1926
|
-
const isApproved = daemonDecision === "allow";
|
|
1927
|
-
return {
|
|
1928
|
-
approved: isApproved,
|
|
1929
|
-
reason: isApproved ? void 0 : "The human user rejected this action via the Node9 Browser Dashboard.",
|
|
1930
|
-
checkedBy: isApproved ? "daemon" : void 0,
|
|
1931
|
-
blockedBy: isApproved ? void 0 : "local-decision",
|
|
1932
|
-
blockedByLabel: "User Decision (Browser)"
|
|
1933
|
-
};
|
|
1934
|
-
} catch (err) {
|
|
1935
|
-
throw err;
|
|
1936
|
-
}
|
|
1937
|
-
})()
|
|
1938
|
-
);
|
|
1939
|
-
}
|
|
1940
|
-
if (approvers.terminal && allowTerminalFallback && process.stdout.isTTY) {
|
|
1941
|
-
racePromises.push(
|
|
1942
|
-
(async () => {
|
|
1943
|
-
try {
|
|
1944
|
-
if (explainableLabel.includes("DLP")) {
|
|
1945
|
-
console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
1946
|
-
console.log(
|
|
1947
|
-
import_chalk2.default.red.bold(` A sensitive secret was detected in the tool arguments!`)
|
|
1948
|
-
);
|
|
1949
|
-
} else {
|
|
1950
|
-
console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
|
|
1951
|
-
}
|
|
1952
|
-
console.log(`${import_chalk2.default.bold("Action:")} ${import_chalk2.default.red(toolName)}`);
|
|
1953
|
-
console.log(`${import_chalk2.default.bold("Flagged By:")} ${import_chalk2.default.yellow(explainableLabel)}`);
|
|
1954
|
-
if (isRemoteLocked) {
|
|
1955
|
-
console.log(import_chalk2.default.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
|
|
1956
|
-
`));
|
|
1957
|
-
await new Promise((_, reject) => {
|
|
1958
|
-
signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
|
|
1959
|
-
});
|
|
1960
|
-
}
|
|
1961
|
-
const TIMEOUT_MS = 6e4;
|
|
1962
|
-
let timer;
|
|
1963
|
-
const result = await new Promise((resolve, reject) => {
|
|
1964
|
-
timer = setTimeout(() => reject(new Error("Terminal Timeout")), TIMEOUT_MS);
|
|
1965
|
-
(0, import_prompts.confirm)(
|
|
1966
|
-
{ message: `Authorize? (auto-deny in ${TIMEOUT_MS / 1e3}s)`, default: false },
|
|
1967
|
-
{ signal }
|
|
1968
|
-
).then(resolve).catch(reject);
|
|
1969
|
-
});
|
|
1970
|
-
clearTimeout(timer);
|
|
1971
|
-
return {
|
|
1972
|
-
approved: result,
|
|
1973
|
-
reason: result ? void 0 : "The human user typed 'N' in the terminal to reject this action.",
|
|
1974
|
-
checkedBy: result ? "terminal" : void 0,
|
|
1975
|
-
blockedBy: result ? void 0 : "local-decision",
|
|
1976
|
-
blockedByLabel: "User Decision (Terminal)"
|
|
1977
|
-
};
|
|
1978
|
-
} catch (err) {
|
|
1979
|
-
const error = err;
|
|
1980
|
-
if (error.name === "AbortError" || error.message?.includes("Prompt was canceled") || error.message?.includes("Aborted by SaaS"))
|
|
1981
|
-
throw err;
|
|
1982
|
-
if (error.message === "Terminal Timeout") {
|
|
1983
|
-
return {
|
|
1984
|
-
approved: false,
|
|
1985
|
-
reason: "The terminal prompt timed out without a human response.",
|
|
1986
|
-
blockedBy: "local-decision"
|
|
1987
|
-
};
|
|
1988
|
-
}
|
|
1989
|
-
throw err;
|
|
1990
|
-
}
|
|
1861
|
+
const { decision: daemonDecision, source: decisionSource } = await waitForDaemonDecision(
|
|
1862
|
+
daemonEntryId,
|
|
1863
|
+
signal
|
|
1864
|
+
);
|
|
1865
|
+
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1866
|
+
const isApproved = daemonDecision === "allow";
|
|
1867
|
+
const src = decisionSource === "terminal" || decisionSource === "browser" ? decisionSource : approvers.browser ? "browser" : "terminal";
|
|
1868
|
+
const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
|
|
1869
|
+
return {
|
|
1870
|
+
approved: isApproved,
|
|
1871
|
+
reason: isApproved ? void 0 : `The human user rejected this action via the Node9 ${via}.`,
|
|
1872
|
+
checkedBy: isApproved ? "daemon" : void 0,
|
|
1873
|
+
blockedBy: isApproved ? void 0 : "local-decision",
|
|
1874
|
+
blockedByLabel: `User Decision (${via})`,
|
|
1875
|
+
decisionSource: src
|
|
1876
|
+
};
|
|
1991
1877
|
})()
|
|
1992
1878
|
);
|
|
1993
1879
|
}
|
|
@@ -2038,7 +1924,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
2038
1924
|
}
|
|
2039
1925
|
});
|
|
2040
1926
|
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
2041
|
-
await resolveNode9SaaS(
|
|
1927
|
+
await resolveNode9SaaS(
|
|
1928
|
+
cloudRequestId,
|
|
1929
|
+
creds,
|
|
1930
|
+
finalResult.approved,
|
|
1931
|
+
finalResult.decisionSource ?? finalResult.checkedBy ?? "local"
|
|
1932
|
+
);
|
|
2042
1933
|
}
|
|
2043
1934
|
if (!isManual) {
|
|
2044
1935
|
appendLocalAudit(
|
|
@@ -2086,6 +1977,8 @@ function getConfig(cwd) {
|
|
|
2086
1977
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
2087
1978
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
2088
1979
|
if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
|
|
1980
|
+
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
1981
|
+
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
2089
1982
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
2090
1983
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
2091
1984
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
@@ -2245,7 +2138,7 @@ function getCredentials() {
|
|
|
2245
2138
|
return null;
|
|
2246
2139
|
}
|
|
2247
2140
|
async function authorizeAction(toolName, args) {
|
|
2248
|
-
const result = await authorizeHeadless(toolName, args
|
|
2141
|
+
const result = await authorizeHeadless(toolName, args);
|
|
2249
2142
|
return result.approved;
|
|
2250
2143
|
}
|
|
2251
2144
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
@@ -2314,11 +2207,9 @@ async function pollNode9SaaS(requestId, creds, signal) {
|
|
|
2314
2207
|
if (!statusRes.ok) continue;
|
|
2315
2208
|
const { status, reason } = await statusRes.json();
|
|
2316
2209
|
if (status === "APPROVED") {
|
|
2317
|
-
console.error(import_chalk2.default.green("\u2705 Approved via Cloud.\n"));
|
|
2318
2210
|
return { approved: true, reason };
|
|
2319
2211
|
}
|
|
2320
2212
|
if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
|
|
2321
|
-
console.error(import_chalk2.default.red("\u274C Denied via Cloud.\n"));
|
|
2322
2213
|
return { approved: false, reason };
|
|
2323
2214
|
}
|
|
2324
2215
|
} catch {
|
|
@@ -2326,19 +2217,34 @@ async function pollNode9SaaS(requestId, creds, signal) {
|
|
|
2326
2217
|
}
|
|
2327
2218
|
return { approved: false, reason: "Cloud approval timed out after 10 minutes." };
|
|
2328
2219
|
}
|
|
2329
|
-
async function resolveNode9SaaS(requestId, creds, approved) {
|
|
2220
|
+
async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
2330
2221
|
try {
|
|
2331
2222
|
const resolveUrl = `${creds.apiUrl}/requests/${requestId}`;
|
|
2332
2223
|
const ctrl = new AbortController();
|
|
2333
2224
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
2334
|
-
await fetch(resolveUrl, {
|
|
2225
|
+
const res = await fetch(resolveUrl, {
|
|
2335
2226
|
method: "PATCH",
|
|
2336
2227
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
2337
|
-
body: JSON.stringify({
|
|
2228
|
+
body: JSON.stringify({
|
|
2229
|
+
decision: approved ? "APPROVED" : "DENIED",
|
|
2230
|
+
...decidedBy && { decidedBy }
|
|
2231
|
+
}),
|
|
2338
2232
|
signal: ctrl.signal
|
|
2339
2233
|
});
|
|
2340
2234
|
clearTimeout(timer);
|
|
2341
|
-
|
|
2235
|
+
if (!res.ok) {
|
|
2236
|
+
import_fs3.default.appendFileSync(
|
|
2237
|
+
HOOK_DEBUG_LOG,
|
|
2238
|
+
`[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
|
|
2239
|
+
`
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
} catch (err) {
|
|
2243
|
+
import_fs3.default.appendFileSync(
|
|
2244
|
+
HOOK_DEBUG_LOG,
|
|
2245
|
+
`[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
|
|
2246
|
+
`
|
|
2247
|
+
);
|
|
2342
2248
|
}
|
|
2343
2249
|
}
|
|
2344
2250
|
|