@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/cli.mjs
CHANGED
|
@@ -120,7 +120,6 @@ var init_context_sniper = __esm({
|
|
|
120
120
|
// src/ui/native.ts
|
|
121
121
|
import { spawn } from "child_process";
|
|
122
122
|
import path2 from "path";
|
|
123
|
-
import chalk from "chalk";
|
|
124
123
|
function formatArgs(args, matchedField, matchedWord) {
|
|
125
124
|
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
126
125
|
let parsed = args;
|
|
@@ -199,21 +198,6 @@ ${smartTruncate(str, 500)}`
|
|
|
199
198
|
}
|
|
200
199
|
return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
|
|
201
200
|
}
|
|
202
|
-
function sendDesktopNotification(title, body) {
|
|
203
|
-
if (isTestEnv()) return;
|
|
204
|
-
try {
|
|
205
|
-
if (process.platform === "darwin") {
|
|
206
|
-
const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
|
|
207
|
-
spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
|
|
208
|
-
} else if (process.platform === "linux") {
|
|
209
|
-
spawn("notify-send", [title, body, "--icon=dialog-warning"], {
|
|
210
|
-
detached: true,
|
|
211
|
-
stdio: "ignore"
|
|
212
|
-
}).unref();
|
|
213
|
-
}
|
|
214
|
-
} catch {
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
201
|
function escapePango(text) {
|
|
218
202
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
219
203
|
}
|
|
@@ -256,9 +240,6 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
256
240
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
257
241
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
258
242
|
const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
|
|
259
|
-
process.stderr.write(chalk.yellow(`
|
|
260
|
-
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
261
|
-
`));
|
|
262
243
|
return new Promise((resolve) => {
|
|
263
244
|
let childProcess = null;
|
|
264
245
|
const onAbort = () => {
|
|
@@ -422,6 +403,7 @@ var init_config_schema = __esm({
|
|
|
422
403
|
enableUndo: z.boolean().optional(),
|
|
423
404
|
enableHookLogDebug: z.boolean().optional(),
|
|
424
405
|
approvalTimeoutMs: z.number().nonnegative().optional(),
|
|
406
|
+
approvalTimeoutSeconds: z.number().nonnegative().optional(),
|
|
425
407
|
flightRecorder: z.boolean().optional(),
|
|
426
408
|
approvers: z.object({
|
|
427
409
|
native: z.boolean().optional(),
|
|
@@ -890,8 +872,6 @@ var init_dlp = __esm({
|
|
|
890
872
|
});
|
|
891
873
|
|
|
892
874
|
// src/core.ts
|
|
893
|
-
import chalk2 from "chalk";
|
|
894
|
-
import { confirm } from "@inquirer/prompts";
|
|
895
875
|
import fs3 from "fs";
|
|
896
876
|
import path5 from "path";
|
|
897
877
|
import os2 from "os";
|
|
@@ -1654,14 +1634,12 @@ function getPersistentDecision(toolName) {
|
|
|
1654
1634
|
}
|
|
1655
1635
|
return null;
|
|
1656
1636
|
}
|
|
1657
|
-
async function
|
|
1637
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
|
|
1658
1638
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1659
|
-
const
|
|
1660
|
-
const
|
|
1661
|
-
const onAbort = () => checkCtrl.abort();
|
|
1662
|
-
if (signal) signal.addEventListener("abort", onAbort);
|
|
1639
|
+
const ctrl = new AbortController();
|
|
1640
|
+
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
1663
1641
|
try {
|
|
1664
|
-
const
|
|
1642
|
+
const res = await fetch(`${base}/check`, {
|
|
1665
1643
|
method: "POST",
|
|
1666
1644
|
headers: { "Content-Type": "application/json" },
|
|
1667
1645
|
body: JSON.stringify({
|
|
@@ -1672,32 +1650,34 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId)
|
|
|
1672
1650
|
fromCLI: true,
|
|
1673
1651
|
// Pass the flight-recorder ID so the daemon uses the same UUID for
|
|
1674
1652
|
// activity-result as the CLI used for the pending activity event.
|
|
1675
|
-
// Without this, the two UUIDs never match and tail.ts never resolves
|
|
1676
|
-
// the pending item.
|
|
1677
1653
|
activityId,
|
|
1678
|
-
...riskMetadata && { riskMetadata }
|
|
1654
|
+
...riskMetadata && { riskMetadata },
|
|
1655
|
+
...cwd && { cwd }
|
|
1679
1656
|
}),
|
|
1680
|
-
signal:
|
|
1657
|
+
signal: ctrl.signal
|
|
1681
1658
|
});
|
|
1682
|
-
if (!
|
|
1683
|
-
const { id } = await
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
}
|
|
1659
|
+
if (!res.ok) throw new Error("Daemon fail");
|
|
1660
|
+
const { id } = await res.json();
|
|
1661
|
+
return id;
|
|
1662
|
+
} finally {
|
|
1663
|
+
clearTimeout(timer);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
async function waitForDaemonDecision(id, signal) {
|
|
1667
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1668
|
+
const waitCtrl = new AbortController();
|
|
1669
|
+
const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
|
|
1670
|
+
const onAbort = () => waitCtrl.abort();
|
|
1671
|
+
if (signal) signal.addEventListener("abort", onAbort);
|
|
1672
|
+
try {
|
|
1673
|
+
const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
|
|
1674
|
+
if (!waitRes.ok) return { decision: "deny" };
|
|
1675
|
+
const { decision, source } = await waitRes.json();
|
|
1676
|
+
if (decision === "allow") return { decision: "allow", source };
|
|
1677
|
+
if (decision === "abandoned") return { decision: "abandoned", source };
|
|
1678
|
+
return { decision: "deny", source };
|
|
1699
1679
|
} finally {
|
|
1700
|
-
clearTimeout(
|
|
1680
|
+
clearTimeout(waitTimer);
|
|
1701
1681
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1702
1682
|
}
|
|
1703
1683
|
}
|
|
@@ -1744,12 +1724,12 @@ function notifyActivity(data) {
|
|
|
1744
1724
|
}
|
|
1745
1725
|
});
|
|
1746
1726
|
}
|
|
1747
|
-
async function authorizeHeadless(toolName, args,
|
|
1727
|
+
async function authorizeHeadless(toolName, args, meta, options) {
|
|
1748
1728
|
if (!options?.calledFromDaemon) {
|
|
1749
1729
|
const actId = randomUUID();
|
|
1750
1730
|
const actTs = Date.now();
|
|
1751
1731
|
await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
|
|
1752
|
-
const result = await _authorizeHeadlessCore(toolName, args,
|
|
1732
|
+
const result = await _authorizeHeadlessCore(toolName, args, meta, {
|
|
1753
1733
|
...options,
|
|
1754
1734
|
activityId: actId
|
|
1755
1735
|
});
|
|
@@ -1764,14 +1744,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1764
1744
|
}
|
|
1765
1745
|
return result;
|
|
1766
1746
|
}
|
|
1767
|
-
return _authorizeHeadlessCore(toolName, args,
|
|
1747
|
+
return _authorizeHeadlessCore(toolName, args, meta, options);
|
|
1768
1748
|
}
|
|
1769
|
-
async function _authorizeHeadlessCore(toolName, args,
|
|
1749
|
+
async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
1770
1750
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
1771
1751
|
const pauseState = checkPause();
|
|
1772
1752
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
1773
1753
|
const creds = getCredentials();
|
|
1774
|
-
const config = getConfig();
|
|
1754
|
+
const config = getConfig(options?.cwd);
|
|
1775
1755
|
const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
|
|
1776
1756
|
const approvers = {
|
|
1777
1757
|
...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
|
|
@@ -1816,10 +1796,6 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1816
1796
|
if (approvers.cloud && creds?.apiKey) {
|
|
1817
1797
|
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
1818
1798
|
}
|
|
1819
|
-
sendDesktopNotification(
|
|
1820
|
-
"Node9 Audit Mode",
|
|
1821
|
-
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
1822
|
-
);
|
|
1823
1799
|
}
|
|
1824
1800
|
}
|
|
1825
1801
|
return { approved: true, checkedBy: "audit" };
|
|
@@ -1879,23 +1855,12 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1879
1855
|
return { approved: true };
|
|
1880
1856
|
}
|
|
1881
1857
|
let cloudRequestId = null;
|
|
1882
|
-
let isRemoteLocked = false;
|
|
1883
1858
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
1884
1859
|
if (cloudEnforced) {
|
|
1885
1860
|
try {
|
|
1886
1861
|
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
1887
1862
|
if (!initResult.pending) {
|
|
1888
1863
|
if (initResult.shadowMode) {
|
|
1889
|
-
console.error(
|
|
1890
|
-
chalk2.yellow(
|
|
1891
|
-
`
|
|
1892
|
-
\u26A0\uFE0F Node9 Shadow Mode: Action allowed, but would have been blocked by company policy.`
|
|
1893
|
-
)
|
|
1894
|
-
);
|
|
1895
|
-
if (initResult.shadowReason) {
|
|
1896
|
-
console.error(chalk2.dim(` Reason: ${initResult.shadowReason}
|
|
1897
|
-
`));
|
|
1898
|
-
}
|
|
1899
1864
|
return { approved: true, checkedBy: "cloud" };
|
|
1900
1865
|
}
|
|
1901
1866
|
return {
|
|
@@ -1907,36 +1872,8 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1907
1872
|
};
|
|
1908
1873
|
}
|
|
1909
1874
|
cloudRequestId = initResult.requestId || null;
|
|
1910
|
-
isRemoteLocked = !!initResult.remoteApprovalOnly;
|
|
1911
1875
|
explainableLabel = "Organization Policy (SaaS)";
|
|
1912
|
-
} catch
|
|
1913
|
-
const error = err;
|
|
1914
|
-
const isAuthError = error.message.includes("401") || error.message.includes("403");
|
|
1915
|
-
const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
|
|
1916
|
-
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;
|
|
1917
|
-
console.error(
|
|
1918
|
-
chalk2.yellow(`
|
|
1919
|
-
\u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + chalk2.dim(`
|
|
1920
|
-
Falling back to local rules...
|
|
1921
|
-
`)
|
|
1922
|
-
);
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
if (!options?.calledFromDaemon) {
|
|
1926
|
-
if (cloudEnforced && cloudRequestId) {
|
|
1927
|
-
console.error(
|
|
1928
|
-
chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
|
|
1929
|
-
);
|
|
1930
|
-
console.error(
|
|
1931
|
-
chalk2.cyan(" Dashboard \u2192 ") + chalk2.bold("Mission Control > Activity Feed\n")
|
|
1932
|
-
);
|
|
1933
|
-
} else if (!cloudEnforced) {
|
|
1934
|
-
const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
|
|
1935
|
-
console.error(
|
|
1936
|
-
chalk2.dim(`
|
|
1937
|
-
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
1938
|
-
`)
|
|
1939
|
-
);
|
|
1876
|
+
} catch {
|
|
1940
1877
|
}
|
|
1941
1878
|
}
|
|
1942
1879
|
const abortController = new AbortController();
|
|
@@ -1963,15 +1900,29 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1963
1900
|
}
|
|
1964
1901
|
let viewerId = null;
|
|
1965
1902
|
const internalToken = getInternalToken();
|
|
1903
|
+
let daemonEntryId = null;
|
|
1904
|
+
if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
1905
|
+
if (cloudEnforced && cloudRequestId) {
|
|
1906
|
+
viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
|
|
1907
|
+
daemonEntryId = viewerId;
|
|
1908
|
+
} else {
|
|
1909
|
+
try {
|
|
1910
|
+
daemonEntryId = await registerDaemonEntry(
|
|
1911
|
+
toolName,
|
|
1912
|
+
args,
|
|
1913
|
+
meta,
|
|
1914
|
+
riskMetadata,
|
|
1915
|
+
options?.activityId,
|
|
1916
|
+
options?.cwd
|
|
1917
|
+
);
|
|
1918
|
+
} catch {
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1966
1922
|
if (cloudEnforced && cloudRequestId) {
|
|
1967
1923
|
racePromises.push(
|
|
1968
1924
|
(async () => {
|
|
1969
1925
|
try {
|
|
1970
|
-
if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
|
|
1971
|
-
viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
|
|
1972
|
-
() => null
|
|
1973
|
-
);
|
|
1974
|
-
}
|
|
1975
1926
|
const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
|
|
1976
1927
|
return {
|
|
1977
1928
|
approved: cloudResult.approved,
|
|
@@ -1996,7 +1947,7 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1996
1947
|
args,
|
|
1997
1948
|
meta?.agent,
|
|
1998
1949
|
explainableLabel,
|
|
1999
|
-
|
|
1950
|
+
false,
|
|
2000
1951
|
signal,
|
|
2001
1952
|
policyMatchedField,
|
|
2002
1953
|
policyMatchedWord
|
|
@@ -2011,96 +1962,31 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
2011
1962
|
reason: isApproved ? void 0 : "The human user clicked 'Block' on the system dialog window.",
|
|
2012
1963
|
checkedBy: isApproved ? "daemon" : void 0,
|
|
2013
1964
|
blockedBy: isApproved ? void 0 : "local-decision",
|
|
2014
|
-
blockedByLabel: "User Decision (Native)"
|
|
1965
|
+
blockedByLabel: "User Decision (Native)",
|
|
1966
|
+
decisionSource: "native"
|
|
2015
1967
|
};
|
|
2016
1968
|
})()
|
|
2017
1969
|
);
|
|
2018
1970
|
}
|
|
2019
|
-
if (
|
|
1971
|
+
if (daemonEntryId && (approvers.browser || approvers.terminal)) {
|
|
2020
1972
|
racePromises.push(
|
|
2021
1973
|
(async () => {
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
2039
|
-
const isApproved = daemonDecision === "allow";
|
|
2040
|
-
return {
|
|
2041
|
-
approved: isApproved,
|
|
2042
|
-
reason: isApproved ? void 0 : "The human user rejected this action via the Node9 Browser Dashboard.",
|
|
2043
|
-
checkedBy: isApproved ? "daemon" : void 0,
|
|
2044
|
-
blockedBy: isApproved ? void 0 : "local-decision",
|
|
2045
|
-
blockedByLabel: "User Decision (Browser)"
|
|
2046
|
-
};
|
|
2047
|
-
} catch (err) {
|
|
2048
|
-
throw err;
|
|
2049
|
-
}
|
|
2050
|
-
})()
|
|
2051
|
-
);
|
|
2052
|
-
}
|
|
2053
|
-
if (approvers.terminal && allowTerminalFallback && process.stdout.isTTY) {
|
|
2054
|
-
racePromises.push(
|
|
2055
|
-
(async () => {
|
|
2056
|
-
try {
|
|
2057
|
-
if (explainableLabel.includes("DLP")) {
|
|
2058
|
-
console.log(chalk2.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
2059
|
-
console.log(
|
|
2060
|
-
chalk2.red.bold(` A sensitive secret was detected in the tool arguments!`)
|
|
2061
|
-
);
|
|
2062
|
-
} else {
|
|
2063
|
-
console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
|
|
2064
|
-
}
|
|
2065
|
-
console.log(`${chalk2.bold("Action:")} ${chalk2.red(toolName)}`);
|
|
2066
|
-
console.log(`${chalk2.bold("Flagged By:")} ${chalk2.yellow(explainableLabel)}`);
|
|
2067
|
-
if (isRemoteLocked) {
|
|
2068
|
-
console.log(chalk2.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
|
|
2069
|
-
`));
|
|
2070
|
-
await new Promise((_, reject) => {
|
|
2071
|
-
signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
|
|
2072
|
-
});
|
|
2073
|
-
}
|
|
2074
|
-
const TIMEOUT_MS = 6e4;
|
|
2075
|
-
let timer;
|
|
2076
|
-
const result = await new Promise((resolve, reject) => {
|
|
2077
|
-
timer = setTimeout(() => reject(new Error("Terminal Timeout")), TIMEOUT_MS);
|
|
2078
|
-
confirm(
|
|
2079
|
-
{ message: `Authorize? (auto-deny in ${TIMEOUT_MS / 1e3}s)`, default: false },
|
|
2080
|
-
{ signal }
|
|
2081
|
-
).then(resolve).catch(reject);
|
|
2082
|
-
});
|
|
2083
|
-
clearTimeout(timer);
|
|
2084
|
-
return {
|
|
2085
|
-
approved: result,
|
|
2086
|
-
reason: result ? void 0 : "The human user typed 'N' in the terminal to reject this action.",
|
|
2087
|
-
checkedBy: result ? "terminal" : void 0,
|
|
2088
|
-
blockedBy: result ? void 0 : "local-decision",
|
|
2089
|
-
blockedByLabel: "User Decision (Terminal)"
|
|
2090
|
-
};
|
|
2091
|
-
} catch (err) {
|
|
2092
|
-
const error = err;
|
|
2093
|
-
if (error.name === "AbortError" || error.message?.includes("Prompt was canceled") || error.message?.includes("Aborted by SaaS"))
|
|
2094
|
-
throw err;
|
|
2095
|
-
if (error.message === "Terminal Timeout") {
|
|
2096
|
-
return {
|
|
2097
|
-
approved: false,
|
|
2098
|
-
reason: "The terminal prompt timed out without a human response.",
|
|
2099
|
-
blockedBy: "local-decision"
|
|
2100
|
-
};
|
|
2101
|
-
}
|
|
2102
|
-
throw err;
|
|
2103
|
-
}
|
|
1974
|
+
const { decision: daemonDecision, source: decisionSource } = await waitForDaemonDecision(
|
|
1975
|
+
daemonEntryId,
|
|
1976
|
+
signal
|
|
1977
|
+
);
|
|
1978
|
+
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1979
|
+
const isApproved = daemonDecision === "allow";
|
|
1980
|
+
const src = decisionSource === "terminal" || decisionSource === "browser" ? decisionSource : approvers.browser ? "browser" : "terminal";
|
|
1981
|
+
const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
|
|
1982
|
+
return {
|
|
1983
|
+
approved: isApproved,
|
|
1984
|
+
reason: isApproved ? void 0 : `The human user rejected this action via the Node9 ${via}.`,
|
|
1985
|
+
checkedBy: isApproved ? "daemon" : void 0,
|
|
1986
|
+
blockedBy: isApproved ? void 0 : "local-decision",
|
|
1987
|
+
blockedByLabel: `User Decision (${via})`,
|
|
1988
|
+
decisionSource: src
|
|
1989
|
+
};
|
|
2104
1990
|
})()
|
|
2105
1991
|
);
|
|
2106
1992
|
}
|
|
@@ -2151,7 +2037,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
2151
2037
|
}
|
|
2152
2038
|
});
|
|
2153
2039
|
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
2154
|
-
await resolveNode9SaaS(
|
|
2040
|
+
await resolveNode9SaaS(
|
|
2041
|
+
cloudRequestId,
|
|
2042
|
+
creds,
|
|
2043
|
+
finalResult.approved,
|
|
2044
|
+
finalResult.decisionSource ?? finalResult.checkedBy ?? "local"
|
|
2045
|
+
);
|
|
2155
2046
|
}
|
|
2156
2047
|
if (!isManual) {
|
|
2157
2048
|
appendLocalAudit(
|
|
@@ -2199,6 +2090,8 @@ function getConfig(cwd) {
|
|
|
2199
2090
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
2200
2091
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
2201
2092
|
if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
|
|
2093
|
+
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
2094
|
+
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
2202
2095
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
2203
2096
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
2204
2097
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
@@ -2423,11 +2316,9 @@ async function pollNode9SaaS(requestId, creds, signal) {
|
|
|
2423
2316
|
if (!statusRes.ok) continue;
|
|
2424
2317
|
const { status, reason } = await statusRes.json();
|
|
2425
2318
|
if (status === "APPROVED") {
|
|
2426
|
-
console.error(chalk2.green("\u2705 Approved via Cloud.\n"));
|
|
2427
2319
|
return { approved: true, reason };
|
|
2428
2320
|
}
|
|
2429
2321
|
if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
|
|
2430
|
-
console.error(chalk2.red("\u274C Denied via Cloud.\n"));
|
|
2431
2322
|
return { approved: false, reason };
|
|
2432
2323
|
}
|
|
2433
2324
|
} catch {
|
|
@@ -2435,19 +2326,34 @@ async function pollNode9SaaS(requestId, creds, signal) {
|
|
|
2435
2326
|
}
|
|
2436
2327
|
return { approved: false, reason: "Cloud approval timed out after 10 minutes." };
|
|
2437
2328
|
}
|
|
2438
|
-
async function resolveNode9SaaS(requestId, creds, approved) {
|
|
2329
|
+
async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
2439
2330
|
try {
|
|
2440
2331
|
const resolveUrl = `${creds.apiUrl}/requests/${requestId}`;
|
|
2441
2332
|
const ctrl = new AbortController();
|
|
2442
2333
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
2443
|
-
await fetch(resolveUrl, {
|
|
2334
|
+
const res = await fetch(resolveUrl, {
|
|
2444
2335
|
method: "PATCH",
|
|
2445
2336
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
2446
|
-
body: JSON.stringify({
|
|
2337
|
+
body: JSON.stringify({
|
|
2338
|
+
decision: approved ? "APPROVED" : "DENIED",
|
|
2339
|
+
...decidedBy && { decidedBy }
|
|
2340
|
+
}),
|
|
2447
2341
|
signal: ctrl.signal
|
|
2448
2342
|
});
|
|
2449
2343
|
clearTimeout(timer);
|
|
2450
|
-
|
|
2344
|
+
if (!res.ok) {
|
|
2345
|
+
fs3.appendFileSync(
|
|
2346
|
+
HOOK_DEBUG_LOG,
|
|
2347
|
+
`[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
|
|
2348
|
+
`
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
} catch (err) {
|
|
2352
|
+
fs3.appendFileSync(
|
|
2353
|
+
HOOK_DEBUG_LOG,
|
|
2354
|
+
`[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
|
|
2355
|
+
`
|
|
2356
|
+
);
|
|
2451
2357
|
}
|
|
2452
2358
|
}
|
|
2453
2359
|
var PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
|
|
@@ -2481,8 +2387,8 @@ var init_core = __esm({
|
|
|
2481
2387
|
enableUndo: true,
|
|
2482
2388
|
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
2483
2389
|
enableHookLogDebug: true,
|
|
2484
|
-
approvalTimeoutMs:
|
|
2485
|
-
//
|
|
2390
|
+
approvalTimeoutMs: 12e4,
|
|
2391
|
+
// 120-second auto-deny timeout
|
|
2486
2392
|
flightRecorder: true,
|
|
2487
2393
|
approvers: { native: true, browser: true, cloud: false, terminal: true }
|
|
2488
2394
|
},
|
|
@@ -3746,7 +3652,7 @@ var init_ui = __esm({
|
|
|
3746
3652
|
const res = await fetch('/decision/' + id, {
|
|
3747
3653
|
method: 'POST',
|
|
3748
3654
|
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3749
|
-
body: JSON.stringify({ decision, persist: !!persist }),
|
|
3655
|
+
body: JSON.stringify({ decision, persist: !!persist, source: 'browser' }),
|
|
3750
3656
|
});
|
|
3751
3657
|
if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
|
|
3752
3658
|
card?.remove();
|
|
@@ -3765,7 +3671,7 @@ var init_ui = __esm({
|
|
|
3765
3671
|
const res = await fetch('/decision/' + id, {
|
|
3766
3672
|
method: 'POST',
|
|
3767
3673
|
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3768
|
-
body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
|
|
3674
|
+
body: JSON.stringify({ decision: 'trust', trustDuration: duration, source: 'browser' }),
|
|
3769
3675
|
});
|
|
3770
3676
|
if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
|
|
3771
3677
|
card?.remove();
|
|
@@ -4176,7 +4082,7 @@ import path7 from "path";
|
|
|
4176
4082
|
import os4 from "os";
|
|
4177
4083
|
import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
|
|
4178
4084
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
4179
|
-
import
|
|
4085
|
+
import chalk2 from "chalk";
|
|
4180
4086
|
function atomicWriteSync2(filePath, data, options) {
|
|
4181
4087
|
const dir = path7.dirname(filePath);
|
|
4182
4088
|
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
@@ -4295,7 +4201,7 @@ data: ${JSON.stringify(data)}
|
|
|
4295
4201
|
`;
|
|
4296
4202
|
sseClients.forEach((client) => {
|
|
4297
4203
|
try {
|
|
4298
|
-
client.write(msg);
|
|
4204
|
+
client.res.write(msg);
|
|
4299
4205
|
} catch {
|
|
4300
4206
|
sseClients.delete(client);
|
|
4301
4207
|
}
|
|
@@ -4341,6 +4247,7 @@ function startDaemon() {
|
|
|
4341
4247
|
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
4342
4248
|
const watchMode = process.env.NODE9_WATCH_MODE === "1";
|
|
4343
4249
|
let idleTimer;
|
|
4250
|
+
let browserOpened = false;
|
|
4344
4251
|
function resetIdleTimer() {
|
|
4345
4252
|
if (watchMode) return;
|
|
4346
4253
|
if (idleTimer) clearTimeout(idleTimer);
|
|
@@ -4357,12 +4264,15 @@ function startDaemon() {
|
|
|
4357
4264
|
}
|
|
4358
4265
|
resetIdleTimer();
|
|
4359
4266
|
const server = http.createServer(async (req, res) => {
|
|
4360
|
-
const
|
|
4267
|
+
const reqUrl = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
4268
|
+
const { pathname } = reqUrl;
|
|
4361
4269
|
if (req.method === "GET" && pathname === "/") {
|
|
4362
4270
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
4363
4271
|
return res.end(UI_HTML);
|
|
4364
4272
|
}
|
|
4365
4273
|
if (req.method === "GET" && pathname === "/events") {
|
|
4274
|
+
const capParam = reqUrl.searchParams.get("capabilities") ?? "";
|
|
4275
|
+
const capabilities = capParam ? capParam.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
4366
4276
|
res.writeHead(200, {
|
|
4367
4277
|
"Content-Type": "text/event-stream",
|
|
4368
4278
|
"Cache-Control": "no-cache",
|
|
@@ -4373,7 +4283,8 @@ function startDaemon() {
|
|
|
4373
4283
|
abandonTimer = null;
|
|
4374
4284
|
}
|
|
4375
4285
|
hadBrowserClient = true;
|
|
4376
|
-
|
|
4286
|
+
const sseClient = { res, capabilities };
|
|
4287
|
+
sseClients.add(sseClient);
|
|
4377
4288
|
res.write(
|
|
4378
4289
|
`event: init
|
|
4379
4290
|
data: ${JSON.stringify({
|
|
@@ -4388,7 +4299,7 @@ data: ${JSON.stringify({
|
|
|
4388
4299
|
mcpServer: e.mcpServer
|
|
4389
4300
|
})),
|
|
4390
4301
|
orgName: getOrgName(),
|
|
4391
|
-
autoDenyMs: AUTO_DENY_MS
|
|
4302
|
+
autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
|
|
4392
4303
|
})}
|
|
4393
4304
|
|
|
4394
4305
|
`
|
|
@@ -4410,6 +4321,10 @@ data: ${JSON.stringify({
|
|
|
4410
4321
|
|
|
4411
4322
|
`
|
|
4412
4323
|
);
|
|
4324
|
+
res.write(`event: csrf
|
|
4325
|
+
data: ${JSON.stringify({ token: csrfToken })}
|
|
4326
|
+
|
|
4327
|
+
`);
|
|
4413
4328
|
for (const item of activityRing) {
|
|
4414
4329
|
res.write(`event: ${item.event}
|
|
4415
4330
|
data: ${JSON.stringify(item.data)}
|
|
@@ -4417,7 +4332,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
4417
4332
|
`);
|
|
4418
4333
|
}
|
|
4419
4334
|
return req.on("close", () => {
|
|
4420
|
-
sseClients.delete(
|
|
4335
|
+
sseClients.delete(sseClient);
|
|
4421
4336
|
if (sseClients.size === 0 && pending.size > 0) {
|
|
4422
4337
|
abandonTimer = setTimeout(abandonPending, hadBrowserClient ? 1e4 : 15e3);
|
|
4423
4338
|
}
|
|
@@ -4437,7 +4352,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
4437
4352
|
mcpServer,
|
|
4438
4353
|
riskMetadata,
|
|
4439
4354
|
fromCLI = false,
|
|
4440
|
-
activityId
|
|
4355
|
+
activityId,
|
|
4356
|
+
cwd
|
|
4441
4357
|
} = JSON.parse(body);
|
|
4442
4358
|
const id = fromCLI && typeof activityId === "string" && activityId || randomUUID2();
|
|
4443
4359
|
const entry = {
|
|
@@ -4467,7 +4383,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
4467
4383
|
pending.delete(id);
|
|
4468
4384
|
broadcast("remove", { id });
|
|
4469
4385
|
}
|
|
4470
|
-
}, AUTO_DENY_MS)
|
|
4386
|
+
}, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
|
|
4471
4387
|
};
|
|
4472
4388
|
pending.set(id, entry);
|
|
4473
4389
|
if (!fromCLI) {
|
|
@@ -4479,8 +4395,11 @@ data: ${JSON.stringify(item.data)}
|
|
|
4479
4395
|
status: "pending"
|
|
4480
4396
|
});
|
|
4481
4397
|
}
|
|
4482
|
-
const
|
|
4483
|
-
|
|
4398
|
+
const projectCwd = typeof cwd === "string" && path7.isAbsolute(cwd) ? cwd : void 0;
|
|
4399
|
+
const projectConfig = getConfig(projectCwd);
|
|
4400
|
+
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
4401
|
+
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
4402
|
+
if (browserEnabled || terminalEnabled) {
|
|
4484
4403
|
broadcast("add", {
|
|
4485
4404
|
id,
|
|
4486
4405
|
toolName,
|
|
@@ -4488,17 +4407,21 @@ data: ${JSON.stringify(item.data)}
|
|
|
4488
4407
|
riskMetadata: entry.riskMetadata,
|
|
4489
4408
|
slackDelegated: entry.slackDelegated,
|
|
4490
4409
|
agent: entry.agent,
|
|
4491
|
-
mcpServer: entry.mcpServer
|
|
4410
|
+
mcpServer: entry.mcpServer,
|
|
4411
|
+
interactive: terminalEnabled
|
|
4492
4412
|
});
|
|
4493
|
-
|
|
4413
|
+
const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
|
|
4414
|
+
if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
|
|
4415
|
+
browserOpened = true;
|
|
4494
4416
|
openBrowser(`http://127.0.0.1:${DAEMON_PORT2}/`);
|
|
4417
|
+
}
|
|
4495
4418
|
}
|
|
4496
4419
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4497
4420
|
res.end(JSON.stringify({ id }));
|
|
4421
|
+
if (slackDelegated) return;
|
|
4498
4422
|
authorizeHeadless(
|
|
4499
4423
|
toolName,
|
|
4500
4424
|
args,
|
|
4501
|
-
false,
|
|
4502
4425
|
{
|
|
4503
4426
|
agent: typeof agent === "string" ? agent : void 0,
|
|
4504
4427
|
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
|
|
@@ -4508,6 +4431,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
4508
4431
|
const e = pending.get(id);
|
|
4509
4432
|
if (!e) return;
|
|
4510
4433
|
if (result.noApprovalMechanism) return;
|
|
4434
|
+
if (result.checkedBy === "audit") return;
|
|
4435
|
+
if (e.earlyDecision !== null) return;
|
|
4511
4436
|
broadcast("activity-result", {
|
|
4512
4437
|
id,
|
|
4513
4438
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
@@ -4548,18 +4473,31 @@ data: ${JSON.stringify(item.data)}
|
|
|
4548
4473
|
if (!entry) return res.writeHead(404).end();
|
|
4549
4474
|
if (entry.earlyDecision) {
|
|
4550
4475
|
clearTimeout(entry.timer);
|
|
4476
|
+
const source = entry.decisionSource;
|
|
4551
4477
|
pending.delete(id);
|
|
4552
4478
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4553
|
-
const body = {
|
|
4479
|
+
const body = {
|
|
4480
|
+
decision: entry.earlyDecision
|
|
4481
|
+
};
|
|
4554
4482
|
if (entry.earlyReason) body.reason = entry.earlyReason;
|
|
4483
|
+
if (source) body.source = source;
|
|
4555
4484
|
return res.end(JSON.stringify(body));
|
|
4556
4485
|
}
|
|
4557
4486
|
entry.waiter = (d, reason) => {
|
|
4558
4487
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4559
4488
|
const body = { decision: d };
|
|
4560
4489
|
if (reason) body.reason = reason;
|
|
4490
|
+
if (entry.decisionSource) body.source = entry.decisionSource;
|
|
4561
4491
|
res.end(JSON.stringify(body));
|
|
4562
4492
|
};
|
|
4493
|
+
req.on("close", () => {
|
|
4494
|
+
const e = pending.get(id);
|
|
4495
|
+
if (e && e.waiter && e.earlyDecision === null) {
|
|
4496
|
+
clearTimeout(e.timer);
|
|
4497
|
+
pending.delete(id);
|
|
4498
|
+
broadcast("remove", { id });
|
|
4499
|
+
}
|
|
4500
|
+
});
|
|
4563
4501
|
return;
|
|
4564
4502
|
}
|
|
4565
4503
|
if (req.method === "POST" && pathname.startsWith("/decision/")) {
|
|
@@ -4568,7 +4506,13 @@ data: ${JSON.stringify(item.data)}
|
|
|
4568
4506
|
const id = pathname.split("/").pop();
|
|
4569
4507
|
const entry = pending.get(id);
|
|
4570
4508
|
if (!entry) return res.writeHead(404).end();
|
|
4571
|
-
|
|
4509
|
+
if (entry.earlyDecision !== null) {
|
|
4510
|
+
res.writeHead(409, { "Content-Type": "application/json" });
|
|
4511
|
+
return res.end(JSON.stringify({ conflict: true, decision: entry.earlyDecision }));
|
|
4512
|
+
}
|
|
4513
|
+
const { decision, persist, trustDuration, reason, source } = JSON.parse(
|
|
4514
|
+
await readBody(req)
|
|
4515
|
+
);
|
|
4572
4516
|
if (decision === "trust" && trustDuration) {
|
|
4573
4517
|
const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
|
|
4574
4518
|
writeTrustEntry(entry.toolName, ms);
|
|
@@ -4598,6 +4542,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
4598
4542
|
decision: resolvedDecision
|
|
4599
4543
|
});
|
|
4600
4544
|
clearTimeout(entry.timer);
|
|
4545
|
+
const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
|
|
4546
|
+
if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
|
|
4601
4547
|
if (entry.waiter) {
|
|
4602
4548
|
entry.waiter(resolvedDecision, reason);
|
|
4603
4549
|
pending.delete(id);
|
|
@@ -4620,7 +4566,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
4620
4566
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4621
4567
|
return res.end(JSON.stringify({ ...s, autoStarted }));
|
|
4622
4568
|
} catch (err) {
|
|
4623
|
-
console.error(
|
|
4569
|
+
console.error(chalk2.red("[node9 daemon] GET /settings failed:"), err);
|
|
4624
4570
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
4625
4571
|
return res.end(JSON.stringify({ error: "internal" }));
|
|
4626
4572
|
}
|
|
@@ -4651,7 +4597,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
4651
4597
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4652
4598
|
return res.end(JSON.stringify({ hasKey: hasStoredSlackKey(), enabled: s.slackEnabled }));
|
|
4653
4599
|
} catch (err) {
|
|
4654
|
-
console.error(
|
|
4600
|
+
console.error(chalk2.red("[node9 daemon] GET /slack-status failed:"), err);
|
|
4655
4601
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
4656
4602
|
return res.end(JSON.stringify({ error: "internal" }));
|
|
4657
4603
|
}
|
|
@@ -4800,14 +4746,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
4800
4746
|
});
|
|
4801
4747
|
return;
|
|
4802
4748
|
}
|
|
4803
|
-
console.error(
|
|
4749
|
+
console.error(chalk2.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
|
|
4804
4750
|
process.exit(1);
|
|
4805
4751
|
});
|
|
4806
4752
|
if (!daemonRejectionHandlerRegistered) {
|
|
4807
4753
|
daemonRejectionHandlerRegistered = true;
|
|
4808
4754
|
process.on("unhandledRejection", (reason) => {
|
|
4809
4755
|
const stack = reason instanceof Error ? reason.stack : String(reason);
|
|
4810
|
-
console.error(
|
|
4756
|
+
console.error(chalk2.red("[node9 daemon] unhandled rejection \u2014 keeping daemon alive:"), stack);
|
|
4811
4757
|
});
|
|
4812
4758
|
}
|
|
4813
4759
|
server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
|
|
@@ -4816,10 +4762,10 @@ data: ${JSON.stringify(item.data)}
|
|
|
4816
4762
|
JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
|
|
4817
4763
|
{ mode: 384 }
|
|
4818
4764
|
);
|
|
4819
|
-
console.log(
|
|
4765
|
+
console.log(chalk2.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
|
|
4820
4766
|
});
|
|
4821
4767
|
if (watchMode) {
|
|
4822
|
-
console.log(
|
|
4768
|
+
console.log(chalk2.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
|
|
4823
4769
|
}
|
|
4824
4770
|
try {
|
|
4825
4771
|
fs5.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
@@ -4870,13 +4816,13 @@ data: ${JSON.stringify(item.data)}
|
|
|
4870
4816
|
});
|
|
4871
4817
|
}
|
|
4872
4818
|
function stopDaemon() {
|
|
4873
|
-
if (!fs5.existsSync(DAEMON_PID_FILE)) return console.log(
|
|
4819
|
+
if (!fs5.existsSync(DAEMON_PID_FILE)) return console.log(chalk2.yellow("Not running."));
|
|
4874
4820
|
try {
|
|
4875
4821
|
const { pid } = JSON.parse(fs5.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4876
4822
|
process.kill(pid, "SIGTERM");
|
|
4877
|
-
console.log(
|
|
4823
|
+
console.log(chalk2.green("\u2705 Stopped."));
|
|
4878
4824
|
} catch {
|
|
4879
|
-
console.log(
|
|
4825
|
+
console.log(chalk2.gray("Cleaned up stale PID file."));
|
|
4880
4826
|
} finally {
|
|
4881
4827
|
try {
|
|
4882
4828
|
fs5.unlinkSync(DAEMON_PID_FILE);
|
|
@@ -4889,10 +4835,10 @@ function daemonStatus() {
|
|
|
4889
4835
|
try {
|
|
4890
4836
|
const { pid } = JSON.parse(fs5.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4891
4837
|
process.kill(pid, 0);
|
|
4892
|
-
console.log(
|
|
4838
|
+
console.log(chalk2.green("Node9 daemon: running"));
|
|
4893
4839
|
return;
|
|
4894
4840
|
} catch {
|
|
4895
|
-
console.log(
|
|
4841
|
+
console.log(chalk2.yellow("Node9 daemon: not running (stale PID)"));
|
|
4896
4842
|
return;
|
|
4897
4843
|
}
|
|
4898
4844
|
}
|
|
@@ -4901,9 +4847,9 @@ function daemonStatus() {
|
|
|
4901
4847
|
timeout: 500
|
|
4902
4848
|
});
|
|
4903
4849
|
if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT2}`)) {
|
|
4904
|
-
console.log(
|
|
4850
|
+
console.log(chalk2.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
|
|
4905
4851
|
} else {
|
|
4906
|
-
console.log(
|
|
4852
|
+
console.log(chalk2.yellow("Node9 daemon: not running"));
|
|
4907
4853
|
}
|
|
4908
4854
|
}
|
|
4909
4855
|
var daemonRejectionHandlerRegistered, ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
|
|
@@ -4948,12 +4894,12 @@ __export(tail_exports, {
|
|
|
4948
4894
|
startTail: () => startTail
|
|
4949
4895
|
});
|
|
4950
4896
|
import http2 from "http";
|
|
4951
|
-
import
|
|
4897
|
+
import chalk3 from "chalk";
|
|
4952
4898
|
import fs7 from "fs";
|
|
4953
4899
|
import os6 from "os";
|
|
4954
4900
|
import path9 from "path";
|
|
4955
4901
|
import readline from "readline";
|
|
4956
|
-
import { spawn as spawn4 } from "child_process";
|
|
4902
|
+
import { spawn as spawn4, execSync } from "child_process";
|
|
4957
4903
|
function getIcon(tool) {
|
|
4958
4904
|
const t = tool.toLowerCase();
|
|
4959
4905
|
for (const [k, v] of Object.entries(ICONS)) {
|
|
@@ -4967,17 +4913,17 @@ function formatBase(activity) {
|
|
|
4967
4913
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
4968
4914
|
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
4969
4915
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
4970
|
-
return `${
|
|
4916
|
+
return `${chalk3.gray(time)} ${icon} ${chalk3.white.bold(toolName)} ${chalk3.dim(argsPreview)}`;
|
|
4971
4917
|
}
|
|
4972
4918
|
function renderResult(activity, result) {
|
|
4973
4919
|
const base = formatBase(activity);
|
|
4974
4920
|
let status;
|
|
4975
4921
|
if (result.status === "allow") {
|
|
4976
|
-
status =
|
|
4922
|
+
status = chalk3.green("\u2713 ALLOW");
|
|
4977
4923
|
} else if (result.status === "dlp") {
|
|
4978
|
-
status =
|
|
4924
|
+
status = chalk3.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
4979
4925
|
} else {
|
|
4980
|
-
status =
|
|
4926
|
+
status = chalk3.red("\u2717 BLOCK");
|
|
4981
4927
|
}
|
|
4982
4928
|
if (process.stdout.isTTY) {
|
|
4983
4929
|
readline.clearLine(process.stdout, 0);
|
|
@@ -4987,7 +4933,7 @@ function renderResult(activity, result) {
|
|
|
4987
4933
|
}
|
|
4988
4934
|
function renderPending(activity) {
|
|
4989
4935
|
if (!process.stdout.isTTY) return;
|
|
4990
|
-
process.stdout.write(`${formatBase(activity)} ${
|
|
4936
|
+
process.stdout.write(`${formatBase(activity)} ${chalk3.yellow("\u25CF \u2026")}\r`);
|
|
4991
4937
|
}
|
|
4992
4938
|
async function ensureDaemon() {
|
|
4993
4939
|
let pidPort = null;
|
|
@@ -4996,7 +4942,7 @@ async function ensureDaemon() {
|
|
|
4996
4942
|
const { port } = JSON.parse(fs7.readFileSync(PID_FILE, "utf-8"));
|
|
4997
4943
|
pidPort = port;
|
|
4998
4944
|
} catch {
|
|
4999
|
-
console.error(
|
|
4945
|
+
console.error(chalk3.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
5000
4946
|
}
|
|
5001
4947
|
}
|
|
5002
4948
|
const checkPort = pidPort ?? DAEMON_PORT2;
|
|
@@ -5007,7 +4953,7 @@ async function ensureDaemon() {
|
|
|
5007
4953
|
if (res.ok) return checkPort;
|
|
5008
4954
|
} catch {
|
|
5009
4955
|
}
|
|
5010
|
-
console.log(
|
|
4956
|
+
console.log(chalk3.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
5011
4957
|
const child = spawn4(process.execPath, [process.argv[1], "daemon"], {
|
|
5012
4958
|
detached: true,
|
|
5013
4959
|
stdio: "ignore",
|
|
@@ -5024,9 +4970,51 @@ async function ensureDaemon() {
|
|
|
5024
4970
|
} catch {
|
|
5025
4971
|
}
|
|
5026
4972
|
}
|
|
5027
|
-
console.error(
|
|
4973
|
+
console.error(chalk3.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
5028
4974
|
process.exit(1);
|
|
5029
4975
|
}
|
|
4976
|
+
function postDecisionHttp(id, decision, csrfToken, port) {
|
|
4977
|
+
return new Promise((resolve, reject) => {
|
|
4978
|
+
const body = JSON.stringify({ decision, source: "terminal" });
|
|
4979
|
+
const req = http2.request(
|
|
4980
|
+
{
|
|
4981
|
+
hostname: "127.0.0.1",
|
|
4982
|
+
port,
|
|
4983
|
+
path: `/decision/${id}`,
|
|
4984
|
+
method: "POST",
|
|
4985
|
+
headers: {
|
|
4986
|
+
"Content-Type": "application/json",
|
|
4987
|
+
"Content-Length": Buffer.byteLength(body),
|
|
4988
|
+
"X-Node9-Token": csrfToken
|
|
4989
|
+
}
|
|
4990
|
+
},
|
|
4991
|
+
(res) => {
|
|
4992
|
+
res.resume();
|
|
4993
|
+
if (res.statusCode === 200 || res.statusCode === 409) resolve();
|
|
4994
|
+
else reject(new Error(`POST /decision returned ${res.statusCode}`));
|
|
4995
|
+
}
|
|
4996
|
+
);
|
|
4997
|
+
req.on("error", reject);
|
|
4998
|
+
req.end(body);
|
|
4999
|
+
});
|
|
5000
|
+
}
|
|
5001
|
+
function buildCardLines(req) {
|
|
5002
|
+
const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
|
|
5003
|
+
const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
|
|
5004
|
+
const tierLabel = req.riskMetadata?.tier != null ? req.riskMetadata.tier <= 2 ? `${YELLOW}\u26A0 Tier ${req.riskMetadata.tier}` : `${RED}\u{1F6D1} Tier ${req.riskMetadata.tier}` : `${YELLOW}\u26A0 Review`;
|
|
5005
|
+
const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
|
|
5006
|
+
return [
|
|
5007
|
+
``,
|
|
5008
|
+
`${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
|
|
5009
|
+
`${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
|
|
5010
|
+
`${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`,
|
|
5011
|
+
`${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`,
|
|
5012
|
+
`${CYAN}\u255A${RESET}`,
|
|
5013
|
+
``,
|
|
5014
|
+
` ${BOLD}${GREEN}[A]${RESET} Allow ${BOLD}${RED}[D]${RESET} Deny`,
|
|
5015
|
+
``
|
|
5016
|
+
];
|
|
5017
|
+
}
|
|
5030
5018
|
async function startTail(options = {}) {
|
|
5031
5019
|
const port = await ensureDaemon();
|
|
5032
5020
|
if (options.clear) {
|
|
@@ -5053,7 +5041,7 @@ async function startTail(options = {}) {
|
|
|
5053
5041
|
req2.end();
|
|
5054
5042
|
});
|
|
5055
5043
|
if (result.ok) {
|
|
5056
|
-
console.log(
|
|
5044
|
+
console.log(chalk3.green("\u2713 Flight Recorder buffer cleared."));
|
|
5057
5045
|
} else if (result.code === "ECONNREFUSED") {
|
|
5058
5046
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
5059
5047
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -5064,27 +5052,130 @@ async function startTail(options = {}) {
|
|
|
5064
5052
|
return;
|
|
5065
5053
|
}
|
|
5066
5054
|
const connectionTime = Date.now();
|
|
5067
|
-
const
|
|
5068
|
-
|
|
5069
|
-
|
|
5055
|
+
const activityPending = /* @__PURE__ */ new Map();
|
|
5056
|
+
let csrfToken = "";
|
|
5057
|
+
const approvalQueue = [];
|
|
5058
|
+
let cardActive = false;
|
|
5059
|
+
let cardLineCount = 0;
|
|
5060
|
+
let cancelActiveCard = null;
|
|
5061
|
+
const canApprove = process.stdout.isTTY && process.stdin.isTTY;
|
|
5062
|
+
if (canApprove) readline.emitKeypressEvents(process.stdin);
|
|
5063
|
+
function clearCard() {
|
|
5064
|
+
if (cardLineCount > 0) {
|
|
5065
|
+
process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
|
|
5066
|
+
cardLineCount = 0;
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
5069
|
+
function printCard(req2) {
|
|
5070
|
+
process.stdout.write(HIDE_CURSOR + SAVE_CURSOR);
|
|
5071
|
+
const lines = buildCardLines(req2);
|
|
5072
|
+
for (const line of lines) process.stdout.write(line + "\n");
|
|
5073
|
+
cardLineCount = lines.length;
|
|
5074
|
+
}
|
|
5075
|
+
function showNextCard() {
|
|
5076
|
+
if (cardActive || approvalQueue.length === 0 || !canApprove) return;
|
|
5077
|
+
try {
|
|
5078
|
+
process.stdin.setRawMode(true);
|
|
5079
|
+
} catch {
|
|
5080
|
+
cardActive = false;
|
|
5081
|
+
return;
|
|
5082
|
+
}
|
|
5083
|
+
cardActive = true;
|
|
5084
|
+
const req2 = approvalQueue[0];
|
|
5085
|
+
printCard(req2);
|
|
5086
|
+
let settled = false;
|
|
5087
|
+
let onKeypress = null;
|
|
5088
|
+
const cleanup = () => {
|
|
5089
|
+
const handler = onKeypress;
|
|
5090
|
+
onKeypress = null;
|
|
5091
|
+
if (handler) process.stdin.removeListener("keypress", handler);
|
|
5092
|
+
try {
|
|
5093
|
+
process.stdin.setRawMode(false);
|
|
5094
|
+
} catch {
|
|
5095
|
+
}
|
|
5096
|
+
process.stdin.pause();
|
|
5097
|
+
cancelActiveCard = null;
|
|
5098
|
+
};
|
|
5099
|
+
const settle = (decision) => {
|
|
5100
|
+
if (settled) return;
|
|
5101
|
+
settled = true;
|
|
5102
|
+
cleanup();
|
|
5103
|
+
clearCard();
|
|
5104
|
+
process.stdout.write(SHOW_CURSOR);
|
|
5105
|
+
postDecisionHttp(req2.id, decision, csrfToken, port).catch((err) => {
|
|
5106
|
+
try {
|
|
5107
|
+
fs7.appendFileSync(
|
|
5108
|
+
path9.join(os6.homedir(), ".node9", "hook-debug.log"),
|
|
5109
|
+
`[tail] POST /decision failed: ${String(err)}
|
|
5110
|
+
`
|
|
5111
|
+
);
|
|
5112
|
+
} catch {
|
|
5113
|
+
}
|
|
5114
|
+
});
|
|
5115
|
+
const decisionLabel = decision === "allow" ? chalk3.green("\u2713 ALLOWED (terminal)") : chalk3.red("\u2717 DENIED (terminal)");
|
|
5116
|
+
console.log(`${chalk3.cyan("\u25C6")} ${chalk3.bold(req2.toolName.padEnd(16))} ${decisionLabel}`);
|
|
5117
|
+
approvalQueue.shift();
|
|
5118
|
+
cardActive = false;
|
|
5119
|
+
showNextCard();
|
|
5120
|
+
};
|
|
5121
|
+
cancelActiveCard = () => {
|
|
5122
|
+
if (settled) return;
|
|
5123
|
+
settled = true;
|
|
5124
|
+
cleanup();
|
|
5125
|
+
clearCard();
|
|
5126
|
+
process.stdout.write(SHOW_CURSOR);
|
|
5127
|
+
approvalQueue.shift();
|
|
5128
|
+
cardActive = false;
|
|
5129
|
+
showNextCard();
|
|
5130
|
+
};
|
|
5131
|
+
process.stdin.resume();
|
|
5132
|
+
onKeypress = (_str, key) => {
|
|
5133
|
+
const name = key?.name ?? "";
|
|
5134
|
+
if (name === "a") {
|
|
5135
|
+
settle("allow");
|
|
5136
|
+
} else if (name === "d" || name === "return" || name === "enter" || key?.ctrl && name === "c") {
|
|
5137
|
+
settle("deny");
|
|
5138
|
+
}
|
|
5139
|
+
};
|
|
5140
|
+
process.stdin.on("keypress", onKeypress);
|
|
5141
|
+
}
|
|
5142
|
+
const dashboardUrl = `http://127.0.0.1:${port}/`;
|
|
5143
|
+
try {
|
|
5144
|
+
const browserEnabled = getConfig().settings.approvers?.browser !== false;
|
|
5145
|
+
if (browserEnabled) {
|
|
5146
|
+
if (process.platform === "darwin") execSync(`open "${dashboardUrl}"`, { stdio: "ignore" });
|
|
5147
|
+
else if (process.platform === "win32")
|
|
5148
|
+
execSync(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
|
|
5149
|
+
else execSync(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
|
|
5150
|
+
}
|
|
5151
|
+
} catch {
|
|
5152
|
+
}
|
|
5153
|
+
console.log(chalk3.cyan.bold(`
|
|
5154
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk3.dim(`\u2192 ${dashboardUrl}`));
|
|
5155
|
+
if (canApprove) {
|
|
5156
|
+
console.log(chalk3.dim("Interactive approvals enabled. [A] Allow [D] Deny"));
|
|
5157
|
+
}
|
|
5070
5158
|
if (options.history) {
|
|
5071
|
-
console.log(
|
|
5159
|
+
console.log(chalk3.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
5072
5160
|
} else {
|
|
5073
5161
|
console.log(
|
|
5074
|
-
|
|
5162
|
+
chalk3.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
5075
5163
|
);
|
|
5076
5164
|
}
|
|
5077
5165
|
process.on("SIGINT", () => {
|
|
5166
|
+
clearCard();
|
|
5167
|
+
process.stdout.write(SHOW_CURSOR);
|
|
5078
5168
|
if (process.stdout.isTTY) {
|
|
5079
5169
|
readline.clearLine(process.stdout, 0);
|
|
5080
5170
|
readline.cursorTo(process.stdout, 0);
|
|
5081
5171
|
}
|
|
5082
|
-
console.log(
|
|
5172
|
+
console.log(chalk3.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
5083
5173
|
process.exit(0);
|
|
5084
5174
|
});
|
|
5085
|
-
const
|
|
5175
|
+
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
5176
|
+
const req = http2.get(sseUrl, (res) => {
|
|
5086
5177
|
if (res.statusCode !== 200) {
|
|
5087
|
-
console.error(
|
|
5178
|
+
console.error(chalk3.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
5088
5179
|
process.exit(1);
|
|
5089
5180
|
}
|
|
5090
5181
|
let currentEvent = "";
|
|
@@ -5108,15 +5199,66 @@ async function startTail(options = {}) {
|
|
|
5108
5199
|
}
|
|
5109
5200
|
});
|
|
5110
5201
|
rl.on("close", () => {
|
|
5202
|
+
clearCard();
|
|
5203
|
+
process.stdout.write(SHOW_CURSOR);
|
|
5111
5204
|
if (process.stdout.isTTY) {
|
|
5112
5205
|
readline.clearLine(process.stdout, 0);
|
|
5113
5206
|
readline.cursorTo(process.stdout, 0);
|
|
5114
5207
|
}
|
|
5115
|
-
console.log(
|
|
5208
|
+
console.log(chalk3.red("\n\u274C Daemon disconnected."));
|
|
5116
5209
|
process.exit(1);
|
|
5117
5210
|
});
|
|
5118
5211
|
});
|
|
5119
5212
|
function handleMessage(event, rawData) {
|
|
5213
|
+
if (event === "csrf") {
|
|
5214
|
+
try {
|
|
5215
|
+
const parsed = JSON.parse(rawData);
|
|
5216
|
+
if (parsed.token) csrfToken = parsed.token;
|
|
5217
|
+
} catch {
|
|
5218
|
+
}
|
|
5219
|
+
return;
|
|
5220
|
+
}
|
|
5221
|
+
if (event === "init") {
|
|
5222
|
+
try {
|
|
5223
|
+
const parsed = JSON.parse(rawData);
|
|
5224
|
+
if (canApprove && Array.isArray(parsed.requests)) {
|
|
5225
|
+
for (const r of parsed.requests) {
|
|
5226
|
+
approvalQueue.push(r);
|
|
5227
|
+
}
|
|
5228
|
+
showNextCard();
|
|
5229
|
+
}
|
|
5230
|
+
} catch {
|
|
5231
|
+
}
|
|
5232
|
+
return;
|
|
5233
|
+
}
|
|
5234
|
+
if (event === "add") {
|
|
5235
|
+
if (canApprove) {
|
|
5236
|
+
try {
|
|
5237
|
+
const parsed = JSON.parse(rawData);
|
|
5238
|
+
if (parsed.interactive !== false) {
|
|
5239
|
+
approvalQueue.push(parsed);
|
|
5240
|
+
showNextCard();
|
|
5241
|
+
}
|
|
5242
|
+
} catch {
|
|
5243
|
+
}
|
|
5244
|
+
}
|
|
5245
|
+
return;
|
|
5246
|
+
}
|
|
5247
|
+
if (event === "remove") {
|
|
5248
|
+
try {
|
|
5249
|
+
const { id } = JSON.parse(rawData);
|
|
5250
|
+
const idx = approvalQueue.findIndex((r) => r.id === id);
|
|
5251
|
+
if (idx !== -1) {
|
|
5252
|
+
if (idx === 0 && cardActive && cancelActiveCard) {
|
|
5253
|
+
cancelActiveCard();
|
|
5254
|
+
} else {
|
|
5255
|
+
approvalQueue.splice(idx, 1);
|
|
5256
|
+
}
|
|
5257
|
+
}
|
|
5258
|
+
} catch {
|
|
5259
|
+
}
|
|
5260
|
+
return;
|
|
5261
|
+
}
|
|
5120
5262
|
let data;
|
|
5121
5263
|
try {
|
|
5122
5264
|
data = JSON.parse(rawData);
|
|
@@ -5129,30 +5271,31 @@ async function startTail(options = {}) {
|
|
|
5129
5271
|
renderResult(data, data);
|
|
5130
5272
|
return;
|
|
5131
5273
|
}
|
|
5132
|
-
|
|
5274
|
+
activityPending.set(data.id, data);
|
|
5133
5275
|
const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
|
|
5134
5276
|
if (slowTool) renderPending(data);
|
|
5135
5277
|
}
|
|
5136
5278
|
if (event === "activity-result") {
|
|
5137
|
-
const original =
|
|
5279
|
+
const original = activityPending.get(data.id);
|
|
5138
5280
|
if (original) {
|
|
5139
5281
|
renderResult(original, data);
|
|
5140
|
-
|
|
5282
|
+
activityPending.delete(data.id);
|
|
5141
5283
|
}
|
|
5142
5284
|
}
|
|
5143
5285
|
}
|
|
5144
5286
|
req.on("error", (err) => {
|
|
5145
5287
|
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
5146
|
-
console.error(
|
|
5288
|
+
console.error(chalk3.red(`
|
|
5147
5289
|
\u274C ${msg}`));
|
|
5148
5290
|
process.exit(1);
|
|
5149
5291
|
});
|
|
5150
5292
|
}
|
|
5151
|
-
var PID_FILE, ICONS;
|
|
5293
|
+
var PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, SAVE_CURSOR, RESTORE_CURSOR;
|
|
5152
5294
|
var init_tail = __esm({
|
|
5153
5295
|
"src/tui/tail.ts"() {
|
|
5154
5296
|
"use strict";
|
|
5155
5297
|
init_daemon();
|
|
5298
|
+
init_core();
|
|
5156
5299
|
PID_FILE = path9.join(os6.homedir(), ".node9", "daemon.pid");
|
|
5157
5300
|
ICONS = {
|
|
5158
5301
|
bash: "\u{1F4BB}",
|
|
@@ -5171,6 +5314,18 @@ var init_tail = __esm({
|
|
|
5171
5314
|
delete: "\u{1F5D1}\uFE0F",
|
|
5172
5315
|
web: "\u{1F310}"
|
|
5173
5316
|
};
|
|
5317
|
+
RESET = "\x1B[0m";
|
|
5318
|
+
BOLD = "\x1B[1m";
|
|
5319
|
+
RED = "\x1B[31m";
|
|
5320
|
+
YELLOW = "\x1B[33m";
|
|
5321
|
+
CYAN = "\x1B[36m";
|
|
5322
|
+
GRAY = "\x1B[90m";
|
|
5323
|
+
GREEN = "\x1B[32m";
|
|
5324
|
+
HIDE_CURSOR = "\x1B[?25l";
|
|
5325
|
+
SHOW_CURSOR = "\x1B[?25h";
|
|
5326
|
+
ERASE_DOWN = "\x1B[J";
|
|
5327
|
+
SAVE_CURSOR = "\x1B7";
|
|
5328
|
+
RESTORE_CURSOR = "\x1B8";
|
|
5174
5329
|
}
|
|
5175
5330
|
});
|
|
5176
5331
|
|
|
@@ -5182,11 +5337,11 @@ import { Command } from "commander";
|
|
|
5182
5337
|
import fs4 from "fs";
|
|
5183
5338
|
import path6 from "path";
|
|
5184
5339
|
import os3 from "os";
|
|
5185
|
-
import
|
|
5186
|
-
import { confirm
|
|
5340
|
+
import chalk from "chalk";
|
|
5341
|
+
import { confirm } from "@inquirer/prompts";
|
|
5187
5342
|
function printDaemonTip() {
|
|
5188
5343
|
console.log(
|
|
5189
|
-
|
|
5344
|
+
chalk.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk.white("\n To view your history or manage persistent rules, run:") + chalk.green("\n node9 daemon --openui")
|
|
5190
5345
|
);
|
|
5191
5346
|
}
|
|
5192
5347
|
function fullPathCommand(subcommand) {
|
|
@@ -5232,10 +5387,10 @@ function teardownClaude() {
|
|
|
5232
5387
|
if (changed) {
|
|
5233
5388
|
writeJson(hooksPath, settings);
|
|
5234
5389
|
console.log(
|
|
5235
|
-
|
|
5390
|
+
chalk.green(" \u2705 Removed PreToolUse / PostToolUse hooks from ~/.claude/settings.json")
|
|
5236
5391
|
);
|
|
5237
5392
|
} else {
|
|
5238
|
-
console.log(
|
|
5393
|
+
console.log(chalk.blue(" \u2139\uFE0F No Node9 hooks found in ~/.claude/settings.json"));
|
|
5239
5394
|
}
|
|
5240
5395
|
}
|
|
5241
5396
|
const claudeConfig = readJson(mcpPath);
|
|
@@ -5252,7 +5407,7 @@ function teardownClaude() {
|
|
|
5252
5407
|
mcpChanged = true;
|
|
5253
5408
|
} else if (server.command === "node9") {
|
|
5254
5409
|
console.warn(
|
|
5255
|
-
|
|
5410
|
+
chalk.yellow(
|
|
5256
5411
|
` \u26A0\uFE0F Cannot unwrap MCP server "${name}" in ~/.claude.json \u2014 args is empty. Remove it manually.`
|
|
5257
5412
|
)
|
|
5258
5413
|
);
|
|
@@ -5260,7 +5415,7 @@ function teardownClaude() {
|
|
|
5260
5415
|
}
|
|
5261
5416
|
if (mcpChanged) {
|
|
5262
5417
|
writeJson(mcpPath, claudeConfig);
|
|
5263
|
-
console.log(
|
|
5418
|
+
console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.claude.json"));
|
|
5264
5419
|
}
|
|
5265
5420
|
}
|
|
5266
5421
|
}
|
|
@@ -5269,7 +5424,7 @@ function teardownGemini() {
|
|
|
5269
5424
|
const settingsPath = path6.join(homeDir2, ".gemini", "settings.json");
|
|
5270
5425
|
const settings = readJson(settingsPath);
|
|
5271
5426
|
if (!settings) {
|
|
5272
|
-
console.log(
|
|
5427
|
+
console.log(chalk.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
|
|
5273
5428
|
return;
|
|
5274
5429
|
}
|
|
5275
5430
|
let changed = false;
|
|
@@ -5298,9 +5453,9 @@ function teardownGemini() {
|
|
|
5298
5453
|
}
|
|
5299
5454
|
if (changed) {
|
|
5300
5455
|
writeJson(settingsPath, settings);
|
|
5301
|
-
console.log(
|
|
5456
|
+
console.log(chalk.green(" \u2705 Removed Node9 hooks from ~/.gemini/settings.json"));
|
|
5302
5457
|
} else {
|
|
5303
|
-
console.log(
|
|
5458
|
+
console.log(chalk.blue(" \u2139\uFE0F No Node9 hooks found in ~/.gemini/settings.json"));
|
|
5304
5459
|
}
|
|
5305
5460
|
}
|
|
5306
5461
|
function teardownCursor() {
|
|
@@ -5308,7 +5463,7 @@ function teardownCursor() {
|
|
|
5308
5463
|
const mcpPath = path6.join(homeDir2, ".cursor", "mcp.json");
|
|
5309
5464
|
const mcpConfig = readJson(mcpPath);
|
|
5310
5465
|
if (!mcpConfig?.mcpServers) {
|
|
5311
|
-
console.log(
|
|
5466
|
+
console.log(chalk.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
|
|
5312
5467
|
return;
|
|
5313
5468
|
}
|
|
5314
5469
|
let changed = false;
|
|
@@ -5325,9 +5480,9 @@ function teardownCursor() {
|
|
|
5325
5480
|
}
|
|
5326
5481
|
if (changed) {
|
|
5327
5482
|
writeJson(mcpPath, mcpConfig);
|
|
5328
|
-
console.log(
|
|
5483
|
+
console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.cursor/mcp.json"));
|
|
5329
5484
|
} else {
|
|
5330
|
-
console.log(
|
|
5485
|
+
console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.cursor/mcp.json"));
|
|
5331
5486
|
}
|
|
5332
5487
|
}
|
|
5333
5488
|
async function setupClaude() {
|
|
@@ -5348,7 +5503,7 @@ async function setupClaude() {
|
|
|
5348
5503
|
matcher: ".*",
|
|
5349
5504
|
hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
|
|
5350
5505
|
});
|
|
5351
|
-
console.log(
|
|
5506
|
+
console.log(chalk.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
|
|
5352
5507
|
anythingChanged = true;
|
|
5353
5508
|
}
|
|
5354
5509
|
const hasPostHook = settings.hooks.PostToolUse?.some(
|
|
@@ -5360,7 +5515,7 @@ async function setupClaude() {
|
|
|
5360
5515
|
matcher: ".*",
|
|
5361
5516
|
hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
|
|
5362
5517
|
});
|
|
5363
|
-
console.log(
|
|
5518
|
+
console.log(chalk.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
|
|
5364
5519
|
anythingChanged = true;
|
|
5365
5520
|
}
|
|
5366
5521
|
if (anythingChanged) {
|
|
@@ -5374,35 +5529,35 @@ async function setupClaude() {
|
|
|
5374
5529
|
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
5375
5530
|
}
|
|
5376
5531
|
if (serversToWrap.length > 0) {
|
|
5377
|
-
console.log(
|
|
5378
|
-
console.log(
|
|
5532
|
+
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
5533
|
+
console.log(chalk.white(` ${mcpPath}`));
|
|
5379
5534
|
for (const { name, originalCmd } of serversToWrap) {
|
|
5380
|
-
console.log(
|
|
5535
|
+
console.log(chalk.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
5381
5536
|
}
|
|
5382
5537
|
console.log("");
|
|
5383
|
-
const proceed = await
|
|
5538
|
+
const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
|
|
5384
5539
|
if (proceed) {
|
|
5385
5540
|
for (const { name, parts } of serversToWrap) {
|
|
5386
5541
|
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
5387
5542
|
}
|
|
5388
5543
|
claudeConfig.mcpServers = servers;
|
|
5389
5544
|
writeJson(mcpPath, claudeConfig);
|
|
5390
|
-
console.log(
|
|
5545
|
+
console.log(chalk.green(`
|
|
5391
5546
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
5392
5547
|
anythingChanged = true;
|
|
5393
5548
|
} else {
|
|
5394
|
-
console.log(
|
|
5549
|
+
console.log(chalk.yellow(" Skipped MCP server wrapping."));
|
|
5395
5550
|
}
|
|
5396
5551
|
console.log("");
|
|
5397
5552
|
}
|
|
5398
5553
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
5399
|
-
console.log(
|
|
5554
|
+
console.log(chalk.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
|
|
5400
5555
|
printDaemonTip();
|
|
5401
5556
|
return;
|
|
5402
5557
|
}
|
|
5403
5558
|
if (anythingChanged) {
|
|
5404
|
-
console.log(
|
|
5405
|
-
console.log(
|
|
5559
|
+
console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
5560
|
+
console.log(chalk.gray(" Restart Claude Code for changes to take effect."));
|
|
5406
5561
|
printDaemonTip();
|
|
5407
5562
|
}
|
|
5408
5563
|
}
|
|
@@ -5430,7 +5585,7 @@ async function setupGemini() {
|
|
|
5430
5585
|
}
|
|
5431
5586
|
]
|
|
5432
5587
|
});
|
|
5433
|
-
console.log(
|
|
5588
|
+
console.log(chalk.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
5434
5589
|
anythingChanged = true;
|
|
5435
5590
|
}
|
|
5436
5591
|
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
@@ -5443,7 +5598,7 @@ async function setupGemini() {
|
|
|
5443
5598
|
matcher: ".*",
|
|
5444
5599
|
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
5445
5600
|
});
|
|
5446
|
-
console.log(
|
|
5601
|
+
console.log(chalk.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
5447
5602
|
anythingChanged = true;
|
|
5448
5603
|
}
|
|
5449
5604
|
if (anythingChanged) {
|
|
@@ -5457,35 +5612,35 @@ async function setupGemini() {
|
|
|
5457
5612
|
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
5458
5613
|
}
|
|
5459
5614
|
if (serversToWrap.length > 0) {
|
|
5460
|
-
console.log(
|
|
5461
|
-
console.log(
|
|
5615
|
+
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
5616
|
+
console.log(chalk.white(` ${settingsPath} (mcpServers)`));
|
|
5462
5617
|
for (const { name, originalCmd } of serversToWrap) {
|
|
5463
|
-
console.log(
|
|
5618
|
+
console.log(chalk.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
5464
5619
|
}
|
|
5465
5620
|
console.log("");
|
|
5466
|
-
const proceed = await
|
|
5621
|
+
const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
|
|
5467
5622
|
if (proceed) {
|
|
5468
5623
|
for (const { name, parts } of serversToWrap) {
|
|
5469
5624
|
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
5470
5625
|
}
|
|
5471
5626
|
settings.mcpServers = servers;
|
|
5472
5627
|
writeJson(settingsPath, settings);
|
|
5473
|
-
console.log(
|
|
5628
|
+
console.log(chalk.green(`
|
|
5474
5629
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
5475
5630
|
anythingChanged = true;
|
|
5476
5631
|
} else {
|
|
5477
|
-
console.log(
|
|
5632
|
+
console.log(chalk.yellow(" Skipped MCP server wrapping."));
|
|
5478
5633
|
}
|
|
5479
5634
|
console.log("");
|
|
5480
5635
|
}
|
|
5481
5636
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
5482
|
-
console.log(
|
|
5637
|
+
console.log(chalk.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
|
|
5483
5638
|
printDaemonTip();
|
|
5484
5639
|
return;
|
|
5485
5640
|
}
|
|
5486
5641
|
if (anythingChanged) {
|
|
5487
|
-
console.log(
|
|
5488
|
-
console.log(
|
|
5642
|
+
console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
|
|
5643
|
+
console.log(chalk.gray(" Restart Gemini CLI for changes to take effect."));
|
|
5489
5644
|
printDaemonTip();
|
|
5490
5645
|
}
|
|
5491
5646
|
}
|
|
@@ -5502,36 +5657,36 @@ async function setupCursor() {
|
|
|
5502
5657
|
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
5503
5658
|
}
|
|
5504
5659
|
if (serversToWrap.length > 0) {
|
|
5505
|
-
console.log(
|
|
5506
|
-
console.log(
|
|
5660
|
+
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
5661
|
+
console.log(chalk.white(` ${mcpPath}`));
|
|
5507
5662
|
for (const { name, originalCmd } of serversToWrap) {
|
|
5508
|
-
console.log(
|
|
5663
|
+
console.log(chalk.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
5509
5664
|
}
|
|
5510
5665
|
console.log("");
|
|
5511
|
-
const proceed = await
|
|
5666
|
+
const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
|
|
5512
5667
|
if (proceed) {
|
|
5513
5668
|
for (const { name, parts } of serversToWrap) {
|
|
5514
5669
|
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
5515
5670
|
}
|
|
5516
5671
|
mcpConfig.mcpServers = servers;
|
|
5517
5672
|
writeJson(mcpPath, mcpConfig);
|
|
5518
|
-
console.log(
|
|
5673
|
+
console.log(chalk.green(`
|
|
5519
5674
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
5520
5675
|
anythingChanged = true;
|
|
5521
5676
|
} else {
|
|
5522
|
-
console.log(
|
|
5677
|
+
console.log(chalk.yellow(" Skipped MCP server wrapping."));
|
|
5523
5678
|
}
|
|
5524
5679
|
console.log("");
|
|
5525
5680
|
}
|
|
5526
5681
|
console.log(
|
|
5527
|
-
|
|
5682
|
+
chalk.yellow(
|
|
5528
5683
|
" \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
|
|
5529
5684
|
)
|
|
5530
5685
|
);
|
|
5531
5686
|
console.log("");
|
|
5532
5687
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
5533
5688
|
console.log(
|
|
5534
|
-
|
|
5689
|
+
chalk.blue(
|
|
5535
5690
|
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
|
|
5536
5691
|
)
|
|
5537
5692
|
);
|
|
@@ -5539,18 +5694,18 @@ async function setupCursor() {
|
|
|
5539
5694
|
return;
|
|
5540
5695
|
}
|
|
5541
5696
|
if (anythingChanged) {
|
|
5542
|
-
console.log(
|
|
5543
|
-
console.log(
|
|
5697
|
+
console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
|
|
5698
|
+
console.log(chalk.gray(" Restart Cursor for changes to take effect."));
|
|
5544
5699
|
printDaemonTip();
|
|
5545
5700
|
}
|
|
5546
5701
|
}
|
|
5547
5702
|
|
|
5548
5703
|
// src/cli.ts
|
|
5549
5704
|
init_daemon();
|
|
5550
|
-
import { spawn as spawn5, execSync } from "child_process";
|
|
5705
|
+
import { spawn as spawn5, execSync as execSync2, spawnSync as spawnSync4 } from "child_process";
|
|
5551
5706
|
import { parseCommandString } from "execa";
|
|
5552
5707
|
import { execa } from "execa";
|
|
5553
|
-
import
|
|
5708
|
+
import chalk4 from "chalk";
|
|
5554
5709
|
import readline2 from "readline";
|
|
5555
5710
|
import fs8 from "fs";
|
|
5556
5711
|
import path10 from "path";
|
|
@@ -5817,7 +5972,7 @@ function applyUndo(hash, cwd) {
|
|
|
5817
5972
|
|
|
5818
5973
|
// src/cli.ts
|
|
5819
5974
|
init_shields();
|
|
5820
|
-
import { confirm as
|
|
5975
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
5821
5976
|
var { version } = JSON.parse(
|
|
5822
5977
|
fs8.readFileSync(path10.join(__dirname, "../package.json"), "utf-8")
|
|
5823
5978
|
);
|
|
@@ -5907,9 +6062,9 @@ function openBrowserLocal() {
|
|
|
5907
6062
|
const url = `http://${DAEMON_HOST2}:${DAEMON_PORT2}/`;
|
|
5908
6063
|
try {
|
|
5909
6064
|
const opts = { stdio: "ignore" };
|
|
5910
|
-
if (process.platform === "darwin")
|
|
5911
|
-
else if (process.platform === "win32")
|
|
5912
|
-
else
|
|
6065
|
+
if (process.platform === "darwin") execSync2(`open "${url}"`, opts);
|
|
6066
|
+
else if (process.platform === "win32") execSync2(`cmd /c start "" "${url}"`, opts);
|
|
6067
|
+
else execSync2(`xdg-open "${url}"`, opts);
|
|
5913
6068
|
} catch {
|
|
5914
6069
|
}
|
|
5915
6070
|
}
|
|
@@ -5918,7 +6073,9 @@ async function autoStartDaemonAndWait() {
|
|
|
5918
6073
|
const child = spawn5(process.execPath, [process.argv[1], "daemon"], {
|
|
5919
6074
|
detached: true,
|
|
5920
6075
|
stdio: "ignore",
|
|
5921
|
-
|
|
6076
|
+
// NODE9_BROWSER_OPENED=1 tells the daemon we will open the browser ourselves
|
|
6077
|
+
// (openBrowserLocal below), so it must not open a duplicate tab on first approval.
|
|
6078
|
+
env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
|
|
5922
6079
|
});
|
|
5923
6080
|
child.unref();
|
|
5924
6081
|
for (let i = 0; i < 20; i++) {
|
|
@@ -5951,7 +6108,7 @@ async function runProxy(targetCommand) {
|
|
|
5951
6108
|
if (stdout) executable = stdout.trim();
|
|
5952
6109
|
} catch {
|
|
5953
6110
|
}
|
|
5954
|
-
console.error(
|
|
6111
|
+
console.error(chalk4.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
|
|
5955
6112
|
const child = spawn5(executable, args, {
|
|
5956
6113
|
stdio: ["pipe", "pipe", "inherit"],
|
|
5957
6114
|
// We control STDIN and STDOUT
|
|
@@ -5972,14 +6129,14 @@ async function runProxy(targetCommand) {
|
|
|
5972
6129
|
try {
|
|
5973
6130
|
const name = message.params?.name || message.params?.tool_name || "unknown";
|
|
5974
6131
|
const toolArgs = message.params?.arguments || message.params?.tool_input || {};
|
|
5975
|
-
const result = await authorizeHeadless(sanitize(name), toolArgs,
|
|
6132
|
+
const result = await authorizeHeadless(sanitize(name), toolArgs, {
|
|
5976
6133
|
agent: "Proxy/MCP"
|
|
5977
6134
|
});
|
|
5978
6135
|
if (!result.approved) {
|
|
5979
|
-
console.error(
|
|
6136
|
+
console.error(chalk4.red(`
|
|
5980
6137
|
\u{1F6D1} Node9 Sudo: Action Blocked`));
|
|
5981
|
-
console.error(
|
|
5982
|
-
console.error(
|
|
6138
|
+
console.error(chalk4.gray(` Tool: ${name}`));
|
|
6139
|
+
console.error(chalk4.gray(` Reason: ${result.reason || "Security Policy"}
|
|
5983
6140
|
`));
|
|
5984
6141
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
5985
6142
|
const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -6066,31 +6223,31 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
6066
6223
|
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
6067
6224
|
}
|
|
6068
6225
|
if (options.profile && profileName !== "default") {
|
|
6069
|
-
console.log(
|
|
6070
|
-
console.log(
|
|
6226
|
+
console.log(chalk4.green(`\u2705 Profile "${profileName}" saved`));
|
|
6227
|
+
console.log(chalk4.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
6071
6228
|
} else if (options.local) {
|
|
6072
|
-
console.log(
|
|
6073
|
-
console.log(
|
|
6229
|
+
console.log(chalk4.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
6230
|
+
console.log(chalk4.gray(` All decisions stay on this machine.`));
|
|
6074
6231
|
} else {
|
|
6075
|
-
console.log(
|
|
6076
|
-
console.log(
|
|
6232
|
+
console.log(chalk4.green(`\u2705 Logged in \u2014 agent mode`));
|
|
6233
|
+
console.log(chalk4.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
6077
6234
|
}
|
|
6078
6235
|
});
|
|
6079
6236
|
program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
6080
6237
|
if (target === "gemini") return await setupGemini();
|
|
6081
6238
|
if (target === "claude") return await setupClaude();
|
|
6082
6239
|
if (target === "cursor") return await setupCursor();
|
|
6083
|
-
console.error(
|
|
6240
|
+
console.error(chalk4.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
6084
6241
|
process.exit(1);
|
|
6085
6242
|
});
|
|
6086
6243
|
program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
6087
6244
|
if (!target) {
|
|
6088
|
-
console.log(
|
|
6089
|
-
console.log(" Usage: " +
|
|
6245
|
+
console.log(chalk4.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
6246
|
+
console.log(" Usage: " + chalk4.white("node9 setup <target>") + "\n");
|
|
6090
6247
|
console.log(" Targets:");
|
|
6091
|
-
console.log(" " +
|
|
6092
|
-
console.log(" " +
|
|
6093
|
-
console.log(" " +
|
|
6248
|
+
console.log(" " + chalk4.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
6249
|
+
console.log(" " + chalk4.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
6250
|
+
console.log(" " + chalk4.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
6094
6251
|
console.log("");
|
|
6095
6252
|
return;
|
|
6096
6253
|
}
|
|
@@ -6098,7 +6255,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
6098
6255
|
if (t === "gemini") return await setupGemini();
|
|
6099
6256
|
if (t === "claude") return await setupClaude();
|
|
6100
6257
|
if (t === "cursor") return await setupCursor();
|
|
6101
|
-
console.error(
|
|
6258
|
+
console.error(chalk4.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
6102
6259
|
process.exit(1);
|
|
6103
6260
|
});
|
|
6104
6261
|
program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to remove from: claude | gemini | cursor").action((target) => {
|
|
@@ -6107,30 +6264,30 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
|
|
|
6107
6264
|
else if (target === "gemini") fn = teardownGemini;
|
|
6108
6265
|
else if (target === "cursor") fn = teardownCursor;
|
|
6109
6266
|
else {
|
|
6110
|
-
console.error(
|
|
6267
|
+
console.error(chalk4.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
6111
6268
|
process.exit(1);
|
|
6112
6269
|
}
|
|
6113
|
-
console.log(
|
|
6270
|
+
console.log(chalk4.cyan(`
|
|
6114
6271
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
6115
6272
|
`));
|
|
6116
6273
|
try {
|
|
6117
6274
|
fn();
|
|
6118
6275
|
} catch (err) {
|
|
6119
|
-
console.error(
|
|
6276
|
+
console.error(chalk4.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
6120
6277
|
process.exit(1);
|
|
6121
6278
|
}
|
|
6122
|
-
console.log(
|
|
6279
|
+
console.log(chalk4.gray("\n Restart the agent for changes to take effect."));
|
|
6123
6280
|
});
|
|
6124
6281
|
program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
|
|
6125
|
-
console.log(
|
|
6126
|
-
console.log(
|
|
6282
|
+
console.log(chalk4.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
6283
|
+
console.log(chalk4.bold("Stopping daemon..."));
|
|
6127
6284
|
try {
|
|
6128
6285
|
stopDaemon();
|
|
6129
|
-
console.log(
|
|
6286
|
+
console.log(chalk4.green(" \u2705 Daemon stopped"));
|
|
6130
6287
|
} catch {
|
|
6131
|
-
console.log(
|
|
6288
|
+
console.log(chalk4.blue(" \u2139\uFE0F Daemon was not running"));
|
|
6132
6289
|
}
|
|
6133
|
-
console.log(
|
|
6290
|
+
console.log(chalk4.bold("\nRemoving hooks..."));
|
|
6134
6291
|
let teardownFailed = false;
|
|
6135
6292
|
for (const [label, fn] of [
|
|
6136
6293
|
["Claude", teardownClaude],
|
|
@@ -6142,7 +6299,7 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
6142
6299
|
} catch (err) {
|
|
6143
6300
|
teardownFailed = true;
|
|
6144
6301
|
console.error(
|
|
6145
|
-
|
|
6302
|
+
chalk4.red(
|
|
6146
6303
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
|
|
6147
6304
|
)
|
|
6148
6305
|
);
|
|
@@ -6151,7 +6308,7 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
6151
6308
|
if (options.purge) {
|
|
6152
6309
|
const node9Dir = path10.join(os7.homedir(), ".node9");
|
|
6153
6310
|
if (fs8.existsSync(node9Dir)) {
|
|
6154
|
-
const confirmed = await
|
|
6311
|
+
const confirmed = await confirm2({
|
|
6155
6312
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
6156
6313
|
default: false
|
|
6157
6314
|
});
|
|
@@ -6159,53 +6316,53 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
6159
6316
|
fs8.rmSync(node9Dir, { recursive: true });
|
|
6160
6317
|
if (fs8.existsSync(node9Dir)) {
|
|
6161
6318
|
console.error(
|
|
6162
|
-
|
|
6319
|
+
chalk4.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
6163
6320
|
);
|
|
6164
6321
|
} else {
|
|
6165
|
-
console.log(
|
|
6322
|
+
console.log(chalk4.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
6166
6323
|
}
|
|
6167
6324
|
} else {
|
|
6168
|
-
console.log(
|
|
6325
|
+
console.log(chalk4.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
6169
6326
|
}
|
|
6170
6327
|
} else {
|
|
6171
|
-
console.log(
|
|
6328
|
+
console.log(chalk4.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
6172
6329
|
}
|
|
6173
6330
|
} else {
|
|
6174
6331
|
console.log(
|
|
6175
|
-
|
|
6332
|
+
chalk4.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
6176
6333
|
);
|
|
6177
6334
|
}
|
|
6178
6335
|
if (teardownFailed) {
|
|
6179
|
-
console.error(
|
|
6336
|
+
console.error(chalk4.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
6180
6337
|
process.exit(1);
|
|
6181
6338
|
}
|
|
6182
|
-
console.log(
|
|
6183
|
-
console.log(
|
|
6339
|
+
console.log(chalk4.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
|
|
6340
|
+
console.log(chalk4.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
6184
6341
|
});
|
|
6185
6342
|
program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
6186
6343
|
const homeDir2 = os7.homedir();
|
|
6187
6344
|
let failures = 0;
|
|
6188
6345
|
function pass(msg) {
|
|
6189
|
-
console.log(
|
|
6346
|
+
console.log(chalk4.green(" \u2705 ") + msg);
|
|
6190
6347
|
}
|
|
6191
6348
|
function fail(msg, hint) {
|
|
6192
|
-
console.log(
|
|
6193
|
-
if (hint) console.log(
|
|
6349
|
+
console.log(chalk4.red(" \u274C ") + msg);
|
|
6350
|
+
if (hint) console.log(chalk4.gray(" " + hint));
|
|
6194
6351
|
failures++;
|
|
6195
6352
|
}
|
|
6196
6353
|
function warn(msg, hint) {
|
|
6197
|
-
console.log(
|
|
6198
|
-
if (hint) console.log(
|
|
6354
|
+
console.log(chalk4.yellow(" \u26A0\uFE0F ") + msg);
|
|
6355
|
+
if (hint) console.log(chalk4.gray(" " + hint));
|
|
6199
6356
|
}
|
|
6200
6357
|
function section(title) {
|
|
6201
|
-
console.log("\n" +
|
|
6358
|
+
console.log("\n" + chalk4.bold(title));
|
|
6202
6359
|
}
|
|
6203
|
-
console.log(
|
|
6360
|
+
console.log(chalk4.cyan.bold(`
|
|
6204
6361
|
\u{1F6E1}\uFE0F Node9 Doctor v${version}
|
|
6205
6362
|
`));
|
|
6206
6363
|
section("Binary");
|
|
6207
6364
|
try {
|
|
6208
|
-
const which =
|
|
6365
|
+
const which = execSync2("which node9", { encoding: "utf-8" }).trim();
|
|
6209
6366
|
pass(`node9 found at ${which}`);
|
|
6210
6367
|
} catch {
|
|
6211
6368
|
warn("node9 not found in $PATH \u2014 hooks may not find it", "Run: npm install -g @node9/proxy");
|
|
@@ -6220,7 +6377,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
6220
6377
|
);
|
|
6221
6378
|
}
|
|
6222
6379
|
try {
|
|
6223
|
-
const gitVersion =
|
|
6380
|
+
const gitVersion = execSync2("git --version", { encoding: "utf-8" }).trim();
|
|
6224
6381
|
pass(gitVersion);
|
|
6225
6382
|
} catch {
|
|
6226
6383
|
warn(
|
|
@@ -6315,9 +6472,9 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
6315
6472
|
}
|
|
6316
6473
|
console.log("");
|
|
6317
6474
|
if (failures === 0) {
|
|
6318
|
-
console.log(
|
|
6475
|
+
console.log(chalk4.green.bold(" All checks passed. Node9 is ready.\n"));
|
|
6319
6476
|
} else {
|
|
6320
|
-
console.log(
|
|
6477
|
+
console.log(chalk4.red.bold(` ${failures} check(s) failed. See hints above.
|
|
6321
6478
|
`));
|
|
6322
6479
|
process.exit(1);
|
|
6323
6480
|
}
|
|
@@ -6332,7 +6489,7 @@ program.command("explain").description(
|
|
|
6332
6489
|
try {
|
|
6333
6490
|
args = JSON.parse(trimmed);
|
|
6334
6491
|
} catch {
|
|
6335
|
-
console.error(
|
|
6492
|
+
console.error(chalk4.red(`
|
|
6336
6493
|
\u274C Invalid JSON: ${trimmed}
|
|
6337
6494
|
`));
|
|
6338
6495
|
process.exit(1);
|
|
@@ -6343,54 +6500,54 @@ program.command("explain").description(
|
|
|
6343
6500
|
}
|
|
6344
6501
|
const result = await explainPolicy(tool, args);
|
|
6345
6502
|
console.log("");
|
|
6346
|
-
console.log(
|
|
6503
|
+
console.log(chalk4.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
6347
6504
|
console.log("");
|
|
6348
|
-
console.log(` ${
|
|
6505
|
+
console.log(` ${chalk4.bold("Tool:")} ${chalk4.white(result.tool)}`);
|
|
6349
6506
|
if (argsRaw) {
|
|
6350
6507
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
6351
|
-
console.log(` ${
|
|
6508
|
+
console.log(` ${chalk4.bold("Input:")} ${chalk4.gray(preview)}`);
|
|
6352
6509
|
}
|
|
6353
6510
|
console.log("");
|
|
6354
|
-
console.log(
|
|
6511
|
+
console.log(chalk4.bold("Config Sources (Waterfall):"));
|
|
6355
6512
|
for (const tier of result.waterfall) {
|
|
6356
|
-
const num =
|
|
6513
|
+
const num = chalk4.gray(` ${tier.tier}.`);
|
|
6357
6514
|
const label = tier.label.padEnd(16);
|
|
6358
6515
|
let statusStr;
|
|
6359
6516
|
if (tier.tier === 1) {
|
|
6360
|
-
statusStr =
|
|
6517
|
+
statusStr = chalk4.gray(tier.note ?? "");
|
|
6361
6518
|
} else if (tier.status === "active") {
|
|
6362
|
-
const loc = tier.path ?
|
|
6363
|
-
const note = tier.note ?
|
|
6364
|
-
statusStr =
|
|
6519
|
+
const loc = tier.path ? chalk4.gray(tier.path) : "";
|
|
6520
|
+
const note = tier.note ? chalk4.gray(`(${tier.note})`) : "";
|
|
6521
|
+
statusStr = chalk4.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
6365
6522
|
} else {
|
|
6366
|
-
statusStr =
|
|
6523
|
+
statusStr = chalk4.gray("\u25CB " + (tier.note ?? "not found"));
|
|
6367
6524
|
}
|
|
6368
|
-
console.log(`${num} ${
|
|
6525
|
+
console.log(`${num} ${chalk4.white(label)} ${statusStr}`);
|
|
6369
6526
|
}
|
|
6370
6527
|
console.log("");
|
|
6371
|
-
console.log(
|
|
6528
|
+
console.log(chalk4.bold("Policy Evaluation:"));
|
|
6372
6529
|
for (const step of result.steps) {
|
|
6373
6530
|
const isFinal = step.isFinal;
|
|
6374
6531
|
let icon;
|
|
6375
|
-
if (step.outcome === "allow") icon =
|
|
6376
|
-
else if (step.outcome === "review") icon =
|
|
6377
|
-
else if (step.outcome === "skip") icon =
|
|
6378
|
-
else icon =
|
|
6532
|
+
if (step.outcome === "allow") icon = chalk4.green(" \u2705");
|
|
6533
|
+
else if (step.outcome === "review") icon = chalk4.red(" \u{1F534}");
|
|
6534
|
+
else if (step.outcome === "skip") icon = chalk4.gray(" \u2500 ");
|
|
6535
|
+
else icon = chalk4.gray(" \u25CB ");
|
|
6379
6536
|
const name = step.name.padEnd(18);
|
|
6380
|
-
const nameStr = isFinal ?
|
|
6381
|
-
const detail = isFinal ?
|
|
6382
|
-
const arrow = isFinal ?
|
|
6537
|
+
const nameStr = isFinal ? chalk4.white.bold(name) : chalk4.white(name);
|
|
6538
|
+
const detail = isFinal ? chalk4.white(step.detail) : chalk4.gray(step.detail);
|
|
6539
|
+
const arrow = isFinal ? chalk4.yellow(" \u2190 STOP") : "";
|
|
6383
6540
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
6384
6541
|
}
|
|
6385
6542
|
console.log("");
|
|
6386
6543
|
if (result.decision === "allow") {
|
|
6387
|
-
console.log(
|
|
6544
|
+
console.log(chalk4.green.bold(" Decision: \u2705 ALLOW") + chalk4.gray(" \u2014 no approval needed"));
|
|
6388
6545
|
} else {
|
|
6389
6546
|
console.log(
|
|
6390
|
-
|
|
6547
|
+
chalk4.red.bold(" Decision: \u{1F534} REVIEW") + chalk4.gray(" \u2014 human approval required")
|
|
6391
6548
|
);
|
|
6392
6549
|
if (result.blockedByLabel) {
|
|
6393
|
-
console.log(
|
|
6550
|
+
console.log(chalk4.gray(` Reason: ${result.blockedByLabel}`));
|
|
6394
6551
|
}
|
|
6395
6552
|
}
|
|
6396
6553
|
console.log("");
|
|
@@ -6398,8 +6555,8 @@ program.command("explain").description(
|
|
|
6398
6555
|
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
|
|
6399
6556
|
const configPath = path10.join(os7.homedir(), ".node9", "config.json");
|
|
6400
6557
|
if (fs8.existsSync(configPath) && !options.force) {
|
|
6401
|
-
console.log(
|
|
6402
|
-
console.log(
|
|
6558
|
+
console.log(chalk4.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
6559
|
+
console.log(chalk4.gray(` Run with --force to overwrite.`));
|
|
6403
6560
|
return;
|
|
6404
6561
|
}
|
|
6405
6562
|
const requestedMode = options.mode.toLowerCase();
|
|
@@ -6414,10 +6571,10 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
6414
6571
|
const dir = path10.dirname(configPath);
|
|
6415
6572
|
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
6416
6573
|
fs8.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
6417
|
-
console.log(
|
|
6418
|
-
console.log(
|
|
6574
|
+
console.log(chalk4.green(`\u2705 Global config created: ${configPath}`));
|
|
6575
|
+
console.log(chalk4.cyan(` Mode set to: ${safeMode}`));
|
|
6419
6576
|
console.log(
|
|
6420
|
-
|
|
6577
|
+
chalk4.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
6421
6578
|
);
|
|
6422
6579
|
});
|
|
6423
6580
|
function formatRelativeTime(timestamp) {
|
|
@@ -6434,7 +6591,7 @@ program.command("audit").description("View local execution audit log").option("-
|
|
|
6434
6591
|
const logPath = path10.join(os7.homedir(), ".node9", "audit.log");
|
|
6435
6592
|
if (!fs8.existsSync(logPath)) {
|
|
6436
6593
|
console.log(
|
|
6437
|
-
|
|
6594
|
+
chalk4.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
6438
6595
|
);
|
|
6439
6596
|
return;
|
|
6440
6597
|
}
|
|
@@ -6460,31 +6617,31 @@ program.command("audit").description("View local execution audit log").option("-
|
|
|
6460
6617
|
return;
|
|
6461
6618
|
}
|
|
6462
6619
|
if (entries.length === 0) {
|
|
6463
|
-
console.log(
|
|
6620
|
+
console.log(chalk4.yellow("No matching audit entries."));
|
|
6464
6621
|
return;
|
|
6465
6622
|
}
|
|
6466
6623
|
console.log(
|
|
6467
6624
|
`
|
|
6468
|
-
${
|
|
6625
|
+
${chalk4.bold("Node9 Audit Log")} ${chalk4.dim(`(${entries.length} entries)`)}`
|
|
6469
6626
|
);
|
|
6470
|
-
console.log(
|
|
6627
|
+
console.log(chalk4.dim(" " + "\u2500".repeat(65)));
|
|
6471
6628
|
console.log(
|
|
6472
6629
|
` ${"Time".padEnd(12)} ${"Tool".padEnd(18)} ${"Result".padEnd(10)} ${"By".padEnd(15)} Agent`
|
|
6473
6630
|
);
|
|
6474
|
-
console.log(
|
|
6631
|
+
console.log(chalk4.dim(" " + "\u2500".repeat(65)));
|
|
6475
6632
|
for (const e of entries) {
|
|
6476
6633
|
const time = formatRelativeTime(String(e.ts)).padEnd(12);
|
|
6477
6634
|
const tool = String(e.tool).slice(0, 17).padEnd(18);
|
|
6478
|
-
const result = e.decision === "allow" ?
|
|
6635
|
+
const result = e.decision === "allow" ? chalk4.green("ALLOW".padEnd(10)) : chalk4.red("DENY".padEnd(10));
|
|
6479
6636
|
const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
|
|
6480
6637
|
const agent = String(e.agent || "unknown");
|
|
6481
6638
|
console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
|
|
6482
6639
|
}
|
|
6483
6640
|
const allowed = entries.filter((e) => e.decision === "allow").length;
|
|
6484
6641
|
const denied = entries.filter((e) => e.decision === "deny").length;
|
|
6485
|
-
console.log(
|
|
6642
|
+
console.log(chalk4.dim(" " + "\u2500".repeat(65)));
|
|
6486
6643
|
console.log(
|
|
6487
|
-
` ${entries.length} entries | ${
|
|
6644
|
+
` ${entries.length} entries | ${chalk4.green(allowed + " allowed")} | ${chalk4.red(denied + " denied")}
|
|
6488
6645
|
`
|
|
6489
6646
|
);
|
|
6490
6647
|
});
|
|
@@ -6495,43 +6652,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
6495
6652
|
const settings = mergedConfig.settings;
|
|
6496
6653
|
console.log("");
|
|
6497
6654
|
if (creds && settings.approvers.cloud) {
|
|
6498
|
-
console.log(
|
|
6655
|
+
console.log(chalk4.green(" \u25CF Agent mode") + chalk4.gray(" \u2014 cloud team policy enforced"));
|
|
6499
6656
|
} else if (creds && !settings.approvers.cloud) {
|
|
6500
6657
|
console.log(
|
|
6501
|
-
|
|
6658
|
+
chalk4.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk4.gray(" \u2014 all decisions stay on this machine")
|
|
6502
6659
|
);
|
|
6503
6660
|
} else {
|
|
6504
6661
|
console.log(
|
|
6505
|
-
|
|
6662
|
+
chalk4.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk4.gray(" \u2014 no API key (Local rules only)")
|
|
6506
6663
|
);
|
|
6507
6664
|
}
|
|
6508
6665
|
console.log("");
|
|
6509
6666
|
if (daemonRunning) {
|
|
6510
6667
|
console.log(
|
|
6511
|
-
|
|
6668
|
+
chalk4.green(" \u25CF Daemon running") + chalk4.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
|
|
6512
6669
|
);
|
|
6513
6670
|
} else {
|
|
6514
|
-
console.log(
|
|
6671
|
+
console.log(chalk4.gray(" \u25CB Daemon stopped"));
|
|
6515
6672
|
}
|
|
6516
6673
|
if (settings.enableUndo) {
|
|
6517
6674
|
console.log(
|
|
6518
|
-
|
|
6675
|
+
chalk4.magenta(" \u25CF Undo Engine") + chalk4.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
|
|
6519
6676
|
);
|
|
6520
6677
|
}
|
|
6521
6678
|
console.log("");
|
|
6522
|
-
const modeLabel = settings.mode === "audit" ?
|
|
6679
|
+
const modeLabel = settings.mode === "audit" ? chalk4.blue("audit") : settings.mode === "strict" ? chalk4.red("strict") : chalk4.white("standard");
|
|
6523
6680
|
console.log(` Mode: ${modeLabel}`);
|
|
6524
6681
|
const projectConfig = path10.join(process.cwd(), "node9.config.json");
|
|
6525
6682
|
const globalConfig = path10.join(os7.homedir(), ".node9", "config.json");
|
|
6526
6683
|
console.log(
|
|
6527
|
-
` Local: ${fs8.existsSync(projectConfig) ?
|
|
6684
|
+
` Local: ${fs8.existsSync(projectConfig) ? chalk4.green("Active (node9.config.json)") : chalk4.gray("Not present")}`
|
|
6528
6685
|
);
|
|
6529
6686
|
console.log(
|
|
6530
|
-
` Global: ${fs8.existsSync(globalConfig) ?
|
|
6687
|
+
` Global: ${fs8.existsSync(globalConfig) ? chalk4.green("Active (~/.node9/config.json)") : chalk4.gray("Not present")}`
|
|
6531
6688
|
);
|
|
6532
6689
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
6533
6690
|
console.log(
|
|
6534
|
-
` Sandbox: ${
|
|
6691
|
+
` Sandbox: ${chalk4.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
6535
6692
|
);
|
|
6536
6693
|
}
|
|
6537
6694
|
const pauseState = checkPause();
|
|
@@ -6539,7 +6696,7 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
6539
6696
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
6540
6697
|
console.log("");
|
|
6541
6698
|
console.log(
|
|
6542
|
-
|
|
6699
|
+
chalk4.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk4.gray(" \u2014 all tool calls allowed")
|
|
6543
6700
|
);
|
|
6544
6701
|
}
|
|
6545
6702
|
console.log("");
|
|
@@ -6553,14 +6710,14 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
6553
6710
|
if (cmd === "stop") return stopDaemon();
|
|
6554
6711
|
if (cmd === "status") return daemonStatus();
|
|
6555
6712
|
if (cmd !== "start" && action !== void 0) {
|
|
6556
|
-
console.error(
|
|
6713
|
+
console.error(chalk4.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
|
|
6557
6714
|
process.exit(1);
|
|
6558
6715
|
}
|
|
6559
6716
|
if (options.watch) {
|
|
6560
6717
|
process.env.NODE9_WATCH_MODE = "1";
|
|
6561
6718
|
setTimeout(() => {
|
|
6562
6719
|
openBrowserLocal();
|
|
6563
|
-
console.log(
|
|
6720
|
+
console.log(chalk4.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
6564
6721
|
}, 600);
|
|
6565
6722
|
startDaemon();
|
|
6566
6723
|
return;
|
|
@@ -6568,7 +6725,7 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
6568
6725
|
if (options.openui) {
|
|
6569
6726
|
if (isDaemonRunning()) {
|
|
6570
6727
|
openBrowserLocal();
|
|
6571
|
-
console.log(
|
|
6728
|
+
console.log(chalk4.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
6572
6729
|
process.exit(0);
|
|
6573
6730
|
}
|
|
6574
6731
|
const child = spawn5(process.execPath, [process.argv[1], "daemon"], {
|
|
@@ -6581,7 +6738,7 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
6581
6738
|
if (isDaemonRunning()) break;
|
|
6582
6739
|
}
|
|
6583
6740
|
openBrowserLocal();
|
|
6584
|
-
console.log(
|
|
6741
|
+
console.log(chalk4.green(`
|
|
6585
6742
|
\u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
|
|
6586
6743
|
process.exit(0);
|
|
6587
6744
|
}
|
|
@@ -6591,7 +6748,7 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
6591
6748
|
stdio: "ignore"
|
|
6592
6749
|
});
|
|
6593
6750
|
child.unref();
|
|
6594
|
-
console.log(
|
|
6751
|
+
console.log(chalk4.green(`
|
|
6595
6752
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
6596
6753
|
process.exit(0);
|
|
6597
6754
|
}
|
|
@@ -6603,10 +6760,64 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
6603
6760
|
try {
|
|
6604
6761
|
await startTail2(options);
|
|
6605
6762
|
} catch (err) {
|
|
6606
|
-
console.error(
|
|
6763
|
+
console.error(chalk4.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
|
|
6607
6764
|
process.exit(1);
|
|
6608
6765
|
}
|
|
6609
6766
|
});
|
|
6767
|
+
program.command("watch").description("Run a command under Node9 watch mode (daemon stays alive for the session)").argument("<command>", "Command to run").argument("[args...]", "Arguments for the command").action(async (cmd, args) => {
|
|
6768
|
+
let port = DAEMON_PORT2;
|
|
6769
|
+
try {
|
|
6770
|
+
const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
|
|
6771
|
+
signal: AbortSignal.timeout(500)
|
|
6772
|
+
});
|
|
6773
|
+
if (res.ok) {
|
|
6774
|
+
const data = await res.json();
|
|
6775
|
+
if (typeof data.port === "number") port = data.port;
|
|
6776
|
+
} else {
|
|
6777
|
+
throw new Error("not running");
|
|
6778
|
+
}
|
|
6779
|
+
} catch {
|
|
6780
|
+
console.error(chalk4.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
|
|
6781
|
+
const child = spawn5(process.execPath, [process.argv[1], "daemon"], {
|
|
6782
|
+
detached: true,
|
|
6783
|
+
stdio: "ignore",
|
|
6784
|
+
env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_WATCH_MODE: "1" }
|
|
6785
|
+
});
|
|
6786
|
+
child.unref();
|
|
6787
|
+
let ready = false;
|
|
6788
|
+
for (let i = 0; i < 20; i++) {
|
|
6789
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
6790
|
+
try {
|
|
6791
|
+
const r = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
|
|
6792
|
+
signal: AbortSignal.timeout(500)
|
|
6793
|
+
});
|
|
6794
|
+
if (r.ok) {
|
|
6795
|
+
ready = true;
|
|
6796
|
+
break;
|
|
6797
|
+
}
|
|
6798
|
+
} catch {
|
|
6799
|
+
}
|
|
6800
|
+
}
|
|
6801
|
+
if (!ready) {
|
|
6802
|
+
console.error(chalk4.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
6803
|
+
process.exit(1);
|
|
6804
|
+
}
|
|
6805
|
+
}
|
|
6806
|
+
console.error(
|
|
6807
|
+
chalk4.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk4.dim(` \u2192 localhost:${port}`) + chalk4.dim(
|
|
6808
|
+
"\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
|
|
6809
|
+
)
|
|
6810
|
+
);
|
|
6811
|
+
const result = spawnSync4(cmd, args, {
|
|
6812
|
+
stdio: "inherit",
|
|
6813
|
+
env: { ...process.env, NODE9_WATCH_MODE: "1" }
|
|
6814
|
+
});
|
|
6815
|
+
if (result.error) {
|
|
6816
|
+
console.error(chalk4.red(`\u274C Failed to run command: ${result.error.message}`));
|
|
6817
|
+
process.exit(1);
|
|
6818
|
+
}
|
|
6819
|
+
process.exit(result.status ?? 0);
|
|
6820
|
+
});
|
|
6610
6821
|
program.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
|
|
6611
6822
|
const processPayload = async (raw) => {
|
|
6612
6823
|
try {
|
|
@@ -6644,19 +6855,30 @@ RAW: ${raw}
|
|
|
6644
6855
|
const sendBlock = (msg, result2) => {
|
|
6645
6856
|
const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
|
|
6646
6857
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
6647
|
-
|
|
6648
|
-
|
|
6858
|
+
let ttyFd = null;
|
|
6859
|
+
try {
|
|
6860
|
+
ttyFd = fs8.openSync("/dev/tty", "w");
|
|
6861
|
+
const writeTty = (line) => fs8.writeSync(ttyFd, line + "\n");
|
|
6862
|
+
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
6863
|
+
writeTty(chalk4.bgRed.white.bold(`
|
|
6649
6864
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6865
|
+
writeTty(chalk4.red.bold(` A sensitive secret was found in the tool arguments!`));
|
|
6866
|
+
} else {
|
|
6867
|
+
writeTty(chalk4.red(`
|
|
6653
6868
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
6869
|
+
}
|
|
6870
|
+
writeTty(chalk4.gray(` Triggered by: ${blockedByContext}`));
|
|
6871
|
+
if (result2?.changeHint) writeTty(chalk4.cyan(` To change: ${result2.changeHint}`));
|
|
6872
|
+
writeTty("");
|
|
6873
|
+
} catch {
|
|
6874
|
+
} finally {
|
|
6875
|
+
if (ttyFd !== null)
|
|
6876
|
+
try {
|
|
6877
|
+
fs8.closeSync(ttyFd);
|
|
6878
|
+
} catch {
|
|
6879
|
+
}
|
|
6654
6880
|
}
|
|
6655
|
-
console.error(chalk6.gray(` Triggered by: ${blockedByContext}`));
|
|
6656
|
-
if (result2?.changeHint) console.error(chalk6.cyan(` To change: ${result2.changeHint}`));
|
|
6657
|
-
console.error("");
|
|
6658
6881
|
const aiFeedbackMessage = buildNegotiationMessage(blockedByContext, isHumanDecision, msg);
|
|
6659
|
-
console.error(chalk6.dim(` (Detailed instructions sent to AI agent)`));
|
|
6660
6882
|
process.stdout.write(
|
|
6661
6883
|
JSON.stringify({
|
|
6662
6884
|
decision: "block",
|
|
@@ -6679,7 +6901,10 @@ RAW: ${raw}
|
|
|
6679
6901
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
6680
6902
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
6681
6903
|
}
|
|
6682
|
-
const
|
|
6904
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && path10.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
6905
|
+
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
6906
|
+
cwd: safeCwdForAuth
|
|
6907
|
+
});
|
|
6683
6908
|
if (result.approved) {
|
|
6684
6909
|
if (result.checkedBy && process.env.NODE9_DEBUG === "1")
|
|
6685
6910
|
process.stderr.write(`\u2713 node9 [${result.checkedBy}]: "${toolName}" allowed
|
|
@@ -6687,10 +6912,20 @@ RAW: ${raw}
|
|
|
6687
6912
|
process.exit(0);
|
|
6688
6913
|
}
|
|
6689
6914
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
6690
|
-
|
|
6915
|
+
try {
|
|
6916
|
+
const tty = fs8.openSync("/dev/tty", "w");
|
|
6917
|
+
fs8.writeSync(
|
|
6918
|
+
tty,
|
|
6919
|
+
chalk4.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
6920
|
+
);
|
|
6921
|
+
fs8.closeSync(tty);
|
|
6922
|
+
} catch {
|
|
6923
|
+
}
|
|
6691
6924
|
const daemonReady = await autoStartDaemonAndWait();
|
|
6692
6925
|
if (daemonReady) {
|
|
6693
|
-
const retry = await authorizeHeadless(toolName, toolInput,
|
|
6926
|
+
const retry = await authorizeHeadless(toolName, toolInput, meta, {
|
|
6927
|
+
cwd: safeCwdForAuth
|
|
6928
|
+
});
|
|
6694
6929
|
if (retry.approved) {
|
|
6695
6930
|
if (retry.checkedBy && process.env.NODE9_DEBUG === "1")
|
|
6696
6931
|
process.stderr.write(`\u2713 node9 [${retry.checkedBy}]: "${toolName}" allowed
|
|
@@ -6797,7 +7032,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
6797
7032
|
const ms = parseDuration(options.duration);
|
|
6798
7033
|
if (ms === null) {
|
|
6799
7034
|
console.error(
|
|
6800
|
-
|
|
7035
|
+
chalk4.red(`
|
|
6801
7036
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
6802
7037
|
`)
|
|
6803
7038
|
);
|
|
@@ -6805,20 +7040,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
6805
7040
|
}
|
|
6806
7041
|
pauseNode9(ms, options.duration);
|
|
6807
7042
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
6808
|
-
console.log(
|
|
7043
|
+
console.log(chalk4.yellow(`
|
|
6809
7044
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
6810
|
-
console.log(
|
|
6811
|
-
console.log(
|
|
7045
|
+
console.log(chalk4.gray(` All tool calls will be allowed without review.`));
|
|
7046
|
+
console.log(chalk4.gray(` Run "node9 resume" to re-enable early.
|
|
6812
7047
|
`));
|
|
6813
7048
|
});
|
|
6814
7049
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
6815
7050
|
const { paused } = checkPause();
|
|
6816
7051
|
if (!paused) {
|
|
6817
|
-
console.log(
|
|
7052
|
+
console.log(chalk4.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
6818
7053
|
return;
|
|
6819
7054
|
}
|
|
6820
7055
|
resumeNode9();
|
|
6821
|
-
console.log(
|
|
7056
|
+
console.log(chalk4.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
6822
7057
|
});
|
|
6823
7058
|
var HOOK_BASED_AGENTS = {
|
|
6824
7059
|
claude: "claude",
|
|
@@ -6831,37 +7066,50 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
6831
7066
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
6832
7067
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
6833
7068
|
console.error(
|
|
6834
|
-
|
|
7069
|
+
chalk4.yellow(`
|
|
6835
7070
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
6836
7071
|
);
|
|
6837
|
-
console.error(
|
|
7072
|
+
console.error(chalk4.white(`
|
|
6838
7073
|
"${target}" uses its own hook system. Use:`));
|
|
6839
7074
|
console.error(
|
|
6840
|
-
|
|
7075
|
+
chalk4.green(` node9 addto ${target} `) + chalk4.gray("# one-time setup")
|
|
6841
7076
|
);
|
|
6842
|
-
console.error(
|
|
7077
|
+
console.error(chalk4.green(` ${target} `) + chalk4.gray("# run normally"));
|
|
6843
7078
|
process.exit(1);
|
|
6844
7079
|
}
|
|
6845
|
-
const
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
7080
|
+
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
7081
|
+
if (runArgs.length === 0) {
|
|
7082
|
+
program.help();
|
|
7083
|
+
return;
|
|
7084
|
+
}
|
|
7085
|
+
const fullCommand = runArgs.join(" ");
|
|
7086
|
+
let result = await authorizeHeadless(
|
|
7087
|
+
"shell",
|
|
7088
|
+
{ command: fullCommand },
|
|
7089
|
+
{
|
|
7090
|
+
agent: "Terminal"
|
|
7091
|
+
}
|
|
7092
|
+
);
|
|
6849
7093
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
6850
|
-
console.error(
|
|
7094
|
+
console.error(chalk4.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
6851
7095
|
const daemonReady = await autoStartDaemonAndWait();
|
|
6852
7096
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
6853
7097
|
}
|
|
6854
7098
|
if (result.noApprovalMechanism && process.stdout.isTTY) {
|
|
6855
|
-
|
|
7099
|
+
const approved = await confirm2({
|
|
7100
|
+
message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand}"?`,
|
|
7101
|
+
default: false
|
|
7102
|
+
});
|
|
7103
|
+
result = { approved, reason: approved ? void 0 : "Denied by user at terminal." };
|
|
6856
7104
|
}
|
|
6857
7105
|
if (!result.approved) {
|
|
6858
7106
|
console.error(
|
|
6859
|
-
|
|
7107
|
+
chalk4.red(`
|
|
6860
7108
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
6861
7109
|
);
|
|
6862
7110
|
process.exit(1);
|
|
6863
7111
|
}
|
|
6864
|
-
console.error(
|
|
7112
|
+
console.error(chalk4.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
6865
7113
|
await runProxy(fullCommand);
|
|
6866
7114
|
} else {
|
|
6867
7115
|
program.help();
|
|
@@ -6876,22 +7124,22 @@ program.command("undo").description(
|
|
|
6876
7124
|
if (history.length === 0) {
|
|
6877
7125
|
if (!options.all && allHistory.length > 0) {
|
|
6878
7126
|
console.log(
|
|
6879
|
-
|
|
7127
|
+
chalk4.yellow(
|
|
6880
7128
|
`
|
|
6881
7129
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
6882
|
-
Run ${
|
|
7130
|
+
Run ${chalk4.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
6883
7131
|
`
|
|
6884
7132
|
)
|
|
6885
7133
|
);
|
|
6886
7134
|
} else {
|
|
6887
|
-
console.log(
|
|
7135
|
+
console.log(chalk4.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
6888
7136
|
}
|
|
6889
7137
|
return;
|
|
6890
7138
|
}
|
|
6891
7139
|
const idx = history.length - steps;
|
|
6892
7140
|
if (idx < 0) {
|
|
6893
7141
|
console.log(
|
|
6894
|
-
|
|
7142
|
+
chalk4.yellow(
|
|
6895
7143
|
`
|
|
6896
7144
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
6897
7145
|
`
|
|
@@ -6902,18 +7150,18 @@ program.command("undo").description(
|
|
|
6902
7150
|
const snapshot = history[idx];
|
|
6903
7151
|
const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
|
|
6904
7152
|
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
|
|
6905
|
-
console.log(
|
|
7153
|
+
console.log(chalk4.magenta.bold(`
|
|
6906
7154
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`));
|
|
6907
7155
|
console.log(
|
|
6908
|
-
|
|
6909
|
-
` Tool: ${
|
|
7156
|
+
chalk4.white(
|
|
7157
|
+
` Tool: ${chalk4.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk4.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
6910
7158
|
)
|
|
6911
7159
|
);
|
|
6912
|
-
console.log(
|
|
6913
|
-
console.log(
|
|
7160
|
+
console.log(chalk4.white(` When: ${chalk4.gray(ageStr)}`));
|
|
7161
|
+
console.log(chalk4.white(` Dir: ${chalk4.gray(snapshot.cwd)}`));
|
|
6914
7162
|
if (steps > 1)
|
|
6915
7163
|
console.log(
|
|
6916
|
-
|
|
7164
|
+
chalk4.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
6917
7165
|
);
|
|
6918
7166
|
console.log("");
|
|
6919
7167
|
const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
@@ -6921,65 +7169,65 @@ program.command("undo").description(
|
|
|
6921
7169
|
const lines = diff.split("\n");
|
|
6922
7170
|
for (const line of lines) {
|
|
6923
7171
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
6924
|
-
console.log(
|
|
7172
|
+
console.log(chalk4.bold(line));
|
|
6925
7173
|
} else if (line.startsWith("+")) {
|
|
6926
|
-
console.log(
|
|
7174
|
+
console.log(chalk4.green(line));
|
|
6927
7175
|
} else if (line.startsWith("-")) {
|
|
6928
|
-
console.log(
|
|
7176
|
+
console.log(chalk4.red(line));
|
|
6929
7177
|
} else if (line.startsWith("@@")) {
|
|
6930
|
-
console.log(
|
|
7178
|
+
console.log(chalk4.cyan(line));
|
|
6931
7179
|
} else {
|
|
6932
|
-
console.log(
|
|
7180
|
+
console.log(chalk4.gray(line));
|
|
6933
7181
|
}
|
|
6934
7182
|
}
|
|
6935
7183
|
console.log("");
|
|
6936
7184
|
} else {
|
|
6937
7185
|
console.log(
|
|
6938
|
-
|
|
7186
|
+
chalk4.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
6939
7187
|
);
|
|
6940
7188
|
}
|
|
6941
|
-
const proceed = await
|
|
7189
|
+
const proceed = await confirm2({
|
|
6942
7190
|
message: `Revert to this snapshot?`,
|
|
6943
7191
|
default: false
|
|
6944
7192
|
});
|
|
6945
7193
|
if (proceed) {
|
|
6946
7194
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
6947
|
-
console.log(
|
|
7195
|
+
console.log(chalk4.green("\n\u2705 Reverted successfully.\n"));
|
|
6948
7196
|
} else {
|
|
6949
|
-
console.error(
|
|
7197
|
+
console.error(chalk4.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
6950
7198
|
}
|
|
6951
7199
|
} else {
|
|
6952
|
-
console.log(
|
|
7200
|
+
console.log(chalk4.gray("\nCancelled.\n"));
|
|
6953
7201
|
}
|
|
6954
7202
|
});
|
|
6955
7203
|
var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
|
|
6956
7204
|
shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
|
|
6957
7205
|
const name = resolveShieldName(service);
|
|
6958
7206
|
if (!name) {
|
|
6959
|
-
console.error(
|
|
7207
|
+
console.error(chalk4.red(`
|
|
6960
7208
|
\u274C Unknown shield: "${service}"
|
|
6961
7209
|
`));
|
|
6962
|
-
console.log(`Run ${
|
|
7210
|
+
console.log(`Run ${chalk4.cyan("node9 shield list")} to see available shields.
|
|
6963
7211
|
`);
|
|
6964
7212
|
process.exit(1);
|
|
6965
7213
|
}
|
|
6966
7214
|
const shield = getShield(name);
|
|
6967
7215
|
const active = readActiveShields();
|
|
6968
7216
|
if (active.includes(name)) {
|
|
6969
|
-
console.log(
|
|
7217
|
+
console.log(chalk4.yellow(`
|
|
6970
7218
|
\u2139\uFE0F Shield "${name}" is already active.
|
|
6971
7219
|
`));
|
|
6972
7220
|
return;
|
|
6973
7221
|
}
|
|
6974
7222
|
writeActiveShields([...active, name]);
|
|
6975
|
-
console.log(
|
|
7223
|
+
console.log(chalk4.green(`
|
|
6976
7224
|
\u{1F6E1}\uFE0F Shield "${name}" enabled.`));
|
|
6977
|
-
console.log(
|
|
7225
|
+
console.log(chalk4.gray(` ${shield.smartRules.length} smart rules now active.`));
|
|
6978
7226
|
if (shield.dangerousWords.length > 0)
|
|
6979
|
-
console.log(
|
|
7227
|
+
console.log(chalk4.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
|
|
6980
7228
|
if (name === "filesystem") {
|
|
6981
7229
|
console.log(
|
|
6982
|
-
|
|
7230
|
+
chalk4.yellow(
|
|
6983
7231
|
`
|
|
6984
7232
|
\u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
|
|
6985
7233
|
Tools like unlink, find -delete, or language-level file ops are not intercepted.`
|
|
@@ -6991,70 +7239,70 @@ shieldCmd.command("enable <service>").description("Enable a security shield for
|
|
|
6991
7239
|
shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
|
|
6992
7240
|
const name = resolveShieldName(service);
|
|
6993
7241
|
if (!name) {
|
|
6994
|
-
console.error(
|
|
7242
|
+
console.error(chalk4.red(`
|
|
6995
7243
|
\u274C Unknown shield: "${service}"
|
|
6996
7244
|
`));
|
|
6997
|
-
console.log(`Run ${
|
|
7245
|
+
console.log(`Run ${chalk4.cyan("node9 shield list")} to see available shields.
|
|
6998
7246
|
`);
|
|
6999
7247
|
process.exit(1);
|
|
7000
7248
|
}
|
|
7001
7249
|
const active = readActiveShields();
|
|
7002
7250
|
if (!active.includes(name)) {
|
|
7003
|
-
console.log(
|
|
7251
|
+
console.log(chalk4.yellow(`
|
|
7004
7252
|
\u2139\uFE0F Shield "${name}" is not active.
|
|
7005
7253
|
`));
|
|
7006
7254
|
return;
|
|
7007
7255
|
}
|
|
7008
7256
|
writeActiveShields(active.filter((s) => s !== name));
|
|
7009
|
-
console.log(
|
|
7257
|
+
console.log(chalk4.green(`
|
|
7010
7258
|
\u{1F6E1}\uFE0F Shield "${name}" disabled.
|
|
7011
7259
|
`));
|
|
7012
7260
|
});
|
|
7013
7261
|
shieldCmd.command("list").description("Show all available shields").action(() => {
|
|
7014
7262
|
const active = new Set(readActiveShields());
|
|
7015
|
-
console.log(
|
|
7263
|
+
console.log(chalk4.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
|
|
7016
7264
|
for (const shield of listShields()) {
|
|
7017
|
-
const status = active.has(shield.name) ?
|
|
7018
|
-
console.log(` ${status} ${
|
|
7265
|
+
const status = active.has(shield.name) ? chalk4.green("\u25CF enabled") : chalk4.gray("\u25CB disabled");
|
|
7266
|
+
console.log(` ${status} ${chalk4.cyan(shield.name.padEnd(12))} ${shield.description}`);
|
|
7019
7267
|
if (shield.aliases.length > 0)
|
|
7020
|
-
console.log(
|
|
7268
|
+
console.log(chalk4.gray(` aliases: ${shield.aliases.join(", ")}`));
|
|
7021
7269
|
}
|
|
7022
7270
|
console.log("");
|
|
7023
7271
|
});
|
|
7024
7272
|
shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
|
|
7025
7273
|
const active = readActiveShields();
|
|
7026
7274
|
if (active.length === 0) {
|
|
7027
|
-
console.error(
|
|
7028
|
-
console.error(`Run ${
|
|
7275
|
+
console.error(chalk4.yellow("\n\u2139\uFE0F No shields are active.\n"));
|
|
7276
|
+
console.error(`Run ${chalk4.cyan("node9 shield list")} to see available shields.
|
|
7029
7277
|
`);
|
|
7030
7278
|
return;
|
|
7031
7279
|
}
|
|
7032
7280
|
const overrides = readShieldOverrides();
|
|
7033
|
-
console.error(
|
|
7281
|
+
console.error(chalk4.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
|
|
7034
7282
|
for (const name of active) {
|
|
7035
7283
|
const shield = getShield(name);
|
|
7036
7284
|
if (!shield) continue;
|
|
7037
|
-
console.error(` ${
|
|
7285
|
+
console.error(` ${chalk4.green("\u25CF")} ${chalk4.cyan(name)} \u2014 ${shield.description}`);
|
|
7038
7286
|
const ruleOverrides = overrides[name] ?? {};
|
|
7039
7287
|
for (const rule of shield.smartRules) {
|
|
7040
7288
|
const shortName = rule.name ? rule.name.replace(`shield:${name}:`, "") : "(unnamed)";
|
|
7041
7289
|
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
7042
7290
|
const effectiveVerdict = overrideVerdict ?? rule.verdict;
|
|
7043
|
-
const verdictLabel = effectiveVerdict === "block" ?
|
|
7044
|
-
const overrideNote = overrideVerdict ?
|
|
7291
|
+
const verdictLabel = effectiveVerdict === "block" ? chalk4.red("block ") : effectiveVerdict === "review" ? chalk4.yellow("review") : chalk4.green("allow ");
|
|
7292
|
+
const overrideNote = overrideVerdict ? chalk4.gray(` \u2190 overridden (was: ${rule.verdict})`) : "";
|
|
7045
7293
|
console.error(
|
|
7046
|
-
` ${verdictLabel} ${shortName.padEnd(24)} ${
|
|
7294
|
+
` ${verdictLabel} ${shortName.padEnd(24)} ${chalk4.gray(rule.reason ?? "")}${overrideNote}`
|
|
7047
7295
|
);
|
|
7048
7296
|
}
|
|
7049
7297
|
if (shield.dangerousWords.length > 0) {
|
|
7050
|
-
console.error(
|
|
7298
|
+
console.error(chalk4.gray(` words: ${shield.dangerousWords.join(", ")}`));
|
|
7051
7299
|
}
|
|
7052
7300
|
console.error("");
|
|
7053
7301
|
}
|
|
7054
7302
|
if (Object.keys(overrides).length > 0) {
|
|
7055
7303
|
console.error(
|
|
7056
|
-
|
|
7057
|
-
` Tip: run ${
|
|
7304
|
+
chalk4.gray(
|
|
7305
|
+
` Tip: run ${chalk4.cyan("node9 shield unset <shield> <rule>")} to remove an override.
|
|
7058
7306
|
`
|
|
7059
7307
|
)
|
|
7060
7308
|
);
|
|
@@ -7063,35 +7311,35 @@ shieldCmd.command("status").description("Show active shields and their individua
|
|
|
7063
7311
|
shieldCmd.command("set <service> <rule> <verdict>").description("Override the verdict for a specific shield rule (block, review, or allow)").option("--force", "Required when setting verdict to allow (silences a block rule)").action((service, rule, verdict, opts) => {
|
|
7064
7312
|
const name = resolveShieldName(service);
|
|
7065
7313
|
if (!name) {
|
|
7066
|
-
console.error(
|
|
7314
|
+
console.error(chalk4.red(`
|
|
7067
7315
|
\u274C Unknown shield: "${service}"
|
|
7068
7316
|
`));
|
|
7069
|
-
console.error(`Run ${
|
|
7317
|
+
console.error(`Run ${chalk4.cyan("node9 shield list")} to see available shields.
|
|
7070
7318
|
`);
|
|
7071
7319
|
process.exit(1);
|
|
7072
7320
|
}
|
|
7073
7321
|
if (!readActiveShields().includes(name)) {
|
|
7074
|
-
console.error(
|
|
7322
|
+
console.error(chalk4.red(`
|
|
7075
7323
|
\u274C Shield "${name}" is not active. Enable it first:
|
|
7076
7324
|
`));
|
|
7077
|
-
console.error(` ${
|
|
7325
|
+
console.error(` ${chalk4.cyan(`node9 shield enable ${name}`)}
|
|
7078
7326
|
`);
|
|
7079
7327
|
process.exit(1);
|
|
7080
7328
|
}
|
|
7081
7329
|
if (!isShieldVerdict(verdict)) {
|
|
7082
|
-
console.error(
|
|
7330
|
+
console.error(chalk4.red(`
|
|
7083
7331
|
\u274C Invalid verdict "${verdict}". Use: block, review, or allow
|
|
7084
7332
|
`));
|
|
7085
7333
|
process.exit(1);
|
|
7086
7334
|
}
|
|
7087
7335
|
if (verdict === "allow" && !opts.force) {
|
|
7088
7336
|
console.error(
|
|
7089
|
-
|
|
7337
|
+
chalk4.red(`
|
|
7090
7338
|
\u26A0\uFE0F Setting a verdict to "allow" silences the rule entirely.
|
|
7091
|
-
`) +
|
|
7339
|
+
`) + chalk4.yellow(
|
|
7092
7340
|
` This disables a shield protection. If you are sure, re-run with --force:
|
|
7093
7341
|
`
|
|
7094
|
-
) +
|
|
7342
|
+
) + chalk4.cyan(`
|
|
7095
7343
|
node9 shield set ${service} ${rule} allow --force
|
|
7096
7344
|
`)
|
|
7097
7345
|
);
|
|
@@ -7100,13 +7348,13 @@ shieldCmd.command("set <service> <rule> <verdict>").description("Override the ve
|
|
|
7100
7348
|
const ruleName = resolveShieldRule(name, rule);
|
|
7101
7349
|
if (!ruleName) {
|
|
7102
7350
|
const shield = getShield(name);
|
|
7103
|
-
console.error(
|
|
7351
|
+
console.error(chalk4.red(`
|
|
7104
7352
|
\u274C Unknown rule "${rule}" for shield "${name}".
|
|
7105
7353
|
`));
|
|
7106
7354
|
console.error(" Available rules:");
|
|
7107
7355
|
for (const r of shield?.smartRules ?? []) {
|
|
7108
7356
|
const short = r.name ? r.name.replace(`shield:${name}:`, "") : "";
|
|
7109
|
-
console.error(` ${
|
|
7357
|
+
console.error(` ${chalk4.cyan(short)}`);
|
|
7110
7358
|
}
|
|
7111
7359
|
console.error("");
|
|
7112
7360
|
process.exit(1);
|
|
@@ -7116,33 +7364,33 @@ shieldCmd.command("set <service> <rule> <verdict>").description("Override the ve
|
|
|
7116
7364
|
appendConfigAudit({ event: "shield-override-allow", shield: name, rule: ruleName });
|
|
7117
7365
|
}
|
|
7118
7366
|
const shortName = ruleName.replace(`shield:${name}:`, "");
|
|
7119
|
-
const verdictLabel = verdict === "block" ?
|
|
7367
|
+
const verdictLabel = verdict === "block" ? chalk4.red("block") : verdict === "review" ? chalk4.yellow("review") : chalk4.green("allow");
|
|
7120
7368
|
if (verdict === "allow") {
|
|
7121
7369
|
console.error(
|
|
7122
|
-
|
|
7123
|
-
\u26A0\uFE0F ${name}/${shortName} \u2192 ${verdictLabel}`) +
|
|
7370
|
+
chalk4.yellow(`
|
|
7371
|
+
\u26A0\uFE0F ${name}/${shortName} \u2192 ${verdictLabel}`) + chalk4.gray(" (rule silenced \u2014 use `node9 shield unset` to restore)\n")
|
|
7124
7372
|
);
|
|
7125
7373
|
} else {
|
|
7126
|
-
console.error(
|
|
7374
|
+
console.error(chalk4.green(`
|
|
7127
7375
|
\u2705 ${name}/${shortName} \u2192 ${verdictLabel}
|
|
7128
7376
|
`));
|
|
7129
7377
|
}
|
|
7130
7378
|
console.error(
|
|
7131
|
-
|
|
7379
|
+
chalk4.gray(` Run ${chalk4.cyan("node9 shield status")} to see all active rules.
|
|
7132
7380
|
`)
|
|
7133
7381
|
);
|
|
7134
7382
|
});
|
|
7135
7383
|
shieldCmd.command("unset <service> <rule>").description("Remove a verdict override, restoring the shield default").action((service, rule) => {
|
|
7136
7384
|
const name = resolveShieldName(service);
|
|
7137
7385
|
if (!name) {
|
|
7138
|
-
console.error(
|
|
7386
|
+
console.error(chalk4.red(`
|
|
7139
7387
|
\u274C Unknown shield: "${service}"
|
|
7140
7388
|
`));
|
|
7141
7389
|
process.exit(1);
|
|
7142
7390
|
}
|
|
7143
7391
|
const ruleName = resolveShieldRule(name, rule);
|
|
7144
7392
|
if (!ruleName) {
|
|
7145
|
-
console.error(
|
|
7393
|
+
console.error(chalk4.red(`
|
|
7146
7394
|
\u274C Unknown rule "${rule}" for shield "${name}".
|
|
7147
7395
|
`));
|
|
7148
7396
|
process.exit(1);
|
|
@@ -7150,7 +7398,7 @@ shieldCmd.command("unset <service> <rule>").description("Remove a verdict overri
|
|
|
7150
7398
|
clearShieldOverride(name, ruleName);
|
|
7151
7399
|
const shortName = ruleName.replace(`shield:${name}:`, "");
|
|
7152
7400
|
console.error(
|
|
7153
|
-
|
|
7401
|
+
chalk4.green(`
|
|
7154
7402
|
\u2705 Override removed \u2014 ${name}/${shortName} restored to default.
|
|
7155
7403
|
`)
|
|
7156
7404
|
);
|
|
@@ -7159,32 +7407,32 @@ program.command("config show").description("Show the full effective runtime conf
|
|
|
7159
7407
|
const config = getConfig();
|
|
7160
7408
|
const active = readActiveShields();
|
|
7161
7409
|
const overrides = readShieldOverrides();
|
|
7162
|
-
console.error(
|
|
7163
|
-
const modeLabel = config.settings.mode === "audit" ?
|
|
7410
|
+
console.error(chalk4.bold("\n\u{1F50D} Node9 Effective Configuration\n"));
|
|
7411
|
+
const modeLabel = config.settings.mode === "audit" ? chalk4.blue("audit") : config.settings.mode === "strict" ? chalk4.red("strict") : chalk4.white("standard");
|
|
7164
7412
|
console.error(` Mode: ${modeLabel}
|
|
7165
7413
|
`);
|
|
7166
7414
|
if (active.length > 0) {
|
|
7167
|
-
console.error(
|
|
7415
|
+
console.error(chalk4.bold(" \u2500\u2500 Active Shields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7168
7416
|
for (const name of active) {
|
|
7169
7417
|
const shield = getShield(name);
|
|
7170
7418
|
if (!shield) continue;
|
|
7171
7419
|
const ruleOverrides = overrides[name] ?? {};
|
|
7172
7420
|
console.error(`
|
|
7173
|
-
${
|
|
7421
|
+
${chalk4.green("\u25CF")} ${chalk4.cyan(name)}`);
|
|
7174
7422
|
for (const rule of shield.smartRules) {
|
|
7175
7423
|
const shortName = rule.name ? rule.name.replace(`shield:${name}:`, "") : "(unnamed)";
|
|
7176
7424
|
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
7177
7425
|
const effectiveVerdict = overrideVerdict ?? rule.verdict;
|
|
7178
|
-
const vLabel = effectiveVerdict === "block" ?
|
|
7179
|
-
const note = overrideVerdict ?
|
|
7426
|
+
const vLabel = effectiveVerdict === "block" ? chalk4.red("block ") : effectiveVerdict === "review" ? chalk4.yellow("review") : chalk4.green("allow ");
|
|
7427
|
+
const note = overrideVerdict ? chalk4.gray(` \u2190 overridden`) : "";
|
|
7180
7428
|
console.error(` ${vLabel} ${shortName}${note}`);
|
|
7181
7429
|
}
|
|
7182
7430
|
}
|
|
7183
7431
|
console.error("");
|
|
7184
7432
|
} else {
|
|
7185
|
-
console.error(
|
|
7433
|
+
console.error(chalk4.gray(" No shields active. Run `node9 shield list` to see options.\n"));
|
|
7186
7434
|
}
|
|
7187
|
-
console.error(
|
|
7435
|
+
console.error(chalk4.bold(" \u2500\u2500 Built-in Rules (always on) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7188
7436
|
for (const rule of config.policy.smartRules) {
|
|
7189
7437
|
const isShieldRule = rule.name?.startsWith("shield:");
|
|
7190
7438
|
const isAdvisory = [
|
|
@@ -7195,11 +7443,11 @@ program.command("config show").description("Show the full effective runtime conf
|
|
|
7195
7443
|
"review-drop-column-sql"
|
|
7196
7444
|
].includes(rule.name ?? "");
|
|
7197
7445
|
if (isShieldRule || isAdvisory) continue;
|
|
7198
|
-
const vLabel = rule.verdict === "block" ?
|
|
7199
|
-
console.error(` ${vLabel} ${
|
|
7446
|
+
const vLabel = rule.verdict === "block" ? chalk4.red("block ") : rule.verdict === "review" ? chalk4.yellow("review") : chalk4.green("allow ");
|
|
7447
|
+
console.error(` ${vLabel} ${chalk4.gray(rule.name ?? rule.tool)}`);
|
|
7200
7448
|
}
|
|
7201
7449
|
console.error("");
|
|
7202
|
-
console.error(
|
|
7450
|
+
console.error(chalk4.bold(" \u2500\u2500 Safe by Default (advisory, overridable) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7203
7451
|
const advisoryNames = /* @__PURE__ */ new Set([
|
|
7204
7452
|
"review-rm",
|
|
7205
7453
|
"allow-rm-safe-paths",
|
|
@@ -7209,12 +7457,12 @@ program.command("config show").description("Show the full effective runtime conf
|
|
|
7209
7457
|
]);
|
|
7210
7458
|
for (const rule of config.policy.smartRules) {
|
|
7211
7459
|
if (!advisoryNames.has(rule.name ?? "")) continue;
|
|
7212
|
-
const vLabel = rule.verdict === "block" ?
|
|
7213
|
-
console.error(` ${vLabel} ${
|
|
7460
|
+
const vLabel = rule.verdict === "block" ? chalk4.red("block ") : rule.verdict === "review" ? chalk4.yellow("review") : chalk4.green("allow ");
|
|
7461
|
+
console.error(` ${vLabel} ${chalk4.gray(rule.name ?? rule.tool)}`);
|
|
7214
7462
|
}
|
|
7215
7463
|
console.error("");
|
|
7216
|
-
console.error(
|
|
7217
|
-
console.error(` ${
|
|
7464
|
+
console.error(chalk4.bold(" \u2500\u2500 Dangerous Words \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7465
|
+
console.error(` ${chalk4.gray(config.policy.dangerousWords.join(", "))}
|
|
7218
7466
|
`);
|
|
7219
7467
|
});
|
|
7220
7468
|
if (process.argv[2] !== "daemon") {
|