@node9/proxy 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -7
- package/dist/cli.js +1544 -438
- package/dist/cli.mjs +1540 -434
- package/dist/index.js +294 -56
- package/dist/index.mjs +290 -46
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -9,7 +9,51 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// src/audit/hasher.ts
|
|
13
|
+
import { createHash } from "crypto";
|
|
14
|
+
function canonicalise(value) {
|
|
15
|
+
return _canonicalise(value, /* @__PURE__ */ new WeakSet());
|
|
16
|
+
}
|
|
17
|
+
function _canonicalise(value, seen) {
|
|
18
|
+
if (value === null || typeof value !== "object") return value;
|
|
19
|
+
if (value instanceof Date) return value.toISOString();
|
|
20
|
+
if (value instanceof RegExp) return value.toString();
|
|
21
|
+
if (Buffer.isBuffer(value)) return value.toString("base64");
|
|
22
|
+
if (seen.has(value)) return "[Circular]";
|
|
23
|
+
seen.add(value);
|
|
24
|
+
let result;
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
result = value.map((v) => _canonicalise(v, seen));
|
|
27
|
+
} else {
|
|
28
|
+
const obj = value;
|
|
29
|
+
result = Object.fromEntries(
|
|
30
|
+
Object.keys(obj).sort().map((k) => [k, _canonicalise(obj[k], seen)])
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
seen.delete(value);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
function hashArgs(args) {
|
|
37
|
+
const canonical = JSON.stringify(canonicalise(args) ?? null);
|
|
38
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
|
|
39
|
+
}
|
|
40
|
+
var init_hasher = __esm({
|
|
41
|
+
"src/audit/hasher.ts"() {
|
|
42
|
+
"use strict";
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
12
46
|
// src/audit/index.ts
|
|
47
|
+
var audit_exports = {};
|
|
48
|
+
__export(audit_exports, {
|
|
49
|
+
HOOK_DEBUG_LOG: () => HOOK_DEBUG_LOG,
|
|
50
|
+
LOCAL_AUDIT_LOG: () => LOCAL_AUDIT_LOG,
|
|
51
|
+
appendConfigAudit: () => appendConfigAudit,
|
|
52
|
+
appendHookDebug: () => appendHookDebug,
|
|
53
|
+
appendLocalAudit: () => appendLocalAudit,
|
|
54
|
+
appendToLog: () => appendToLog,
|
|
55
|
+
redactSecrets: () => redactSecrets
|
|
56
|
+
});
|
|
13
57
|
import fs from "fs";
|
|
14
58
|
import path from "path";
|
|
15
59
|
import os from "os";
|
|
@@ -34,24 +78,24 @@ function appendToLog(logPath, entry) {
|
|
|
34
78
|
} catch {
|
|
35
79
|
}
|
|
36
80
|
}
|
|
37
|
-
function appendHookDebug(toolName, args, meta) {
|
|
38
|
-
const
|
|
81
|
+
function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
82
|
+
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
39
83
|
appendToLog(HOOK_DEBUG_LOG, {
|
|
40
84
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
41
85
|
tool: toolName,
|
|
42
|
-
|
|
86
|
+
...argsField,
|
|
43
87
|
agent: meta?.agent,
|
|
44
88
|
mcpServer: meta?.mcpServer,
|
|
45
89
|
hostname: os.hostname(),
|
|
46
90
|
cwd: process.cwd()
|
|
47
91
|
});
|
|
48
92
|
}
|
|
49
|
-
function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
50
|
-
const
|
|
93
|
+
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
94
|
+
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
51
95
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
52
96
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
53
97
|
tool: toolName,
|
|
54
|
-
|
|
98
|
+
...argsField,
|
|
55
99
|
decision,
|
|
56
100
|
checkedBy,
|
|
57
101
|
agent: meta?.agent,
|
|
@@ -70,6 +114,7 @@ var LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG;
|
|
|
70
114
|
var init_audit = __esm({
|
|
71
115
|
"src/audit/index.ts"() {
|
|
72
116
|
"use strict";
|
|
117
|
+
init_hasher();
|
|
73
118
|
LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
|
|
74
119
|
HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
|
|
75
120
|
}
|
|
@@ -94,8 +139,8 @@ function sanitizeConfig(raw) {
|
|
|
94
139
|
}
|
|
95
140
|
}
|
|
96
141
|
const lines = result.error.issues.map((issue) => {
|
|
97
|
-
const
|
|
98
|
-
return ` \u2022 ${
|
|
142
|
+
const path28 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
143
|
+
return ` \u2022 ${path28}: ${issue.message}`;
|
|
99
144
|
});
|
|
100
145
|
return {
|
|
101
146
|
sanitized,
|
|
@@ -167,7 +212,8 @@ var init_config_schema = __esm({
|
|
|
167
212
|
environment: z.string().optional(),
|
|
168
213
|
slackEnabled: z.boolean().optional(),
|
|
169
214
|
enableTrustSessions: z.boolean().optional(),
|
|
170
|
-
allowGlobalPause: z.boolean().optional()
|
|
215
|
+
allowGlobalPause: z.boolean().optional(),
|
|
216
|
+
auditHashArgs: z.boolean().optional()
|
|
171
217
|
}).optional(),
|
|
172
218
|
policy: z.object({
|
|
173
219
|
sandboxPaths: z.array(z.string()).optional(),
|
|
@@ -725,6 +771,7 @@ var init_config = __esm({
|
|
|
725
771
|
approvalTimeoutMs: 12e4,
|
|
726
772
|
// 120-second auto-deny timeout
|
|
727
773
|
flightRecorder: true,
|
|
774
|
+
auditHashArgs: true,
|
|
728
775
|
approvers: { native: true, browser: true, cloud: false, terminal: true }
|
|
729
776
|
},
|
|
730
777
|
policy: {
|
|
@@ -1605,17 +1652,44 @@ function readTrustedHosts() {
|
|
|
1605
1652
|
return [];
|
|
1606
1653
|
}
|
|
1607
1654
|
}
|
|
1655
|
+
function getFileMtime() {
|
|
1656
|
+
try {
|
|
1657
|
+
return fs6.statSync(getTrustedHostsPath()).mtimeMs;
|
|
1658
|
+
} catch {
|
|
1659
|
+
return 0;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
function getCachedHosts() {
|
|
1663
|
+
const now = Date.now();
|
|
1664
|
+
if (_cache && now < _cache.expiry) {
|
|
1665
|
+
const mtime = getFileMtime();
|
|
1666
|
+
if (mtime === _cache.mtime) return _cache.hosts;
|
|
1667
|
+
}
|
|
1668
|
+
const hosts = readTrustedHosts();
|
|
1669
|
+
_cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
1670
|
+
return hosts;
|
|
1671
|
+
}
|
|
1608
1672
|
function writeTrustedHosts(hosts) {
|
|
1609
1673
|
const filePath = getTrustedHostsPath();
|
|
1610
1674
|
fs6.mkdirSync(path7.dirname(filePath), { recursive: true });
|
|
1611
1675
|
const tmp = filePath + ".node9-tmp";
|
|
1612
|
-
fs6.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2));
|
|
1676
|
+
fs6.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2), { mode: 384 });
|
|
1613
1677
|
fs6.renameSync(tmp, filePath);
|
|
1678
|
+
_cache = { hosts, expiry: Date.now() + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
1614
1679
|
}
|
|
1615
1680
|
function addTrustedHost(host) {
|
|
1681
|
+
const normalized = normalizeHost(host);
|
|
1682
|
+
if (normalized.startsWith("*.")) {
|
|
1683
|
+
const base = normalized.slice(2);
|
|
1684
|
+
if (!base.includes(".")) {
|
|
1685
|
+
throw new Error(
|
|
1686
|
+
`Wildcard pattern '${normalized}' is too broad \u2014 the base domain must have at least one dot (e.g. '*.mycompany.com', not '*.com').`
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1616
1690
|
const hosts = readTrustedHosts();
|
|
1617
|
-
if (hosts.some((h) => h.host ===
|
|
1618
|
-
hosts.push({ host, addedAt: Date.now(), addedBy: "user" });
|
|
1691
|
+
if (hosts.some((h) => h.host === normalized)) return;
|
|
1692
|
+
hosts.push({ host: normalized, addedAt: Date.now(), addedBy: "user" });
|
|
1619
1693
|
writeTrustedHosts(hosts);
|
|
1620
1694
|
}
|
|
1621
1695
|
function removeTrustedHost(host) {
|
|
@@ -1630,18 +1704,21 @@ function normalizeHost(raw) {
|
|
|
1630
1704
|
}
|
|
1631
1705
|
function isTrustedHost(host) {
|
|
1632
1706
|
const normalized = normalizeHost(host);
|
|
1633
|
-
return
|
|
1707
|
+
return getCachedHosts().some((entry) => {
|
|
1634
1708
|
const entryHost = entry.host.toLowerCase();
|
|
1635
1709
|
if (entryHost.startsWith("*.")) {
|
|
1636
1710
|
const domain = entryHost.slice(2);
|
|
1637
|
-
return normalized
|
|
1711
|
+
return normalized.endsWith("." + domain);
|
|
1638
1712
|
}
|
|
1639
1713
|
return normalized === entryHost;
|
|
1640
1714
|
});
|
|
1641
1715
|
}
|
|
1716
|
+
var _cache, CACHE_TTL_MS;
|
|
1642
1717
|
var init_trusted_hosts = __esm({
|
|
1643
1718
|
"src/auth/trusted-hosts.ts"() {
|
|
1644
1719
|
"use strict";
|
|
1720
|
+
_cache = null;
|
|
1721
|
+
CACHE_TTL_MS = 5e3;
|
|
1645
1722
|
}
|
|
1646
1723
|
});
|
|
1647
1724
|
|
|
@@ -1664,9 +1741,9 @@ function matchesPattern(text, patterns) {
|
|
|
1664
1741
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1665
1742
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1666
1743
|
}
|
|
1667
|
-
function getNestedValue(obj,
|
|
1744
|
+
function getNestedValue(obj, path28) {
|
|
1668
1745
|
if (!obj || typeof obj !== "object") return null;
|
|
1669
|
-
return
|
|
1746
|
+
return path28.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1670
1747
|
}
|
|
1671
1748
|
function shouldSnapshot(toolName, args, config) {
|
|
1672
1749
|
if (!config.settings.enableUndo) return false;
|
|
@@ -1852,7 +1929,12 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1852
1929
|
};
|
|
1853
1930
|
}
|
|
1854
1931
|
if (allTrusted) {
|
|
1855
|
-
return {
|
|
1932
|
+
return {
|
|
1933
|
+
decision: "allow",
|
|
1934
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
1935
|
+
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
1936
|
+
tier: 3
|
|
1937
|
+
};
|
|
1856
1938
|
}
|
|
1857
1939
|
return {
|
|
1858
1940
|
decision: "review",
|
|
@@ -2393,8 +2475,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2393
2475
|
signal: ctrl.signal
|
|
2394
2476
|
});
|
|
2395
2477
|
if (!res.ok) throw new Error("Daemon fail");
|
|
2396
|
-
const { id } = await res.json();
|
|
2397
|
-
return id;
|
|
2478
|
+
const { id, allowCount } = await res.json();
|
|
2479
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
2398
2480
|
} finally {
|
|
2399
2481
|
clearTimeout(timer);
|
|
2400
2482
|
}
|
|
@@ -2433,15 +2515,67 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
|
2433
2515
|
signal: AbortSignal.timeout(3e3)
|
|
2434
2516
|
});
|
|
2435
2517
|
if (!res.ok) throw new Error("Daemon unreachable");
|
|
2436
|
-
const { id } = await res.json();
|
|
2437
|
-
return id;
|
|
2518
|
+
const { id, allowCount } = await res.json();
|
|
2519
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
2520
|
+
}
|
|
2521
|
+
async function notifyTaint(filePath, source) {
|
|
2522
|
+
if (!isDaemonRunning()) return;
|
|
2523
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2524
|
+
try {
|
|
2525
|
+
await fetch(`${base}/taint`, {
|
|
2526
|
+
method: "POST",
|
|
2527
|
+
headers: { "Content-Type": "application/json" },
|
|
2528
|
+
body: JSON.stringify({ path: filePath, source }),
|
|
2529
|
+
signal: AbortSignal.timeout(1e3)
|
|
2530
|
+
});
|
|
2531
|
+
} catch {
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
async function notifyTaintPropagate(src, dest, clearSource = false) {
|
|
2535
|
+
if (!isDaemonRunning()) return;
|
|
2536
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2537
|
+
try {
|
|
2538
|
+
await fetch(`${base}/taint/propagate`, {
|
|
2539
|
+
method: "POST",
|
|
2540
|
+
headers: { "Content-Type": "application/json" },
|
|
2541
|
+
body: JSON.stringify({ src, dest, clearSource }),
|
|
2542
|
+
signal: AbortSignal.timeout(1e3)
|
|
2543
|
+
});
|
|
2544
|
+
} catch {
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
async function checkTaint(paths) {
|
|
2548
|
+
if (paths.length === 0) return { tainted: false };
|
|
2549
|
+
if (!isDaemonRunning()) return { tainted: false, daemonUnavailable: true };
|
|
2550
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2551
|
+
try {
|
|
2552
|
+
const res = await fetch(`${base}/taint/check`, {
|
|
2553
|
+
method: "POST",
|
|
2554
|
+
headers: { "Content-Type": "application/json" },
|
|
2555
|
+
body: JSON.stringify({ paths }),
|
|
2556
|
+
signal: AbortSignal.timeout(2e3)
|
|
2557
|
+
});
|
|
2558
|
+
return await res.json();
|
|
2559
|
+
} catch (err) {
|
|
2560
|
+
try {
|
|
2561
|
+
const { appendToLog: appendToLog2, HOOK_DEBUG_LOG: HOOK_DEBUG_LOG2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
|
|
2562
|
+
appendToLog2(HOOK_DEBUG_LOG2, {
|
|
2563
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2564
|
+
event: "checkTaint-error",
|
|
2565
|
+
error: String(err),
|
|
2566
|
+
paths
|
|
2567
|
+
});
|
|
2568
|
+
} catch {
|
|
2569
|
+
}
|
|
2570
|
+
return { tainted: false, daemonUnavailable: true };
|
|
2571
|
+
}
|
|
2438
2572
|
}
|
|
2439
|
-
async function resolveViaDaemon(id, decision, internalToken) {
|
|
2573
|
+
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
2440
2574
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2441
2575
|
await fetch(`${base}/resolve/${id}`, {
|
|
2442
2576
|
method: "POST",
|
|
2443
2577
|
headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
|
|
2444
|
-
body: JSON.stringify({ decision }),
|
|
2578
|
+
body: JSON.stringify({ decision, ...source && { source } }),
|
|
2445
2579
|
signal: AbortSignal.timeout(3e3)
|
|
2446
2580
|
});
|
|
2447
2581
|
}
|
|
@@ -2646,20 +2780,24 @@ ${smartTruncate(str, 500)}`
|
|
|
2646
2780
|
function escapePango(text) {
|
|
2647
2781
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2648
2782
|
}
|
|
2649
|
-
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2783
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2650
2784
|
const lines = [];
|
|
2651
2785
|
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
2652
2786
|
lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
|
|
2653
2787
|
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
2654
2788
|
lines.push("");
|
|
2655
2789
|
lines.push(formattedArgs);
|
|
2790
|
+
if (allowCount >= 3) {
|
|
2791
|
+
lines.push("");
|
|
2792
|
+
lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
|
|
2793
|
+
}
|
|
2656
2794
|
if (!locked) {
|
|
2657
2795
|
lines.push("");
|
|
2658
2796
|
lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
|
|
2659
2797
|
}
|
|
2660
2798
|
return lines.join("\n");
|
|
2661
2799
|
}
|
|
2662
|
-
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2800
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2663
2801
|
const lines = [];
|
|
2664
2802
|
if (locked) {
|
|
2665
2803
|
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
@@ -2671,6 +2809,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2671
2809
|
lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
|
|
2672
2810
|
lines.push("");
|
|
2673
2811
|
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
2812
|
+
if (allowCount >= 3) {
|
|
2813
|
+
lines.push("");
|
|
2814
|
+
lines.push(
|
|
2815
|
+
`<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
|
|
2816
|
+
);
|
|
2817
|
+
}
|
|
2674
2818
|
if (!locked) {
|
|
2675
2819
|
lines.push("");
|
|
2676
2820
|
lines.push(
|
|
@@ -2679,12 +2823,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2679
2823
|
}
|
|
2680
2824
|
return lines.join("\n");
|
|
2681
2825
|
}
|
|
2682
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
|
|
2826
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
|
|
2683
2827
|
if (isTestEnv()) return "deny";
|
|
2684
2828
|
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
2685
2829
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
2686
2830
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
2687
|
-
const message = buildPlainMessage(
|
|
2831
|
+
const message = buildPlainMessage(
|
|
2832
|
+
toolName,
|
|
2833
|
+
formattedArgs,
|
|
2834
|
+
agent,
|
|
2835
|
+
explainableLabel,
|
|
2836
|
+
locked,
|
|
2837
|
+
allowCount
|
|
2838
|
+
);
|
|
2688
2839
|
return new Promise((resolve) => {
|
|
2689
2840
|
let childProcess = null;
|
|
2690
2841
|
const onAbort = () => {
|
|
@@ -2716,7 +2867,8 @@ end run`;
|
|
|
2716
2867
|
formattedArgs,
|
|
2717
2868
|
agent,
|
|
2718
2869
|
explainableLabel,
|
|
2719
|
-
locked
|
|
2870
|
+
locked,
|
|
2871
|
+
allowCount
|
|
2720
2872
|
);
|
|
2721
2873
|
const argsList = [
|
|
2722
2874
|
locked ? "--info" : "--question",
|
|
@@ -2888,6 +3040,40 @@ import net from "net";
|
|
|
2888
3040
|
import path13 from "path";
|
|
2889
3041
|
import os10 from "os";
|
|
2890
3042
|
import { randomUUID } from "crypto";
|
|
3043
|
+
function isWriteTool(toolName) {
|
|
3044
|
+
const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
3045
|
+
return WRITE_TOOLS.has(t);
|
|
3046
|
+
}
|
|
3047
|
+
function extractFilePaths(toolName, args) {
|
|
3048
|
+
const paths = [];
|
|
3049
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return paths;
|
|
3050
|
+
const a = args;
|
|
3051
|
+
for (const key of ["file_path", "path", "filename", "source", "src", "input"]) {
|
|
3052
|
+
if (typeof a[key] === "string" && a[key]) paths.push(a[key]);
|
|
3053
|
+
}
|
|
3054
|
+
const cmd = typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : "";
|
|
3055
|
+
if (cmd) {
|
|
3056
|
+
for (const m of cmd.matchAll(/(?:-T|--upload-file|--data(?:-binary)?)\s+@?(\S+)/g)) {
|
|
3057
|
+
paths.push(m[1]);
|
|
3058
|
+
}
|
|
3059
|
+
for (const m of cmd.matchAll(/\b(?:scp|rsync)\s+(?:-\S+\s+)*(\S+)\s+\S+@/g)) {
|
|
3060
|
+
paths.push(m[1]);
|
|
3061
|
+
}
|
|
3062
|
+
for (const m of cmd.matchAll(/<\s*(\S+)/g)) {
|
|
3063
|
+
paths.push(m[1]);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
return paths.filter(Boolean);
|
|
3067
|
+
}
|
|
3068
|
+
function isNetworkTool(toolName, args) {
|
|
3069
|
+
const t = toolName.toLowerCase();
|
|
3070
|
+
if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal.execute") {
|
|
3071
|
+
const a = args;
|
|
3072
|
+
const cmd = typeof a?.command === "string" ? a.command : typeof a?.cmd === "string" ? a.cmd : "";
|
|
3073
|
+
return /\b(curl|wget|scp|rsync|nc|ncat|netcat|ssh)\b/.test(cmd);
|
|
3074
|
+
}
|
|
3075
|
+
return false;
|
|
3076
|
+
}
|
|
2891
3077
|
function notifyActivity(data) {
|
|
2892
3078
|
return new Promise((resolve) => {
|
|
2893
3079
|
try {
|
|
@@ -2917,7 +3103,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
2917
3103
|
id: actId,
|
|
2918
3104
|
tool: toolName,
|
|
2919
3105
|
ts: actTs,
|
|
2920
|
-
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
3106
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
2921
3107
|
label: result.blockedByLabel
|
|
2922
3108
|
});
|
|
2923
3109
|
}
|
|
@@ -2931,6 +3117,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2931
3117
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
2932
3118
|
const creds = getCredentials();
|
|
2933
3119
|
const config = getConfig(options?.cwd);
|
|
3120
|
+
const hashAuditArgs = config.settings.auditHashArgs === true;
|
|
2934
3121
|
const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
|
|
2935
3122
|
const approvers = {
|
|
2936
3123
|
...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
|
|
@@ -2941,13 +3128,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2941
3128
|
approvers.terminal = false;
|
|
2942
3129
|
}
|
|
2943
3130
|
if (config.settings.enableHookLogDebug && !isTestEnv2) {
|
|
2944
|
-
appendHookDebug(toolName, args, meta);
|
|
3131
|
+
appendHookDebug(toolName, args, meta, hashAuditArgs);
|
|
2945
3132
|
}
|
|
2946
3133
|
const isManual = meta?.agent === "Terminal";
|
|
2947
3134
|
let explainableLabel = "Local Config";
|
|
2948
3135
|
let policyMatchedField;
|
|
2949
3136
|
let policyMatchedWord;
|
|
2950
3137
|
let riskMetadata;
|
|
3138
|
+
let taintWarning = null;
|
|
3139
|
+
if (isNetworkTool(toolName, args)) {
|
|
3140
|
+
const filePaths = extractFilePaths(toolName, args);
|
|
3141
|
+
if (filePaths.length > 0) {
|
|
3142
|
+
const taintResult = await checkTaint(filePaths);
|
|
3143
|
+
if (taintResult.tainted && taintResult.record) {
|
|
3144
|
+
const { path: taintedPath, source: taintSource } = taintResult.record;
|
|
3145
|
+
taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
|
|
3146
|
+
} else if (taintResult.daemonUnavailable) {
|
|
3147
|
+
taintWarning = `\u26A0\uFE0F Taint service unavailable \u2014 cannot verify if ${filePaths.join(", ")} is clean`;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
2951
3151
|
if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
2952
3152
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
2953
3153
|
const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
|
|
@@ -2955,7 +3155,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2955
3155
|
if (dlpMatch) {
|
|
2956
3156
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
2957
3157
|
if (dlpMatch.severity === "block") {
|
|
2958
|
-
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
|
|
3158
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta, true);
|
|
3159
|
+
if (isWriteTool(toolName) && filePath) {
|
|
3160
|
+
await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
|
|
3161
|
+
}
|
|
2959
3162
|
return {
|
|
2960
3163
|
approved: false,
|
|
2961
3164
|
reason: dlpReason,
|
|
@@ -2963,7 +3166,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2963
3166
|
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
2964
3167
|
};
|
|
2965
3168
|
}
|
|
2966
|
-
if (!isManual)
|
|
3169
|
+
if (!isManual)
|
|
3170
|
+
appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta, hashAuditArgs);
|
|
2967
3171
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
2968
3172
|
}
|
|
2969
3173
|
}
|
|
@@ -2971,7 +3175,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2971
3175
|
if (!isIgnoredTool(toolName)) {
|
|
2972
3176
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
2973
3177
|
if (policyResult.decision === "review") {
|
|
2974
|
-
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
3178
|
+
appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
|
|
2975
3179
|
if (approvers.cloud && creds?.apiKey) {
|
|
2976
3180
|
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
2977
3181
|
}
|
|
@@ -2979,22 +3183,23 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2979
3183
|
}
|
|
2980
3184
|
return { approved: true, checkedBy: "audit" };
|
|
2981
3185
|
}
|
|
2982
|
-
if (!isIgnoredTool(toolName)) {
|
|
3186
|
+
if (!taintWarning && !isIgnoredTool(toolName)) {
|
|
2983
3187
|
if (getActiveTrustSession(toolName)) {
|
|
2984
3188
|
if (approvers.cloud && creds?.apiKey)
|
|
2985
3189
|
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
2986
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
3190
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
|
|
2987
3191
|
return { approved: true, checkedBy: "trust" };
|
|
2988
3192
|
}
|
|
2989
3193
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
2990
3194
|
if (policyResult.decision === "allow") {
|
|
2991
3195
|
if (approvers.cloud && creds?.apiKey)
|
|
2992
3196
|
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
2993
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
3197
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
|
|
2994
3198
|
return { approved: true, checkedBy: "local-policy" };
|
|
2995
3199
|
}
|
|
2996
3200
|
if (policyResult.decision === "block") {
|
|
2997
|
-
if (!isManual)
|
|
3201
|
+
if (!isManual)
|
|
3202
|
+
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
2998
3203
|
return {
|
|
2999
3204
|
approved: false,
|
|
3000
3205
|
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
@@ -3013,15 +3218,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3013
3218
|
policyMatchedWord,
|
|
3014
3219
|
policyResult.ruleName
|
|
3015
3220
|
);
|
|
3016
|
-
const persistent = getPersistentDecision(toolName);
|
|
3221
|
+
const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
|
|
3017
3222
|
if (persistent === "allow") {
|
|
3018
3223
|
if (approvers.cloud && creds?.apiKey)
|
|
3019
3224
|
await auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
3020
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
3225
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
|
|
3021
3226
|
return { approved: true, checkedBy: "persistent" };
|
|
3022
3227
|
}
|
|
3023
3228
|
if (persistent === "deny") {
|
|
3024
|
-
if (!isManual)
|
|
3229
|
+
if (!isManual)
|
|
3230
|
+
appendLocalAudit(toolName, args, "deny", "persistent-deny", meta, hashAuditArgs);
|
|
3025
3231
|
return {
|
|
3026
3232
|
approved: false,
|
|
3027
3233
|
reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
|
|
@@ -3029,10 +3235,21 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3029
3235
|
blockedByLabel: "Persistent User Rule"
|
|
3030
3236
|
};
|
|
3031
3237
|
}
|
|
3032
|
-
} else {
|
|
3033
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
3238
|
+
} else if (!taintWarning) {
|
|
3239
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
|
|
3034
3240
|
return { approved: true };
|
|
3035
3241
|
}
|
|
3242
|
+
if (taintWarning) {
|
|
3243
|
+
explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
|
|
3244
|
+
riskMetadata = computeRiskMetadata(
|
|
3245
|
+
args,
|
|
3246
|
+
7,
|
|
3247
|
+
explainableLabel,
|
|
3248
|
+
void 0,
|
|
3249
|
+
void 0,
|
|
3250
|
+
taintWarning
|
|
3251
|
+
);
|
|
3252
|
+
}
|
|
3036
3253
|
let cloudRequestId = null;
|
|
3037
3254
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
3038
3255
|
if (cloudEnforced) {
|
|
@@ -3051,7 +3268,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3051
3268
|
};
|
|
3052
3269
|
}
|
|
3053
3270
|
cloudRequestId = initResult.requestId || null;
|
|
3054
|
-
explainableLabel = "Organization Policy (SaaS)";
|
|
3271
|
+
if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
|
|
3055
3272
|
} catch {
|
|
3056
3273
|
}
|
|
3057
3274
|
}
|
|
@@ -3080,13 +3297,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3080
3297
|
let viewerId = null;
|
|
3081
3298
|
const internalToken = getInternalToken();
|
|
3082
3299
|
let daemonEntryId = null;
|
|
3300
|
+
let daemonAllowCount = 1;
|
|
3083
3301
|
if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
3084
3302
|
if (cloudEnforced && cloudRequestId) {
|
|
3085
|
-
|
|
3303
|
+
const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
|
|
3304
|
+
viewerId = viewer?.id ?? null;
|
|
3086
3305
|
daemonEntryId = viewerId;
|
|
3306
|
+
if (viewer) daemonAllowCount = viewer.allowCount;
|
|
3087
3307
|
} else {
|
|
3088
3308
|
try {
|
|
3089
|
-
|
|
3309
|
+
const entry = await registerDaemonEntry(
|
|
3090
3310
|
toolName,
|
|
3091
3311
|
args,
|
|
3092
3312
|
meta,
|
|
@@ -3094,6 +3314,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3094
3314
|
options?.activityId,
|
|
3095
3315
|
options?.cwd
|
|
3096
3316
|
);
|
|
3317
|
+
daemonEntryId = entry.id;
|
|
3318
|
+
daemonAllowCount = entry.allowCount;
|
|
3097
3319
|
} catch {
|
|
3098
3320
|
}
|
|
3099
3321
|
}
|
|
@@ -3129,7 +3351,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3129
3351
|
false,
|
|
3130
3352
|
signal,
|
|
3131
3353
|
policyMatchedField,
|
|
3132
|
-
policyMatchedWord
|
|
3354
|
+
policyMatchedWord,
|
|
3355
|
+
daemonAllowCount
|
|
3133
3356
|
);
|
|
3134
3357
|
if (decision === "always_allow") {
|
|
3135
3358
|
writeTrustSession(toolName, 36e5);
|
|
@@ -3187,10 +3410,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3187
3410
|
if (!resolved) {
|
|
3188
3411
|
resolved = true;
|
|
3189
3412
|
abortController.abort();
|
|
3190
|
-
if (
|
|
3191
|
-
resolveViaDaemon(
|
|
3192
|
-
|
|
3193
|
-
|
|
3413
|
+
if (daemonEntryId && internalToken) {
|
|
3414
|
+
resolveViaDaemon(
|
|
3415
|
+
daemonEntryId,
|
|
3416
|
+
res.approved ? "allow" : "deny",
|
|
3417
|
+
internalToken,
|
|
3418
|
+
res.decisionSource
|
|
3419
|
+
).catch(() => null);
|
|
3194
3420
|
}
|
|
3195
3421
|
resolve(res);
|
|
3196
3422
|
}
|
|
@@ -3229,12 +3455,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3229
3455
|
args,
|
|
3230
3456
|
finalResult.approved ? "allow" : "deny",
|
|
3231
3457
|
finalResult.checkedBy || finalResult.blockedBy || "unknown",
|
|
3232
|
-
meta
|
|
3458
|
+
meta,
|
|
3459
|
+
hashAuditArgs
|
|
3233
3460
|
);
|
|
3234
3461
|
}
|
|
3235
3462
|
return finalResult;
|
|
3236
3463
|
}
|
|
3237
|
-
var ACTIVITY_SOCKET_PATH;
|
|
3464
|
+
var WRITE_TOOLS, ACTIVITY_SOCKET_PATH;
|
|
3238
3465
|
var init_orchestrator = __esm({
|
|
3239
3466
|
"src/auth/orchestrator.ts"() {
|
|
3240
3467
|
"use strict";
|
|
@@ -3247,6 +3474,17 @@ var init_orchestrator = __esm({
|
|
|
3247
3474
|
init_state();
|
|
3248
3475
|
init_daemon();
|
|
3249
3476
|
init_cloud();
|
|
3477
|
+
WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
3478
|
+
"write",
|
|
3479
|
+
"write_file",
|
|
3480
|
+
"create_file",
|
|
3481
|
+
"edit",
|
|
3482
|
+
"multiedit",
|
|
3483
|
+
"str_replace_based_edit_tool",
|
|
3484
|
+
"replace",
|
|
3485
|
+
"notebook_edit",
|
|
3486
|
+
"notebookedit"
|
|
3487
|
+
]);
|
|
3250
3488
|
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os10.tmpdir(), "node9-activity.sock");
|
|
3251
3489
|
}
|
|
3252
3490
|
});
|
|
@@ -3543,6 +3781,15 @@ var init_ui = __esm({
|
|
|
3543
3781
|
padding: 5px 10px;
|
|
3544
3782
|
margin-bottom: 14px;
|
|
3545
3783
|
}
|
|
3784
|
+
.insight-hint {
|
|
3785
|
+
font-size: 12px;
|
|
3786
|
+
color: #f0c040;
|
|
3787
|
+
background: rgba(240, 192, 64, 0.08);
|
|
3788
|
+
border: 1px solid rgba(240, 192, 64, 0.25);
|
|
3789
|
+
border-radius: 6px;
|
|
3790
|
+
padding: 6px 10px;
|
|
3791
|
+
margin-bottom: 12px;
|
|
3792
|
+
}
|
|
3546
3793
|
pre {
|
|
3547
3794
|
background: #0d1117;
|
|
3548
3795
|
padding: 14px 16px;
|
|
@@ -4015,6 +4262,78 @@ var init_ui = __esm({
|
|
|
4015
4262
|
color: var(--danger);
|
|
4016
4263
|
}
|
|
4017
4264
|
|
|
4265
|
+
/* \u2500\u2500 Suggestion cards \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\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
4266
|
+
.suggestion-card {
|
|
4267
|
+
background: rgba(82, 130, 255, 0.06);
|
|
4268
|
+
border: 1px solid rgba(82, 130, 255, 0.25);
|
|
4269
|
+
border-radius: 8px;
|
|
4270
|
+
padding: 10px 12px;
|
|
4271
|
+
margin-bottom: 8px;
|
|
4272
|
+
}
|
|
4273
|
+
.suggestion-card:last-child {
|
|
4274
|
+
margin-bottom: 0;
|
|
4275
|
+
}
|
|
4276
|
+
.suggestion-header {
|
|
4277
|
+
display: flex;
|
|
4278
|
+
align-items: center;
|
|
4279
|
+
gap: 8px;
|
|
4280
|
+
margin-bottom: 6px;
|
|
4281
|
+
}
|
|
4282
|
+
.suggestion-tool {
|
|
4283
|
+
font-family: 'Fira Code', monospace;
|
|
4284
|
+
font-size: 11px;
|
|
4285
|
+
color: var(--text-bright);
|
|
4286
|
+
flex: 1;
|
|
4287
|
+
word-break: break-all;
|
|
4288
|
+
}
|
|
4289
|
+
.suggestion-count {
|
|
4290
|
+
font-size: 10px;
|
|
4291
|
+
color: var(--muted);
|
|
4292
|
+
white-space: nowrap;
|
|
4293
|
+
}
|
|
4294
|
+
.suggestion-rule {
|
|
4295
|
+
font-family: 'Fira Code', monospace;
|
|
4296
|
+
font-size: 10px;
|
|
4297
|
+
color: #79c0ff;
|
|
4298
|
+
background: rgba(0, 0, 0, 0.25);
|
|
4299
|
+
border-radius: 4px;
|
|
4300
|
+
padding: 4px 8px;
|
|
4301
|
+
margin-bottom: 8px;
|
|
4302
|
+
word-break: break-all;
|
|
4303
|
+
white-space: pre-wrap;
|
|
4304
|
+
}
|
|
4305
|
+
.suggestion-actions {
|
|
4306
|
+
display: flex;
|
|
4307
|
+
gap: 6px;
|
|
4308
|
+
}
|
|
4309
|
+
.btn-apply {
|
|
4310
|
+
background: rgba(52, 125, 57, 0.2);
|
|
4311
|
+
border: 1px solid rgba(87, 171, 90, 0.4);
|
|
4312
|
+
color: #57ab5a;
|
|
4313
|
+
padding: 4px 10px;
|
|
4314
|
+
font-size: 11px;
|
|
4315
|
+
border-radius: 5px;
|
|
4316
|
+
font-family: inherit;
|
|
4317
|
+
cursor: pointer;
|
|
4318
|
+
}
|
|
4319
|
+
.btn-apply:hover {
|
|
4320
|
+
background: rgba(52, 125, 57, 0.35);
|
|
4321
|
+
}
|
|
4322
|
+
.btn-dismiss-suggestion {
|
|
4323
|
+
background: transparent;
|
|
4324
|
+
border: 1px solid var(--border);
|
|
4325
|
+
color: var(--muted);
|
|
4326
|
+
padding: 4px 10px;
|
|
4327
|
+
font-size: 11px;
|
|
4328
|
+
border-radius: 5px;
|
|
4329
|
+
font-family: inherit;
|
|
4330
|
+
cursor: pointer;
|
|
4331
|
+
}
|
|
4332
|
+
.btn-dismiss-suggestion:hover {
|
|
4333
|
+
border-color: var(--danger);
|
|
4334
|
+
color: var(--danger);
|
|
4335
|
+
}
|
|
4336
|
+
|
|
4018
4337
|
.modal-overlay {
|
|
4019
4338
|
display: none;
|
|
4020
4339
|
position: fixed;
|
|
@@ -4196,6 +4515,11 @@ var init_ui = __esm({
|
|
|
4196
4515
|
<div class="panel-title">\u{1F4CB} Persistent Decisions</div>
|
|
4197
4516
|
<div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
|
|
4198
4517
|
</div>
|
|
4518
|
+
|
|
4519
|
+
<div class="panel" id="suggestionsPanel" style="display: none">
|
|
4520
|
+
<div class="panel-title">\u{1F4A1} Smart Rule Suggestions</div>
|
|
4521
|
+
<div id="suggestionsList"></div>
|
|
4522
|
+
</div>
|
|
4199
4523
|
</div>
|
|
4200
4524
|
</div>
|
|
4201
4525
|
</div>
|
|
@@ -4345,12 +4669,15 @@ var init_ui = __esm({
|
|
|
4345
4669
|
const badgeClass = isEdit ? 'sniper-badge-edit' : 'sniper-badge-exec';
|
|
4346
4670
|
const badgeLabel = isEdit ? '\u{1F4DD} Code Edit' : '\u{1F6D1} Execution';
|
|
4347
4671
|
const tierLabel = \`Tier \${rm.tier} \xB7 \${esc(rm.blockedByLabel)}\`;
|
|
4672
|
+
const isTaint = rm.blockedByLabel?.includes('Taint');
|
|
4348
4673
|
const fileLine =
|
|
4349
|
-
|
|
4350
|
-
? \`<div class="sniper-
|
|
4351
|
-
:
|
|
4352
|
-
? \`<div class="sniper-
|
|
4353
|
-
:
|
|
4674
|
+
isTaint && rm.ruleName
|
|
4675
|
+
? \`<div class="sniper-match">\u26A0\uFE0F \${esc(rm.ruleName)}</div>\`
|
|
4676
|
+
: isEdit && rm.editFilePath
|
|
4677
|
+
? \`<div class="sniper-filepath">\u{1F4C2} \${esc(rm.editFilePath)}</div>\`
|
|
4678
|
+
: !isEdit && rm.matchedWord
|
|
4679
|
+
? \`<div class="sniper-match">Matched: <code>\${esc(rm.matchedWord)}</code>\${rm.matchedField ? \` in <code>\${esc(rm.matchedField)}</code>\` : ''}</div>\`
|
|
4680
|
+
: '';
|
|
4354
4681
|
const snippetHtml = rm.contextSnippet ? \`<pre>\${esc(rm.contextSnippet)}</pre>\` : '';
|
|
4355
4682
|
return \`
|
|
4356
4683
|
<div class="sniper-header">
|
|
@@ -4385,6 +4712,7 @@ var init_ui = __esm({
|
|
|
4385
4712
|
</div>
|
|
4386
4713
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
4387
4714
|
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
|
|
4715
|
+
\${req.allowCount >= 3 ? \`<div class="insight-hint">\u{1F4A1} Approved \${req.allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</div>\` : ''}
|
|
4388
4716
|
\${renderPayload(req)}
|
|
4389
4717
|
<div class="actions" id="act-\${req.id}">
|
|
4390
4718
|
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
|
|
@@ -4451,6 +4779,14 @@ var init_ui = __esm({
|
|
|
4451
4779
|
ev.addEventListener('shields-status', (e) => {
|
|
4452
4780
|
renderShields(JSON.parse(e.data).shields);
|
|
4453
4781
|
});
|
|
4782
|
+
ev.addEventListener('suggestion:new', (e) => {
|
|
4783
|
+
const s = JSON.parse(e.data);
|
|
4784
|
+
addSuggestionCard(s);
|
|
4785
|
+
});
|
|
4786
|
+
ev.addEventListener('suggestion:resolved', (e) => {
|
|
4787
|
+
const { id } = JSON.parse(e.data);
|
|
4788
|
+
removeSuggestionCard(id);
|
|
4789
|
+
});
|
|
4454
4790
|
|
|
4455
4791
|
// \u2500\u2500 Flight Recorder \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4456
4792
|
ev.addEventListener('activity', (e) => {
|
|
@@ -4700,6 +5036,74 @@ var init_ui = __esm({
|
|
|
4700
5036
|
.then((r) => r.json())
|
|
4701
5037
|
.then(renderDecisions)
|
|
4702
5038
|
.catch(() => {});
|
|
5039
|
+
|
|
5040
|
+
// \u2500\u2500 Smart Rule Suggestions \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\u2500\u2500\u2500
|
|
5041
|
+
function rulePreview(suggestion) {
|
|
5042
|
+
const r = suggestion.suggestedRule;
|
|
5043
|
+
if (r.type === 'ignoredTool') return \`ignoredTool: "\${r.toolName}"\`;
|
|
5044
|
+
const cond = r.rule.conditions?.[0];
|
|
5045
|
+
const condStr = cond ? \` where \${cond.field} \${cond.op} "\${cond.value}"\` : '';
|
|
5046
|
+
return \`allow \${r.rule.tool}\${condStr}\`;
|
|
5047
|
+
}
|
|
5048
|
+
|
|
5049
|
+
function addSuggestionCard(s) {
|
|
5050
|
+
const panel = document.getElementById('suggestionsPanel');
|
|
5051
|
+
const list = document.getElementById('suggestionsList');
|
|
5052
|
+
panel.style.display = '';
|
|
5053
|
+
|
|
5054
|
+
const card = document.createElement('div');
|
|
5055
|
+
card.className = 'suggestion-card';
|
|
5056
|
+
card.id = 'sg-' + s.id;
|
|
5057
|
+
card.innerHTML = \`
|
|
5058
|
+
<div class="suggestion-header">
|
|
5059
|
+
<span class="suggestion-tool">\${esc(s.toolName)}</span>
|
|
5060
|
+
<span class="suggestion-count">allowed \${s.allowCount}\xD7</span>
|
|
5061
|
+
</div>
|
|
5062
|
+
<div class="suggestion-rule">\${esc(rulePreview(s))}</div>
|
|
5063
|
+
<div class="suggestion-actions">
|
|
5064
|
+
<button class="btn-apply" onclick="applySuggestion('\${esc(s.id)}')">Apply rule</button>
|
|
5065
|
+
<button class="btn-dismiss-suggestion" onclick="dismissSuggestion('\${esc(s.id)}')">Dismiss</button>
|
|
5066
|
+
</div>
|
|
5067
|
+
\`;
|
|
5068
|
+
list.appendChild(card);
|
|
5069
|
+
}
|
|
5070
|
+
|
|
5071
|
+
function removeSuggestionCard(id) {
|
|
5072
|
+
document.getElementById('sg-' + id)?.remove();
|
|
5073
|
+
const list = document.getElementById('suggestionsList');
|
|
5074
|
+
if (!list.querySelector('.suggestion-card')) {
|
|
5075
|
+
document.getElementById('suggestionsPanel').style.display = 'none';
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
|
|
5079
|
+
function applySuggestion(id) {
|
|
5080
|
+
fetch('/suggestions/' + id + '/apply', {
|
|
5081
|
+
method: 'POST',
|
|
5082
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
5083
|
+
body: JSON.stringify({}),
|
|
5084
|
+
})
|
|
5085
|
+
.then((r) => {
|
|
5086
|
+
if (r.ok) removeSuggestionCard(id);
|
|
5087
|
+
})
|
|
5088
|
+
.catch(() => {});
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
function dismissSuggestion(id) {
|
|
5092
|
+
fetch('/suggestions/' + id + '/dismiss', {
|
|
5093
|
+
method: 'POST',
|
|
5094
|
+
headers: { 'X-Node9-Token': CSRF_TOKEN },
|
|
5095
|
+
})
|
|
5096
|
+
.then((r) => {
|
|
5097
|
+
if (r.ok) removeSuggestionCard(id);
|
|
5098
|
+
})
|
|
5099
|
+
.catch(() => {});
|
|
5100
|
+
}
|
|
5101
|
+
|
|
5102
|
+
// Load any suggestions that survived a page reload (daemon still running)
|
|
5103
|
+
fetch('/suggestions')
|
|
5104
|
+
.then((r) => r.json())
|
|
5105
|
+
.then((list) => list.filter((s) => s.status === 'pending').forEach(addSuggestionCard))
|
|
5106
|
+
.catch(() => {});
|
|
4703
5107
|
</script>
|
|
4704
5108
|
</body>
|
|
4705
5109
|
</html>
|
|
@@ -4717,13 +5121,203 @@ var init_ui2 = __esm({
|
|
|
4717
5121
|
}
|
|
4718
5122
|
});
|
|
4719
5123
|
|
|
4720
|
-
// src/daemon/
|
|
4721
|
-
import
|
|
5124
|
+
// src/daemon/suggestion-tracker.ts
|
|
5125
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
5126
|
+
function extractPath(args) {
|
|
5127
|
+
if (!args || typeof args !== "object") return null;
|
|
5128
|
+
const a = args;
|
|
5129
|
+
for (const key of ["path", "file_path", "filename", "filepath", "dest", "destination"]) {
|
|
5130
|
+
if (typeof a[key] === "string" && a[key]) return a[key];
|
|
5131
|
+
}
|
|
5132
|
+
return null;
|
|
5133
|
+
}
|
|
5134
|
+
function commonPathPrefix(paths) {
|
|
5135
|
+
if (paths.length < 2) return null;
|
|
5136
|
+
const dirParts = paths.map((p) => {
|
|
5137
|
+
const lastSlash = p.lastIndexOf("/");
|
|
5138
|
+
return lastSlash > 0 ? p.slice(0, lastSlash + 1) : "/";
|
|
5139
|
+
});
|
|
5140
|
+
const first = dirParts[0].split("/");
|
|
5141
|
+
const common = [];
|
|
5142
|
+
for (let i = 0; i < first.length; i++) {
|
|
5143
|
+
if (dirParts.every((d) => d.split("/")[i] === first[i])) {
|
|
5144
|
+
common.push(first[i]);
|
|
5145
|
+
} else {
|
|
5146
|
+
break;
|
|
5147
|
+
}
|
|
5148
|
+
}
|
|
5149
|
+
const prefix = common.join("/").replace(/\/?$/, "/");
|
|
5150
|
+
return prefix.length > 1 ? prefix : null;
|
|
5151
|
+
}
|
|
5152
|
+
var SuggestionTracker;
|
|
5153
|
+
var init_suggestion_tracker = __esm({
|
|
5154
|
+
"src/daemon/suggestion-tracker.ts"() {
|
|
5155
|
+
"use strict";
|
|
5156
|
+
SuggestionTracker = class {
|
|
5157
|
+
events = /* @__PURE__ */ new Map();
|
|
5158
|
+
threshold;
|
|
5159
|
+
constructor(threshold = 3) {
|
|
5160
|
+
this.threshold = threshold;
|
|
5161
|
+
}
|
|
5162
|
+
/**
|
|
5163
|
+
* Record a human-allowed review for a tool.
|
|
5164
|
+
* Returns a Suggestion when the threshold is reached, null otherwise.
|
|
5165
|
+
*/
|
|
5166
|
+
recordAllow(toolName, args) {
|
|
5167
|
+
const events = this.events.get(toolName) ?? [];
|
|
5168
|
+
events.push({ args, ts: Date.now() });
|
|
5169
|
+
this.events.set(toolName, events);
|
|
5170
|
+
if (events.length >= this.threshold) {
|
|
5171
|
+
this.events.delete(toolName);
|
|
5172
|
+
return this.generateSuggestion(toolName, events);
|
|
5173
|
+
}
|
|
5174
|
+
return null;
|
|
5175
|
+
}
|
|
5176
|
+
/**
|
|
5177
|
+
* Reset the counter for a tool (e.g. when the user clicks Deny —
|
|
5178
|
+
* don't suggest allowing something they just blocked).
|
|
5179
|
+
*/
|
|
5180
|
+
resetTool(toolName) {
|
|
5181
|
+
this.events.delete(toolName);
|
|
5182
|
+
}
|
|
5183
|
+
/** Current allow count for a tool (for tests). */
|
|
5184
|
+
getCount(toolName) {
|
|
5185
|
+
return this.events.get(toolName)?.length ?? 0;
|
|
5186
|
+
}
|
|
5187
|
+
generateSuggestion(toolName, events) {
|
|
5188
|
+
const paths = events.map((e) => extractPath(e.args)).filter((p) => typeof p === "string" && p.length > 0);
|
|
5189
|
+
const prefix = commonPathPrefix(paths);
|
|
5190
|
+
const suggestedRule = prefix ? {
|
|
5191
|
+
type: "smartRule",
|
|
5192
|
+
rule: {
|
|
5193
|
+
name: `allow-${toolName}-${prefix.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}`,
|
|
5194
|
+
tool: toolName,
|
|
5195
|
+
conditions: [{ field: "path", op: "matchesGlob", value: `${prefix}**` }],
|
|
5196
|
+
verdict: "allow",
|
|
5197
|
+
reason: `Auto-suggested: ${toolName} allowed ${events.length}\xD7 in ${prefix}`
|
|
5198
|
+
}
|
|
5199
|
+
} : { type: "ignoredTool", toolName };
|
|
5200
|
+
return {
|
|
5201
|
+
id: randomUUID2(),
|
|
5202
|
+
toolName,
|
|
5203
|
+
allowCount: events.length,
|
|
5204
|
+
suggestedRule,
|
|
5205
|
+
status: "pending",
|
|
5206
|
+
createdAt: Date.now(),
|
|
5207
|
+
exampleArgs: events.slice(0, 3).map((e) => e.args)
|
|
5208
|
+
};
|
|
5209
|
+
}
|
|
5210
|
+
};
|
|
5211
|
+
}
|
|
5212
|
+
});
|
|
5213
|
+
|
|
5214
|
+
// src/daemon/taint-store.ts
|
|
4722
5215
|
import fs12 from "fs";
|
|
4723
5216
|
import path15 from "path";
|
|
5217
|
+
var DEFAULT_TTL_MS, TaintStore;
|
|
5218
|
+
var init_taint_store = __esm({
|
|
5219
|
+
"src/daemon/taint-store.ts"() {
|
|
5220
|
+
"use strict";
|
|
5221
|
+
DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
5222
|
+
TaintStore = class {
|
|
5223
|
+
records = /* @__PURE__ */ new Map();
|
|
5224
|
+
/** Add or refresh taint on an absolute path. */
|
|
5225
|
+
taint(filePath, source, ttlMs = DEFAULT_TTL_MS) {
|
|
5226
|
+
const resolved = this._resolve(filePath);
|
|
5227
|
+
const now = Date.now();
|
|
5228
|
+
this.records.set(resolved, {
|
|
5229
|
+
path: resolved,
|
|
5230
|
+
source,
|
|
5231
|
+
createdAt: now,
|
|
5232
|
+
expiresAt: now + ttlMs
|
|
5233
|
+
});
|
|
5234
|
+
}
|
|
5235
|
+
/**
|
|
5236
|
+
* Check whether a path is currently tainted.
|
|
5237
|
+
* Returns the TaintRecord if tainted (and not expired), null otherwise.
|
|
5238
|
+
* Expired records are pruned on access.
|
|
5239
|
+
*/
|
|
5240
|
+
check(filePath) {
|
|
5241
|
+
const resolved = this._resolve(filePath);
|
|
5242
|
+
const record = this.records.get(resolved);
|
|
5243
|
+
if (!record) return null;
|
|
5244
|
+
if (Date.now() > record.expiresAt) {
|
|
5245
|
+
this.records.delete(resolved);
|
|
5246
|
+
return null;
|
|
5247
|
+
}
|
|
5248
|
+
return record;
|
|
5249
|
+
}
|
|
5250
|
+
/**
|
|
5251
|
+
* Propagate taint from sourcePath to destPath (e.g. cp, mv).
|
|
5252
|
+
* For mv semantics (clearSource=true) the source taint is removed.
|
|
5253
|
+
*/
|
|
5254
|
+
propagate(sourcePath, destPath, clearSource = false) {
|
|
5255
|
+
const taintRecord = this.check(sourcePath);
|
|
5256
|
+
if (!taintRecord) return;
|
|
5257
|
+
const remainingMs = taintRecord.expiresAt - Date.now();
|
|
5258
|
+
if (remainingMs > 0) {
|
|
5259
|
+
const baseSource = taintRecord.source.replace(/^(propagated:)+/, "");
|
|
5260
|
+
this.taint(destPath, `propagated:${baseSource}`, remainingMs);
|
|
5261
|
+
}
|
|
5262
|
+
if (clearSource) {
|
|
5263
|
+
this.records.delete(this._resolve(sourcePath));
|
|
5264
|
+
}
|
|
5265
|
+
}
|
|
5266
|
+
/** Remove all expired records. Called periodically by the daemon. */
|
|
5267
|
+
prune() {
|
|
5268
|
+
const now = Date.now();
|
|
5269
|
+
for (const [key, record] of this.records) {
|
|
5270
|
+
if (now > record.expiresAt) this.records.delete(key);
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
/** Return all non-expired taint records (for audit/debug). */
|
|
5274
|
+
list() {
|
|
5275
|
+
this.prune();
|
|
5276
|
+
return [...this.records.values()];
|
|
5277
|
+
}
|
|
5278
|
+
/** Remove all taint records atomically. Used by tests to reset state between runs. */
|
|
5279
|
+
clear() {
|
|
5280
|
+
this.records.clear();
|
|
5281
|
+
}
|
|
5282
|
+
/** Resolve to absolute path, falling back to path.resolve if file doesn't exist yet. */
|
|
5283
|
+
_resolve(filePath) {
|
|
5284
|
+
try {
|
|
5285
|
+
return fs12.realpathSync.native(path15.resolve(filePath));
|
|
5286
|
+
} catch {
|
|
5287
|
+
return path15.resolve(filePath);
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5290
|
+
};
|
|
5291
|
+
}
|
|
5292
|
+
});
|
|
5293
|
+
|
|
5294
|
+
// src/daemon/state.ts
|
|
5295
|
+
import net2 from "net";
|
|
5296
|
+
import fs13 from "fs";
|
|
5297
|
+
import path16 from "path";
|
|
4724
5298
|
import os12 from "os";
|
|
4725
5299
|
import { spawn as spawn2 } from "child_process";
|
|
4726
|
-
import { randomUUID as
|
|
5300
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
5301
|
+
function loadInsightCounts() {
|
|
5302
|
+
try {
|
|
5303
|
+
if (!fs13.existsSync(INSIGHT_COUNTS_FILE)) return;
|
|
5304
|
+
const data = JSON.parse(fs13.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
|
|
5305
|
+
for (const [tool, count] of Object.entries(data)) {
|
|
5306
|
+
if (typeof count === "number" && count > 0) insightCounts.set(tool, count);
|
|
5307
|
+
}
|
|
5308
|
+
} catch {
|
|
5309
|
+
}
|
|
5310
|
+
}
|
|
5311
|
+
function saveInsightCounts() {
|
|
5312
|
+
try {
|
|
5313
|
+
const data = {};
|
|
5314
|
+
insightCounts.forEach((count, tool) => {
|
|
5315
|
+
data[tool] = count;
|
|
5316
|
+
});
|
|
5317
|
+
atomicWriteSync2(INSIGHT_COUNTS_FILE, JSON.stringify(data, null, 2), { mode: 384 });
|
|
5318
|
+
} catch {
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
4727
5321
|
function getAbandonTimer() {
|
|
4728
5322
|
return _abandonTimer;
|
|
4729
5323
|
}
|
|
@@ -4746,11 +5340,27 @@ function markRejectionHandlerRegistered() {
|
|
|
4746
5340
|
daemonRejectionHandlerRegistered = true;
|
|
4747
5341
|
}
|
|
4748
5342
|
function atomicWriteSync2(filePath, data, options) {
|
|
4749
|
-
const dir =
|
|
4750
|
-
if (!
|
|
4751
|
-
const tmpPath = `${filePath}.${
|
|
4752
|
-
|
|
4753
|
-
|
|
5343
|
+
const dir = path16.dirname(filePath);
|
|
5344
|
+
if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
|
|
5345
|
+
const tmpPath = `${filePath}.${randomUUID3()}.tmp`;
|
|
5346
|
+
try {
|
|
5347
|
+
fs13.writeFileSync(tmpPath, data, options);
|
|
5348
|
+
} catch (err) {
|
|
5349
|
+
try {
|
|
5350
|
+
fs13.unlinkSync(tmpPath);
|
|
5351
|
+
} catch {
|
|
5352
|
+
}
|
|
5353
|
+
throw err;
|
|
5354
|
+
}
|
|
5355
|
+
try {
|
|
5356
|
+
fs13.renameSync(tmpPath, filePath);
|
|
5357
|
+
} catch (err) {
|
|
5358
|
+
try {
|
|
5359
|
+
fs13.unlinkSync(tmpPath);
|
|
5360
|
+
} catch {
|
|
5361
|
+
}
|
|
5362
|
+
throw err;
|
|
5363
|
+
}
|
|
4754
5364
|
}
|
|
4755
5365
|
function redactArgs(value) {
|
|
4756
5366
|
if (!value || typeof value !== "object") return value;
|
|
@@ -4770,16 +5380,16 @@ function appendAuditLog(data) {
|
|
|
4770
5380
|
decision: data.decision,
|
|
4771
5381
|
source: "daemon"
|
|
4772
5382
|
};
|
|
4773
|
-
const dir =
|
|
4774
|
-
if (!
|
|
4775
|
-
|
|
5383
|
+
const dir = path16.dirname(AUDIT_LOG_FILE);
|
|
5384
|
+
if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
|
|
5385
|
+
fs13.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
4776
5386
|
} catch {
|
|
4777
5387
|
}
|
|
4778
5388
|
}
|
|
4779
5389
|
function getAuditHistory(limit = 20) {
|
|
4780
5390
|
try {
|
|
4781
|
-
if (!
|
|
4782
|
-
const lines =
|
|
5391
|
+
if (!fs13.existsSync(AUDIT_LOG_FILE)) return [];
|
|
5392
|
+
const lines = fs13.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
|
|
4783
5393
|
if (lines.length === 1 && lines[0] === "") return [];
|
|
4784
5394
|
return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
|
|
4785
5395
|
} catch {
|
|
@@ -4788,19 +5398,19 @@ function getAuditHistory(limit = 20) {
|
|
|
4788
5398
|
}
|
|
4789
5399
|
function getOrgName() {
|
|
4790
5400
|
try {
|
|
4791
|
-
if (
|
|
5401
|
+
if (fs13.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
|
|
4792
5402
|
} catch {
|
|
4793
5403
|
}
|
|
4794
5404
|
return null;
|
|
4795
5405
|
}
|
|
4796
5406
|
function hasStoredSlackKey() {
|
|
4797
|
-
return
|
|
5407
|
+
return fs13.existsSync(CREDENTIALS_FILE);
|
|
4798
5408
|
}
|
|
4799
5409
|
function writeGlobalSetting(key, value) {
|
|
4800
5410
|
let config = {};
|
|
4801
5411
|
try {
|
|
4802
|
-
if (
|
|
4803
|
-
config = JSON.parse(
|
|
5412
|
+
if (fs13.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
5413
|
+
config = JSON.parse(fs13.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
|
|
4804
5414
|
}
|
|
4805
5415
|
} catch {
|
|
4806
5416
|
}
|
|
@@ -4812,8 +5422,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
4812
5422
|
try {
|
|
4813
5423
|
let trust = { entries: [] };
|
|
4814
5424
|
try {
|
|
4815
|
-
if (
|
|
4816
|
-
trust = JSON.parse(
|
|
5425
|
+
if (fs13.existsSync(TRUST_FILE2))
|
|
5426
|
+
trust = JSON.parse(fs13.readFileSync(TRUST_FILE2, "utf-8"));
|
|
4817
5427
|
} catch {
|
|
4818
5428
|
}
|
|
4819
5429
|
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
|
|
@@ -4824,8 +5434,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
4824
5434
|
}
|
|
4825
5435
|
function readPersistentDecisions() {
|
|
4826
5436
|
try {
|
|
4827
|
-
if (
|
|
4828
|
-
return JSON.parse(
|
|
5437
|
+
if (fs13.existsSync(DECISIONS_FILE)) {
|
|
5438
|
+
return JSON.parse(fs13.readFileSync(DECISIONS_FILE, "utf-8"));
|
|
4829
5439
|
}
|
|
4830
5440
|
} catch {
|
|
4831
5441
|
}
|
|
@@ -4890,7 +5500,7 @@ function abandonPending() {
|
|
|
4890
5500
|
});
|
|
4891
5501
|
if (autoStarted) {
|
|
4892
5502
|
try {
|
|
4893
|
-
|
|
5503
|
+
fs13.unlinkSync(DAEMON_PID_FILE);
|
|
4894
5504
|
} catch {
|
|
4895
5505
|
}
|
|
4896
5506
|
setTimeout(() => {
|
|
@@ -4901,7 +5511,7 @@ function abandonPending() {
|
|
|
4901
5511
|
}
|
|
4902
5512
|
function startActivitySocket() {
|
|
4903
5513
|
try {
|
|
4904
|
-
|
|
5514
|
+
fs13.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4905
5515
|
} catch {
|
|
4906
5516
|
}
|
|
4907
5517
|
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
@@ -4943,25 +5553,32 @@ function startActivitySocket() {
|
|
|
4943
5553
|
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
4944
5554
|
process.on("exit", () => {
|
|
4945
5555
|
try {
|
|
4946
|
-
|
|
5556
|
+
fs13.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4947
5557
|
} catch {
|
|
4948
5558
|
}
|
|
4949
5559
|
});
|
|
4950
5560
|
}
|
|
4951
|
-
var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, pending, sseClients, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
|
|
5561
|
+
var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
|
|
4952
5562
|
var init_state2 = __esm({
|
|
4953
5563
|
"src/daemon/state.ts"() {
|
|
4954
5564
|
"use strict";
|
|
4955
5565
|
init_daemon();
|
|
5566
|
+
init_suggestion_tracker();
|
|
5567
|
+
init_taint_store();
|
|
4956
5568
|
homeDir = os12.homedir();
|
|
4957
|
-
DAEMON_PID_FILE =
|
|
4958
|
-
DECISIONS_FILE =
|
|
4959
|
-
AUDIT_LOG_FILE =
|
|
4960
|
-
TRUST_FILE2 =
|
|
4961
|
-
GLOBAL_CONFIG_FILE =
|
|
4962
|
-
CREDENTIALS_FILE =
|
|
5569
|
+
DAEMON_PID_FILE = path16.join(homeDir, ".node9", "daemon.pid");
|
|
5570
|
+
DECISIONS_FILE = path16.join(homeDir, ".node9", "decisions.json");
|
|
5571
|
+
AUDIT_LOG_FILE = path16.join(homeDir, ".node9", "audit.log");
|
|
5572
|
+
TRUST_FILE2 = path16.join(homeDir, ".node9", "trust.json");
|
|
5573
|
+
GLOBAL_CONFIG_FILE = path16.join(homeDir, ".node9", "config.json");
|
|
5574
|
+
CREDENTIALS_FILE = path16.join(homeDir, ".node9", "credentials.json");
|
|
5575
|
+
INSIGHT_COUNTS_FILE = path16.join(homeDir, ".node9", "insight-counts.json");
|
|
4963
5576
|
pending = /* @__PURE__ */ new Map();
|
|
4964
5577
|
sseClients = /* @__PURE__ */ new Set();
|
|
5578
|
+
suggestionTracker = new SuggestionTracker(3);
|
|
5579
|
+
suggestions = /* @__PURE__ */ new Map();
|
|
5580
|
+
taintStore = new TaintStore();
|
|
5581
|
+
insightCounts = /* @__PURE__ */ new Map();
|
|
4965
5582
|
_abandonTimer = null;
|
|
4966
5583
|
_hadBrowserClient = false;
|
|
4967
5584
|
_daemonServer = null;
|
|
@@ -4973,24 +5590,82 @@ var init_state2 = __esm({
|
|
|
4973
5590
|
"2h": 2 * 60 * 6e4
|
|
4974
5591
|
};
|
|
4975
5592
|
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
4976
|
-
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
5593
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path16.join(os12.tmpdir(), "node9-activity.sock");
|
|
4977
5594
|
ACTIVITY_RING_SIZE = 100;
|
|
4978
5595
|
activityRing = [];
|
|
4979
5596
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
4980
5597
|
}
|
|
4981
5598
|
});
|
|
4982
5599
|
|
|
4983
|
-
// src/
|
|
4984
|
-
import
|
|
4985
|
-
import
|
|
4986
|
-
import
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
5600
|
+
// src/config/patch.ts
|
|
5601
|
+
import fs14 from "fs";
|
|
5602
|
+
import path17 from "path";
|
|
5603
|
+
import os13 from "os";
|
|
5604
|
+
function patchConfig(configPath, patch) {
|
|
5605
|
+
let config = {};
|
|
5606
|
+
try {
|
|
5607
|
+
if (fs14.existsSync(configPath)) {
|
|
5608
|
+
config = JSON.parse(fs14.readFileSync(configPath, "utf8"));
|
|
5609
|
+
}
|
|
5610
|
+
} catch {
|
|
5611
|
+
throw new Error(`Cannot read config at ${configPath} \u2014 file may be corrupted`);
|
|
5612
|
+
}
|
|
5613
|
+
if (!config.policy || typeof config.policy !== "object") config.policy = {};
|
|
5614
|
+
const policy = config.policy;
|
|
5615
|
+
if (patch.type === "smartRule") {
|
|
5616
|
+
if (!Array.isArray(policy.smartRules)) policy.smartRules = [];
|
|
5617
|
+
const rules = policy.smartRules;
|
|
5618
|
+
if (patch.rule.name && rules.some((r) => r.name === patch.rule.name)) return;
|
|
5619
|
+
rules.push(patch.rule);
|
|
5620
|
+
} else {
|
|
5621
|
+
if (!Array.isArray(policy.ignoredTools)) policy.ignoredTools = [];
|
|
5622
|
+
const ignored = policy.ignoredTools;
|
|
5623
|
+
if (!ignored.includes(patch.toolName)) {
|
|
5624
|
+
ignored.push(patch.toolName);
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
const dir = path17.dirname(configPath);
|
|
5628
|
+
fs14.mkdirSync(dir, { recursive: true });
|
|
5629
|
+
const tmp = configPath + ".node9-tmp";
|
|
5630
|
+
try {
|
|
5631
|
+
fs14.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
|
|
5632
|
+
} catch (err) {
|
|
5633
|
+
try {
|
|
5634
|
+
fs14.unlinkSync(tmp);
|
|
5635
|
+
} catch {
|
|
5636
|
+
}
|
|
5637
|
+
throw err;
|
|
5638
|
+
}
|
|
5639
|
+
try {
|
|
5640
|
+
fs14.renameSync(tmp, configPath);
|
|
5641
|
+
} catch (err) {
|
|
5642
|
+
try {
|
|
5643
|
+
fs14.unlinkSync(tmp);
|
|
5644
|
+
} catch {
|
|
5645
|
+
}
|
|
5646
|
+
throw err;
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
5649
|
+
var GLOBAL_CONFIG_PATH;
|
|
5650
|
+
var init_patch = __esm({
|
|
5651
|
+
"src/config/patch.ts"() {
|
|
5652
|
+
"use strict";
|
|
5653
|
+
GLOBAL_CONFIG_PATH = path17.join(os13.homedir(), ".node9", "config.json");
|
|
5654
|
+
}
|
|
5655
|
+
});
|
|
5656
|
+
|
|
5657
|
+
// src/daemon/server.ts
|
|
5658
|
+
import http from "http";
|
|
5659
|
+
import fs15 from "fs";
|
|
5660
|
+
import path18 from "path";
|
|
5661
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
5662
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
5663
|
+
import chalk2 from "chalk";
|
|
5664
|
+
function startDaemon() {
|
|
5665
|
+
loadInsightCounts();
|
|
5666
|
+
const csrfToken = randomUUID4();
|
|
5667
|
+
const internalToken = randomUUID4();
|
|
5668
|
+
const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
|
|
4994
5669
|
const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
|
|
4995
5670
|
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
4996
5671
|
const watchMode = process.env.NODE9_WATCH_MODE === "1";
|
|
@@ -5002,7 +5677,7 @@ function startDaemon() {
|
|
|
5002
5677
|
idleTimer = setTimeout(() => {
|
|
5003
5678
|
if (autoStarted) {
|
|
5004
5679
|
try {
|
|
5005
|
-
|
|
5680
|
+
fs15.unlinkSync(DAEMON_PID_FILE);
|
|
5006
5681
|
} catch {
|
|
5007
5682
|
}
|
|
5008
5683
|
}
|
|
@@ -5011,8 +5686,14 @@ function startDaemon() {
|
|
|
5011
5686
|
idleTimer.unref();
|
|
5012
5687
|
}
|
|
5013
5688
|
resetIdleTimer();
|
|
5689
|
+
const allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${DAEMON_PORT}`, `localhost:${DAEMON_PORT}`]);
|
|
5014
5690
|
const server = http.createServer(async (req, res) => {
|
|
5015
|
-
const
|
|
5691
|
+
const host = req.headers.host ?? "";
|
|
5692
|
+
if (!allowedHosts.has(host)) {
|
|
5693
|
+
res.writeHead(421, { "Content-Type": "text/plain" });
|
|
5694
|
+
return res.end("Misdirected Request");
|
|
5695
|
+
}
|
|
5696
|
+
const reqUrl = new URL(req.url || "/", `http://${host}`);
|
|
5016
5697
|
const { pathname } = reqUrl;
|
|
5017
5698
|
if (req.method === "GET" && pathname === "/") {
|
|
5018
5699
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
@@ -5045,7 +5726,8 @@ data: ${JSON.stringify({
|
|
|
5045
5726
|
slackDelegated: e.slackDelegated,
|
|
5046
5727
|
timestamp: e.timestamp,
|
|
5047
5728
|
agent: e.agent,
|
|
5048
|
-
mcpServer: e.mcpServer
|
|
5729
|
+
mcpServer: e.mcpServer,
|
|
5730
|
+
allowCount: (insightCounts.get(e.toolName) ?? 0) + 1
|
|
5049
5731
|
})),
|
|
5050
5732
|
orgName: getOrgName(),
|
|
5051
5733
|
autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
|
|
@@ -5087,6 +5769,12 @@ data: ${JSON.stringify(item.data)}
|
|
|
5087
5769
|
}
|
|
5088
5770
|
});
|
|
5089
5771
|
}
|
|
5772
|
+
if (req.method === "POST" && pathname === "/browser-opened") {
|
|
5773
|
+
if (req.headers["x-node9-internal"] !== internalToken) return res.writeHead(403).end();
|
|
5774
|
+
browserOpened = true;
|
|
5775
|
+
res.writeHead(200).end();
|
|
5776
|
+
return;
|
|
5777
|
+
}
|
|
5090
5778
|
if (req.method === "POST" && pathname === "/check") {
|
|
5091
5779
|
try {
|
|
5092
5780
|
resetIdleTimer();
|
|
@@ -5104,7 +5792,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5104
5792
|
activityId,
|
|
5105
5793
|
cwd
|
|
5106
5794
|
} = JSON.parse(body);
|
|
5107
|
-
const id = fromCLI && typeof activityId === "string" && activityId ||
|
|
5795
|
+
const id = fromCLI && typeof activityId === "string" && activityId || randomUUID4();
|
|
5108
5796
|
const entry = {
|
|
5109
5797
|
id,
|
|
5110
5798
|
toolName,
|
|
@@ -5130,7 +5818,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5130
5818
|
e.earlyReason = "No response \u2014 auto-denied after timeout";
|
|
5131
5819
|
}
|
|
5132
5820
|
pending.delete(id);
|
|
5133
|
-
broadcast("remove", { id });
|
|
5821
|
+
broadcast("remove", { id, decision: "deny" });
|
|
5134
5822
|
}
|
|
5135
5823
|
}, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
|
|
5136
5824
|
};
|
|
@@ -5144,7 +5832,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5144
5832
|
status: "pending"
|
|
5145
5833
|
});
|
|
5146
5834
|
}
|
|
5147
|
-
const projectCwd = typeof cwd === "string" &&
|
|
5835
|
+
const projectCwd = typeof cwd === "string" && path18.isAbsolute(cwd) ? cwd : void 0;
|
|
5148
5836
|
const projectConfig = getConfig(projectCwd);
|
|
5149
5837
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
5150
5838
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -5157,7 +5845,10 @@ data: ${JSON.stringify(item.data)}
|
|
|
5157
5845
|
slackDelegated: entry.slackDelegated,
|
|
5158
5846
|
agent: entry.agent,
|
|
5159
5847
|
mcpServer: entry.mcpServer,
|
|
5160
|
-
interactive: terminalEnabled
|
|
5848
|
+
interactive: terminalEnabled,
|
|
5849
|
+
// allowCount = what this count will be if the user allows.
|
|
5850
|
+
// Terminal uses this to show the 💡 insight line on the Nth consecutive approval.
|
|
5851
|
+
allowCount: (insightCounts.get(toolName) ?? 0) + 1
|
|
5161
5852
|
});
|
|
5162
5853
|
const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
|
|
5163
5854
|
if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
|
|
@@ -5166,7 +5857,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5166
5857
|
}
|
|
5167
5858
|
}
|
|
5168
5859
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5169
|
-
res.end(JSON.stringify({ id }));
|
|
5860
|
+
res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
|
|
5170
5861
|
if (slackDelegated) return;
|
|
5171
5862
|
authorizeHeadless(
|
|
5172
5863
|
toolName,
|
|
@@ -5193,7 +5884,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5193
5884
|
if (e.waiter) {
|
|
5194
5885
|
e.waiter(decision, result.reason);
|
|
5195
5886
|
pending.delete(id);
|
|
5196
|
-
broadcast("remove", { id });
|
|
5887
|
+
broadcast("remove", { id, decision });
|
|
5197
5888
|
} else {
|
|
5198
5889
|
e.earlyDecision = decision;
|
|
5199
5890
|
e.earlyReason = result.reason;
|
|
@@ -5209,7 +5900,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5209
5900
|
e.earlyReason = reason;
|
|
5210
5901
|
}
|
|
5211
5902
|
pending.delete(id);
|
|
5212
|
-
broadcast("remove", { id });
|
|
5903
|
+
broadcast("remove", { id, decision: "deny" });
|
|
5213
5904
|
});
|
|
5214
5905
|
return;
|
|
5215
5906
|
} catch {
|
|
@@ -5240,12 +5931,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
5240
5931
|
res.end(JSON.stringify(body));
|
|
5241
5932
|
};
|
|
5242
5933
|
req.on("close", () => {
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5934
|
+
setTimeout(() => {
|
|
5935
|
+
const e = pending.get(id);
|
|
5936
|
+
if (e && e.waiter && e.earlyDecision === null) {
|
|
5937
|
+
clearTimeout(e.timer);
|
|
5938
|
+
pending.delete(id);
|
|
5939
|
+
broadcast("remove", { id });
|
|
5940
|
+
}
|
|
5941
|
+
}, 200);
|
|
5249
5942
|
});
|
|
5250
5943
|
return;
|
|
5251
5944
|
}
|
|
@@ -5274,10 +5967,10 @@ data: ${JSON.stringify(item.data)}
|
|
|
5274
5967
|
if (entry.waiter) {
|
|
5275
5968
|
entry.waiter("allow");
|
|
5276
5969
|
pending.delete(id);
|
|
5277
|
-
broadcast("remove", { id });
|
|
5970
|
+
broadcast("remove", { id, decision: "allow" });
|
|
5278
5971
|
} else {
|
|
5279
5972
|
entry.earlyDecision = "allow";
|
|
5280
|
-
broadcast("remove", { id });
|
|
5973
|
+
broadcast("remove", { id, decision: "allow" });
|
|
5281
5974
|
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
5282
5975
|
}
|
|
5283
5976
|
res.writeHead(200);
|
|
@@ -5291,16 +5984,29 @@ data: ${JSON.stringify(item.data)}
|
|
|
5291
5984
|
decision: resolvedDecision
|
|
5292
5985
|
});
|
|
5293
5986
|
clearTimeout(entry.timer);
|
|
5987
|
+
if (resolvedDecision === "allow" && !persist) {
|
|
5988
|
+
insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
|
|
5989
|
+
saveInsightCounts();
|
|
5990
|
+
const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
|
|
5991
|
+
if (suggestion) {
|
|
5992
|
+
suggestions.set(suggestion.id, suggestion);
|
|
5993
|
+
broadcast("suggestion:new", suggestion);
|
|
5994
|
+
}
|
|
5995
|
+
} else if (resolvedDecision === "deny") {
|
|
5996
|
+
insightCounts.delete(entry.toolName);
|
|
5997
|
+
saveInsightCounts();
|
|
5998
|
+
suggestionTracker.resetTool(entry.toolName);
|
|
5999
|
+
}
|
|
5294
6000
|
const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
|
|
5295
6001
|
if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
|
|
5296
6002
|
if (entry.waiter) {
|
|
5297
6003
|
entry.waiter(resolvedDecision, reason);
|
|
5298
6004
|
pending.delete(id);
|
|
5299
|
-
broadcast("remove", { id });
|
|
6005
|
+
broadcast("remove", { id, decision: resolvedDecision });
|
|
5300
6006
|
} else {
|
|
5301
6007
|
entry.earlyDecision = resolvedDecision;
|
|
5302
6008
|
entry.earlyReason = reason;
|
|
5303
|
-
broadcast("remove", { id });
|
|
6009
|
+
broadcast("remove", { id, decision: resolvedDecision });
|
|
5304
6010
|
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
5305
6011
|
}
|
|
5306
6012
|
res.writeHead(200);
|
|
@@ -5388,13 +6094,38 @@ data: ${JSON.stringify(item.data)}
|
|
|
5388
6094
|
const id = pathname.split("/").pop();
|
|
5389
6095
|
const entry = pending.get(id);
|
|
5390
6096
|
if (!entry) return res.writeHead(404).end();
|
|
5391
|
-
const { decision } = JSON.parse(await readBody(req));
|
|
5392
|
-
|
|
6097
|
+
const { decision, source } = JSON.parse(await readBody(req));
|
|
6098
|
+
const resolvedResolveDecision = decision === "allow" ? "allow" : "deny";
|
|
6099
|
+
appendAuditLog({
|
|
6100
|
+
toolName: entry.toolName,
|
|
6101
|
+
args: entry.args,
|
|
6102
|
+
decision: resolvedResolveDecision
|
|
6103
|
+
});
|
|
5393
6104
|
clearTimeout(entry.timer);
|
|
5394
|
-
if (
|
|
5395
|
-
|
|
6105
|
+
if (resolvedResolveDecision === "allow") {
|
|
6106
|
+
insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
|
|
6107
|
+
saveInsightCounts();
|
|
6108
|
+
} else {
|
|
6109
|
+
insightCounts.delete(entry.toolName);
|
|
6110
|
+
saveInsightCounts();
|
|
6111
|
+
}
|
|
6112
|
+
if (!entry.slackDelegated) {
|
|
6113
|
+
if (resolvedResolveDecision === "allow") {
|
|
6114
|
+
const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
|
|
6115
|
+
if (suggestion) {
|
|
6116
|
+
suggestions.set(suggestion.id, suggestion);
|
|
6117
|
+
broadcast("suggestion:new", suggestion);
|
|
6118
|
+
}
|
|
6119
|
+
} else {
|
|
6120
|
+
suggestionTracker.resetTool(entry.toolName);
|
|
6121
|
+
}
|
|
6122
|
+
}
|
|
6123
|
+
const VALID_RESOLVE_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
|
|
6124
|
+
if (source && VALID_RESOLVE_SOURCES.has(source)) entry.decisionSource = source;
|
|
6125
|
+
if (entry.waiter) entry.waiter(resolvedResolveDecision);
|
|
6126
|
+
else entry.earlyDecision = resolvedResolveDecision;
|
|
5396
6127
|
pending.delete(id);
|
|
5397
|
-
broadcast("remove", { id });
|
|
6128
|
+
broadcast("remove", { id, decision: resolvedResolveDecision });
|
|
5398
6129
|
res.writeHead(200);
|
|
5399
6130
|
return res.end(JSON.stringify({ ok: true }));
|
|
5400
6131
|
} catch {
|
|
@@ -5442,20 +6173,136 @@ data: ${JSON.stringify(item.data)}
|
|
|
5442
6173
|
res.writeHead(400).end();
|
|
5443
6174
|
}
|
|
5444
6175
|
}
|
|
6176
|
+
if (req.method === "GET" && pathname === "/suggestions") {
|
|
6177
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6178
|
+
return res.end(JSON.stringify([...suggestions.values()]));
|
|
6179
|
+
}
|
|
6180
|
+
if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/apply")) {
|
|
6181
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
6182
|
+
try {
|
|
6183
|
+
const body = await readBody(req);
|
|
6184
|
+
const data = body ? JSON.parse(body) : {};
|
|
6185
|
+
const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
|
|
6186
|
+
const node9Dir = path18.dirname(GLOBAL_CONFIG_PATH);
|
|
6187
|
+
if (!path18.resolve(configPath).startsWith(node9Dir + path18.sep)) {
|
|
6188
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6189
|
+
return res.end(
|
|
6190
|
+
JSON.stringify({ error: "configPath must be within the node9 config directory" })
|
|
6191
|
+
);
|
|
6192
|
+
}
|
|
6193
|
+
const id = pathname.split("/")[2];
|
|
6194
|
+
const suggestion = suggestions.get(id);
|
|
6195
|
+
if (!suggestion) return res.writeHead(404).end();
|
|
6196
|
+
let patch;
|
|
6197
|
+
if (data.rule !== void 0) {
|
|
6198
|
+
const parsed = SmartRuleSchema.safeParse(data.rule);
|
|
6199
|
+
if (!parsed.success) {
|
|
6200
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6201
|
+
return res.end(JSON.stringify({ error: parsed.error.message }));
|
|
6202
|
+
}
|
|
6203
|
+
patch = { type: "smartRule", rule: parsed.data };
|
|
6204
|
+
} else {
|
|
6205
|
+
patch = suggestion.suggestedRule;
|
|
6206
|
+
}
|
|
6207
|
+
patchConfig(configPath, patch);
|
|
6208
|
+
_resetConfigCache();
|
|
6209
|
+
insightCounts.delete(suggestion.toolName);
|
|
6210
|
+
saveInsightCounts();
|
|
6211
|
+
suggestion.status = "applied";
|
|
6212
|
+
broadcast("suggestion:resolved", { id, status: "applied" });
|
|
6213
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6214
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
6215
|
+
} catch (err) {
|
|
6216
|
+
console.error(chalk2.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err);
|
|
6217
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6218
|
+
return res.end(JSON.stringify({ error: String(err) }));
|
|
6219
|
+
}
|
|
6220
|
+
}
|
|
6221
|
+
if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/dismiss")) {
|
|
6222
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
6223
|
+
try {
|
|
6224
|
+
const id = pathname.split("/")[2];
|
|
6225
|
+
const suggestion = suggestions.get(id);
|
|
6226
|
+
if (!suggestion) return res.writeHead(404).end();
|
|
6227
|
+
suggestion.status = "dismissed";
|
|
6228
|
+
broadcast("suggestion:resolved", { id, status: "dismissed" });
|
|
6229
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6230
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
6231
|
+
} catch {
|
|
6232
|
+
res.writeHead(400).end();
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6235
|
+
if (req.method === "POST" && pathname === "/taint") {
|
|
6236
|
+
try {
|
|
6237
|
+
const body = JSON.parse(await readBody(req));
|
|
6238
|
+
if (typeof body.path !== "string" || typeof body.source !== "string") {
|
|
6239
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6240
|
+
return res.end(JSON.stringify({ error: "path and source are required strings" }));
|
|
6241
|
+
}
|
|
6242
|
+
const ttlMs = typeof body.ttlMs === "number" ? body.ttlMs : void 0;
|
|
6243
|
+
taintStore.taint(body.path, body.source, ttlMs);
|
|
6244
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6245
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
6246
|
+
} catch {
|
|
6247
|
+
res.writeHead(400).end();
|
|
6248
|
+
return;
|
|
6249
|
+
}
|
|
6250
|
+
}
|
|
6251
|
+
if (req.method === "POST" && pathname === "/taint/check") {
|
|
6252
|
+
try {
|
|
6253
|
+
const body = JSON.parse(await readBody(req));
|
|
6254
|
+
if (!Array.isArray(body.paths)) {
|
|
6255
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6256
|
+
return res.end(JSON.stringify({ error: "paths must be an array" }));
|
|
6257
|
+
}
|
|
6258
|
+
if (body.paths.some((p) => typeof p !== "string")) {
|
|
6259
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6260
|
+
return res.end(JSON.stringify({ error: "all paths must be strings" }));
|
|
6261
|
+
}
|
|
6262
|
+
for (const p of body.paths) {
|
|
6263
|
+
const record = taintStore.check(p);
|
|
6264
|
+
if (record) {
|
|
6265
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6266
|
+
return res.end(JSON.stringify({ tainted: true, record }));
|
|
6267
|
+
}
|
|
6268
|
+
}
|
|
6269
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6270
|
+
return res.end(JSON.stringify({ tainted: false }));
|
|
6271
|
+
} catch {
|
|
6272
|
+
res.writeHead(400).end();
|
|
6273
|
+
return;
|
|
6274
|
+
}
|
|
6275
|
+
}
|
|
6276
|
+
if (req.method === "POST" && pathname === "/taint/propagate") {
|
|
6277
|
+
try {
|
|
6278
|
+
const body = JSON.parse(await readBody(req));
|
|
6279
|
+
if (typeof body.src !== "string" || typeof body.dest !== "string") {
|
|
6280
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6281
|
+
return res.end(JSON.stringify({ error: "src and dest are required strings" }));
|
|
6282
|
+
}
|
|
6283
|
+
const clearSource = body.clearSource === true;
|
|
6284
|
+
taintStore.propagate(body.src, body.dest, clearSource);
|
|
6285
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6286
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
6287
|
+
} catch {
|
|
6288
|
+
res.writeHead(400).end();
|
|
6289
|
+
return;
|
|
6290
|
+
}
|
|
6291
|
+
}
|
|
5445
6292
|
res.writeHead(404).end();
|
|
5446
6293
|
});
|
|
5447
6294
|
setDaemonServer(server);
|
|
5448
6295
|
server.on("error", (e) => {
|
|
5449
6296
|
if (e.code === "EADDRINUSE") {
|
|
5450
6297
|
try {
|
|
5451
|
-
if (
|
|
5452
|
-
const { pid } = JSON.parse(
|
|
6298
|
+
if (fs15.existsSync(DAEMON_PID_FILE)) {
|
|
6299
|
+
const { pid } = JSON.parse(fs15.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5453
6300
|
process.kill(pid, 0);
|
|
5454
6301
|
return process.exit(0);
|
|
5455
6302
|
}
|
|
5456
6303
|
} catch {
|
|
5457
6304
|
try {
|
|
5458
|
-
|
|
6305
|
+
fs15.unlinkSync(DAEMON_PID_FILE);
|
|
5459
6306
|
} catch {
|
|
5460
6307
|
}
|
|
5461
6308
|
server.listen(DAEMON_PORT, DAEMON_HOST);
|
|
@@ -5521,32 +6368,34 @@ var init_server = __esm({
|
|
|
5521
6368
|
init_shields();
|
|
5522
6369
|
init_ui2();
|
|
5523
6370
|
init_state2();
|
|
6371
|
+
init_patch();
|
|
6372
|
+
init_config_schema();
|
|
5524
6373
|
}
|
|
5525
6374
|
});
|
|
5526
6375
|
|
|
5527
6376
|
// src/daemon/index.ts
|
|
5528
|
-
import
|
|
6377
|
+
import fs16 from "fs";
|
|
5529
6378
|
import chalk3 from "chalk";
|
|
5530
6379
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
5531
6380
|
function stopDaemon() {
|
|
5532
|
-
if (!
|
|
6381
|
+
if (!fs16.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
|
|
5533
6382
|
try {
|
|
5534
|
-
const { pid } = JSON.parse(
|
|
6383
|
+
const { pid } = JSON.parse(fs16.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5535
6384
|
process.kill(pid, "SIGTERM");
|
|
5536
6385
|
console.log(chalk3.green("\u2705 Stopped."));
|
|
5537
6386
|
} catch {
|
|
5538
6387
|
console.log(chalk3.gray("Cleaned up stale PID file."));
|
|
5539
6388
|
} finally {
|
|
5540
6389
|
try {
|
|
5541
|
-
|
|
6390
|
+
fs16.unlinkSync(DAEMON_PID_FILE);
|
|
5542
6391
|
} catch {
|
|
5543
6392
|
}
|
|
5544
6393
|
}
|
|
5545
6394
|
}
|
|
5546
6395
|
function daemonStatus() {
|
|
5547
|
-
if (
|
|
6396
|
+
if (fs16.existsSync(DAEMON_PID_FILE)) {
|
|
5548
6397
|
try {
|
|
5549
|
-
const { pid } = JSON.parse(
|
|
6398
|
+
const { pid } = JSON.parse(fs16.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5550
6399
|
process.kill(pid, 0);
|
|
5551
6400
|
console.log(chalk3.green("Node9 daemon: running"));
|
|
5552
6401
|
return;
|
|
@@ -5580,10 +6429,10 @@ __export(tail_exports, {
|
|
|
5580
6429
|
startTail: () => startTail
|
|
5581
6430
|
});
|
|
5582
6431
|
import http2 from "http";
|
|
5583
|
-
import
|
|
5584
|
-
import
|
|
5585
|
-
import
|
|
5586
|
-
import
|
|
6432
|
+
import chalk16 from "chalk";
|
|
6433
|
+
import fs24 from "fs";
|
|
6434
|
+
import os21 from "os";
|
|
6435
|
+
import path26 from "path";
|
|
5587
6436
|
import readline3 from "readline";
|
|
5588
6437
|
import { spawn as spawn9, execSync as execSync3 } from "child_process";
|
|
5589
6438
|
function getIcon(tool) {
|
|
@@ -5599,17 +6448,17 @@ function formatBase(activity) {
|
|
|
5599
6448
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
5600
6449
|
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
5601
6450
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
5602
|
-
return `${
|
|
6451
|
+
return `${chalk16.gray(time)} ${icon} ${chalk16.white.bold(toolName)} ${chalk16.dim(argsPreview)}`;
|
|
5603
6452
|
}
|
|
5604
6453
|
function renderResult(activity, result) {
|
|
5605
6454
|
const base = formatBase(activity);
|
|
5606
6455
|
let status;
|
|
5607
6456
|
if (result.status === "allow") {
|
|
5608
|
-
status =
|
|
6457
|
+
status = chalk16.green("\u2713 ALLOW");
|
|
5609
6458
|
} else if (result.status === "dlp") {
|
|
5610
|
-
status =
|
|
6459
|
+
status = chalk16.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
5611
6460
|
} else {
|
|
5612
|
-
status =
|
|
6461
|
+
status = chalk16.red("\u2717 BLOCK");
|
|
5613
6462
|
}
|
|
5614
6463
|
if (process.stdout.isTTY) {
|
|
5615
6464
|
readline3.clearLine(process.stdout, 0);
|
|
@@ -5619,16 +6468,16 @@ function renderResult(activity, result) {
|
|
|
5619
6468
|
}
|
|
5620
6469
|
function renderPending(activity) {
|
|
5621
6470
|
if (!process.stdout.isTTY) return;
|
|
5622
|
-
process.stdout.write(`${formatBase(activity)} ${
|
|
6471
|
+
process.stdout.write(`${formatBase(activity)} ${chalk16.yellow("\u25CF \u2026")}\r`);
|
|
5623
6472
|
}
|
|
5624
6473
|
async function ensureDaemon() {
|
|
5625
6474
|
let pidPort = null;
|
|
5626
|
-
if (
|
|
6475
|
+
if (fs24.existsSync(PID_FILE)) {
|
|
5627
6476
|
try {
|
|
5628
|
-
const { port } = JSON.parse(
|
|
6477
|
+
const { port } = JSON.parse(fs24.readFileSync(PID_FILE, "utf-8"));
|
|
5629
6478
|
pidPort = port;
|
|
5630
6479
|
} catch {
|
|
5631
|
-
console.error(
|
|
6480
|
+
console.error(chalk16.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
5632
6481
|
}
|
|
5633
6482
|
}
|
|
5634
6483
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -5639,7 +6488,7 @@ async function ensureDaemon() {
|
|
|
5639
6488
|
if (res.ok) return checkPort;
|
|
5640
6489
|
} catch {
|
|
5641
6490
|
}
|
|
5642
|
-
console.log(
|
|
6491
|
+
console.log(chalk16.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
5643
6492
|
const child = spawn9(process.execPath, [process.argv[1], "daemon"], {
|
|
5644
6493
|
detached: true,
|
|
5645
6494
|
stdio: "ignore",
|
|
@@ -5656,12 +6505,15 @@ async function ensureDaemon() {
|
|
|
5656
6505
|
} catch {
|
|
5657
6506
|
}
|
|
5658
6507
|
}
|
|
5659
|
-
console.error(
|
|
6508
|
+
console.error(chalk16.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
5660
6509
|
process.exit(1);
|
|
5661
6510
|
}
|
|
5662
|
-
function postDecisionHttp(id, decision, csrfToken, port) {
|
|
6511
|
+
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
5663
6512
|
return new Promise((resolve, reject) => {
|
|
5664
|
-
const
|
|
6513
|
+
const bodyObj = { decision, source: "terminal" };
|
|
6514
|
+
if (opts?.persist) bodyObj.persist = true;
|
|
6515
|
+
if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
|
|
6516
|
+
const body = JSON.stringify(bodyObj);
|
|
5665
6517
|
const req = http2.request(
|
|
5666
6518
|
{
|
|
5667
6519
|
hostname: "127.0.0.1",
|
|
@@ -5684,22 +6536,33 @@ function postDecisionHttp(id, decision, csrfToken, port) {
|
|
|
5684
6536
|
req.end(body);
|
|
5685
6537
|
});
|
|
5686
6538
|
}
|
|
5687
|
-
function buildCardLines(req) {
|
|
6539
|
+
function buildCardLines(req, localCount = 0) {
|
|
5688
6540
|
const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
|
|
5689
6541
|
const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
|
|
5690
6542
|
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`;
|
|
5691
6543
|
const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
|
|
5692
|
-
|
|
6544
|
+
const lines = [
|
|
5693
6545
|
``,
|
|
5694
6546
|
`${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
|
|
5695
6547
|
`${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
|
|
5696
|
-
`${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}
|
|
5697
|
-
|
|
6548
|
+
`${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`
|
|
6549
|
+
];
|
|
6550
|
+
if (req.riskMetadata?.ruleName && blockedBy.includes("Taint")) {
|
|
6551
|
+
lines.push(`${CYAN}\u2551${RESET} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET}`);
|
|
6552
|
+
}
|
|
6553
|
+
lines.push(`${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`);
|
|
6554
|
+
if (localCount >= 2) {
|
|
6555
|
+
lines.push(
|
|
6556
|
+
`${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
|
|
6557
|
+
);
|
|
6558
|
+
}
|
|
6559
|
+
lines.push(
|
|
5698
6560
|
`${CYAN}\u255A${RESET}`,
|
|
5699
6561
|
``,
|
|
5700
|
-
` ${BOLD}${GREEN}[
|
|
6562
|
+
` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${YELLOW}[a]${RESET} Always Allow ${BOLD}${CYAN}[t]${RESET} Trust 30m`,
|
|
5701
6563
|
``
|
|
5702
|
-
|
|
6564
|
+
);
|
|
6565
|
+
return lines;
|
|
5703
6566
|
}
|
|
5704
6567
|
async function startTail(options = {}) {
|
|
5705
6568
|
const port = await ensureDaemon();
|
|
@@ -5727,7 +6590,7 @@ async function startTail(options = {}) {
|
|
|
5727
6590
|
req2.end();
|
|
5728
6591
|
});
|
|
5729
6592
|
if (result.ok) {
|
|
5730
|
-
console.log(
|
|
6593
|
+
console.log(chalk16.green("\u2713 Flight Recorder buffer cleared."));
|
|
5731
6594
|
} else if (result.code === "ECONNREFUSED") {
|
|
5732
6595
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
5733
6596
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -5744,17 +6607,22 @@ async function startTail(options = {}) {
|
|
|
5744
6607
|
let cardActive = false;
|
|
5745
6608
|
let cardLineCount = 0;
|
|
5746
6609
|
let cancelActiveCard = null;
|
|
6610
|
+
const localAllowCounts = /* @__PURE__ */ new Map();
|
|
5747
6611
|
const canApprove = process.stdout.isTTY && process.stdin.isTTY;
|
|
5748
6612
|
if (canApprove) readline3.emitKeypressEvents(process.stdin);
|
|
5749
6613
|
function clearCard() {
|
|
5750
6614
|
if (cardLineCount > 0) {
|
|
5751
|
-
process.stdout
|
|
6615
|
+
readline3.moveCursor(process.stdout, 0, -cardLineCount);
|
|
6616
|
+
process.stdout.write(ERASE_DOWN);
|
|
5752
6617
|
cardLineCount = 0;
|
|
5753
6618
|
}
|
|
5754
6619
|
}
|
|
5755
6620
|
function printCard(req2) {
|
|
5756
|
-
process.stdout.write(HIDE_CURSOR
|
|
5757
|
-
const
|
|
6621
|
+
process.stdout.write(HIDE_CURSOR);
|
|
6622
|
+
const daemonPrior = req2.allowCount !== void 0 ? req2.allowCount - 1 : 0;
|
|
6623
|
+
const localPrior = localAllowCounts.get(req2.toolName) ?? 0;
|
|
6624
|
+
const priorCount = Math.max(daemonPrior, localPrior);
|
|
6625
|
+
const lines = buildCardLines(req2, priorCount);
|
|
5758
6626
|
for (const line of lines) process.stdout.write(line + "\n");
|
|
5759
6627
|
cardLineCount = lines.length;
|
|
5760
6628
|
}
|
|
@@ -5782,34 +6650,70 @@ async function startTail(options = {}) {
|
|
|
5782
6650
|
process.stdin.pause();
|
|
5783
6651
|
cancelActiveCard = null;
|
|
5784
6652
|
};
|
|
5785
|
-
const settle = (
|
|
6653
|
+
const settle = (action) => {
|
|
5786
6654
|
if (settled) return;
|
|
5787
6655
|
settled = true;
|
|
5788
6656
|
cleanup();
|
|
5789
6657
|
clearCard();
|
|
6658
|
+
const stampedLines = buildCardLines(
|
|
6659
|
+
req2,
|
|
6660
|
+
Math.max(
|
|
6661
|
+
req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
|
|
6662
|
+
localAllowCounts.get(req2.toolName) ?? 0
|
|
6663
|
+
)
|
|
6664
|
+
);
|
|
6665
|
+
const decisionStamp = action === "always-allow" ? chalk16.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk16.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk16.green("\u2713 ALLOWED") : chalk16.red("\u2717 DENIED");
|
|
6666
|
+
stampedLines.push(` ${BOLD}\u2192${RESET} ${decisionStamp} ${GRAY}(terminal)${RESET}`, ``);
|
|
6667
|
+
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
5790
6668
|
process.stdout.write(SHOW_CURSOR);
|
|
5791
|
-
|
|
6669
|
+
cardLineCount = 0;
|
|
6670
|
+
if (action === "allow" || action === "always-allow" || action === "trust") {
|
|
6671
|
+
localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
|
|
6672
|
+
} else if (action === "deny") {
|
|
6673
|
+
localAllowCounts.delete(req2.toolName);
|
|
6674
|
+
}
|
|
6675
|
+
let httpDecision;
|
|
6676
|
+
let httpOpts;
|
|
6677
|
+
if (action === "always-allow") {
|
|
6678
|
+
httpDecision = "allow";
|
|
6679
|
+
httpOpts = { persist: true };
|
|
6680
|
+
} else if (action === "trust") {
|
|
6681
|
+
httpDecision = "trust";
|
|
6682
|
+
httpOpts = { trustDuration: "30m" };
|
|
6683
|
+
} else {
|
|
6684
|
+
httpDecision = action;
|
|
6685
|
+
}
|
|
6686
|
+
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
|
|
5792
6687
|
try {
|
|
5793
|
-
|
|
5794
|
-
|
|
6688
|
+
fs24.appendFileSync(
|
|
6689
|
+
path26.join(os21.homedir(), ".node9", "hook-debug.log"),
|
|
5795
6690
|
`[tail] POST /decision failed: ${String(err)}
|
|
5796
6691
|
`
|
|
5797
6692
|
);
|
|
5798
6693
|
} catch {
|
|
5799
6694
|
}
|
|
5800
6695
|
});
|
|
5801
|
-
const decisionLabel = decision === "allow" ? chalk15.green("\u2713 ALLOWED (terminal)") : chalk15.red("\u2717 DENIED (terminal)");
|
|
5802
|
-
console.log(`${chalk15.cyan("\u25C6")} ${chalk15.bold(req2.toolName.padEnd(16))} ${decisionLabel}`);
|
|
5803
6696
|
approvalQueue.shift();
|
|
5804
6697
|
cardActive = false;
|
|
5805
6698
|
showNextCard();
|
|
5806
6699
|
};
|
|
5807
|
-
cancelActiveCard = () => {
|
|
6700
|
+
cancelActiveCard = (externalDecision) => {
|
|
5808
6701
|
if (settled) return;
|
|
5809
6702
|
settled = true;
|
|
5810
6703
|
cleanup();
|
|
5811
6704
|
clearCard();
|
|
6705
|
+
const priorCount = Math.max(
|
|
6706
|
+
req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
|
|
6707
|
+
localAllowCounts.get(req2.toolName) ?? 0
|
|
6708
|
+
);
|
|
6709
|
+
const stampedLines = buildCardLines(req2, priorCount);
|
|
6710
|
+
if (externalDecision) {
|
|
6711
|
+
const source = externalDecision === "allow" ? chalk16.green("\u2713 ALLOWED") : chalk16.red("\u2717 DENIED");
|
|
6712
|
+
stampedLines.push(` ${BOLD}\u2192${RESET} ${source} ${GRAY}(external)${RESET}`, ``);
|
|
6713
|
+
}
|
|
6714
|
+
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
5812
6715
|
process.stdout.write(SHOW_CURSOR);
|
|
6716
|
+
cardLineCount = 0;
|
|
5813
6717
|
approvalQueue.shift();
|
|
5814
6718
|
cardActive = false;
|
|
5815
6719
|
showNextCard();
|
|
@@ -5817,10 +6721,14 @@ async function startTail(options = {}) {
|
|
|
5817
6721
|
process.stdin.resume();
|
|
5818
6722
|
onKeypress = (_str, key) => {
|
|
5819
6723
|
const name = key?.name ?? "";
|
|
5820
|
-
if (name === "
|
|
6724
|
+
if (name === "y" || name === "return") {
|
|
5821
6725
|
settle("allow");
|
|
5822
|
-
} else if (name === "
|
|
6726
|
+
} else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
|
|
5823
6727
|
settle("deny");
|
|
6728
|
+
} else if (name === "a") {
|
|
6729
|
+
settle("always-allow");
|
|
6730
|
+
} else if (name === "t") {
|
|
6731
|
+
settle("trust");
|
|
5824
6732
|
}
|
|
5825
6733
|
};
|
|
5826
6734
|
process.stdin.on("keypress", onKeypress);
|
|
@@ -5833,19 +6741,27 @@ async function startTail(options = {}) {
|
|
|
5833
6741
|
else if (process.platform === "win32")
|
|
5834
6742
|
execSync3(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
|
|
5835
6743
|
else execSync3(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
|
|
6744
|
+
const intToken = getInternalToken();
|
|
6745
|
+
fetch(`http://127.0.0.1:${port}/browser-opened`, {
|
|
6746
|
+
method: "POST",
|
|
6747
|
+
headers: intToken ? { "X-Node9-Internal": intToken } : {}
|
|
6748
|
+
}).catch(() => {
|
|
6749
|
+
});
|
|
5836
6750
|
}
|
|
5837
6751
|
} catch {
|
|
5838
6752
|
}
|
|
5839
|
-
console.log(
|
|
5840
|
-
\u{1F6F0}\uFE0F Node9 tail `) +
|
|
6753
|
+
console.log(chalk16.cyan.bold(`
|
|
6754
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk16.dim(`\u2192 ${dashboardUrl}`));
|
|
5841
6755
|
if (canApprove) {
|
|
5842
|
-
console.log(
|
|
6756
|
+
console.log(
|
|
6757
|
+
chalk16.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
|
|
6758
|
+
);
|
|
5843
6759
|
}
|
|
5844
6760
|
if (options.history) {
|
|
5845
|
-
console.log(
|
|
6761
|
+
console.log(chalk16.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
5846
6762
|
} else {
|
|
5847
6763
|
console.log(
|
|
5848
|
-
|
|
6764
|
+
chalk16.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
5849
6765
|
);
|
|
5850
6766
|
}
|
|
5851
6767
|
process.on("SIGINT", () => {
|
|
@@ -5855,13 +6771,13 @@ async function startTail(options = {}) {
|
|
|
5855
6771
|
readline3.clearLine(process.stdout, 0);
|
|
5856
6772
|
readline3.cursorTo(process.stdout, 0);
|
|
5857
6773
|
}
|
|
5858
|
-
console.log(
|
|
6774
|
+
console.log(chalk16.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
5859
6775
|
process.exit(0);
|
|
5860
6776
|
});
|
|
5861
6777
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
5862
6778
|
const req = http2.get(sseUrl, (res) => {
|
|
5863
6779
|
if (res.statusCode !== 200) {
|
|
5864
|
-
console.error(
|
|
6780
|
+
console.error(chalk16.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
5865
6781
|
process.exit(1);
|
|
5866
6782
|
}
|
|
5867
6783
|
let currentEvent = "";
|
|
@@ -5891,7 +6807,7 @@ async function startTail(options = {}) {
|
|
|
5891
6807
|
readline3.clearLine(process.stdout, 0);
|
|
5892
6808
|
readline3.cursorTo(process.stdout, 0);
|
|
5893
6809
|
}
|
|
5894
|
-
console.log(
|
|
6810
|
+
console.log(chalk16.red("\n\u274C Daemon disconnected."));
|
|
5895
6811
|
process.exit(1);
|
|
5896
6812
|
});
|
|
5897
6813
|
});
|
|
@@ -5932,11 +6848,17 @@ async function startTail(options = {}) {
|
|
|
5932
6848
|
}
|
|
5933
6849
|
if (event === "remove") {
|
|
5934
6850
|
try {
|
|
5935
|
-
const { id } = JSON.parse(rawData);
|
|
6851
|
+
const { id, decision } = JSON.parse(rawData);
|
|
5936
6852
|
const idx = approvalQueue.findIndex((r) => r.id === id);
|
|
5937
6853
|
if (idx !== -1) {
|
|
5938
6854
|
if (idx === 0 && cardActive && cancelActiveCard) {
|
|
5939
|
-
|
|
6855
|
+
const toolName = approvalQueue[0].toolName;
|
|
6856
|
+
if (decision === "allow") {
|
|
6857
|
+
localAllowCounts.set(toolName, (localAllowCounts.get(toolName) ?? 0) + 1);
|
|
6858
|
+
} else if (decision === "deny") {
|
|
6859
|
+
localAllowCounts.delete(toolName);
|
|
6860
|
+
}
|
|
6861
|
+
cancelActiveCard(decision);
|
|
5940
6862
|
} else {
|
|
5941
6863
|
approvalQueue.splice(idx, 1);
|
|
5942
6864
|
}
|
|
@@ -5971,18 +6893,19 @@ async function startTail(options = {}) {
|
|
|
5971
6893
|
}
|
|
5972
6894
|
req.on("error", (err) => {
|
|
5973
6895
|
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
5974
|
-
console.error(
|
|
6896
|
+
console.error(chalk16.red(`
|
|
5975
6897
|
\u274C ${msg}`));
|
|
5976
6898
|
process.exit(1);
|
|
5977
6899
|
});
|
|
5978
6900
|
}
|
|
5979
|
-
var PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN
|
|
6901
|
+
var PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN;
|
|
5980
6902
|
var init_tail = __esm({
|
|
5981
6903
|
"src/tui/tail.ts"() {
|
|
5982
6904
|
"use strict";
|
|
5983
6905
|
init_daemon2();
|
|
6906
|
+
init_daemon();
|
|
5984
6907
|
init_core();
|
|
5985
|
-
PID_FILE =
|
|
6908
|
+
PID_FILE = path26.join(os21.homedir(), ".node9", "daemon.pid");
|
|
5986
6909
|
ICONS = {
|
|
5987
6910
|
bash: "\u{1F4BB}",
|
|
5988
6911
|
shell: "\u{1F4BB}",
|
|
@@ -6010,8 +6933,6 @@ var init_tail = __esm({
|
|
|
6010
6933
|
HIDE_CURSOR = "\x1B[?25l";
|
|
6011
6934
|
SHOW_CURSOR = "\x1B[?25h";
|
|
6012
6935
|
ERASE_DOWN = "\x1B[J";
|
|
6013
|
-
SAVE_CURSOR = "\x1B7";
|
|
6014
|
-
RESTORE_CURSOR = "\x1B8";
|
|
6015
6936
|
}
|
|
6016
6937
|
});
|
|
6017
6938
|
|
|
@@ -6330,6 +7251,25 @@ async function setupGemini() {
|
|
|
6330
7251
|
printDaemonTip();
|
|
6331
7252
|
}
|
|
6332
7253
|
}
|
|
7254
|
+
function detectAgents(homeDir2 = os11.homedir()) {
|
|
7255
|
+
const exists = (p) => {
|
|
7256
|
+
try {
|
|
7257
|
+
return fs11.existsSync(p);
|
|
7258
|
+
} catch (err) {
|
|
7259
|
+
const code = err.code;
|
|
7260
|
+
if (code !== "ENOENT") {
|
|
7261
|
+
process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err)}
|
|
7262
|
+
`);
|
|
7263
|
+
}
|
|
7264
|
+
return false;
|
|
7265
|
+
}
|
|
7266
|
+
};
|
|
7267
|
+
return {
|
|
7268
|
+
claude: exists(path14.join(homeDir2, ".claude")) || exists(path14.join(homeDir2, ".claude.json")),
|
|
7269
|
+
gemini: exists(path14.join(homeDir2, ".gemini")),
|
|
7270
|
+
cursor: exists(path14.join(homeDir2, ".cursor"))
|
|
7271
|
+
};
|
|
7272
|
+
}
|
|
6333
7273
|
async function setupCursor() {
|
|
6334
7274
|
const homeDir2 = os11.homedir();
|
|
6335
7275
|
const mcpPath = path14.join(homeDir2, ".cursor", "mcp.json");
|
|
@@ -6388,10 +7328,10 @@ async function setupCursor() {
|
|
|
6388
7328
|
|
|
6389
7329
|
// src/cli.ts
|
|
6390
7330
|
init_daemon2();
|
|
6391
|
-
import
|
|
6392
|
-
import
|
|
6393
|
-
import
|
|
6394
|
-
import
|
|
7331
|
+
import chalk17 from "chalk";
|
|
7332
|
+
import fs25 from "fs";
|
|
7333
|
+
import path27 from "path";
|
|
7334
|
+
import os22 from "os";
|
|
6395
7335
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
6396
7336
|
|
|
6397
7337
|
// src/utils/duration.ts
|
|
@@ -6616,32 +7556,32 @@ init_daemon();
|
|
|
6616
7556
|
init_config();
|
|
6617
7557
|
init_policy();
|
|
6618
7558
|
import chalk5 from "chalk";
|
|
6619
|
-
import
|
|
6620
|
-
import
|
|
6621
|
-
import
|
|
7559
|
+
import fs18 from "fs";
|
|
7560
|
+
import path20 from "path";
|
|
7561
|
+
import os15 from "os";
|
|
6622
7562
|
|
|
6623
7563
|
// src/undo.ts
|
|
6624
7564
|
import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
|
|
6625
7565
|
import crypto2 from "crypto";
|
|
6626
|
-
import
|
|
6627
|
-
import
|
|
6628
|
-
import
|
|
6629
|
-
var SNAPSHOT_STACK_PATH =
|
|
6630
|
-
var UNDO_LATEST_PATH =
|
|
7566
|
+
import fs17 from "fs";
|
|
7567
|
+
import path19 from "path";
|
|
7568
|
+
import os14 from "os";
|
|
7569
|
+
var SNAPSHOT_STACK_PATH = path19.join(os14.homedir(), ".node9", "snapshots.json");
|
|
7570
|
+
var UNDO_LATEST_PATH = path19.join(os14.homedir(), ".node9", "undo_latest.txt");
|
|
6631
7571
|
var MAX_SNAPSHOTS = 10;
|
|
6632
7572
|
var GIT_TIMEOUT = 15e3;
|
|
6633
7573
|
function readStack() {
|
|
6634
7574
|
try {
|
|
6635
|
-
if (
|
|
6636
|
-
return JSON.parse(
|
|
7575
|
+
if (fs17.existsSync(SNAPSHOT_STACK_PATH))
|
|
7576
|
+
return JSON.parse(fs17.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
6637
7577
|
} catch {
|
|
6638
7578
|
}
|
|
6639
7579
|
return [];
|
|
6640
7580
|
}
|
|
6641
7581
|
function writeStack(stack) {
|
|
6642
|
-
const dir =
|
|
6643
|
-
if (!
|
|
6644
|
-
|
|
7582
|
+
const dir = path19.dirname(SNAPSHOT_STACK_PATH);
|
|
7583
|
+
if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
|
|
7584
|
+
fs17.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
6645
7585
|
}
|
|
6646
7586
|
function buildArgsSummary(tool, args) {
|
|
6647
7587
|
if (!args || typeof args !== "object") return "";
|
|
@@ -6657,7 +7597,7 @@ function buildArgsSummary(tool, args) {
|
|
|
6657
7597
|
function normalizeCwdForHash(cwd) {
|
|
6658
7598
|
let normalized;
|
|
6659
7599
|
try {
|
|
6660
|
-
normalized =
|
|
7600
|
+
normalized = fs17.realpathSync(cwd);
|
|
6661
7601
|
} catch {
|
|
6662
7602
|
normalized = cwd;
|
|
6663
7603
|
}
|
|
@@ -6667,16 +7607,16 @@ function normalizeCwdForHash(cwd) {
|
|
|
6667
7607
|
}
|
|
6668
7608
|
function getShadowRepoDir(cwd) {
|
|
6669
7609
|
const hash = crypto2.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
6670
|
-
return
|
|
7610
|
+
return path19.join(os14.homedir(), ".node9", "snapshots", hash);
|
|
6671
7611
|
}
|
|
6672
7612
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
6673
7613
|
try {
|
|
6674
7614
|
const cutoff = Date.now() - 6e4;
|
|
6675
|
-
for (const f of
|
|
7615
|
+
for (const f of fs17.readdirSync(shadowDir)) {
|
|
6676
7616
|
if (f.startsWith("index_")) {
|
|
6677
|
-
const fp =
|
|
7617
|
+
const fp = path19.join(shadowDir, f);
|
|
6678
7618
|
try {
|
|
6679
|
-
if (
|
|
7619
|
+
if (fs17.statSync(fp).mtimeMs < cutoff) fs17.unlinkSync(fp);
|
|
6680
7620
|
} catch {
|
|
6681
7621
|
}
|
|
6682
7622
|
}
|
|
@@ -6688,7 +7628,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
6688
7628
|
const hardcoded = [".git", ".node9"];
|
|
6689
7629
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
6690
7630
|
try {
|
|
6691
|
-
|
|
7631
|
+
fs17.writeFileSync(path19.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
6692
7632
|
} catch {
|
|
6693
7633
|
}
|
|
6694
7634
|
}
|
|
@@ -6701,25 +7641,25 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6701
7641
|
timeout: 3e3
|
|
6702
7642
|
});
|
|
6703
7643
|
if (check.status === 0) {
|
|
6704
|
-
const ptPath =
|
|
7644
|
+
const ptPath = path19.join(shadowDir, "project-path.txt");
|
|
6705
7645
|
try {
|
|
6706
|
-
const stored =
|
|
7646
|
+
const stored = fs17.readFileSync(ptPath, "utf8").trim();
|
|
6707
7647
|
if (stored === normalizedCwd) return true;
|
|
6708
7648
|
if (process.env.NODE9_DEBUG === "1")
|
|
6709
7649
|
console.error(
|
|
6710
7650
|
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
6711
7651
|
);
|
|
6712
|
-
|
|
7652
|
+
fs17.rmSync(shadowDir, { recursive: true, force: true });
|
|
6713
7653
|
} catch {
|
|
6714
7654
|
try {
|
|
6715
|
-
|
|
7655
|
+
fs17.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
6716
7656
|
} catch {
|
|
6717
7657
|
}
|
|
6718
7658
|
return true;
|
|
6719
7659
|
}
|
|
6720
7660
|
}
|
|
6721
7661
|
try {
|
|
6722
|
-
|
|
7662
|
+
fs17.mkdirSync(shadowDir, { recursive: true });
|
|
6723
7663
|
} catch {
|
|
6724
7664
|
}
|
|
6725
7665
|
const init = spawnSync4("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
@@ -6728,7 +7668,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6728
7668
|
console.error("[Node9] git init --bare failed:", init.stderr?.toString());
|
|
6729
7669
|
return false;
|
|
6730
7670
|
}
|
|
6731
|
-
const configFile =
|
|
7671
|
+
const configFile = path19.join(shadowDir, "config");
|
|
6732
7672
|
spawnSync4("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
6733
7673
|
timeout: 3e3
|
|
6734
7674
|
});
|
|
@@ -6736,7 +7676,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6736
7676
|
timeout: 3e3
|
|
6737
7677
|
});
|
|
6738
7678
|
try {
|
|
6739
|
-
|
|
7679
|
+
fs17.writeFileSync(path19.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
6740
7680
|
} catch {
|
|
6741
7681
|
}
|
|
6742
7682
|
return true;
|
|
@@ -6759,7 +7699,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6759
7699
|
const shadowDir = getShadowRepoDir(cwd);
|
|
6760
7700
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
6761
7701
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
6762
|
-
indexFile =
|
|
7702
|
+
indexFile = path19.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
6763
7703
|
const shadowEnv = {
|
|
6764
7704
|
...process.env,
|
|
6765
7705
|
GIT_DIR: shadowDir,
|
|
@@ -6788,7 +7728,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6788
7728
|
const shouldGc = stack.length % 5 === 0;
|
|
6789
7729
|
if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
6790
7730
|
writeStack(stack);
|
|
6791
|
-
|
|
7731
|
+
fs17.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
6792
7732
|
if (shouldGc) {
|
|
6793
7733
|
spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
6794
7734
|
}
|
|
@@ -6799,7 +7739,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6799
7739
|
} finally {
|
|
6800
7740
|
if (indexFile) {
|
|
6801
7741
|
try {
|
|
6802
|
-
|
|
7742
|
+
fs17.unlinkSync(indexFile);
|
|
6803
7743
|
} catch {
|
|
6804
7744
|
}
|
|
6805
7745
|
}
|
|
@@ -6868,9 +7808,9 @@ function applyUndo(hash, cwd) {
|
|
|
6868
7808
|
timeout: GIT_TIMEOUT
|
|
6869
7809
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
6870
7810
|
for (const file of [...tracked, ...untracked]) {
|
|
6871
|
-
const fullPath =
|
|
6872
|
-
if (!snapshotFiles.has(file) &&
|
|
6873
|
-
|
|
7811
|
+
const fullPath = path19.join(dir, file);
|
|
7812
|
+
if (!snapshotFiles.has(file) && fs17.existsSync(fullPath)) {
|
|
7813
|
+
fs17.unlinkSync(fullPath);
|
|
6874
7814
|
}
|
|
6875
7815
|
}
|
|
6876
7816
|
return true;
|
|
@@ -6894,9 +7834,9 @@ function registerCheckCommand(program2) {
|
|
|
6894
7834
|
} catch (err) {
|
|
6895
7835
|
const tempConfig = getConfig();
|
|
6896
7836
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
6897
|
-
const logPath =
|
|
7837
|
+
const logPath = path20.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
6898
7838
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6899
|
-
|
|
7839
|
+
fs18.appendFileSync(
|
|
6900
7840
|
logPath,
|
|
6901
7841
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
6902
7842
|
RAW: ${raw}
|
|
@@ -6907,10 +7847,10 @@ RAW: ${raw}
|
|
|
6907
7847
|
}
|
|
6908
7848
|
const config = getConfig(payload.cwd || void 0);
|
|
6909
7849
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
6910
|
-
const logPath =
|
|
6911
|
-
if (!
|
|
6912
|
-
|
|
6913
|
-
|
|
7850
|
+
const logPath = path20.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
7851
|
+
if (!fs18.existsSync(path20.dirname(logPath)))
|
|
7852
|
+
fs18.mkdirSync(path20.dirname(logPath), { recursive: true });
|
|
7853
|
+
fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
6914
7854
|
`);
|
|
6915
7855
|
}
|
|
6916
7856
|
const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
|
|
@@ -6923,8 +7863,8 @@ RAW: ${raw}
|
|
|
6923
7863
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
6924
7864
|
let ttyFd = null;
|
|
6925
7865
|
try {
|
|
6926
|
-
ttyFd =
|
|
6927
|
-
const writeTty = (line) =>
|
|
7866
|
+
ttyFd = fs18.openSync("/dev/tty", "w");
|
|
7867
|
+
const writeTty = (line) => fs18.writeSync(ttyFd, line + "\n");
|
|
6928
7868
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
6929
7869
|
writeTty(chalk5.bgRed.white.bold(`
|
|
6930
7870
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
@@ -6940,7 +7880,7 @@ RAW: ${raw}
|
|
|
6940
7880
|
} finally {
|
|
6941
7881
|
if (ttyFd !== null)
|
|
6942
7882
|
try {
|
|
6943
|
-
|
|
7883
|
+
fs18.closeSync(ttyFd);
|
|
6944
7884
|
} catch {
|
|
6945
7885
|
}
|
|
6946
7886
|
}
|
|
@@ -6971,7 +7911,7 @@ RAW: ${raw}
|
|
|
6971
7911
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
6972
7912
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
6973
7913
|
}
|
|
6974
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
7914
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && path20.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
6975
7915
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
6976
7916
|
cwd: safeCwdForAuth
|
|
6977
7917
|
});
|
|
@@ -6983,12 +7923,12 @@ RAW: ${raw}
|
|
|
6983
7923
|
}
|
|
6984
7924
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
6985
7925
|
try {
|
|
6986
|
-
const tty =
|
|
6987
|
-
|
|
7926
|
+
const tty = fs18.openSync("/dev/tty", "w");
|
|
7927
|
+
fs18.writeSync(
|
|
6988
7928
|
tty,
|
|
6989
7929
|
chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
6990
7930
|
);
|
|
6991
|
-
|
|
7931
|
+
fs18.closeSync(tty);
|
|
6992
7932
|
} catch {
|
|
6993
7933
|
}
|
|
6994
7934
|
const daemonReady = await autoStartDaemonAndWait();
|
|
@@ -7015,9 +7955,9 @@ RAW: ${raw}
|
|
|
7015
7955
|
});
|
|
7016
7956
|
} catch (err) {
|
|
7017
7957
|
if (process.env.NODE9_DEBUG === "1") {
|
|
7018
|
-
const logPath =
|
|
7958
|
+
const logPath = path20.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
7019
7959
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
7020
|
-
|
|
7960
|
+
fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
7021
7961
|
`);
|
|
7022
7962
|
}
|
|
7023
7963
|
process.exit(0);
|
|
@@ -7054,9 +7994,49 @@ RAW: ${raw}
|
|
|
7054
7994
|
init_audit();
|
|
7055
7995
|
init_config();
|
|
7056
7996
|
init_policy();
|
|
7057
|
-
import
|
|
7058
|
-
import
|
|
7059
|
-
import
|
|
7997
|
+
import fs19 from "fs";
|
|
7998
|
+
import path21 from "path";
|
|
7999
|
+
import os16 from "os";
|
|
8000
|
+
init_daemon();
|
|
8001
|
+
|
|
8002
|
+
// src/utils/cp-mv-parser.ts
|
|
8003
|
+
function parseCpMvOp(command) {
|
|
8004
|
+
const trimmed = command.trim();
|
|
8005
|
+
const tokens = trimmed.split(/\s+/);
|
|
8006
|
+
if (tokens.length < 3) return null;
|
|
8007
|
+
const [cmd, ...rest] = tokens;
|
|
8008
|
+
const base = cmd.split("/").pop() ?? cmd;
|
|
8009
|
+
if (base !== "cp" && base !== "mv") return null;
|
|
8010
|
+
const args = [];
|
|
8011
|
+
for (const tok of rest) {
|
|
8012
|
+
if (tok === "--") {
|
|
8013
|
+
args.push(...rest.slice(rest.indexOf("--") + 1));
|
|
8014
|
+
break;
|
|
8015
|
+
}
|
|
8016
|
+
if (tok === "-t" || tok === "--target-directory") return null;
|
|
8017
|
+
if (tok.startsWith("--target-directory=")) return null;
|
|
8018
|
+
if (tok.startsWith("-") && !tok.startsWith("--")) {
|
|
8019
|
+
if (tok.includes("t")) return null;
|
|
8020
|
+
continue;
|
|
8021
|
+
}
|
|
8022
|
+
if (tok.startsWith("--")) {
|
|
8023
|
+
continue;
|
|
8024
|
+
}
|
|
8025
|
+
args.push(tok);
|
|
8026
|
+
}
|
|
8027
|
+
if (args.length !== 2) return null;
|
|
8028
|
+
const [src, dest] = args;
|
|
8029
|
+
if (!src || !dest) return null;
|
|
8030
|
+
if (containsShellMetachar(src) || containsShellMetachar(dest)) return null;
|
|
8031
|
+
return { src, dest, clearSource: base === "mv" };
|
|
8032
|
+
}
|
|
8033
|
+
function containsShellMetachar(token) {
|
|
8034
|
+
if (/[$`{;*?]/.test(token)) return true;
|
|
8035
|
+
if (token.includes("\0")) return true;
|
|
8036
|
+
return false;
|
|
8037
|
+
}
|
|
8038
|
+
|
|
8039
|
+
// src/cli/commands/log.ts
|
|
7060
8040
|
function sanitize3(value) {
|
|
7061
8041
|
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
7062
8042
|
}
|
|
@@ -7075,11 +8055,20 @@ function registerLogCommand(program2) {
|
|
|
7075
8055
|
decision: "allowed",
|
|
7076
8056
|
source: "post-hook"
|
|
7077
8057
|
};
|
|
7078
|
-
const logPath =
|
|
7079
|
-
if (!
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
8058
|
+
const logPath = path21.join(os16.homedir(), ".node9", "audit.log");
|
|
8059
|
+
if (!fs19.existsSync(path21.dirname(logPath)))
|
|
8060
|
+
fs19.mkdirSync(path21.dirname(logPath), { recursive: true });
|
|
8061
|
+
fs19.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
8062
|
+
if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
|
|
8063
|
+
const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
8064
|
+
if (command) {
|
|
8065
|
+
const op = parseCpMvOp(command);
|
|
8066
|
+
if (op) {
|
|
8067
|
+
await notifyTaintPropagate(op.src, op.dest, op.clearSource);
|
|
8068
|
+
}
|
|
8069
|
+
}
|
|
8070
|
+
}
|
|
8071
|
+
const safeCwd = typeof payload.cwd === "string" && path21.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
7083
8072
|
const config = getConfig(safeCwd);
|
|
7084
8073
|
if (shouldSnapshot(tool, {}, config)) {
|
|
7085
8074
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -7088,9 +8077,9 @@ function registerLogCommand(program2) {
|
|
|
7088
8077
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7089
8078
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
7090
8079
|
`);
|
|
7091
|
-
const debugPath =
|
|
8080
|
+
const debugPath = path21.join(os16.homedir(), ".node9", "hook-debug.log");
|
|
7092
8081
|
try {
|
|
7093
|
-
|
|
8082
|
+
fs19.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
7094
8083
|
`);
|
|
7095
8084
|
} catch {
|
|
7096
8085
|
}
|
|
@@ -7395,13 +8384,13 @@ function registerConfigShowCommand(program2) {
|
|
|
7395
8384
|
// src/cli/commands/doctor.ts
|
|
7396
8385
|
init_daemon();
|
|
7397
8386
|
import chalk7 from "chalk";
|
|
7398
|
-
import
|
|
7399
|
-
import
|
|
7400
|
-
import
|
|
8387
|
+
import fs20 from "fs";
|
|
8388
|
+
import path22 from "path";
|
|
8389
|
+
import os17 from "os";
|
|
7401
8390
|
import { execSync as execSync2 } from "child_process";
|
|
7402
8391
|
function registerDoctorCommand(program2, version2) {
|
|
7403
8392
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
7404
|
-
const homeDir2 =
|
|
8393
|
+
const homeDir2 = os17.homedir();
|
|
7405
8394
|
let failures = 0;
|
|
7406
8395
|
function pass(msg) {
|
|
7407
8396
|
console.log(chalk7.green(" \u2705 ") + msg);
|
|
@@ -7450,10 +8439,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7450
8439
|
);
|
|
7451
8440
|
}
|
|
7452
8441
|
section("Configuration");
|
|
7453
|
-
const globalConfigPath =
|
|
7454
|
-
if (
|
|
8442
|
+
const globalConfigPath = path22.join(homeDir2, ".node9", "config.json");
|
|
8443
|
+
if (fs20.existsSync(globalConfigPath)) {
|
|
7455
8444
|
try {
|
|
7456
|
-
JSON.parse(
|
|
8445
|
+
JSON.parse(fs20.readFileSync(globalConfigPath, "utf-8"));
|
|
7457
8446
|
pass("~/.node9/config.json found and valid");
|
|
7458
8447
|
} catch {
|
|
7459
8448
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -7461,10 +8450,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7461
8450
|
} else {
|
|
7462
8451
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
7463
8452
|
}
|
|
7464
|
-
const projectConfigPath =
|
|
7465
|
-
if (
|
|
8453
|
+
const projectConfigPath = path22.join(process.cwd(), "node9.config.json");
|
|
8454
|
+
if (fs20.existsSync(projectConfigPath)) {
|
|
7466
8455
|
try {
|
|
7467
|
-
JSON.parse(
|
|
8456
|
+
JSON.parse(fs20.readFileSync(projectConfigPath, "utf-8"));
|
|
7468
8457
|
pass("node9.config.json found and valid (project)");
|
|
7469
8458
|
} catch {
|
|
7470
8459
|
fail(
|
|
@@ -7473,8 +8462,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7473
8462
|
);
|
|
7474
8463
|
}
|
|
7475
8464
|
}
|
|
7476
|
-
const credsPath =
|
|
7477
|
-
if (
|
|
8465
|
+
const credsPath = path22.join(homeDir2, ".node9", "credentials.json");
|
|
8466
|
+
if (fs20.existsSync(credsPath)) {
|
|
7478
8467
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
7479
8468
|
} else {
|
|
7480
8469
|
warn(
|
|
@@ -7483,10 +8472,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7483
8472
|
);
|
|
7484
8473
|
}
|
|
7485
8474
|
section("Agent Hooks");
|
|
7486
|
-
const claudeSettingsPath =
|
|
7487
|
-
if (
|
|
8475
|
+
const claudeSettingsPath = path22.join(homeDir2, ".claude", "settings.json");
|
|
8476
|
+
if (fs20.existsSync(claudeSettingsPath)) {
|
|
7488
8477
|
try {
|
|
7489
|
-
const cs = JSON.parse(
|
|
8478
|
+
const cs = JSON.parse(fs20.readFileSync(claudeSettingsPath, "utf-8"));
|
|
7490
8479
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
7491
8480
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
7492
8481
|
);
|
|
@@ -7502,10 +8491,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7502
8491
|
} else {
|
|
7503
8492
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
7504
8493
|
}
|
|
7505
|
-
const geminiSettingsPath =
|
|
7506
|
-
if (
|
|
8494
|
+
const geminiSettingsPath = path22.join(homeDir2, ".gemini", "settings.json");
|
|
8495
|
+
if (fs20.existsSync(geminiSettingsPath)) {
|
|
7507
8496
|
try {
|
|
7508
|
-
const gs = JSON.parse(
|
|
8497
|
+
const gs = JSON.parse(fs20.readFileSync(geminiSettingsPath, "utf-8"));
|
|
7509
8498
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
7510
8499
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
7511
8500
|
);
|
|
@@ -7521,10 +8510,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7521
8510
|
} else {
|
|
7522
8511
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
7523
8512
|
}
|
|
7524
|
-
const cursorHooksPath =
|
|
7525
|
-
if (
|
|
8513
|
+
const cursorHooksPath = path22.join(homeDir2, ".cursor", "hooks.json");
|
|
8514
|
+
if (fs20.existsSync(cursorHooksPath)) {
|
|
7526
8515
|
try {
|
|
7527
|
-
const cur = JSON.parse(
|
|
8516
|
+
const cur = JSON.parse(fs20.readFileSync(cursorHooksPath, "utf-8"));
|
|
7528
8517
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
7529
8518
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
7530
8519
|
);
|
|
@@ -7562,9 +8551,9 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7562
8551
|
|
|
7563
8552
|
// src/cli/commands/audit.ts
|
|
7564
8553
|
import chalk8 from "chalk";
|
|
7565
|
-
import
|
|
7566
|
-
import
|
|
7567
|
-
import
|
|
8554
|
+
import fs21 from "fs";
|
|
8555
|
+
import path23 from "path";
|
|
8556
|
+
import os18 from "os";
|
|
7568
8557
|
function formatRelativeTime(timestamp) {
|
|
7569
8558
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
7570
8559
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -7577,14 +8566,14 @@ function formatRelativeTime(timestamp) {
|
|
|
7577
8566
|
}
|
|
7578
8567
|
function registerAuditCommand(program2) {
|
|
7579
8568
|
program2.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
|
|
7580
|
-
const logPath =
|
|
7581
|
-
if (!
|
|
8569
|
+
const logPath = path23.join(os18.homedir(), ".node9", "audit.log");
|
|
8570
|
+
if (!fs21.existsSync(logPath)) {
|
|
7582
8571
|
console.log(
|
|
7583
8572
|
chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
7584
8573
|
);
|
|
7585
8574
|
return;
|
|
7586
8575
|
}
|
|
7587
|
-
const raw =
|
|
8576
|
+
const raw = fs21.readFileSync(logPath, "utf-8");
|
|
7588
8577
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
7589
8578
|
let entries = lines.flatMap((line) => {
|
|
7590
8579
|
try {
|
|
@@ -7704,9 +8693,42 @@ function registerDaemonCommand(program2) {
|
|
|
7704
8693
|
init_core();
|
|
7705
8694
|
init_daemon();
|
|
7706
8695
|
import chalk10 from "chalk";
|
|
7707
|
-
import
|
|
7708
|
-
import
|
|
7709
|
-
import
|
|
8696
|
+
import fs22 from "fs";
|
|
8697
|
+
import path24 from "path";
|
|
8698
|
+
import os19 from "os";
|
|
8699
|
+
function readJson2(filePath) {
|
|
8700
|
+
try {
|
|
8701
|
+
if (fs22.existsSync(filePath)) return JSON.parse(fs22.readFileSync(filePath, "utf-8"));
|
|
8702
|
+
} catch {
|
|
8703
|
+
}
|
|
8704
|
+
return null;
|
|
8705
|
+
}
|
|
8706
|
+
function isNode9Hook2(cmd) {
|
|
8707
|
+
if (!cmd) return false;
|
|
8708
|
+
return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
|
|
8709
|
+
}
|
|
8710
|
+
function wrappedMcpServers(servers) {
|
|
8711
|
+
if (!servers) return [];
|
|
8712
|
+
return Object.entries(servers).filter(([, s]) => s.command === "node9" && Array.isArray(s.args) && s.args.length > 0).map(([name, s]) => `${name} \u2192 ${s.args.join(" ")}`);
|
|
8713
|
+
}
|
|
8714
|
+
function printAgentSection(label, hookPairs, wrapped) {
|
|
8715
|
+
console.log(chalk10.bold(` ${label}`));
|
|
8716
|
+
for (const { name, present } of hookPairs) {
|
|
8717
|
+
if (present) {
|
|
8718
|
+
console.log(chalk10.green(` \u2713 ${name}`));
|
|
8719
|
+
} else {
|
|
8720
|
+
console.log(chalk10.red(` \u2717 ${name}`) + chalk10.gray(" (not wired)"));
|
|
8721
|
+
}
|
|
8722
|
+
}
|
|
8723
|
+
if (wrapped.length > 0) {
|
|
8724
|
+
console.log(chalk10.cyan(` MCP proxied:`));
|
|
8725
|
+
for (const entry of wrapped) {
|
|
8726
|
+
console.log(chalk10.gray(` \u2022 ${entry}`));
|
|
8727
|
+
}
|
|
8728
|
+
} else {
|
|
8729
|
+
console.log(chalk10.gray(` MCP proxied: none`));
|
|
8730
|
+
}
|
|
8731
|
+
}
|
|
7710
8732
|
function registerStatusCommand(program2) {
|
|
7711
8733
|
program2.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
|
|
7712
8734
|
const creds = getCredentials();
|
|
@@ -7741,19 +8763,72 @@ function registerStatusCommand(program2) {
|
|
|
7741
8763
|
console.log("");
|
|
7742
8764
|
const modeLabel = settings.mode === "audit" ? chalk10.blue("audit") : settings.mode === "strict" ? chalk10.red("strict") : chalk10.white("standard");
|
|
7743
8765
|
console.log(` Mode: ${modeLabel}`);
|
|
7744
|
-
const projectConfig =
|
|
7745
|
-
const globalConfig =
|
|
8766
|
+
const projectConfig = path24.join(process.cwd(), "node9.config.json");
|
|
8767
|
+
const globalConfig = path24.join(os19.homedir(), ".node9", "config.json");
|
|
7746
8768
|
console.log(
|
|
7747
|
-
` Local: ${
|
|
8769
|
+
` Local: ${fs22.existsSync(projectConfig) ? chalk10.green("Active (node9.config.json)") : chalk10.gray("Not present")}`
|
|
7748
8770
|
);
|
|
7749
8771
|
console.log(
|
|
7750
|
-
` Global: ${
|
|
8772
|
+
` Global: ${fs22.existsSync(globalConfig) ? chalk10.green("Active (~/.node9/config.json)") : chalk10.gray("Not present")}`
|
|
7751
8773
|
);
|
|
7752
8774
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
7753
8775
|
console.log(
|
|
7754
8776
|
` Sandbox: ${chalk10.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
7755
8777
|
);
|
|
7756
8778
|
}
|
|
8779
|
+
const homeDir2 = os19.homedir();
|
|
8780
|
+
const claudeSettings = readJson2(
|
|
8781
|
+
path24.join(homeDir2, ".claude", "settings.json")
|
|
8782
|
+
);
|
|
8783
|
+
const claudeConfig = readJson2(path24.join(homeDir2, ".claude.json"));
|
|
8784
|
+
const geminiSettings = readJson2(
|
|
8785
|
+
path24.join(homeDir2, ".gemini", "settings.json")
|
|
8786
|
+
);
|
|
8787
|
+
const cursorConfig = readJson2(path24.join(homeDir2, ".cursor", "mcp.json"));
|
|
8788
|
+
const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
|
|
8789
|
+
if (agentFound) {
|
|
8790
|
+
console.log("");
|
|
8791
|
+
console.log(chalk10.bold(" Agent Wiring:"));
|
|
8792
|
+
console.log("");
|
|
8793
|
+
if (claudeSettings || claudeConfig) {
|
|
8794
|
+
const preHook = claudeSettings?.hooks?.PreToolUse?.some(
|
|
8795
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8796
|
+
) ?? false;
|
|
8797
|
+
const postHook = claudeSettings?.hooks?.PostToolUse?.some(
|
|
8798
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8799
|
+
) ?? false;
|
|
8800
|
+
printAgentSection(
|
|
8801
|
+
"Claude Code",
|
|
8802
|
+
[
|
|
8803
|
+
{ name: "PreToolUse (node9 check)", present: preHook },
|
|
8804
|
+
{ name: "PostToolUse (node9 log)", present: postHook }
|
|
8805
|
+
],
|
|
8806
|
+
wrappedMcpServers(claudeConfig?.mcpServers)
|
|
8807
|
+
);
|
|
8808
|
+
console.log("");
|
|
8809
|
+
}
|
|
8810
|
+
if (geminiSettings) {
|
|
8811
|
+
const beforeHook = geminiSettings.hooks?.BeforeTool?.some(
|
|
8812
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8813
|
+
) ?? false;
|
|
8814
|
+
const afterHook = geminiSettings.hooks?.AfterTool?.some(
|
|
8815
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8816
|
+
) ?? false;
|
|
8817
|
+
printAgentSection(
|
|
8818
|
+
"Gemini CLI",
|
|
8819
|
+
[
|
|
8820
|
+
{ name: "BeforeTool (node9 check)", present: beforeHook },
|
|
8821
|
+
{ name: "AfterTool (node9 log)", present: afterHook }
|
|
8822
|
+
],
|
|
8823
|
+
wrappedMcpServers(geminiSettings.mcpServers)
|
|
8824
|
+
);
|
|
8825
|
+
console.log("");
|
|
8826
|
+
}
|
|
8827
|
+
if (cursorConfig) {
|
|
8828
|
+
printAgentSection("Cursor", [], wrappedMcpServers(cursorConfig.mcpServers));
|
|
8829
|
+
console.log("");
|
|
8830
|
+
}
|
|
8831
|
+
}
|
|
7757
8832
|
const pauseState = checkPause();
|
|
7758
8833
|
if (pauseState.paused) {
|
|
7759
8834
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
@@ -7766,8 +8841,63 @@ function registerStatusCommand(program2) {
|
|
|
7766
8841
|
});
|
|
7767
8842
|
}
|
|
7768
8843
|
|
|
7769
|
-
// src/cli/commands/
|
|
8844
|
+
// src/cli/commands/init.ts
|
|
8845
|
+
init_core();
|
|
7770
8846
|
import chalk11 from "chalk";
|
|
8847
|
+
import fs23 from "fs";
|
|
8848
|
+
import path25 from "path";
|
|
8849
|
+
import os20 from "os";
|
|
8850
|
+
function registerInitCommand(program2) {
|
|
8851
|
+
program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").option("--skip-setup", "Only create config \u2014 do not wire AI agents").action(async (options) => {
|
|
8852
|
+
console.log(chalk11.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
8853
|
+
const configPath = path25.join(os20.homedir(), ".node9", "config.json");
|
|
8854
|
+
if (fs23.existsSync(configPath) && !options.force) {
|
|
8855
|
+
console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
8856
|
+
} else {
|
|
8857
|
+
const requestedMode = options.mode.toLowerCase();
|
|
8858
|
+
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
8859
|
+
const configToSave = {
|
|
8860
|
+
...DEFAULT_CONFIG,
|
|
8861
|
+
settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
|
|
8862
|
+
};
|
|
8863
|
+
const dir = path25.dirname(configPath);
|
|
8864
|
+
if (!fs23.existsSync(dir)) fs23.mkdirSync(dir, { recursive: true });
|
|
8865
|
+
fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
8866
|
+
console.log(chalk11.green(`\u2705 Config created: ${configPath}`));
|
|
8867
|
+
console.log(chalk11.gray(` Mode: ${safeMode}`));
|
|
8868
|
+
}
|
|
8869
|
+
if (options.skipSetup) return;
|
|
8870
|
+
console.log("");
|
|
8871
|
+
const detected = detectAgents();
|
|
8872
|
+
const found = Object.keys(detected).filter(
|
|
8873
|
+
(k) => detected[k]
|
|
8874
|
+
);
|
|
8875
|
+
if (found.length === 0) {
|
|
8876
|
+
console.log(
|
|
8877
|
+
chalk11.gray("No AI agents detected. Install Claude Code, Gemini CLI, or Cursor")
|
|
8878
|
+
);
|
|
8879
|
+
console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor>"));
|
|
8880
|
+
return;
|
|
8881
|
+
}
|
|
8882
|
+
console.log(chalk11.bold("Detected agents:"));
|
|
8883
|
+
for (const agent of found) {
|
|
8884
|
+
console.log(chalk11.green(` \u2713 ${agent}`));
|
|
8885
|
+
}
|
|
8886
|
+
console.log("");
|
|
8887
|
+
for (const agent of found) {
|
|
8888
|
+
console.log(chalk11.bold(`Wiring ${agent}...`));
|
|
8889
|
+
if (agent === "claude") await setupClaude();
|
|
8890
|
+
else if (agent === "gemini") await setupGemini();
|
|
8891
|
+
else if (agent === "cursor") await setupCursor();
|
|
8892
|
+
console.log("");
|
|
8893
|
+
}
|
|
8894
|
+
console.log(chalk11.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
|
|
8895
|
+
console.log(chalk11.gray(" Run: node9 daemon start"));
|
|
8896
|
+
});
|
|
8897
|
+
}
|
|
8898
|
+
|
|
8899
|
+
// src/cli/commands/undo.ts
|
|
8900
|
+
import chalk12 from "chalk";
|
|
7771
8901
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
7772
8902
|
function registerUndoCommand(program2) {
|
|
7773
8903
|
program2.command("undo").description(
|
|
@@ -7779,22 +8909,22 @@ function registerUndoCommand(program2) {
|
|
|
7779
8909
|
if (history.length === 0) {
|
|
7780
8910
|
if (!options.all && allHistory.length > 0) {
|
|
7781
8911
|
console.log(
|
|
7782
|
-
|
|
8912
|
+
chalk12.yellow(
|
|
7783
8913
|
`
|
|
7784
8914
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
7785
|
-
Run ${
|
|
8915
|
+
Run ${chalk12.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
7786
8916
|
`
|
|
7787
8917
|
)
|
|
7788
8918
|
);
|
|
7789
8919
|
} else {
|
|
7790
|
-
console.log(
|
|
8920
|
+
console.log(chalk12.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
7791
8921
|
}
|
|
7792
8922
|
return;
|
|
7793
8923
|
}
|
|
7794
8924
|
const idx = history.length - steps;
|
|
7795
8925
|
if (idx < 0) {
|
|
7796
8926
|
console.log(
|
|
7797
|
-
|
|
8927
|
+
chalk12.yellow(
|
|
7798
8928
|
`
|
|
7799
8929
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
7800
8930
|
`
|
|
@@ -7806,19 +8936,19 @@ function registerUndoCommand(program2) {
|
|
|
7806
8936
|
const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
|
|
7807
8937
|
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
|
|
7808
8938
|
console.log(
|
|
7809
|
-
|
|
8939
|
+
chalk12.magenta.bold(`
|
|
7810
8940
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
|
|
7811
8941
|
);
|
|
7812
8942
|
console.log(
|
|
7813
|
-
|
|
7814
|
-
` Tool: ${
|
|
8943
|
+
chalk12.white(
|
|
8944
|
+
` Tool: ${chalk12.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk12.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
7815
8945
|
)
|
|
7816
8946
|
);
|
|
7817
|
-
console.log(
|
|
7818
|
-
console.log(
|
|
8947
|
+
console.log(chalk12.white(` When: ${chalk12.gray(ageStr)}`));
|
|
8948
|
+
console.log(chalk12.white(` Dir: ${chalk12.gray(snapshot.cwd)}`));
|
|
7819
8949
|
if (steps > 1)
|
|
7820
8950
|
console.log(
|
|
7821
|
-
|
|
8951
|
+
chalk12.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
7822
8952
|
);
|
|
7823
8953
|
console.log("");
|
|
7824
8954
|
const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
@@ -7826,21 +8956,21 @@ function registerUndoCommand(program2) {
|
|
|
7826
8956
|
const lines = diff.split("\n");
|
|
7827
8957
|
for (const line of lines) {
|
|
7828
8958
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
7829
|
-
console.log(
|
|
8959
|
+
console.log(chalk12.bold(line));
|
|
7830
8960
|
} else if (line.startsWith("+")) {
|
|
7831
|
-
console.log(
|
|
8961
|
+
console.log(chalk12.green(line));
|
|
7832
8962
|
} else if (line.startsWith("-")) {
|
|
7833
|
-
console.log(
|
|
8963
|
+
console.log(chalk12.red(line));
|
|
7834
8964
|
} else if (line.startsWith("@@")) {
|
|
7835
|
-
console.log(
|
|
8965
|
+
console.log(chalk12.cyan(line));
|
|
7836
8966
|
} else {
|
|
7837
|
-
console.log(
|
|
8967
|
+
console.log(chalk12.gray(line));
|
|
7838
8968
|
}
|
|
7839
8969
|
}
|
|
7840
8970
|
console.log("");
|
|
7841
8971
|
} else {
|
|
7842
8972
|
console.log(
|
|
7843
|
-
|
|
8973
|
+
chalk12.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
7844
8974
|
);
|
|
7845
8975
|
}
|
|
7846
8976
|
const proceed = await confirm2({
|
|
@@ -7849,19 +8979,19 @@ function registerUndoCommand(program2) {
|
|
|
7849
8979
|
});
|
|
7850
8980
|
if (proceed) {
|
|
7851
8981
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
7852
|
-
console.log(
|
|
8982
|
+
console.log(chalk12.green("\n\u2705 Reverted successfully.\n"));
|
|
7853
8983
|
} else {
|
|
7854
|
-
console.error(
|
|
8984
|
+
console.error(chalk12.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
7855
8985
|
}
|
|
7856
8986
|
} else {
|
|
7857
|
-
console.log(
|
|
8987
|
+
console.log(chalk12.gray("\nCancelled.\n"));
|
|
7858
8988
|
}
|
|
7859
8989
|
});
|
|
7860
8990
|
}
|
|
7861
8991
|
|
|
7862
8992
|
// src/cli/commands/watch.ts
|
|
7863
8993
|
init_daemon();
|
|
7864
|
-
import
|
|
8994
|
+
import chalk13 from "chalk";
|
|
7865
8995
|
import { spawn as spawn7, spawnSync as spawnSync5 } from "child_process";
|
|
7866
8996
|
function registerWatchCommand(program2) {
|
|
7867
8997
|
program2.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) => {
|
|
@@ -7877,7 +9007,7 @@ function registerWatchCommand(program2) {
|
|
|
7877
9007
|
throw new Error("not running");
|
|
7878
9008
|
}
|
|
7879
9009
|
} catch {
|
|
7880
|
-
console.error(
|
|
9010
|
+
console.error(chalk13.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
|
|
7881
9011
|
const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
|
|
7882
9012
|
detached: true,
|
|
7883
9013
|
stdio: "ignore",
|
|
@@ -7899,12 +9029,12 @@ function registerWatchCommand(program2) {
|
|
|
7899
9029
|
}
|
|
7900
9030
|
}
|
|
7901
9031
|
if (!ready) {
|
|
7902
|
-
console.error(
|
|
9032
|
+
console.error(chalk13.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
7903
9033
|
process.exit(1);
|
|
7904
9034
|
}
|
|
7905
9035
|
}
|
|
7906
9036
|
console.error(
|
|
7907
|
-
|
|
9037
|
+
chalk13.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk13.dim(` \u2192 localhost:${port}`) + chalk13.dim(
|
|
7908
9038
|
"\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
|
|
7909
9039
|
)
|
|
7910
9040
|
);
|
|
@@ -7913,7 +9043,7 @@ function registerWatchCommand(program2) {
|
|
|
7913
9043
|
env: { ...process.env, NODE9_WATCH_MODE: "1" }
|
|
7914
9044
|
});
|
|
7915
9045
|
if (result.error) {
|
|
7916
|
-
console.error(
|
|
9046
|
+
console.error(chalk13.red(`\u274C Failed to run command: ${result.error.message}`));
|
|
7917
9047
|
process.exit(1);
|
|
7918
9048
|
}
|
|
7919
9049
|
process.exit(result.status ?? 0);
|
|
@@ -7923,7 +9053,7 @@ function registerWatchCommand(program2) {
|
|
|
7923
9053
|
// src/mcp-gateway/index.ts
|
|
7924
9054
|
init_orchestrator();
|
|
7925
9055
|
import readline2 from "readline";
|
|
7926
|
-
import
|
|
9056
|
+
import chalk14 from "chalk";
|
|
7927
9057
|
import { spawn as spawn8 } from "child_process";
|
|
7928
9058
|
import { execa as execa2 } from "execa";
|
|
7929
9059
|
init_provenance();
|
|
@@ -7986,13 +9116,13 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
7986
9116
|
const prov = checkProvenance(executable);
|
|
7987
9117
|
if (prov.trustLevel === "suspect") {
|
|
7988
9118
|
console.error(
|
|
7989
|
-
|
|
9119
|
+
chalk14.red(
|
|
7990
9120
|
`\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
|
|
7991
9121
|
)
|
|
7992
9122
|
);
|
|
7993
|
-
console.error(
|
|
9123
|
+
console.error(chalk14.red(" Verify this binary is trusted before proceeding."));
|
|
7994
9124
|
}
|
|
7995
|
-
console.error(
|
|
9125
|
+
console.error(chalk14.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
|
|
7996
9126
|
const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
|
|
7997
9127
|
"NODE_OPTIONS",
|
|
7998
9128
|
"NODE_PATH",
|
|
@@ -8056,10 +9186,10 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
8056
9186
|
mcpServer
|
|
8057
9187
|
});
|
|
8058
9188
|
if (!result.approved) {
|
|
8059
|
-
console.error(
|
|
9189
|
+
console.error(chalk14.red(`
|
|
8060
9190
|
\u{1F6D1} Node9 MCP Gateway: Action Blocked`));
|
|
8061
|
-
console.error(
|
|
8062
|
-
console.error(
|
|
9191
|
+
console.error(chalk14.gray(` Tool: ${toolName}`));
|
|
9192
|
+
console.error(chalk14.gray(` Reason: ${result.reason ?? "Security Policy"}
|
|
8063
9193
|
`));
|
|
8064
9194
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
8065
9195
|
const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -8133,7 +9263,7 @@ function registerMcpGatewayCommand(program2) {
|
|
|
8133
9263
|
|
|
8134
9264
|
// src/cli/commands/trust.ts
|
|
8135
9265
|
init_trusted_hosts();
|
|
8136
|
-
import
|
|
9266
|
+
import chalk15 from "chalk";
|
|
8137
9267
|
function isValidHost(host) {
|
|
8138
9268
|
return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
|
|
8139
9269
|
}
|
|
@@ -8143,44 +9273,44 @@ function registerTrustCommand(program2) {
|
|
|
8143
9273
|
const normalized = normalizeHost(host.trim());
|
|
8144
9274
|
if (!isValidHost(normalized)) {
|
|
8145
9275
|
console.error(
|
|
8146
|
-
|
|
9276
|
+
chalk15.red(`
|
|
8147
9277
|
\u274C Invalid host: "${host}"
|
|
8148
|
-
`) +
|
|
9278
|
+
`) + chalk15.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
|
|
8149
9279
|
);
|
|
8150
9280
|
process.exit(1);
|
|
8151
9281
|
}
|
|
8152
9282
|
addTrustedHost(normalized);
|
|
8153
|
-
console.log(
|
|
9283
|
+
console.log(chalk15.green(`
|
|
8154
9284
|
\u2705 ${normalized} added to trusted hosts.`));
|
|
8155
9285
|
console.log(
|
|
8156
|
-
|
|
9286
|
+
chalk15.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
|
|
8157
9287
|
);
|
|
8158
9288
|
});
|
|
8159
9289
|
trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
|
|
8160
9290
|
const normalized = normalizeHost(host.trim());
|
|
8161
9291
|
const removed = removeTrustedHost(normalized);
|
|
8162
9292
|
if (!removed) {
|
|
8163
|
-
console.error(
|
|
9293
|
+
console.error(chalk15.yellow(`
|
|
8164
9294
|
\u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
|
|
8165
9295
|
`));
|
|
8166
9296
|
process.exit(1);
|
|
8167
9297
|
}
|
|
8168
|
-
console.log(
|
|
9298
|
+
console.log(chalk15.green(`
|
|
8169
9299
|
\u2705 ${normalized} removed from trusted hosts.
|
|
8170
9300
|
`));
|
|
8171
9301
|
});
|
|
8172
9302
|
trustCmd.command("list").description("Show all trusted hosts").action(() => {
|
|
8173
9303
|
const hosts = readTrustedHosts();
|
|
8174
9304
|
if (hosts.length === 0) {
|
|
8175
|
-
console.log(
|
|
8176
|
-
console.log(` Add one: ${
|
|
9305
|
+
console.log(chalk15.gray("\n No trusted hosts configured.\n"));
|
|
9306
|
+
console.log(` Add one: ${chalk15.cyan("node9 trust add api.mycompany.com")}
|
|
8177
9307
|
`);
|
|
8178
9308
|
return;
|
|
8179
9309
|
}
|
|
8180
|
-
console.log(
|
|
9310
|
+
console.log(chalk15.bold("\n\u{1F513} Trusted Hosts\n"));
|
|
8181
9311
|
for (const entry of hosts) {
|
|
8182
9312
|
const date = new Date(entry.addedAt).toLocaleDateString();
|
|
8183
|
-
console.log(` ${
|
|
9313
|
+
console.log(` ${chalk15.cyan(entry.host.padEnd(40))} ${chalk15.gray(`added ${date}`)}`);
|
|
8184
9314
|
}
|
|
8185
9315
|
console.log("");
|
|
8186
9316
|
});
|
|
@@ -8188,20 +9318,20 @@ function registerTrustCommand(program2) {
|
|
|
8188
9318
|
|
|
8189
9319
|
// src/cli.ts
|
|
8190
9320
|
var { version } = JSON.parse(
|
|
8191
|
-
|
|
9321
|
+
fs25.readFileSync(path27.join(__dirname, "../package.json"), "utf-8")
|
|
8192
9322
|
);
|
|
8193
9323
|
var program = new Command();
|
|
8194
9324
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
8195
9325
|
program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
|
|
8196
9326
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
8197
|
-
const credPath =
|
|
8198
|
-
if (!
|
|
8199
|
-
|
|
9327
|
+
const credPath = path27.join(os22.homedir(), ".node9", "credentials.json");
|
|
9328
|
+
if (!fs25.existsSync(path27.dirname(credPath)))
|
|
9329
|
+
fs25.mkdirSync(path27.dirname(credPath), { recursive: true });
|
|
8200
9330
|
const profileName = options.profile || "default";
|
|
8201
9331
|
let existingCreds = {};
|
|
8202
9332
|
try {
|
|
8203
|
-
if (
|
|
8204
|
-
const raw = JSON.parse(
|
|
9333
|
+
if (fs25.existsSync(credPath)) {
|
|
9334
|
+
const raw = JSON.parse(fs25.readFileSync(credPath, "utf-8"));
|
|
8205
9335
|
if (raw.apiKey) {
|
|
8206
9336
|
existingCreds = {
|
|
8207
9337
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -8213,13 +9343,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
8213
9343
|
} catch {
|
|
8214
9344
|
}
|
|
8215
9345
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
8216
|
-
|
|
9346
|
+
fs25.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
8217
9347
|
if (profileName === "default") {
|
|
8218
|
-
const configPath =
|
|
9348
|
+
const configPath = path27.join(os22.homedir(), ".node9", "config.json");
|
|
8219
9349
|
let config = {};
|
|
8220
9350
|
try {
|
|
8221
|
-
if (
|
|
8222
|
-
config = JSON.parse(
|
|
9351
|
+
if (fs25.existsSync(configPath))
|
|
9352
|
+
config = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
|
|
8223
9353
|
} catch {
|
|
8224
9354
|
}
|
|
8225
9355
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -8234,36 +9364,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
8234
9364
|
approvers.cloud = false;
|
|
8235
9365
|
}
|
|
8236
9366
|
s.approvers = approvers;
|
|
8237
|
-
if (!
|
|
8238
|
-
|
|
8239
|
-
|
|
9367
|
+
if (!fs25.existsSync(path27.dirname(configPath)))
|
|
9368
|
+
fs25.mkdirSync(path27.dirname(configPath), { recursive: true });
|
|
9369
|
+
fs25.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
8240
9370
|
}
|
|
8241
9371
|
if (options.profile && profileName !== "default") {
|
|
8242
|
-
console.log(
|
|
8243
|
-
console.log(
|
|
9372
|
+
console.log(chalk17.green(`\u2705 Profile "${profileName}" saved`));
|
|
9373
|
+
console.log(chalk17.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
8244
9374
|
} else if (options.local) {
|
|
8245
|
-
console.log(
|
|
8246
|
-
console.log(
|
|
9375
|
+
console.log(chalk17.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
9376
|
+
console.log(chalk17.gray(` All decisions stay on this machine.`));
|
|
8247
9377
|
} else {
|
|
8248
|
-
console.log(
|
|
8249
|
-
console.log(
|
|
9378
|
+
console.log(chalk17.green(`\u2705 Logged in \u2014 agent mode`));
|
|
9379
|
+
console.log(chalk17.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
8250
9380
|
}
|
|
8251
9381
|
});
|
|
8252
9382
|
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) => {
|
|
8253
9383
|
if (target === "gemini") return await setupGemini();
|
|
8254
9384
|
if (target === "claude") return await setupClaude();
|
|
8255
9385
|
if (target === "cursor") return await setupCursor();
|
|
8256
|
-
console.error(
|
|
9386
|
+
console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8257
9387
|
process.exit(1);
|
|
8258
9388
|
});
|
|
8259
9389
|
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) => {
|
|
8260
9390
|
if (!target) {
|
|
8261
|
-
console.log(
|
|
8262
|
-
console.log(" Usage: " +
|
|
9391
|
+
console.log(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
9392
|
+
console.log(" Usage: " + chalk17.white("node9 setup <target>") + "\n");
|
|
8263
9393
|
console.log(" Targets:");
|
|
8264
|
-
console.log(" " +
|
|
8265
|
-
console.log(" " +
|
|
8266
|
-
console.log(" " +
|
|
9394
|
+
console.log(" " + chalk17.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
9395
|
+
console.log(" " + chalk17.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
9396
|
+
console.log(" " + chalk17.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
8267
9397
|
console.log("");
|
|
8268
9398
|
return;
|
|
8269
9399
|
}
|
|
@@ -8271,7 +9401,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
8271
9401
|
if (t === "gemini") return await setupGemini();
|
|
8272
9402
|
if (t === "claude") return await setupClaude();
|
|
8273
9403
|
if (t === "cursor") return await setupCursor();
|
|
8274
|
-
console.error(
|
|
9404
|
+
console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8275
9405
|
process.exit(1);
|
|
8276
9406
|
});
|
|
8277
9407
|
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) => {
|
|
@@ -8280,30 +9410,30 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
|
|
|
8280
9410
|
else if (target === "gemini") fn = teardownGemini;
|
|
8281
9411
|
else if (target === "cursor") fn = teardownCursor;
|
|
8282
9412
|
else {
|
|
8283
|
-
console.error(
|
|
9413
|
+
console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8284
9414
|
process.exit(1);
|
|
8285
9415
|
}
|
|
8286
|
-
console.log(
|
|
9416
|
+
console.log(chalk17.cyan(`
|
|
8287
9417
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
8288
9418
|
`));
|
|
8289
9419
|
try {
|
|
8290
9420
|
fn();
|
|
8291
9421
|
} catch (err) {
|
|
8292
|
-
console.error(
|
|
9422
|
+
console.error(chalk17.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
8293
9423
|
process.exit(1);
|
|
8294
9424
|
}
|
|
8295
|
-
console.log(
|
|
9425
|
+
console.log(chalk17.gray("\n Restart the agent for changes to take effect."));
|
|
8296
9426
|
});
|
|
8297
9427
|
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) => {
|
|
8298
|
-
console.log(
|
|
8299
|
-
console.log(
|
|
9428
|
+
console.log(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
9429
|
+
console.log(chalk17.bold("Stopping daemon..."));
|
|
8300
9430
|
try {
|
|
8301
9431
|
stopDaemon();
|
|
8302
|
-
console.log(
|
|
9432
|
+
console.log(chalk17.green(" \u2705 Daemon stopped"));
|
|
8303
9433
|
} catch {
|
|
8304
|
-
console.log(
|
|
9434
|
+
console.log(chalk17.blue(" \u2139\uFE0F Daemon was not running"));
|
|
8305
9435
|
}
|
|
8306
|
-
console.log(
|
|
9436
|
+
console.log(chalk17.bold("\nRemoving hooks..."));
|
|
8307
9437
|
let teardownFailed = false;
|
|
8308
9438
|
for (const [label, fn] of [
|
|
8309
9439
|
["Claude", teardownClaude],
|
|
@@ -8315,45 +9445,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
8315
9445
|
} catch (err) {
|
|
8316
9446
|
teardownFailed = true;
|
|
8317
9447
|
console.error(
|
|
8318
|
-
|
|
9448
|
+
chalk17.red(
|
|
8319
9449
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
|
|
8320
9450
|
)
|
|
8321
9451
|
);
|
|
8322
9452
|
}
|
|
8323
9453
|
}
|
|
8324
9454
|
if (options.purge) {
|
|
8325
|
-
const node9Dir =
|
|
8326
|
-
if (
|
|
9455
|
+
const node9Dir = path27.join(os22.homedir(), ".node9");
|
|
9456
|
+
if (fs25.existsSync(node9Dir)) {
|
|
8327
9457
|
const confirmed = await confirm3({
|
|
8328
9458
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
8329
9459
|
default: false
|
|
8330
9460
|
});
|
|
8331
9461
|
if (confirmed) {
|
|
8332
|
-
|
|
8333
|
-
if (
|
|
9462
|
+
fs25.rmSync(node9Dir, { recursive: true });
|
|
9463
|
+
if (fs25.existsSync(node9Dir)) {
|
|
8334
9464
|
console.error(
|
|
8335
|
-
|
|
9465
|
+
chalk17.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
8336
9466
|
);
|
|
8337
9467
|
} else {
|
|
8338
|
-
console.log(
|
|
9468
|
+
console.log(chalk17.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
8339
9469
|
}
|
|
8340
9470
|
} else {
|
|
8341
|
-
console.log(
|
|
9471
|
+
console.log(chalk17.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
8342
9472
|
}
|
|
8343
9473
|
} else {
|
|
8344
|
-
console.log(
|
|
9474
|
+
console.log(chalk17.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
8345
9475
|
}
|
|
8346
9476
|
} else {
|
|
8347
9477
|
console.log(
|
|
8348
|
-
|
|
9478
|
+
chalk17.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
8349
9479
|
);
|
|
8350
9480
|
}
|
|
8351
9481
|
if (teardownFailed) {
|
|
8352
|
-
console.error(
|
|
9482
|
+
console.error(chalk17.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
8353
9483
|
process.exit(1);
|
|
8354
9484
|
}
|
|
8355
|
-
console.log(
|
|
8356
|
-
console.log(
|
|
9485
|
+
console.log(chalk17.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
|
|
9486
|
+
console.log(chalk17.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
8357
9487
|
});
|
|
8358
9488
|
registerDoctorCommand(program, version);
|
|
8359
9489
|
program.command("explain").description(
|
|
@@ -8366,7 +9496,7 @@ program.command("explain").description(
|
|
|
8366
9496
|
try {
|
|
8367
9497
|
args = JSON.parse(trimmed);
|
|
8368
9498
|
} catch {
|
|
8369
|
-
console.error(
|
|
9499
|
+
console.error(chalk17.red(`
|
|
8370
9500
|
\u274C Invalid JSON: ${trimmed}
|
|
8371
9501
|
`));
|
|
8372
9502
|
process.exit(1);
|
|
@@ -8377,83 +9507,59 @@ program.command("explain").description(
|
|
|
8377
9507
|
}
|
|
8378
9508
|
const result = await explainPolicy(tool, args);
|
|
8379
9509
|
console.log("");
|
|
8380
|
-
console.log(
|
|
9510
|
+
console.log(chalk17.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
8381
9511
|
console.log("");
|
|
8382
|
-
console.log(` ${
|
|
9512
|
+
console.log(` ${chalk17.bold("Tool:")} ${chalk17.white(result.tool)}`);
|
|
8383
9513
|
if (argsRaw) {
|
|
8384
9514
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
8385
|
-
console.log(` ${
|
|
9515
|
+
console.log(` ${chalk17.bold("Input:")} ${chalk17.gray(preview)}`);
|
|
8386
9516
|
}
|
|
8387
9517
|
console.log("");
|
|
8388
|
-
console.log(
|
|
9518
|
+
console.log(chalk17.bold("Config Sources (Waterfall):"));
|
|
8389
9519
|
for (const tier of result.waterfall) {
|
|
8390
|
-
const num =
|
|
9520
|
+
const num = chalk17.gray(` ${tier.tier}.`);
|
|
8391
9521
|
const label = tier.label.padEnd(16);
|
|
8392
9522
|
let statusStr;
|
|
8393
9523
|
if (tier.tier === 1) {
|
|
8394
|
-
statusStr =
|
|
9524
|
+
statusStr = chalk17.gray(tier.note ?? "");
|
|
8395
9525
|
} else if (tier.status === "active") {
|
|
8396
|
-
const loc = tier.path ?
|
|
8397
|
-
const note = tier.note ?
|
|
8398
|
-
statusStr =
|
|
9526
|
+
const loc = tier.path ? chalk17.gray(tier.path) : "";
|
|
9527
|
+
const note = tier.note ? chalk17.gray(`(${tier.note})`) : "";
|
|
9528
|
+
statusStr = chalk17.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
8399
9529
|
} else {
|
|
8400
|
-
statusStr =
|
|
9530
|
+
statusStr = chalk17.gray("\u25CB " + (tier.note ?? "not found"));
|
|
8401
9531
|
}
|
|
8402
|
-
console.log(`${num} ${
|
|
9532
|
+
console.log(`${num} ${chalk17.white(label)} ${statusStr}`);
|
|
8403
9533
|
}
|
|
8404
9534
|
console.log("");
|
|
8405
|
-
console.log(
|
|
9535
|
+
console.log(chalk17.bold("Policy Evaluation:"));
|
|
8406
9536
|
for (const step of result.steps) {
|
|
8407
9537
|
const isFinal = step.isFinal;
|
|
8408
9538
|
let icon;
|
|
8409
|
-
if (step.outcome === "allow") icon =
|
|
8410
|
-
else if (step.outcome === "review") icon =
|
|
8411
|
-
else if (step.outcome === "skip") icon =
|
|
8412
|
-
else icon =
|
|
9539
|
+
if (step.outcome === "allow") icon = chalk17.green(" \u2705");
|
|
9540
|
+
else if (step.outcome === "review") icon = chalk17.red(" \u{1F534}");
|
|
9541
|
+
else if (step.outcome === "skip") icon = chalk17.gray(" \u2500 ");
|
|
9542
|
+
else icon = chalk17.gray(" \u25CB ");
|
|
8413
9543
|
const name = step.name.padEnd(18);
|
|
8414
|
-
const nameStr = isFinal ?
|
|
8415
|
-
const detail = isFinal ?
|
|
8416
|
-
const arrow = isFinal ?
|
|
9544
|
+
const nameStr = isFinal ? chalk17.white.bold(name) : chalk17.white(name);
|
|
9545
|
+
const detail = isFinal ? chalk17.white(step.detail) : chalk17.gray(step.detail);
|
|
9546
|
+
const arrow = isFinal ? chalk17.yellow(" \u2190 STOP") : "";
|
|
8417
9547
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
8418
9548
|
}
|
|
8419
9549
|
console.log("");
|
|
8420
9550
|
if (result.decision === "allow") {
|
|
8421
|
-
console.log(
|
|
9551
|
+
console.log(chalk17.green.bold(" Decision: \u2705 ALLOW") + chalk17.gray(" \u2014 no approval needed"));
|
|
8422
9552
|
} else {
|
|
8423
9553
|
console.log(
|
|
8424
|
-
|
|
9554
|
+
chalk17.red.bold(" Decision: \u{1F534} REVIEW") + chalk17.gray(" \u2014 human approval required")
|
|
8425
9555
|
);
|
|
8426
9556
|
if (result.blockedByLabel) {
|
|
8427
|
-
console.log(
|
|
9557
|
+
console.log(chalk17.gray(` Reason: ${result.blockedByLabel}`));
|
|
8428
9558
|
}
|
|
8429
9559
|
}
|
|
8430
9560
|
console.log("");
|
|
8431
9561
|
});
|
|
8432
|
-
program
|
|
8433
|
-
const configPath = path24.join(os20.homedir(), ".node9", "config.json");
|
|
8434
|
-
if (fs22.existsSync(configPath) && !options.force) {
|
|
8435
|
-
console.log(chalk16.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
8436
|
-
console.log(chalk16.gray(` Run with --force to overwrite.`));
|
|
8437
|
-
return;
|
|
8438
|
-
}
|
|
8439
|
-
const requestedMode = options.mode.toLowerCase();
|
|
8440
|
-
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
8441
|
-
const configToSave = {
|
|
8442
|
-
...DEFAULT_CONFIG,
|
|
8443
|
-
settings: {
|
|
8444
|
-
...DEFAULT_CONFIG.settings,
|
|
8445
|
-
mode: safeMode
|
|
8446
|
-
}
|
|
8447
|
-
};
|
|
8448
|
-
const dir = path24.dirname(configPath);
|
|
8449
|
-
if (!fs22.existsSync(dir)) fs22.mkdirSync(dir, { recursive: true });
|
|
8450
|
-
fs22.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
8451
|
-
console.log(chalk16.green(`\u2705 Global config created: ${configPath}`));
|
|
8452
|
-
console.log(chalk16.cyan(` Mode set to: ${safeMode}`));
|
|
8453
|
-
console.log(
|
|
8454
|
-
chalk16.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
8455
|
-
);
|
|
8456
|
-
});
|
|
9562
|
+
registerInitCommand(program);
|
|
8457
9563
|
registerAuditCommand(program);
|
|
8458
9564
|
registerStatusCommand(program);
|
|
8459
9565
|
registerDaemonCommand(program);
|
|
@@ -8462,7 +9568,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
8462
9568
|
try {
|
|
8463
9569
|
await startTail2(options);
|
|
8464
9570
|
} catch (err) {
|
|
8465
|
-
console.error(
|
|
9571
|
+
console.error(chalk17.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
|
|
8466
9572
|
process.exit(1);
|
|
8467
9573
|
}
|
|
8468
9574
|
});
|
|
@@ -8474,7 +9580,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
8474
9580
|
const ms = parseDuration(options.duration);
|
|
8475
9581
|
if (ms === null) {
|
|
8476
9582
|
console.error(
|
|
8477
|
-
|
|
9583
|
+
chalk17.red(`
|
|
8478
9584
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
8479
9585
|
`)
|
|
8480
9586
|
);
|
|
@@ -8482,20 +9588,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
8482
9588
|
}
|
|
8483
9589
|
pauseNode9(ms, options.duration);
|
|
8484
9590
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
8485
|
-
console.log(
|
|
9591
|
+
console.log(chalk17.yellow(`
|
|
8486
9592
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
8487
|
-
console.log(
|
|
8488
|
-
console.log(
|
|
9593
|
+
console.log(chalk17.gray(` All tool calls will be allowed without review.`));
|
|
9594
|
+
console.log(chalk17.gray(` Run "node9 resume" to re-enable early.
|
|
8489
9595
|
`));
|
|
8490
9596
|
});
|
|
8491
9597
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
8492
9598
|
const { paused } = checkPause();
|
|
8493
9599
|
if (!paused) {
|
|
8494
|
-
console.log(
|
|
9600
|
+
console.log(chalk17.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
8495
9601
|
return;
|
|
8496
9602
|
}
|
|
8497
9603
|
resumeNode9();
|
|
8498
|
-
console.log(
|
|
9604
|
+
console.log(chalk17.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
8499
9605
|
});
|
|
8500
9606
|
var HOOK_BASED_AGENTS = {
|
|
8501
9607
|
claude: "claude",
|
|
@@ -8508,15 +9614,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8508
9614
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
8509
9615
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
8510
9616
|
console.error(
|
|
8511
|
-
|
|
9617
|
+
chalk17.yellow(`
|
|
8512
9618
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
8513
9619
|
);
|
|
8514
|
-
console.error(
|
|
9620
|
+
console.error(chalk17.white(`
|
|
8515
9621
|
"${target}" uses its own hook system. Use:`));
|
|
8516
9622
|
console.error(
|
|
8517
|
-
|
|
9623
|
+
chalk17.green(` node9 addto ${target} `) + chalk17.gray("# one-time setup")
|
|
8518
9624
|
);
|
|
8519
|
-
console.error(
|
|
9625
|
+
console.error(chalk17.green(` ${target} `) + chalk17.gray("# run normally"));
|
|
8520
9626
|
process.exit(1);
|
|
8521
9627
|
}
|
|
8522
9628
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -8533,7 +9639,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8533
9639
|
}
|
|
8534
9640
|
);
|
|
8535
9641
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
8536
|
-
console.error(
|
|
9642
|
+
console.error(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
8537
9643
|
const daemonReady = await autoStartDaemonAndWait();
|
|
8538
9644
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
8539
9645
|
}
|
|
@@ -8546,12 +9652,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8546
9652
|
}
|
|
8547
9653
|
if (!result.approved) {
|
|
8548
9654
|
console.error(
|
|
8549
|
-
|
|
9655
|
+
chalk17.red(`
|
|
8550
9656
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
8551
9657
|
);
|
|
8552
9658
|
process.exit(1);
|
|
8553
9659
|
}
|
|
8554
|
-
console.error(
|
|
9660
|
+
console.error(chalk17.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
8555
9661
|
await runProxy(fullCommand);
|
|
8556
9662
|
} else {
|
|
8557
9663
|
program.help();
|
|
@@ -8566,9 +9672,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
8566
9672
|
const isCheckHook = process.argv[2] === "check";
|
|
8567
9673
|
if (isCheckHook) {
|
|
8568
9674
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
8569
|
-
const logPath =
|
|
9675
|
+
const logPath = path27.join(os22.homedir(), ".node9", "hook-debug.log");
|
|
8570
9676
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
8571
|
-
|
|
9677
|
+
fs25.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
8572
9678
|
`);
|
|
8573
9679
|
}
|
|
8574
9680
|
process.exit(0);
|