@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.js
CHANGED
|
@@ -30,7 +30,52 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
mod
|
|
31
31
|
));
|
|
32
32
|
|
|
33
|
+
// src/audit/hasher.ts
|
|
34
|
+
function canonicalise(value) {
|
|
35
|
+
return _canonicalise(value, /* @__PURE__ */ new WeakSet());
|
|
36
|
+
}
|
|
37
|
+
function _canonicalise(value, seen) {
|
|
38
|
+
if (value === null || typeof value !== "object") return value;
|
|
39
|
+
if (value instanceof Date) return value.toISOString();
|
|
40
|
+
if (value instanceof RegExp) return value.toString();
|
|
41
|
+
if (Buffer.isBuffer(value)) return value.toString("base64");
|
|
42
|
+
if (seen.has(value)) return "[Circular]";
|
|
43
|
+
seen.add(value);
|
|
44
|
+
let result;
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
result = value.map((v) => _canonicalise(v, seen));
|
|
47
|
+
} else {
|
|
48
|
+
const obj = value;
|
|
49
|
+
result = Object.fromEntries(
|
|
50
|
+
Object.keys(obj).sort().map((k) => [k, _canonicalise(obj[k], seen)])
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
seen.delete(value);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
function hashArgs(args) {
|
|
57
|
+
const canonical = JSON.stringify(canonicalise(args) ?? null);
|
|
58
|
+
return (0, import_crypto.createHash)("sha256").update(canonical).digest("hex").slice(0, 32);
|
|
59
|
+
}
|
|
60
|
+
var import_crypto;
|
|
61
|
+
var init_hasher = __esm({
|
|
62
|
+
"src/audit/hasher.ts"() {
|
|
63
|
+
"use strict";
|
|
64
|
+
import_crypto = require("crypto");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
33
68
|
// src/audit/index.ts
|
|
69
|
+
var audit_exports = {};
|
|
70
|
+
__export(audit_exports, {
|
|
71
|
+
HOOK_DEBUG_LOG: () => HOOK_DEBUG_LOG,
|
|
72
|
+
LOCAL_AUDIT_LOG: () => LOCAL_AUDIT_LOG,
|
|
73
|
+
appendConfigAudit: () => appendConfigAudit,
|
|
74
|
+
appendHookDebug: () => appendHookDebug,
|
|
75
|
+
appendLocalAudit: () => appendLocalAudit,
|
|
76
|
+
appendToLog: () => appendToLog,
|
|
77
|
+
redactSecrets: () => redactSecrets
|
|
78
|
+
});
|
|
34
79
|
function redactSecrets(text) {
|
|
35
80
|
if (!text) return text;
|
|
36
81
|
let redacted = text;
|
|
@@ -52,24 +97,24 @@ function appendToLog(logPath, entry) {
|
|
|
52
97
|
} catch {
|
|
53
98
|
}
|
|
54
99
|
}
|
|
55
|
-
function appendHookDebug(toolName, args, meta) {
|
|
56
|
-
const
|
|
100
|
+
function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
101
|
+
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
57
102
|
appendToLog(HOOK_DEBUG_LOG, {
|
|
58
103
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
59
104
|
tool: toolName,
|
|
60
|
-
|
|
105
|
+
...argsField,
|
|
61
106
|
agent: meta?.agent,
|
|
62
107
|
mcpServer: meta?.mcpServer,
|
|
63
108
|
hostname: import_os.default.hostname(),
|
|
64
109
|
cwd: process.cwd()
|
|
65
110
|
});
|
|
66
111
|
}
|
|
67
|
-
function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
68
|
-
const
|
|
112
|
+
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
113
|
+
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
69
114
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
70
115
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
71
116
|
tool: toolName,
|
|
72
|
-
|
|
117
|
+
...argsField,
|
|
73
118
|
decision,
|
|
74
119
|
checkedBy,
|
|
75
120
|
agent: meta?.agent,
|
|
@@ -91,6 +136,7 @@ var init_audit = __esm({
|
|
|
91
136
|
import_fs = __toESM(require("fs"));
|
|
92
137
|
import_path = __toESM(require("path"));
|
|
93
138
|
import_os = __toESM(require("os"));
|
|
139
|
+
init_hasher();
|
|
94
140
|
LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
95
141
|
HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
|
|
96
142
|
}
|
|
@@ -114,8 +160,8 @@ function sanitizeConfig(raw) {
|
|
|
114
160
|
}
|
|
115
161
|
}
|
|
116
162
|
const lines = result.error.issues.map((issue) => {
|
|
117
|
-
const
|
|
118
|
-
return ` \u2022 ${
|
|
163
|
+
const path28 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
164
|
+
return ` \u2022 ${path28}: ${issue.message}`;
|
|
119
165
|
});
|
|
120
166
|
return {
|
|
121
167
|
sanitized,
|
|
@@ -188,7 +234,8 @@ var init_config_schema = __esm({
|
|
|
188
234
|
environment: import_zod.z.string().optional(),
|
|
189
235
|
slackEnabled: import_zod.z.boolean().optional(),
|
|
190
236
|
enableTrustSessions: import_zod.z.boolean().optional(),
|
|
191
|
-
allowGlobalPause: import_zod.z.boolean().optional()
|
|
237
|
+
allowGlobalPause: import_zod.z.boolean().optional(),
|
|
238
|
+
auditHashArgs: import_zod.z.boolean().optional()
|
|
192
239
|
}).optional(),
|
|
193
240
|
policy: import_zod.z.object({
|
|
194
241
|
sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
@@ -269,7 +316,7 @@ function readShieldsFile() {
|
|
|
269
316
|
}
|
|
270
317
|
function writeShieldsFile(data) {
|
|
271
318
|
import_fs2.default.mkdirSync(import_path2.default.dirname(SHIELDS_STATE_FILE), { recursive: true });
|
|
272
|
-
const tmp = `${SHIELDS_STATE_FILE}.${
|
|
319
|
+
const tmp = `${SHIELDS_STATE_FILE}.${import_crypto2.default.randomBytes(6).toString("hex")}.tmp`;
|
|
273
320
|
const toWrite = { active: data.active };
|
|
274
321
|
if (data.overrides && Object.keys(data.overrides).length > 0) toWrite.overrides = data.overrides;
|
|
275
322
|
import_fs2.default.writeFileSync(tmp, JSON.stringify(toWrite, null, 2), { mode: 384 });
|
|
@@ -318,14 +365,14 @@ function resolveShieldRule(shieldName, identifier) {
|
|
|
318
365
|
}
|
|
319
366
|
return null;
|
|
320
367
|
}
|
|
321
|
-
var import_fs2, import_path2, import_os2,
|
|
368
|
+
var import_fs2, import_path2, import_os2, import_crypto2, SHIELDS, SHIELDS_STATE_FILE;
|
|
322
369
|
var init_shields = __esm({
|
|
323
370
|
"src/shields.ts"() {
|
|
324
371
|
"use strict";
|
|
325
372
|
import_fs2 = __toESM(require("fs"));
|
|
326
373
|
import_path2 = __toESM(require("path"));
|
|
327
374
|
import_os2 = __toESM(require("os"));
|
|
328
|
-
|
|
375
|
+
import_crypto2 = __toESM(require("crypto"));
|
|
329
376
|
SHIELDS = {
|
|
330
377
|
postgres: {
|
|
331
378
|
name: "postgres",
|
|
@@ -746,6 +793,7 @@ var init_config = __esm({
|
|
|
746
793
|
approvalTimeoutMs: 12e4,
|
|
747
794
|
// 120-second auto-deny timeout
|
|
748
795
|
flightRecorder: true,
|
|
796
|
+
auditHashArgs: true,
|
|
749
797
|
approvers: { native: true, browser: true, cloud: false, terminal: true }
|
|
750
798
|
},
|
|
751
799
|
policy: {
|
|
@@ -1623,17 +1671,44 @@ function readTrustedHosts() {
|
|
|
1623
1671
|
return [];
|
|
1624
1672
|
}
|
|
1625
1673
|
}
|
|
1674
|
+
function getFileMtime() {
|
|
1675
|
+
try {
|
|
1676
|
+
return import_fs6.default.statSync(getTrustedHostsPath()).mtimeMs;
|
|
1677
|
+
} catch {
|
|
1678
|
+
return 0;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
function getCachedHosts() {
|
|
1682
|
+
const now = Date.now();
|
|
1683
|
+
if (_cache && now < _cache.expiry) {
|
|
1684
|
+
const mtime = getFileMtime();
|
|
1685
|
+
if (mtime === _cache.mtime) return _cache.hosts;
|
|
1686
|
+
}
|
|
1687
|
+
const hosts = readTrustedHosts();
|
|
1688
|
+
_cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
1689
|
+
return hosts;
|
|
1690
|
+
}
|
|
1626
1691
|
function writeTrustedHosts(hosts) {
|
|
1627
1692
|
const filePath = getTrustedHostsPath();
|
|
1628
1693
|
import_fs6.default.mkdirSync(import_path7.default.dirname(filePath), { recursive: true });
|
|
1629
1694
|
const tmp = filePath + ".node9-tmp";
|
|
1630
|
-
import_fs6.default.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2));
|
|
1695
|
+
import_fs6.default.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2), { mode: 384 });
|
|
1631
1696
|
import_fs6.default.renameSync(tmp, filePath);
|
|
1697
|
+
_cache = { hosts, expiry: Date.now() + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
1632
1698
|
}
|
|
1633
1699
|
function addTrustedHost(host) {
|
|
1700
|
+
const normalized = normalizeHost(host);
|
|
1701
|
+
if (normalized.startsWith("*.")) {
|
|
1702
|
+
const base = normalized.slice(2);
|
|
1703
|
+
if (!base.includes(".")) {
|
|
1704
|
+
throw new Error(
|
|
1705
|
+
`Wildcard pattern '${normalized}' is too broad \u2014 the base domain must have at least one dot (e.g. '*.mycompany.com', not '*.com').`
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1634
1709
|
const hosts = readTrustedHosts();
|
|
1635
|
-
if (hosts.some((h) => h.host ===
|
|
1636
|
-
hosts.push({ host, addedAt: Date.now(), addedBy: "user" });
|
|
1710
|
+
if (hosts.some((h) => h.host === normalized)) return;
|
|
1711
|
+
hosts.push({ host: normalized, addedAt: Date.now(), addedBy: "user" });
|
|
1637
1712
|
writeTrustedHosts(hosts);
|
|
1638
1713
|
}
|
|
1639
1714
|
function removeTrustedHost(host) {
|
|
@@ -1648,22 +1723,24 @@ function normalizeHost(raw) {
|
|
|
1648
1723
|
}
|
|
1649
1724
|
function isTrustedHost(host) {
|
|
1650
1725
|
const normalized = normalizeHost(host);
|
|
1651
|
-
return
|
|
1726
|
+
return getCachedHosts().some((entry) => {
|
|
1652
1727
|
const entryHost = entry.host.toLowerCase();
|
|
1653
1728
|
if (entryHost.startsWith("*.")) {
|
|
1654
1729
|
const domain = entryHost.slice(2);
|
|
1655
|
-
return normalized
|
|
1730
|
+
return normalized.endsWith("." + domain);
|
|
1656
1731
|
}
|
|
1657
1732
|
return normalized === entryHost;
|
|
1658
1733
|
});
|
|
1659
1734
|
}
|
|
1660
|
-
var import_fs6, import_path7, import_os5;
|
|
1735
|
+
var import_fs6, import_path7, import_os5, _cache, CACHE_TTL_MS;
|
|
1661
1736
|
var init_trusted_hosts = __esm({
|
|
1662
1737
|
"src/auth/trusted-hosts.ts"() {
|
|
1663
1738
|
"use strict";
|
|
1664
1739
|
import_fs6 = __toESM(require("fs"));
|
|
1665
1740
|
import_path7 = __toESM(require("path"));
|
|
1666
1741
|
import_os5 = __toESM(require("os"));
|
|
1742
|
+
_cache = null;
|
|
1743
|
+
CACHE_TTL_MS = 5e3;
|
|
1667
1744
|
}
|
|
1668
1745
|
});
|
|
1669
1746
|
|
|
@@ -1681,9 +1758,9 @@ function matchesPattern(text, patterns) {
|
|
|
1681
1758
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1682
1759
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1683
1760
|
}
|
|
1684
|
-
function getNestedValue(obj,
|
|
1761
|
+
function getNestedValue(obj, path28) {
|
|
1685
1762
|
if (!obj || typeof obj !== "object") return null;
|
|
1686
|
-
return
|
|
1763
|
+
return path28.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1687
1764
|
}
|
|
1688
1765
|
function shouldSnapshot(toolName, args, config) {
|
|
1689
1766
|
if (!config.settings.enableUndo) return false;
|
|
@@ -1869,7 +1946,12 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1869
1946
|
};
|
|
1870
1947
|
}
|
|
1871
1948
|
if (allTrusted) {
|
|
1872
|
-
return {
|
|
1949
|
+
return {
|
|
1950
|
+
decision: "allow",
|
|
1951
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
1952
|
+
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
1953
|
+
tier: 3
|
|
1954
|
+
};
|
|
1873
1955
|
}
|
|
1874
1956
|
return {
|
|
1875
1957
|
decision: "review",
|
|
@@ -2411,8 +2493,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2411
2493
|
signal: ctrl.signal
|
|
2412
2494
|
});
|
|
2413
2495
|
if (!res.ok) throw new Error("Daemon fail");
|
|
2414
|
-
const { id } = await res.json();
|
|
2415
|
-
return id;
|
|
2496
|
+
const { id, allowCount } = await res.json();
|
|
2497
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
2416
2498
|
} finally {
|
|
2417
2499
|
clearTimeout(timer);
|
|
2418
2500
|
}
|
|
@@ -2451,15 +2533,67 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
|
2451
2533
|
signal: AbortSignal.timeout(3e3)
|
|
2452
2534
|
});
|
|
2453
2535
|
if (!res.ok) throw new Error("Daemon unreachable");
|
|
2454
|
-
const { id } = await res.json();
|
|
2455
|
-
return id;
|
|
2536
|
+
const { id, allowCount } = await res.json();
|
|
2537
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
2538
|
+
}
|
|
2539
|
+
async function notifyTaint(filePath, source) {
|
|
2540
|
+
if (!isDaemonRunning()) return;
|
|
2541
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2542
|
+
try {
|
|
2543
|
+
await fetch(`${base}/taint`, {
|
|
2544
|
+
method: "POST",
|
|
2545
|
+
headers: { "Content-Type": "application/json" },
|
|
2546
|
+
body: JSON.stringify({ path: filePath, source }),
|
|
2547
|
+
signal: AbortSignal.timeout(1e3)
|
|
2548
|
+
});
|
|
2549
|
+
} catch {
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
async function notifyTaintPropagate(src, dest, clearSource = false) {
|
|
2553
|
+
if (!isDaemonRunning()) return;
|
|
2554
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2555
|
+
try {
|
|
2556
|
+
await fetch(`${base}/taint/propagate`, {
|
|
2557
|
+
method: "POST",
|
|
2558
|
+
headers: { "Content-Type": "application/json" },
|
|
2559
|
+
body: JSON.stringify({ src, dest, clearSource }),
|
|
2560
|
+
signal: AbortSignal.timeout(1e3)
|
|
2561
|
+
});
|
|
2562
|
+
} catch {
|
|
2563
|
+
}
|
|
2456
2564
|
}
|
|
2457
|
-
async function
|
|
2565
|
+
async function checkTaint(paths) {
|
|
2566
|
+
if (paths.length === 0) return { tainted: false };
|
|
2567
|
+
if (!isDaemonRunning()) return { tainted: false, daemonUnavailable: true };
|
|
2568
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2569
|
+
try {
|
|
2570
|
+
const res = await fetch(`${base}/taint/check`, {
|
|
2571
|
+
method: "POST",
|
|
2572
|
+
headers: { "Content-Type": "application/json" },
|
|
2573
|
+
body: JSON.stringify({ paths }),
|
|
2574
|
+
signal: AbortSignal.timeout(2e3)
|
|
2575
|
+
});
|
|
2576
|
+
return await res.json();
|
|
2577
|
+
} catch (err) {
|
|
2578
|
+
try {
|
|
2579
|
+
const { appendToLog: appendToLog2, HOOK_DEBUG_LOG: HOOK_DEBUG_LOG2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
|
|
2580
|
+
appendToLog2(HOOK_DEBUG_LOG2, {
|
|
2581
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2582
|
+
event: "checkTaint-error",
|
|
2583
|
+
error: String(err),
|
|
2584
|
+
paths
|
|
2585
|
+
});
|
|
2586
|
+
} catch {
|
|
2587
|
+
}
|
|
2588
|
+
return { tainted: false, daemonUnavailable: true };
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
2458
2592
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2459
2593
|
await fetch(`${base}/resolve/${id}`, {
|
|
2460
2594
|
method: "POST",
|
|
2461
2595
|
headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
|
|
2462
|
-
body: JSON.stringify({ decision }),
|
|
2596
|
+
body: JSON.stringify({ decision, ...source && { source } }),
|
|
2463
2597
|
signal: AbortSignal.timeout(3e3)
|
|
2464
2598
|
});
|
|
2465
2599
|
}
|
|
@@ -2666,20 +2800,24 @@ ${smartTruncate(str, 500)}`
|
|
|
2666
2800
|
function escapePango(text) {
|
|
2667
2801
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2668
2802
|
}
|
|
2669
|
-
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2803
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2670
2804
|
const lines = [];
|
|
2671
2805
|
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
2672
2806
|
lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
|
|
2673
2807
|
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
2674
2808
|
lines.push("");
|
|
2675
2809
|
lines.push(formattedArgs);
|
|
2810
|
+
if (allowCount >= 3) {
|
|
2811
|
+
lines.push("");
|
|
2812
|
+
lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
|
|
2813
|
+
}
|
|
2676
2814
|
if (!locked) {
|
|
2677
2815
|
lines.push("");
|
|
2678
2816
|
lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
|
|
2679
2817
|
}
|
|
2680
2818
|
return lines.join("\n");
|
|
2681
2819
|
}
|
|
2682
|
-
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2820
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2683
2821
|
const lines = [];
|
|
2684
2822
|
if (locked) {
|
|
2685
2823
|
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
@@ -2691,6 +2829,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2691
2829
|
lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
|
|
2692
2830
|
lines.push("");
|
|
2693
2831
|
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
2832
|
+
if (allowCount >= 3) {
|
|
2833
|
+
lines.push("");
|
|
2834
|
+
lines.push(
|
|
2835
|
+
`<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
|
|
2836
|
+
);
|
|
2837
|
+
}
|
|
2694
2838
|
if (!locked) {
|
|
2695
2839
|
lines.push("");
|
|
2696
2840
|
lines.push(
|
|
@@ -2699,12 +2843,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2699
2843
|
}
|
|
2700
2844
|
return lines.join("\n");
|
|
2701
2845
|
}
|
|
2702
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
|
|
2846
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
|
|
2703
2847
|
if (isTestEnv()) return "deny";
|
|
2704
2848
|
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
2705
2849
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
2706
2850
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
2707
|
-
const message = buildPlainMessage(
|
|
2851
|
+
const message = buildPlainMessage(
|
|
2852
|
+
toolName,
|
|
2853
|
+
formattedArgs,
|
|
2854
|
+
agent,
|
|
2855
|
+
explainableLabel,
|
|
2856
|
+
locked,
|
|
2857
|
+
allowCount
|
|
2858
|
+
);
|
|
2708
2859
|
return new Promise((resolve) => {
|
|
2709
2860
|
let childProcess = null;
|
|
2710
2861
|
const onAbort = () => {
|
|
@@ -2736,7 +2887,8 @@ end run`;
|
|
|
2736
2887
|
formattedArgs,
|
|
2737
2888
|
agent,
|
|
2738
2889
|
explainableLabel,
|
|
2739
|
-
locked
|
|
2890
|
+
locked,
|
|
2891
|
+
allowCount
|
|
2740
2892
|
);
|
|
2741
2893
|
const argsList = [
|
|
2742
2894
|
locked ? "--info" : "--question",
|
|
@@ -2907,6 +3059,40 @@ var init_cloud = __esm({
|
|
|
2907
3059
|
});
|
|
2908
3060
|
|
|
2909
3061
|
// src/auth/orchestrator.ts
|
|
3062
|
+
function isWriteTool(toolName) {
|
|
3063
|
+
const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
3064
|
+
return WRITE_TOOLS.has(t);
|
|
3065
|
+
}
|
|
3066
|
+
function extractFilePaths(toolName, args) {
|
|
3067
|
+
const paths = [];
|
|
3068
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return paths;
|
|
3069
|
+
const a = args;
|
|
3070
|
+
for (const key of ["file_path", "path", "filename", "source", "src", "input"]) {
|
|
3071
|
+
if (typeof a[key] === "string" && a[key]) paths.push(a[key]);
|
|
3072
|
+
}
|
|
3073
|
+
const cmd = typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : "";
|
|
3074
|
+
if (cmd) {
|
|
3075
|
+
for (const m of cmd.matchAll(/(?:-T|--upload-file|--data(?:-binary)?)\s+@?(\S+)/g)) {
|
|
3076
|
+
paths.push(m[1]);
|
|
3077
|
+
}
|
|
3078
|
+
for (const m of cmd.matchAll(/\b(?:scp|rsync)\s+(?:-\S+\s+)*(\S+)\s+\S+@/g)) {
|
|
3079
|
+
paths.push(m[1]);
|
|
3080
|
+
}
|
|
3081
|
+
for (const m of cmd.matchAll(/<\s*(\S+)/g)) {
|
|
3082
|
+
paths.push(m[1]);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
return paths.filter(Boolean);
|
|
3086
|
+
}
|
|
3087
|
+
function isNetworkTool(toolName, args) {
|
|
3088
|
+
const t = toolName.toLowerCase();
|
|
3089
|
+
if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal.execute") {
|
|
3090
|
+
const a = args;
|
|
3091
|
+
const cmd = typeof a?.command === "string" ? a.command : typeof a?.cmd === "string" ? a.cmd : "";
|
|
3092
|
+
return /\b(curl|wget|scp|rsync|nc|ncat|netcat|ssh)\b/.test(cmd);
|
|
3093
|
+
}
|
|
3094
|
+
return false;
|
|
3095
|
+
}
|
|
2910
3096
|
function notifyActivity(data) {
|
|
2911
3097
|
return new Promise((resolve) => {
|
|
2912
3098
|
try {
|
|
@@ -2924,7 +3110,7 @@ function notifyActivity(data) {
|
|
|
2924
3110
|
}
|
|
2925
3111
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
2926
3112
|
if (!options?.calledFromDaemon) {
|
|
2927
|
-
const actId = (0,
|
|
3113
|
+
const actId = (0, import_crypto3.randomUUID)();
|
|
2928
3114
|
const actTs = Date.now();
|
|
2929
3115
|
await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
|
|
2930
3116
|
const result = await _authorizeHeadlessCore(toolName, args, meta, {
|
|
@@ -2936,7 +3122,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
2936
3122
|
id: actId,
|
|
2937
3123
|
tool: toolName,
|
|
2938
3124
|
ts: actTs,
|
|
2939
|
-
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
3125
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
2940
3126
|
label: result.blockedByLabel
|
|
2941
3127
|
});
|
|
2942
3128
|
}
|
|
@@ -2950,6 +3136,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2950
3136
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
2951
3137
|
const creds = getCredentials();
|
|
2952
3138
|
const config = getConfig(options?.cwd);
|
|
3139
|
+
const hashAuditArgs = config.settings.auditHashArgs === true;
|
|
2953
3140
|
const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
|
|
2954
3141
|
const approvers = {
|
|
2955
3142
|
...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
|
|
@@ -2960,13 +3147,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2960
3147
|
approvers.terminal = false;
|
|
2961
3148
|
}
|
|
2962
3149
|
if (config.settings.enableHookLogDebug && !isTestEnv2) {
|
|
2963
|
-
appendHookDebug(toolName, args, meta);
|
|
3150
|
+
appendHookDebug(toolName, args, meta, hashAuditArgs);
|
|
2964
3151
|
}
|
|
2965
3152
|
const isManual = meta?.agent === "Terminal";
|
|
2966
3153
|
let explainableLabel = "Local Config";
|
|
2967
3154
|
let policyMatchedField;
|
|
2968
3155
|
let policyMatchedWord;
|
|
2969
3156
|
let riskMetadata;
|
|
3157
|
+
let taintWarning = null;
|
|
3158
|
+
if (isNetworkTool(toolName, args)) {
|
|
3159
|
+
const filePaths = extractFilePaths(toolName, args);
|
|
3160
|
+
if (filePaths.length > 0) {
|
|
3161
|
+
const taintResult = await checkTaint(filePaths);
|
|
3162
|
+
if (taintResult.tainted && taintResult.record) {
|
|
3163
|
+
const { path: taintedPath, source: taintSource } = taintResult.record;
|
|
3164
|
+
taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
|
|
3165
|
+
} else if (taintResult.daemonUnavailable) {
|
|
3166
|
+
taintWarning = `\u26A0\uFE0F Taint service unavailable \u2014 cannot verify if ${filePaths.join(", ")} is clean`;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
2970
3170
|
if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
2971
3171
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
2972
3172
|
const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
|
|
@@ -2974,7 +3174,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2974
3174
|
if (dlpMatch) {
|
|
2975
3175
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
2976
3176
|
if (dlpMatch.severity === "block") {
|
|
2977
|
-
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
|
|
3177
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta, true);
|
|
3178
|
+
if (isWriteTool(toolName) && filePath) {
|
|
3179
|
+
await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
|
|
3180
|
+
}
|
|
2978
3181
|
return {
|
|
2979
3182
|
approved: false,
|
|
2980
3183
|
reason: dlpReason,
|
|
@@ -2982,7 +3185,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2982
3185
|
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
2983
3186
|
};
|
|
2984
3187
|
}
|
|
2985
|
-
if (!isManual)
|
|
3188
|
+
if (!isManual)
|
|
3189
|
+
appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta, hashAuditArgs);
|
|
2986
3190
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
2987
3191
|
}
|
|
2988
3192
|
}
|
|
@@ -2990,7 +3194,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2990
3194
|
if (!isIgnoredTool(toolName)) {
|
|
2991
3195
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
2992
3196
|
if (policyResult.decision === "review") {
|
|
2993
|
-
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
3197
|
+
appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
|
|
2994
3198
|
if (approvers.cloud && creds?.apiKey) {
|
|
2995
3199
|
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
2996
3200
|
}
|
|
@@ -2998,22 +3202,23 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2998
3202
|
}
|
|
2999
3203
|
return { approved: true, checkedBy: "audit" };
|
|
3000
3204
|
}
|
|
3001
|
-
if (!isIgnoredTool(toolName)) {
|
|
3205
|
+
if (!taintWarning && !isIgnoredTool(toolName)) {
|
|
3002
3206
|
if (getActiveTrustSession(toolName)) {
|
|
3003
3207
|
if (approvers.cloud && creds?.apiKey)
|
|
3004
3208
|
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
3005
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
3209
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
|
|
3006
3210
|
return { approved: true, checkedBy: "trust" };
|
|
3007
3211
|
}
|
|
3008
3212
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
3009
3213
|
if (policyResult.decision === "allow") {
|
|
3010
3214
|
if (approvers.cloud && creds?.apiKey)
|
|
3011
3215
|
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
3012
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
3216
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
|
|
3013
3217
|
return { approved: true, checkedBy: "local-policy" };
|
|
3014
3218
|
}
|
|
3015
3219
|
if (policyResult.decision === "block") {
|
|
3016
|
-
if (!isManual)
|
|
3220
|
+
if (!isManual)
|
|
3221
|
+
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
3017
3222
|
return {
|
|
3018
3223
|
approved: false,
|
|
3019
3224
|
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
@@ -3032,15 +3237,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3032
3237
|
policyMatchedWord,
|
|
3033
3238
|
policyResult.ruleName
|
|
3034
3239
|
);
|
|
3035
|
-
const persistent = getPersistentDecision(toolName);
|
|
3240
|
+
const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
|
|
3036
3241
|
if (persistent === "allow") {
|
|
3037
3242
|
if (approvers.cloud && creds?.apiKey)
|
|
3038
3243
|
await auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
3039
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
3244
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
|
|
3040
3245
|
return { approved: true, checkedBy: "persistent" };
|
|
3041
3246
|
}
|
|
3042
3247
|
if (persistent === "deny") {
|
|
3043
|
-
if (!isManual)
|
|
3248
|
+
if (!isManual)
|
|
3249
|
+
appendLocalAudit(toolName, args, "deny", "persistent-deny", meta, hashAuditArgs);
|
|
3044
3250
|
return {
|
|
3045
3251
|
approved: false,
|
|
3046
3252
|
reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
|
|
@@ -3048,10 +3254,21 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3048
3254
|
blockedByLabel: "Persistent User Rule"
|
|
3049
3255
|
};
|
|
3050
3256
|
}
|
|
3051
|
-
} else {
|
|
3052
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
3257
|
+
} else if (!taintWarning) {
|
|
3258
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
|
|
3053
3259
|
return { approved: true };
|
|
3054
3260
|
}
|
|
3261
|
+
if (taintWarning) {
|
|
3262
|
+
explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
|
|
3263
|
+
riskMetadata = computeRiskMetadata(
|
|
3264
|
+
args,
|
|
3265
|
+
7,
|
|
3266
|
+
explainableLabel,
|
|
3267
|
+
void 0,
|
|
3268
|
+
void 0,
|
|
3269
|
+
taintWarning
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
3055
3272
|
let cloudRequestId = null;
|
|
3056
3273
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
3057
3274
|
if (cloudEnforced) {
|
|
@@ -3070,7 +3287,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3070
3287
|
};
|
|
3071
3288
|
}
|
|
3072
3289
|
cloudRequestId = initResult.requestId || null;
|
|
3073
|
-
explainableLabel = "Organization Policy (SaaS)";
|
|
3290
|
+
if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
|
|
3074
3291
|
} catch {
|
|
3075
3292
|
}
|
|
3076
3293
|
}
|
|
@@ -3099,13 +3316,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3099
3316
|
let viewerId = null;
|
|
3100
3317
|
const internalToken = getInternalToken();
|
|
3101
3318
|
let daemonEntryId = null;
|
|
3319
|
+
let daemonAllowCount = 1;
|
|
3102
3320
|
if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
3103
3321
|
if (cloudEnforced && cloudRequestId) {
|
|
3104
|
-
|
|
3322
|
+
const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
|
|
3323
|
+
viewerId = viewer?.id ?? null;
|
|
3105
3324
|
daemonEntryId = viewerId;
|
|
3325
|
+
if (viewer) daemonAllowCount = viewer.allowCount;
|
|
3106
3326
|
} else {
|
|
3107
3327
|
try {
|
|
3108
|
-
|
|
3328
|
+
const entry = await registerDaemonEntry(
|
|
3109
3329
|
toolName,
|
|
3110
3330
|
args,
|
|
3111
3331
|
meta,
|
|
@@ -3113,6 +3333,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3113
3333
|
options?.activityId,
|
|
3114
3334
|
options?.cwd
|
|
3115
3335
|
);
|
|
3336
|
+
daemonEntryId = entry.id;
|
|
3337
|
+
daemonAllowCount = entry.allowCount;
|
|
3116
3338
|
} catch {
|
|
3117
3339
|
}
|
|
3118
3340
|
}
|
|
@@ -3148,7 +3370,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3148
3370
|
false,
|
|
3149
3371
|
signal,
|
|
3150
3372
|
policyMatchedField,
|
|
3151
|
-
policyMatchedWord
|
|
3373
|
+
policyMatchedWord,
|
|
3374
|
+
daemonAllowCount
|
|
3152
3375
|
);
|
|
3153
3376
|
if (decision === "always_allow") {
|
|
3154
3377
|
writeTrustSession(toolName, 36e5);
|
|
@@ -3206,10 +3429,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3206
3429
|
if (!resolved) {
|
|
3207
3430
|
resolved = true;
|
|
3208
3431
|
abortController.abort();
|
|
3209
|
-
if (
|
|
3210
|
-
resolveViaDaemon(
|
|
3211
|
-
|
|
3212
|
-
|
|
3432
|
+
if (daemonEntryId && internalToken) {
|
|
3433
|
+
resolveViaDaemon(
|
|
3434
|
+
daemonEntryId,
|
|
3435
|
+
res.approved ? "allow" : "deny",
|
|
3436
|
+
internalToken,
|
|
3437
|
+
res.decisionSource
|
|
3438
|
+
).catch(() => null);
|
|
3213
3439
|
}
|
|
3214
3440
|
resolve(res);
|
|
3215
3441
|
}
|
|
@@ -3248,19 +3474,20 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3248
3474
|
args,
|
|
3249
3475
|
finalResult.approved ? "allow" : "deny",
|
|
3250
3476
|
finalResult.checkedBy || finalResult.blockedBy || "unknown",
|
|
3251
|
-
meta
|
|
3477
|
+
meta,
|
|
3478
|
+
hashAuditArgs
|
|
3252
3479
|
);
|
|
3253
3480
|
}
|
|
3254
3481
|
return finalResult;
|
|
3255
3482
|
}
|
|
3256
|
-
var import_net, import_path13, import_os10,
|
|
3483
|
+
var import_net, import_path13, import_os10, import_crypto3, WRITE_TOOLS, ACTIVITY_SOCKET_PATH;
|
|
3257
3484
|
var init_orchestrator = __esm({
|
|
3258
3485
|
"src/auth/orchestrator.ts"() {
|
|
3259
3486
|
"use strict";
|
|
3260
3487
|
import_net = __toESM(require("net"));
|
|
3261
3488
|
import_path13 = __toESM(require("path"));
|
|
3262
3489
|
import_os10 = __toESM(require("os"));
|
|
3263
|
-
|
|
3490
|
+
import_crypto3 = require("crypto");
|
|
3264
3491
|
init_native();
|
|
3265
3492
|
init_context_sniper();
|
|
3266
3493
|
init_dlp();
|
|
@@ -3270,6 +3497,17 @@ var init_orchestrator = __esm({
|
|
|
3270
3497
|
init_state();
|
|
3271
3498
|
init_daemon();
|
|
3272
3499
|
init_cloud();
|
|
3500
|
+
WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
3501
|
+
"write",
|
|
3502
|
+
"write_file",
|
|
3503
|
+
"create_file",
|
|
3504
|
+
"edit",
|
|
3505
|
+
"multiedit",
|
|
3506
|
+
"str_replace_based_edit_tool",
|
|
3507
|
+
"replace",
|
|
3508
|
+
"notebook_edit",
|
|
3509
|
+
"notebookedit"
|
|
3510
|
+
]);
|
|
3273
3511
|
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path13.default.join(import_os10.default.tmpdir(), "node9-activity.sock");
|
|
3274
3512
|
}
|
|
3275
3513
|
});
|
|
@@ -3566,6 +3804,15 @@ var init_ui = __esm({
|
|
|
3566
3804
|
padding: 5px 10px;
|
|
3567
3805
|
margin-bottom: 14px;
|
|
3568
3806
|
}
|
|
3807
|
+
.insight-hint {
|
|
3808
|
+
font-size: 12px;
|
|
3809
|
+
color: #f0c040;
|
|
3810
|
+
background: rgba(240, 192, 64, 0.08);
|
|
3811
|
+
border: 1px solid rgba(240, 192, 64, 0.25);
|
|
3812
|
+
border-radius: 6px;
|
|
3813
|
+
padding: 6px 10px;
|
|
3814
|
+
margin-bottom: 12px;
|
|
3815
|
+
}
|
|
3569
3816
|
pre {
|
|
3570
3817
|
background: #0d1117;
|
|
3571
3818
|
padding: 14px 16px;
|
|
@@ -4038,6 +4285,78 @@ var init_ui = __esm({
|
|
|
4038
4285
|
color: var(--danger);
|
|
4039
4286
|
}
|
|
4040
4287
|
|
|
4288
|
+
/* \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 */
|
|
4289
|
+
.suggestion-card {
|
|
4290
|
+
background: rgba(82, 130, 255, 0.06);
|
|
4291
|
+
border: 1px solid rgba(82, 130, 255, 0.25);
|
|
4292
|
+
border-radius: 8px;
|
|
4293
|
+
padding: 10px 12px;
|
|
4294
|
+
margin-bottom: 8px;
|
|
4295
|
+
}
|
|
4296
|
+
.suggestion-card:last-child {
|
|
4297
|
+
margin-bottom: 0;
|
|
4298
|
+
}
|
|
4299
|
+
.suggestion-header {
|
|
4300
|
+
display: flex;
|
|
4301
|
+
align-items: center;
|
|
4302
|
+
gap: 8px;
|
|
4303
|
+
margin-bottom: 6px;
|
|
4304
|
+
}
|
|
4305
|
+
.suggestion-tool {
|
|
4306
|
+
font-family: 'Fira Code', monospace;
|
|
4307
|
+
font-size: 11px;
|
|
4308
|
+
color: var(--text-bright);
|
|
4309
|
+
flex: 1;
|
|
4310
|
+
word-break: break-all;
|
|
4311
|
+
}
|
|
4312
|
+
.suggestion-count {
|
|
4313
|
+
font-size: 10px;
|
|
4314
|
+
color: var(--muted);
|
|
4315
|
+
white-space: nowrap;
|
|
4316
|
+
}
|
|
4317
|
+
.suggestion-rule {
|
|
4318
|
+
font-family: 'Fira Code', monospace;
|
|
4319
|
+
font-size: 10px;
|
|
4320
|
+
color: #79c0ff;
|
|
4321
|
+
background: rgba(0, 0, 0, 0.25);
|
|
4322
|
+
border-radius: 4px;
|
|
4323
|
+
padding: 4px 8px;
|
|
4324
|
+
margin-bottom: 8px;
|
|
4325
|
+
word-break: break-all;
|
|
4326
|
+
white-space: pre-wrap;
|
|
4327
|
+
}
|
|
4328
|
+
.suggestion-actions {
|
|
4329
|
+
display: flex;
|
|
4330
|
+
gap: 6px;
|
|
4331
|
+
}
|
|
4332
|
+
.btn-apply {
|
|
4333
|
+
background: rgba(52, 125, 57, 0.2);
|
|
4334
|
+
border: 1px solid rgba(87, 171, 90, 0.4);
|
|
4335
|
+
color: #57ab5a;
|
|
4336
|
+
padding: 4px 10px;
|
|
4337
|
+
font-size: 11px;
|
|
4338
|
+
border-radius: 5px;
|
|
4339
|
+
font-family: inherit;
|
|
4340
|
+
cursor: pointer;
|
|
4341
|
+
}
|
|
4342
|
+
.btn-apply:hover {
|
|
4343
|
+
background: rgba(52, 125, 57, 0.35);
|
|
4344
|
+
}
|
|
4345
|
+
.btn-dismiss-suggestion {
|
|
4346
|
+
background: transparent;
|
|
4347
|
+
border: 1px solid var(--border);
|
|
4348
|
+
color: var(--muted);
|
|
4349
|
+
padding: 4px 10px;
|
|
4350
|
+
font-size: 11px;
|
|
4351
|
+
border-radius: 5px;
|
|
4352
|
+
font-family: inherit;
|
|
4353
|
+
cursor: pointer;
|
|
4354
|
+
}
|
|
4355
|
+
.btn-dismiss-suggestion:hover {
|
|
4356
|
+
border-color: var(--danger);
|
|
4357
|
+
color: var(--danger);
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4041
4360
|
.modal-overlay {
|
|
4042
4361
|
display: none;
|
|
4043
4362
|
position: fixed;
|
|
@@ -4219,6 +4538,11 @@ var init_ui = __esm({
|
|
|
4219
4538
|
<div class="panel-title">\u{1F4CB} Persistent Decisions</div>
|
|
4220
4539
|
<div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
|
|
4221
4540
|
</div>
|
|
4541
|
+
|
|
4542
|
+
<div class="panel" id="suggestionsPanel" style="display: none">
|
|
4543
|
+
<div class="panel-title">\u{1F4A1} Smart Rule Suggestions</div>
|
|
4544
|
+
<div id="suggestionsList"></div>
|
|
4545
|
+
</div>
|
|
4222
4546
|
</div>
|
|
4223
4547
|
</div>
|
|
4224
4548
|
</div>
|
|
@@ -4368,12 +4692,15 @@ var init_ui = __esm({
|
|
|
4368
4692
|
const badgeClass = isEdit ? 'sniper-badge-edit' : 'sniper-badge-exec';
|
|
4369
4693
|
const badgeLabel = isEdit ? '\u{1F4DD} Code Edit' : '\u{1F6D1} Execution';
|
|
4370
4694
|
const tierLabel = \`Tier \${rm.tier} \xB7 \${esc(rm.blockedByLabel)}\`;
|
|
4695
|
+
const isTaint = rm.blockedByLabel?.includes('Taint');
|
|
4371
4696
|
const fileLine =
|
|
4372
|
-
|
|
4373
|
-
? \`<div class="sniper-
|
|
4374
|
-
:
|
|
4375
|
-
? \`<div class="sniper-
|
|
4376
|
-
:
|
|
4697
|
+
isTaint && rm.ruleName
|
|
4698
|
+
? \`<div class="sniper-match">\u26A0\uFE0F \${esc(rm.ruleName)}</div>\`
|
|
4699
|
+
: isEdit && rm.editFilePath
|
|
4700
|
+
? \`<div class="sniper-filepath">\u{1F4C2} \${esc(rm.editFilePath)}</div>\`
|
|
4701
|
+
: !isEdit && rm.matchedWord
|
|
4702
|
+
? \`<div class="sniper-match">Matched: <code>\${esc(rm.matchedWord)}</code>\${rm.matchedField ? \` in <code>\${esc(rm.matchedField)}</code>\` : ''}</div>\`
|
|
4703
|
+
: '';
|
|
4377
4704
|
const snippetHtml = rm.contextSnippet ? \`<pre>\${esc(rm.contextSnippet)}</pre>\` : '';
|
|
4378
4705
|
return \`
|
|
4379
4706
|
<div class="sniper-header">
|
|
@@ -4408,6 +4735,7 @@ var init_ui = __esm({
|
|
|
4408
4735
|
</div>
|
|
4409
4736
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
4410
4737
|
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
|
|
4738
|
+
\${req.allowCount >= 3 ? \`<div class="insight-hint">\u{1F4A1} Approved \${req.allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</div>\` : ''}
|
|
4411
4739
|
\${renderPayload(req)}
|
|
4412
4740
|
<div class="actions" id="act-\${req.id}">
|
|
4413
4741
|
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
|
|
@@ -4474,6 +4802,14 @@ var init_ui = __esm({
|
|
|
4474
4802
|
ev.addEventListener('shields-status', (e) => {
|
|
4475
4803
|
renderShields(JSON.parse(e.data).shields);
|
|
4476
4804
|
});
|
|
4805
|
+
ev.addEventListener('suggestion:new', (e) => {
|
|
4806
|
+
const s = JSON.parse(e.data);
|
|
4807
|
+
addSuggestionCard(s);
|
|
4808
|
+
});
|
|
4809
|
+
ev.addEventListener('suggestion:resolved', (e) => {
|
|
4810
|
+
const { id } = JSON.parse(e.data);
|
|
4811
|
+
removeSuggestionCard(id);
|
|
4812
|
+
});
|
|
4477
4813
|
|
|
4478
4814
|
// \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
|
|
4479
4815
|
ev.addEventListener('activity', (e) => {
|
|
@@ -4723,6 +5059,74 @@ var init_ui = __esm({
|
|
|
4723
5059
|
.then((r) => r.json())
|
|
4724
5060
|
.then(renderDecisions)
|
|
4725
5061
|
.catch(() => {});
|
|
5062
|
+
|
|
5063
|
+
// \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
|
|
5064
|
+
function rulePreview(suggestion) {
|
|
5065
|
+
const r = suggestion.suggestedRule;
|
|
5066
|
+
if (r.type === 'ignoredTool') return \`ignoredTool: "\${r.toolName}"\`;
|
|
5067
|
+
const cond = r.rule.conditions?.[0];
|
|
5068
|
+
const condStr = cond ? \` where \${cond.field} \${cond.op} "\${cond.value}"\` : '';
|
|
5069
|
+
return \`allow \${r.rule.tool}\${condStr}\`;
|
|
5070
|
+
}
|
|
5071
|
+
|
|
5072
|
+
function addSuggestionCard(s) {
|
|
5073
|
+
const panel = document.getElementById('suggestionsPanel');
|
|
5074
|
+
const list = document.getElementById('suggestionsList');
|
|
5075
|
+
panel.style.display = '';
|
|
5076
|
+
|
|
5077
|
+
const card = document.createElement('div');
|
|
5078
|
+
card.className = 'suggestion-card';
|
|
5079
|
+
card.id = 'sg-' + s.id;
|
|
5080
|
+
card.innerHTML = \`
|
|
5081
|
+
<div class="suggestion-header">
|
|
5082
|
+
<span class="suggestion-tool">\${esc(s.toolName)}</span>
|
|
5083
|
+
<span class="suggestion-count">allowed \${s.allowCount}\xD7</span>
|
|
5084
|
+
</div>
|
|
5085
|
+
<div class="suggestion-rule">\${esc(rulePreview(s))}</div>
|
|
5086
|
+
<div class="suggestion-actions">
|
|
5087
|
+
<button class="btn-apply" onclick="applySuggestion('\${esc(s.id)}')">Apply rule</button>
|
|
5088
|
+
<button class="btn-dismiss-suggestion" onclick="dismissSuggestion('\${esc(s.id)}')">Dismiss</button>
|
|
5089
|
+
</div>
|
|
5090
|
+
\`;
|
|
5091
|
+
list.appendChild(card);
|
|
5092
|
+
}
|
|
5093
|
+
|
|
5094
|
+
function removeSuggestionCard(id) {
|
|
5095
|
+
document.getElementById('sg-' + id)?.remove();
|
|
5096
|
+
const list = document.getElementById('suggestionsList');
|
|
5097
|
+
if (!list.querySelector('.suggestion-card')) {
|
|
5098
|
+
document.getElementById('suggestionsPanel').style.display = 'none';
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
|
|
5102
|
+
function applySuggestion(id) {
|
|
5103
|
+
fetch('/suggestions/' + id + '/apply', {
|
|
5104
|
+
method: 'POST',
|
|
5105
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
5106
|
+
body: JSON.stringify({}),
|
|
5107
|
+
})
|
|
5108
|
+
.then((r) => {
|
|
5109
|
+
if (r.ok) removeSuggestionCard(id);
|
|
5110
|
+
})
|
|
5111
|
+
.catch(() => {});
|
|
5112
|
+
}
|
|
5113
|
+
|
|
5114
|
+
function dismissSuggestion(id) {
|
|
5115
|
+
fetch('/suggestions/' + id + '/dismiss', {
|
|
5116
|
+
method: 'POST',
|
|
5117
|
+
headers: { 'X-Node9-Token': CSRF_TOKEN },
|
|
5118
|
+
})
|
|
5119
|
+
.then((r) => {
|
|
5120
|
+
if (r.ok) removeSuggestionCard(id);
|
|
5121
|
+
})
|
|
5122
|
+
.catch(() => {});
|
|
5123
|
+
}
|
|
5124
|
+
|
|
5125
|
+
// Load any suggestions that survived a page reload (daemon still running)
|
|
5126
|
+
fetch('/suggestions')
|
|
5127
|
+
.then((r) => r.json())
|
|
5128
|
+
.then((list) => list.filter((s) => s.status === 'pending').forEach(addSuggestionCard))
|
|
5129
|
+
.catch(() => {});
|
|
4726
5130
|
</script>
|
|
4727
5131
|
</body>
|
|
4728
5132
|
</html>
|
|
@@ -4740,7 +5144,197 @@ var init_ui2 = __esm({
|
|
|
4740
5144
|
}
|
|
4741
5145
|
});
|
|
4742
5146
|
|
|
5147
|
+
// src/daemon/suggestion-tracker.ts
|
|
5148
|
+
function extractPath(args) {
|
|
5149
|
+
if (!args || typeof args !== "object") return null;
|
|
5150
|
+
const a = args;
|
|
5151
|
+
for (const key of ["path", "file_path", "filename", "filepath", "dest", "destination"]) {
|
|
5152
|
+
if (typeof a[key] === "string" && a[key]) return a[key];
|
|
5153
|
+
}
|
|
5154
|
+
return null;
|
|
5155
|
+
}
|
|
5156
|
+
function commonPathPrefix(paths) {
|
|
5157
|
+
if (paths.length < 2) return null;
|
|
5158
|
+
const dirParts = paths.map((p) => {
|
|
5159
|
+
const lastSlash = p.lastIndexOf("/");
|
|
5160
|
+
return lastSlash > 0 ? p.slice(0, lastSlash + 1) : "/";
|
|
5161
|
+
});
|
|
5162
|
+
const first = dirParts[0].split("/");
|
|
5163
|
+
const common = [];
|
|
5164
|
+
for (let i = 0; i < first.length; i++) {
|
|
5165
|
+
if (dirParts.every((d) => d.split("/")[i] === first[i])) {
|
|
5166
|
+
common.push(first[i]);
|
|
5167
|
+
} else {
|
|
5168
|
+
break;
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
const prefix = common.join("/").replace(/\/?$/, "/");
|
|
5172
|
+
return prefix.length > 1 ? prefix : null;
|
|
5173
|
+
}
|
|
5174
|
+
var import_crypto4, SuggestionTracker;
|
|
5175
|
+
var init_suggestion_tracker = __esm({
|
|
5176
|
+
"src/daemon/suggestion-tracker.ts"() {
|
|
5177
|
+
"use strict";
|
|
5178
|
+
import_crypto4 = require("crypto");
|
|
5179
|
+
SuggestionTracker = class {
|
|
5180
|
+
events = /* @__PURE__ */ new Map();
|
|
5181
|
+
threshold;
|
|
5182
|
+
constructor(threshold = 3) {
|
|
5183
|
+
this.threshold = threshold;
|
|
5184
|
+
}
|
|
5185
|
+
/**
|
|
5186
|
+
* Record a human-allowed review for a tool.
|
|
5187
|
+
* Returns a Suggestion when the threshold is reached, null otherwise.
|
|
5188
|
+
*/
|
|
5189
|
+
recordAllow(toolName, args) {
|
|
5190
|
+
const events = this.events.get(toolName) ?? [];
|
|
5191
|
+
events.push({ args, ts: Date.now() });
|
|
5192
|
+
this.events.set(toolName, events);
|
|
5193
|
+
if (events.length >= this.threshold) {
|
|
5194
|
+
this.events.delete(toolName);
|
|
5195
|
+
return this.generateSuggestion(toolName, events);
|
|
5196
|
+
}
|
|
5197
|
+
return null;
|
|
5198
|
+
}
|
|
5199
|
+
/**
|
|
5200
|
+
* Reset the counter for a tool (e.g. when the user clicks Deny —
|
|
5201
|
+
* don't suggest allowing something they just blocked).
|
|
5202
|
+
*/
|
|
5203
|
+
resetTool(toolName) {
|
|
5204
|
+
this.events.delete(toolName);
|
|
5205
|
+
}
|
|
5206
|
+
/** Current allow count for a tool (for tests). */
|
|
5207
|
+
getCount(toolName) {
|
|
5208
|
+
return this.events.get(toolName)?.length ?? 0;
|
|
5209
|
+
}
|
|
5210
|
+
generateSuggestion(toolName, events) {
|
|
5211
|
+
const paths = events.map((e) => extractPath(e.args)).filter((p) => typeof p === "string" && p.length > 0);
|
|
5212
|
+
const prefix = commonPathPrefix(paths);
|
|
5213
|
+
const suggestedRule = prefix ? {
|
|
5214
|
+
type: "smartRule",
|
|
5215
|
+
rule: {
|
|
5216
|
+
name: `allow-${toolName}-${prefix.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}`,
|
|
5217
|
+
tool: toolName,
|
|
5218
|
+
conditions: [{ field: "path", op: "matchesGlob", value: `${prefix}**` }],
|
|
5219
|
+
verdict: "allow",
|
|
5220
|
+
reason: `Auto-suggested: ${toolName} allowed ${events.length}\xD7 in ${prefix}`
|
|
5221
|
+
}
|
|
5222
|
+
} : { type: "ignoredTool", toolName };
|
|
5223
|
+
return {
|
|
5224
|
+
id: (0, import_crypto4.randomUUID)(),
|
|
5225
|
+
toolName,
|
|
5226
|
+
allowCount: events.length,
|
|
5227
|
+
suggestedRule,
|
|
5228
|
+
status: "pending",
|
|
5229
|
+
createdAt: Date.now(),
|
|
5230
|
+
exampleArgs: events.slice(0, 3).map((e) => e.args)
|
|
5231
|
+
};
|
|
5232
|
+
}
|
|
5233
|
+
};
|
|
5234
|
+
}
|
|
5235
|
+
});
|
|
5236
|
+
|
|
5237
|
+
// src/daemon/taint-store.ts
|
|
5238
|
+
var import_fs12, import_path15, DEFAULT_TTL_MS, TaintStore;
|
|
5239
|
+
var init_taint_store = __esm({
|
|
5240
|
+
"src/daemon/taint-store.ts"() {
|
|
5241
|
+
"use strict";
|
|
5242
|
+
import_fs12 = __toESM(require("fs"));
|
|
5243
|
+
import_path15 = __toESM(require("path"));
|
|
5244
|
+
DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
5245
|
+
TaintStore = class {
|
|
5246
|
+
records = /* @__PURE__ */ new Map();
|
|
5247
|
+
/** Add or refresh taint on an absolute path. */
|
|
5248
|
+
taint(filePath, source, ttlMs = DEFAULT_TTL_MS) {
|
|
5249
|
+
const resolved = this._resolve(filePath);
|
|
5250
|
+
const now = Date.now();
|
|
5251
|
+
this.records.set(resolved, {
|
|
5252
|
+
path: resolved,
|
|
5253
|
+
source,
|
|
5254
|
+
createdAt: now,
|
|
5255
|
+
expiresAt: now + ttlMs
|
|
5256
|
+
});
|
|
5257
|
+
}
|
|
5258
|
+
/**
|
|
5259
|
+
* Check whether a path is currently tainted.
|
|
5260
|
+
* Returns the TaintRecord if tainted (and not expired), null otherwise.
|
|
5261
|
+
* Expired records are pruned on access.
|
|
5262
|
+
*/
|
|
5263
|
+
check(filePath) {
|
|
5264
|
+
const resolved = this._resolve(filePath);
|
|
5265
|
+
const record = this.records.get(resolved);
|
|
5266
|
+
if (!record) return null;
|
|
5267
|
+
if (Date.now() > record.expiresAt) {
|
|
5268
|
+
this.records.delete(resolved);
|
|
5269
|
+
return null;
|
|
5270
|
+
}
|
|
5271
|
+
return record;
|
|
5272
|
+
}
|
|
5273
|
+
/**
|
|
5274
|
+
* Propagate taint from sourcePath to destPath (e.g. cp, mv).
|
|
5275
|
+
* For mv semantics (clearSource=true) the source taint is removed.
|
|
5276
|
+
*/
|
|
5277
|
+
propagate(sourcePath, destPath, clearSource = false) {
|
|
5278
|
+
const taintRecord = this.check(sourcePath);
|
|
5279
|
+
if (!taintRecord) return;
|
|
5280
|
+
const remainingMs = taintRecord.expiresAt - Date.now();
|
|
5281
|
+
if (remainingMs > 0) {
|
|
5282
|
+
const baseSource = taintRecord.source.replace(/^(propagated:)+/, "");
|
|
5283
|
+
this.taint(destPath, `propagated:${baseSource}`, remainingMs);
|
|
5284
|
+
}
|
|
5285
|
+
if (clearSource) {
|
|
5286
|
+
this.records.delete(this._resolve(sourcePath));
|
|
5287
|
+
}
|
|
5288
|
+
}
|
|
5289
|
+
/** Remove all expired records. Called periodically by the daemon. */
|
|
5290
|
+
prune() {
|
|
5291
|
+
const now = Date.now();
|
|
5292
|
+
for (const [key, record] of this.records) {
|
|
5293
|
+
if (now > record.expiresAt) this.records.delete(key);
|
|
5294
|
+
}
|
|
5295
|
+
}
|
|
5296
|
+
/** Return all non-expired taint records (for audit/debug). */
|
|
5297
|
+
list() {
|
|
5298
|
+
this.prune();
|
|
5299
|
+
return [...this.records.values()];
|
|
5300
|
+
}
|
|
5301
|
+
/** Remove all taint records atomically. Used by tests to reset state between runs. */
|
|
5302
|
+
clear() {
|
|
5303
|
+
this.records.clear();
|
|
5304
|
+
}
|
|
5305
|
+
/** Resolve to absolute path, falling back to path.resolve if file doesn't exist yet. */
|
|
5306
|
+
_resolve(filePath) {
|
|
5307
|
+
try {
|
|
5308
|
+
return import_fs12.default.realpathSync.native(import_path15.default.resolve(filePath));
|
|
5309
|
+
} catch {
|
|
5310
|
+
return import_path15.default.resolve(filePath);
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
};
|
|
5314
|
+
}
|
|
5315
|
+
});
|
|
5316
|
+
|
|
4743
5317
|
// src/daemon/state.ts
|
|
5318
|
+
function loadInsightCounts() {
|
|
5319
|
+
try {
|
|
5320
|
+
if (!import_fs13.default.existsSync(INSIGHT_COUNTS_FILE)) return;
|
|
5321
|
+
const data = JSON.parse(import_fs13.default.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
|
|
5322
|
+
for (const [tool, count] of Object.entries(data)) {
|
|
5323
|
+
if (typeof count === "number" && count > 0) insightCounts.set(tool, count);
|
|
5324
|
+
}
|
|
5325
|
+
} catch {
|
|
5326
|
+
}
|
|
5327
|
+
}
|
|
5328
|
+
function saveInsightCounts() {
|
|
5329
|
+
try {
|
|
5330
|
+
const data = {};
|
|
5331
|
+
insightCounts.forEach((count, tool) => {
|
|
5332
|
+
data[tool] = count;
|
|
5333
|
+
});
|
|
5334
|
+
atomicWriteSync2(INSIGHT_COUNTS_FILE, JSON.stringify(data, null, 2), { mode: 384 });
|
|
5335
|
+
} catch {
|
|
5336
|
+
}
|
|
5337
|
+
}
|
|
4744
5338
|
function getAbandonTimer() {
|
|
4745
5339
|
return _abandonTimer;
|
|
4746
5340
|
}
|
|
@@ -4763,11 +5357,27 @@ function markRejectionHandlerRegistered() {
|
|
|
4763
5357
|
daemonRejectionHandlerRegistered = true;
|
|
4764
5358
|
}
|
|
4765
5359
|
function atomicWriteSync2(filePath, data, options) {
|
|
4766
|
-
const dir =
|
|
4767
|
-
if (!
|
|
4768
|
-
const tmpPath = `${filePath}.${(0,
|
|
4769
|
-
|
|
4770
|
-
|
|
5360
|
+
const dir = import_path16.default.dirname(filePath);
|
|
5361
|
+
if (!import_fs13.default.existsSync(dir)) import_fs13.default.mkdirSync(dir, { recursive: true });
|
|
5362
|
+
const tmpPath = `${filePath}.${(0, import_crypto5.randomUUID)()}.tmp`;
|
|
5363
|
+
try {
|
|
5364
|
+
import_fs13.default.writeFileSync(tmpPath, data, options);
|
|
5365
|
+
} catch (err) {
|
|
5366
|
+
try {
|
|
5367
|
+
import_fs13.default.unlinkSync(tmpPath);
|
|
5368
|
+
} catch {
|
|
5369
|
+
}
|
|
5370
|
+
throw err;
|
|
5371
|
+
}
|
|
5372
|
+
try {
|
|
5373
|
+
import_fs13.default.renameSync(tmpPath, filePath);
|
|
5374
|
+
} catch (err) {
|
|
5375
|
+
try {
|
|
5376
|
+
import_fs13.default.unlinkSync(tmpPath);
|
|
5377
|
+
} catch {
|
|
5378
|
+
}
|
|
5379
|
+
throw err;
|
|
5380
|
+
}
|
|
4771
5381
|
}
|
|
4772
5382
|
function redactArgs(value) {
|
|
4773
5383
|
if (!value || typeof value !== "object") return value;
|
|
@@ -4787,16 +5397,16 @@ function appendAuditLog(data) {
|
|
|
4787
5397
|
decision: data.decision,
|
|
4788
5398
|
source: "daemon"
|
|
4789
5399
|
};
|
|
4790
|
-
const dir =
|
|
4791
|
-
if (!
|
|
4792
|
-
|
|
5400
|
+
const dir = import_path16.default.dirname(AUDIT_LOG_FILE);
|
|
5401
|
+
if (!import_fs13.default.existsSync(dir)) import_fs13.default.mkdirSync(dir, { recursive: true });
|
|
5402
|
+
import_fs13.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
4793
5403
|
} catch {
|
|
4794
5404
|
}
|
|
4795
5405
|
}
|
|
4796
5406
|
function getAuditHistory(limit = 20) {
|
|
4797
5407
|
try {
|
|
4798
|
-
if (!
|
|
4799
|
-
const lines =
|
|
5408
|
+
if (!import_fs13.default.existsSync(AUDIT_LOG_FILE)) return [];
|
|
5409
|
+
const lines = import_fs13.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
|
|
4800
5410
|
if (lines.length === 1 && lines[0] === "") return [];
|
|
4801
5411
|
return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
|
|
4802
5412
|
} catch {
|
|
@@ -4805,19 +5415,19 @@ function getAuditHistory(limit = 20) {
|
|
|
4805
5415
|
}
|
|
4806
5416
|
function getOrgName() {
|
|
4807
5417
|
try {
|
|
4808
|
-
if (
|
|
5418
|
+
if (import_fs13.default.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
|
|
4809
5419
|
} catch {
|
|
4810
5420
|
}
|
|
4811
5421
|
return null;
|
|
4812
5422
|
}
|
|
4813
5423
|
function hasStoredSlackKey() {
|
|
4814
|
-
return
|
|
5424
|
+
return import_fs13.default.existsSync(CREDENTIALS_FILE);
|
|
4815
5425
|
}
|
|
4816
5426
|
function writeGlobalSetting(key, value) {
|
|
4817
5427
|
let config = {};
|
|
4818
5428
|
try {
|
|
4819
|
-
if (
|
|
4820
|
-
config = JSON.parse(
|
|
5429
|
+
if (import_fs13.default.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
5430
|
+
config = JSON.parse(import_fs13.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
|
|
4821
5431
|
}
|
|
4822
5432
|
} catch {
|
|
4823
5433
|
}
|
|
@@ -4829,8 +5439,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
4829
5439
|
try {
|
|
4830
5440
|
let trust = { entries: [] };
|
|
4831
5441
|
try {
|
|
4832
|
-
if (
|
|
4833
|
-
trust = JSON.parse(
|
|
5442
|
+
if (import_fs13.default.existsSync(TRUST_FILE2))
|
|
5443
|
+
trust = JSON.parse(import_fs13.default.readFileSync(TRUST_FILE2, "utf-8"));
|
|
4834
5444
|
} catch {
|
|
4835
5445
|
}
|
|
4836
5446
|
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
|
|
@@ -4841,8 +5451,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
4841
5451
|
}
|
|
4842
5452
|
function readPersistentDecisions() {
|
|
4843
5453
|
try {
|
|
4844
|
-
if (
|
|
4845
|
-
return JSON.parse(
|
|
5454
|
+
if (import_fs13.default.existsSync(DECISIONS_FILE)) {
|
|
5455
|
+
return JSON.parse(import_fs13.default.readFileSync(DECISIONS_FILE, "utf-8"));
|
|
4846
5456
|
}
|
|
4847
5457
|
} catch {
|
|
4848
5458
|
}
|
|
@@ -4907,7 +5517,7 @@ function abandonPending() {
|
|
|
4907
5517
|
});
|
|
4908
5518
|
if (autoStarted) {
|
|
4909
5519
|
try {
|
|
4910
|
-
|
|
5520
|
+
import_fs13.default.unlinkSync(DAEMON_PID_FILE);
|
|
4911
5521
|
} catch {
|
|
4912
5522
|
}
|
|
4913
5523
|
setTimeout(() => {
|
|
@@ -4918,7 +5528,7 @@ function abandonPending() {
|
|
|
4918
5528
|
}
|
|
4919
5529
|
function startActivitySocket() {
|
|
4920
5530
|
try {
|
|
4921
|
-
|
|
5531
|
+
import_fs13.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4922
5532
|
} catch {
|
|
4923
5533
|
}
|
|
4924
5534
|
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
@@ -4960,31 +5570,38 @@ function startActivitySocket() {
|
|
|
4960
5570
|
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
4961
5571
|
process.on("exit", () => {
|
|
4962
5572
|
try {
|
|
4963
|
-
|
|
5573
|
+
import_fs13.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4964
5574
|
} catch {
|
|
4965
5575
|
}
|
|
4966
5576
|
});
|
|
4967
5577
|
}
|
|
4968
|
-
var import_net2,
|
|
5578
|
+
var import_net2, import_fs13, import_path16, import_os12, import_child_process3, import_crypto5, 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;
|
|
4969
5579
|
var init_state2 = __esm({
|
|
4970
5580
|
"src/daemon/state.ts"() {
|
|
4971
5581
|
"use strict";
|
|
4972
5582
|
import_net2 = __toESM(require("net"));
|
|
4973
|
-
|
|
4974
|
-
|
|
5583
|
+
import_fs13 = __toESM(require("fs"));
|
|
5584
|
+
import_path16 = __toESM(require("path"));
|
|
4975
5585
|
import_os12 = __toESM(require("os"));
|
|
4976
5586
|
import_child_process3 = require("child_process");
|
|
4977
|
-
|
|
5587
|
+
import_crypto5 = require("crypto");
|
|
4978
5588
|
init_daemon();
|
|
5589
|
+
init_suggestion_tracker();
|
|
5590
|
+
init_taint_store();
|
|
4979
5591
|
homeDir = import_os12.default.homedir();
|
|
4980
|
-
DAEMON_PID_FILE =
|
|
4981
|
-
DECISIONS_FILE =
|
|
4982
|
-
AUDIT_LOG_FILE =
|
|
4983
|
-
TRUST_FILE2 =
|
|
4984
|
-
GLOBAL_CONFIG_FILE =
|
|
4985
|
-
CREDENTIALS_FILE =
|
|
5592
|
+
DAEMON_PID_FILE = import_path16.default.join(homeDir, ".node9", "daemon.pid");
|
|
5593
|
+
DECISIONS_FILE = import_path16.default.join(homeDir, ".node9", "decisions.json");
|
|
5594
|
+
AUDIT_LOG_FILE = import_path16.default.join(homeDir, ".node9", "audit.log");
|
|
5595
|
+
TRUST_FILE2 = import_path16.default.join(homeDir, ".node9", "trust.json");
|
|
5596
|
+
GLOBAL_CONFIG_FILE = import_path16.default.join(homeDir, ".node9", "config.json");
|
|
5597
|
+
CREDENTIALS_FILE = import_path16.default.join(homeDir, ".node9", "credentials.json");
|
|
5598
|
+
INSIGHT_COUNTS_FILE = import_path16.default.join(homeDir, ".node9", "insight-counts.json");
|
|
4986
5599
|
pending = /* @__PURE__ */ new Map();
|
|
4987
5600
|
sseClients = /* @__PURE__ */ new Set();
|
|
5601
|
+
suggestionTracker = new SuggestionTracker(3);
|
|
5602
|
+
suggestions = /* @__PURE__ */ new Map();
|
|
5603
|
+
taintStore = new TaintStore();
|
|
5604
|
+
insightCounts = /* @__PURE__ */ new Map();
|
|
4988
5605
|
_abandonTimer = null;
|
|
4989
5606
|
_hadBrowserClient = false;
|
|
4990
5607
|
_daemonServer = null;
|
|
@@ -4996,17 +5613,75 @@ var init_state2 = __esm({
|
|
|
4996
5613
|
"2h": 2 * 60 * 6e4
|
|
4997
5614
|
};
|
|
4998
5615
|
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
4999
|
-
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
5616
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path16.default.join(import_os12.default.tmpdir(), "node9-activity.sock");
|
|
5000
5617
|
ACTIVITY_RING_SIZE = 100;
|
|
5001
5618
|
activityRing = [];
|
|
5002
5619
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
5003
5620
|
}
|
|
5004
5621
|
});
|
|
5005
5622
|
|
|
5623
|
+
// src/config/patch.ts
|
|
5624
|
+
function patchConfig(configPath, patch) {
|
|
5625
|
+
let config = {};
|
|
5626
|
+
try {
|
|
5627
|
+
if (import_fs14.default.existsSync(configPath)) {
|
|
5628
|
+
config = JSON.parse(import_fs14.default.readFileSync(configPath, "utf8"));
|
|
5629
|
+
}
|
|
5630
|
+
} catch {
|
|
5631
|
+
throw new Error(`Cannot read config at ${configPath} \u2014 file may be corrupted`);
|
|
5632
|
+
}
|
|
5633
|
+
if (!config.policy || typeof config.policy !== "object") config.policy = {};
|
|
5634
|
+
const policy = config.policy;
|
|
5635
|
+
if (patch.type === "smartRule") {
|
|
5636
|
+
if (!Array.isArray(policy.smartRules)) policy.smartRules = [];
|
|
5637
|
+
const rules = policy.smartRules;
|
|
5638
|
+
if (patch.rule.name && rules.some((r) => r.name === patch.rule.name)) return;
|
|
5639
|
+
rules.push(patch.rule);
|
|
5640
|
+
} else {
|
|
5641
|
+
if (!Array.isArray(policy.ignoredTools)) policy.ignoredTools = [];
|
|
5642
|
+
const ignored = policy.ignoredTools;
|
|
5643
|
+
if (!ignored.includes(patch.toolName)) {
|
|
5644
|
+
ignored.push(patch.toolName);
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5647
|
+
const dir = import_path17.default.dirname(configPath);
|
|
5648
|
+
import_fs14.default.mkdirSync(dir, { recursive: true });
|
|
5649
|
+
const tmp = configPath + ".node9-tmp";
|
|
5650
|
+
try {
|
|
5651
|
+
import_fs14.default.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
|
|
5652
|
+
} catch (err) {
|
|
5653
|
+
try {
|
|
5654
|
+
import_fs14.default.unlinkSync(tmp);
|
|
5655
|
+
} catch {
|
|
5656
|
+
}
|
|
5657
|
+
throw err;
|
|
5658
|
+
}
|
|
5659
|
+
try {
|
|
5660
|
+
import_fs14.default.renameSync(tmp, configPath);
|
|
5661
|
+
} catch (err) {
|
|
5662
|
+
try {
|
|
5663
|
+
import_fs14.default.unlinkSync(tmp);
|
|
5664
|
+
} catch {
|
|
5665
|
+
}
|
|
5666
|
+
throw err;
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5669
|
+
var import_fs14, import_path17, import_os13, GLOBAL_CONFIG_PATH;
|
|
5670
|
+
var init_patch = __esm({
|
|
5671
|
+
"src/config/patch.ts"() {
|
|
5672
|
+
"use strict";
|
|
5673
|
+
import_fs14 = __toESM(require("fs"));
|
|
5674
|
+
import_path17 = __toESM(require("path"));
|
|
5675
|
+
import_os13 = __toESM(require("os"));
|
|
5676
|
+
GLOBAL_CONFIG_PATH = import_path17.default.join(import_os13.default.homedir(), ".node9", "config.json");
|
|
5677
|
+
}
|
|
5678
|
+
});
|
|
5679
|
+
|
|
5006
5680
|
// src/daemon/server.ts
|
|
5007
5681
|
function startDaemon() {
|
|
5008
|
-
|
|
5009
|
-
const
|
|
5682
|
+
loadInsightCounts();
|
|
5683
|
+
const csrfToken = (0, import_crypto6.randomUUID)();
|
|
5684
|
+
const internalToken = (0, import_crypto6.randomUUID)();
|
|
5010
5685
|
const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
|
|
5011
5686
|
const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
|
|
5012
5687
|
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
@@ -5019,7 +5694,7 @@ function startDaemon() {
|
|
|
5019
5694
|
idleTimer = setTimeout(() => {
|
|
5020
5695
|
if (autoStarted) {
|
|
5021
5696
|
try {
|
|
5022
|
-
|
|
5697
|
+
import_fs15.default.unlinkSync(DAEMON_PID_FILE);
|
|
5023
5698
|
} catch {
|
|
5024
5699
|
}
|
|
5025
5700
|
}
|
|
@@ -5028,8 +5703,14 @@ function startDaemon() {
|
|
|
5028
5703
|
idleTimer.unref();
|
|
5029
5704
|
}
|
|
5030
5705
|
resetIdleTimer();
|
|
5706
|
+
const allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${DAEMON_PORT}`, `localhost:${DAEMON_PORT}`]);
|
|
5031
5707
|
const server = import_http.default.createServer(async (req, res) => {
|
|
5032
|
-
const
|
|
5708
|
+
const host = req.headers.host ?? "";
|
|
5709
|
+
if (!allowedHosts.has(host)) {
|
|
5710
|
+
res.writeHead(421, { "Content-Type": "text/plain" });
|
|
5711
|
+
return res.end("Misdirected Request");
|
|
5712
|
+
}
|
|
5713
|
+
const reqUrl = new URL(req.url || "/", `http://${host}`);
|
|
5033
5714
|
const { pathname } = reqUrl;
|
|
5034
5715
|
if (req.method === "GET" && pathname === "/") {
|
|
5035
5716
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
@@ -5062,7 +5743,8 @@ data: ${JSON.stringify({
|
|
|
5062
5743
|
slackDelegated: e.slackDelegated,
|
|
5063
5744
|
timestamp: e.timestamp,
|
|
5064
5745
|
agent: e.agent,
|
|
5065
|
-
mcpServer: e.mcpServer
|
|
5746
|
+
mcpServer: e.mcpServer,
|
|
5747
|
+
allowCount: (insightCounts.get(e.toolName) ?? 0) + 1
|
|
5066
5748
|
})),
|
|
5067
5749
|
orgName: getOrgName(),
|
|
5068
5750
|
autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
|
|
@@ -5104,6 +5786,12 @@ data: ${JSON.stringify(item.data)}
|
|
|
5104
5786
|
}
|
|
5105
5787
|
});
|
|
5106
5788
|
}
|
|
5789
|
+
if (req.method === "POST" && pathname === "/browser-opened") {
|
|
5790
|
+
if (req.headers["x-node9-internal"] !== internalToken) return res.writeHead(403).end();
|
|
5791
|
+
browserOpened = true;
|
|
5792
|
+
res.writeHead(200).end();
|
|
5793
|
+
return;
|
|
5794
|
+
}
|
|
5107
5795
|
if (req.method === "POST" && pathname === "/check") {
|
|
5108
5796
|
try {
|
|
5109
5797
|
resetIdleTimer();
|
|
@@ -5121,7 +5809,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5121
5809
|
activityId,
|
|
5122
5810
|
cwd
|
|
5123
5811
|
} = JSON.parse(body);
|
|
5124
|
-
const id = fromCLI && typeof activityId === "string" && activityId || (0,
|
|
5812
|
+
const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto6.randomUUID)();
|
|
5125
5813
|
const entry = {
|
|
5126
5814
|
id,
|
|
5127
5815
|
toolName,
|
|
@@ -5147,7 +5835,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5147
5835
|
e.earlyReason = "No response \u2014 auto-denied after timeout";
|
|
5148
5836
|
}
|
|
5149
5837
|
pending.delete(id);
|
|
5150
|
-
broadcast("remove", { id });
|
|
5838
|
+
broadcast("remove", { id, decision: "deny" });
|
|
5151
5839
|
}
|
|
5152
5840
|
}, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
|
|
5153
5841
|
};
|
|
@@ -5161,7 +5849,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5161
5849
|
status: "pending"
|
|
5162
5850
|
});
|
|
5163
5851
|
}
|
|
5164
|
-
const projectCwd = typeof cwd === "string" &&
|
|
5852
|
+
const projectCwd = typeof cwd === "string" && import_path18.default.isAbsolute(cwd) ? cwd : void 0;
|
|
5165
5853
|
const projectConfig = getConfig(projectCwd);
|
|
5166
5854
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
5167
5855
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -5174,7 +5862,10 @@ data: ${JSON.stringify(item.data)}
|
|
|
5174
5862
|
slackDelegated: entry.slackDelegated,
|
|
5175
5863
|
agent: entry.agent,
|
|
5176
5864
|
mcpServer: entry.mcpServer,
|
|
5177
|
-
interactive: terminalEnabled
|
|
5865
|
+
interactive: terminalEnabled,
|
|
5866
|
+
// allowCount = what this count will be if the user allows.
|
|
5867
|
+
// Terminal uses this to show the 💡 insight line on the Nth consecutive approval.
|
|
5868
|
+
allowCount: (insightCounts.get(toolName) ?? 0) + 1
|
|
5178
5869
|
});
|
|
5179
5870
|
const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
|
|
5180
5871
|
if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
|
|
@@ -5183,7 +5874,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5183
5874
|
}
|
|
5184
5875
|
}
|
|
5185
5876
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5186
|
-
res.end(JSON.stringify({ id }));
|
|
5877
|
+
res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
|
|
5187
5878
|
if (slackDelegated) return;
|
|
5188
5879
|
authorizeHeadless(
|
|
5189
5880
|
toolName,
|
|
@@ -5210,7 +5901,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5210
5901
|
if (e.waiter) {
|
|
5211
5902
|
e.waiter(decision, result.reason);
|
|
5212
5903
|
pending.delete(id);
|
|
5213
|
-
broadcast("remove", { id });
|
|
5904
|
+
broadcast("remove", { id, decision });
|
|
5214
5905
|
} else {
|
|
5215
5906
|
e.earlyDecision = decision;
|
|
5216
5907
|
e.earlyReason = result.reason;
|
|
@@ -5226,7 +5917,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5226
5917
|
e.earlyReason = reason;
|
|
5227
5918
|
}
|
|
5228
5919
|
pending.delete(id);
|
|
5229
|
-
broadcast("remove", { id });
|
|
5920
|
+
broadcast("remove", { id, decision: "deny" });
|
|
5230
5921
|
});
|
|
5231
5922
|
return;
|
|
5232
5923
|
} catch {
|
|
@@ -5257,12 +5948,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
5257
5948
|
res.end(JSON.stringify(body));
|
|
5258
5949
|
};
|
|
5259
5950
|
req.on("close", () => {
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5951
|
+
setTimeout(() => {
|
|
5952
|
+
const e = pending.get(id);
|
|
5953
|
+
if (e && e.waiter && e.earlyDecision === null) {
|
|
5954
|
+
clearTimeout(e.timer);
|
|
5955
|
+
pending.delete(id);
|
|
5956
|
+
broadcast("remove", { id });
|
|
5957
|
+
}
|
|
5958
|
+
}, 200);
|
|
5266
5959
|
});
|
|
5267
5960
|
return;
|
|
5268
5961
|
}
|
|
@@ -5291,10 +5984,10 @@ data: ${JSON.stringify(item.data)}
|
|
|
5291
5984
|
if (entry.waiter) {
|
|
5292
5985
|
entry.waiter("allow");
|
|
5293
5986
|
pending.delete(id);
|
|
5294
|
-
broadcast("remove", { id });
|
|
5987
|
+
broadcast("remove", { id, decision: "allow" });
|
|
5295
5988
|
} else {
|
|
5296
5989
|
entry.earlyDecision = "allow";
|
|
5297
|
-
broadcast("remove", { id });
|
|
5990
|
+
broadcast("remove", { id, decision: "allow" });
|
|
5298
5991
|
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
5299
5992
|
}
|
|
5300
5993
|
res.writeHead(200);
|
|
@@ -5308,16 +6001,29 @@ data: ${JSON.stringify(item.data)}
|
|
|
5308
6001
|
decision: resolvedDecision
|
|
5309
6002
|
});
|
|
5310
6003
|
clearTimeout(entry.timer);
|
|
6004
|
+
if (resolvedDecision === "allow" && !persist) {
|
|
6005
|
+
insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
|
|
6006
|
+
saveInsightCounts();
|
|
6007
|
+
const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
|
|
6008
|
+
if (suggestion) {
|
|
6009
|
+
suggestions.set(suggestion.id, suggestion);
|
|
6010
|
+
broadcast("suggestion:new", suggestion);
|
|
6011
|
+
}
|
|
6012
|
+
} else if (resolvedDecision === "deny") {
|
|
6013
|
+
insightCounts.delete(entry.toolName);
|
|
6014
|
+
saveInsightCounts();
|
|
6015
|
+
suggestionTracker.resetTool(entry.toolName);
|
|
6016
|
+
}
|
|
5311
6017
|
const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
|
|
5312
6018
|
if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
|
|
5313
6019
|
if (entry.waiter) {
|
|
5314
6020
|
entry.waiter(resolvedDecision, reason);
|
|
5315
6021
|
pending.delete(id);
|
|
5316
|
-
broadcast("remove", { id });
|
|
6022
|
+
broadcast("remove", { id, decision: resolvedDecision });
|
|
5317
6023
|
} else {
|
|
5318
6024
|
entry.earlyDecision = resolvedDecision;
|
|
5319
6025
|
entry.earlyReason = reason;
|
|
5320
|
-
broadcast("remove", { id });
|
|
6026
|
+
broadcast("remove", { id, decision: resolvedDecision });
|
|
5321
6027
|
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
5322
6028
|
}
|
|
5323
6029
|
res.writeHead(200);
|
|
@@ -5405,13 +6111,38 @@ data: ${JSON.stringify(item.data)}
|
|
|
5405
6111
|
const id = pathname.split("/").pop();
|
|
5406
6112
|
const entry = pending.get(id);
|
|
5407
6113
|
if (!entry) return res.writeHead(404).end();
|
|
5408
|
-
const { decision } = JSON.parse(await readBody(req));
|
|
5409
|
-
|
|
6114
|
+
const { decision, source } = JSON.parse(await readBody(req));
|
|
6115
|
+
const resolvedResolveDecision = decision === "allow" ? "allow" : "deny";
|
|
6116
|
+
appendAuditLog({
|
|
6117
|
+
toolName: entry.toolName,
|
|
6118
|
+
args: entry.args,
|
|
6119
|
+
decision: resolvedResolveDecision
|
|
6120
|
+
});
|
|
5410
6121
|
clearTimeout(entry.timer);
|
|
5411
|
-
if (
|
|
5412
|
-
|
|
6122
|
+
if (resolvedResolveDecision === "allow") {
|
|
6123
|
+
insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
|
|
6124
|
+
saveInsightCounts();
|
|
6125
|
+
} else {
|
|
6126
|
+
insightCounts.delete(entry.toolName);
|
|
6127
|
+
saveInsightCounts();
|
|
6128
|
+
}
|
|
6129
|
+
if (!entry.slackDelegated) {
|
|
6130
|
+
if (resolvedResolveDecision === "allow") {
|
|
6131
|
+
const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
|
|
6132
|
+
if (suggestion) {
|
|
6133
|
+
suggestions.set(suggestion.id, suggestion);
|
|
6134
|
+
broadcast("suggestion:new", suggestion);
|
|
6135
|
+
}
|
|
6136
|
+
} else {
|
|
6137
|
+
suggestionTracker.resetTool(entry.toolName);
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
6140
|
+
const VALID_RESOLVE_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
|
|
6141
|
+
if (source && VALID_RESOLVE_SOURCES.has(source)) entry.decisionSource = source;
|
|
6142
|
+
if (entry.waiter) entry.waiter(resolvedResolveDecision);
|
|
6143
|
+
else entry.earlyDecision = resolvedResolveDecision;
|
|
5413
6144
|
pending.delete(id);
|
|
5414
|
-
broadcast("remove", { id });
|
|
6145
|
+
broadcast("remove", { id, decision: resolvedResolveDecision });
|
|
5415
6146
|
res.writeHead(200);
|
|
5416
6147
|
return res.end(JSON.stringify({ ok: true }));
|
|
5417
6148
|
} catch {
|
|
@@ -5459,20 +6190,136 @@ data: ${JSON.stringify(item.data)}
|
|
|
5459
6190
|
res.writeHead(400).end();
|
|
5460
6191
|
}
|
|
5461
6192
|
}
|
|
6193
|
+
if (req.method === "GET" && pathname === "/suggestions") {
|
|
6194
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6195
|
+
return res.end(JSON.stringify([...suggestions.values()]));
|
|
6196
|
+
}
|
|
6197
|
+
if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/apply")) {
|
|
6198
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
6199
|
+
try {
|
|
6200
|
+
const body = await readBody(req);
|
|
6201
|
+
const data = body ? JSON.parse(body) : {};
|
|
6202
|
+
const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
|
|
6203
|
+
const node9Dir = import_path18.default.dirname(GLOBAL_CONFIG_PATH);
|
|
6204
|
+
if (!import_path18.default.resolve(configPath).startsWith(node9Dir + import_path18.default.sep)) {
|
|
6205
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6206
|
+
return res.end(
|
|
6207
|
+
JSON.stringify({ error: "configPath must be within the node9 config directory" })
|
|
6208
|
+
);
|
|
6209
|
+
}
|
|
6210
|
+
const id = pathname.split("/")[2];
|
|
6211
|
+
const suggestion = suggestions.get(id);
|
|
6212
|
+
if (!suggestion) return res.writeHead(404).end();
|
|
6213
|
+
let patch;
|
|
6214
|
+
if (data.rule !== void 0) {
|
|
6215
|
+
const parsed = SmartRuleSchema.safeParse(data.rule);
|
|
6216
|
+
if (!parsed.success) {
|
|
6217
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6218
|
+
return res.end(JSON.stringify({ error: parsed.error.message }));
|
|
6219
|
+
}
|
|
6220
|
+
patch = { type: "smartRule", rule: parsed.data };
|
|
6221
|
+
} else {
|
|
6222
|
+
patch = suggestion.suggestedRule;
|
|
6223
|
+
}
|
|
6224
|
+
patchConfig(configPath, patch);
|
|
6225
|
+
_resetConfigCache();
|
|
6226
|
+
insightCounts.delete(suggestion.toolName);
|
|
6227
|
+
saveInsightCounts();
|
|
6228
|
+
suggestion.status = "applied";
|
|
6229
|
+
broadcast("suggestion:resolved", { id, status: "applied" });
|
|
6230
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6231
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
6232
|
+
} catch (err) {
|
|
6233
|
+
console.error(import_chalk2.default.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err);
|
|
6234
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6235
|
+
return res.end(JSON.stringify({ error: String(err) }));
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/dismiss")) {
|
|
6239
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
6240
|
+
try {
|
|
6241
|
+
const id = pathname.split("/")[2];
|
|
6242
|
+
const suggestion = suggestions.get(id);
|
|
6243
|
+
if (!suggestion) return res.writeHead(404).end();
|
|
6244
|
+
suggestion.status = "dismissed";
|
|
6245
|
+
broadcast("suggestion:resolved", { id, status: "dismissed" });
|
|
6246
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6247
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
6248
|
+
} catch {
|
|
6249
|
+
res.writeHead(400).end();
|
|
6250
|
+
}
|
|
6251
|
+
}
|
|
6252
|
+
if (req.method === "POST" && pathname === "/taint") {
|
|
6253
|
+
try {
|
|
6254
|
+
const body = JSON.parse(await readBody(req));
|
|
6255
|
+
if (typeof body.path !== "string" || typeof body.source !== "string") {
|
|
6256
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6257
|
+
return res.end(JSON.stringify({ error: "path and source are required strings" }));
|
|
6258
|
+
}
|
|
6259
|
+
const ttlMs = typeof body.ttlMs === "number" ? body.ttlMs : void 0;
|
|
6260
|
+
taintStore.taint(body.path, body.source, ttlMs);
|
|
6261
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6262
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
6263
|
+
} catch {
|
|
6264
|
+
res.writeHead(400).end();
|
|
6265
|
+
return;
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
if (req.method === "POST" && pathname === "/taint/check") {
|
|
6269
|
+
try {
|
|
6270
|
+
const body = JSON.parse(await readBody(req));
|
|
6271
|
+
if (!Array.isArray(body.paths)) {
|
|
6272
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6273
|
+
return res.end(JSON.stringify({ error: "paths must be an array" }));
|
|
6274
|
+
}
|
|
6275
|
+
if (body.paths.some((p) => typeof p !== "string")) {
|
|
6276
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6277
|
+
return res.end(JSON.stringify({ error: "all paths must be strings" }));
|
|
6278
|
+
}
|
|
6279
|
+
for (const p of body.paths) {
|
|
6280
|
+
const record = taintStore.check(p);
|
|
6281
|
+
if (record) {
|
|
6282
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6283
|
+
return res.end(JSON.stringify({ tainted: true, record }));
|
|
6284
|
+
}
|
|
6285
|
+
}
|
|
6286
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6287
|
+
return res.end(JSON.stringify({ tainted: false }));
|
|
6288
|
+
} catch {
|
|
6289
|
+
res.writeHead(400).end();
|
|
6290
|
+
return;
|
|
6291
|
+
}
|
|
6292
|
+
}
|
|
6293
|
+
if (req.method === "POST" && pathname === "/taint/propagate") {
|
|
6294
|
+
try {
|
|
6295
|
+
const body = JSON.parse(await readBody(req));
|
|
6296
|
+
if (typeof body.src !== "string" || typeof body.dest !== "string") {
|
|
6297
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6298
|
+
return res.end(JSON.stringify({ error: "src and dest are required strings" }));
|
|
6299
|
+
}
|
|
6300
|
+
const clearSource = body.clearSource === true;
|
|
6301
|
+
taintStore.propagate(body.src, body.dest, clearSource);
|
|
6302
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6303
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
6304
|
+
} catch {
|
|
6305
|
+
res.writeHead(400).end();
|
|
6306
|
+
return;
|
|
6307
|
+
}
|
|
6308
|
+
}
|
|
5462
6309
|
res.writeHead(404).end();
|
|
5463
6310
|
});
|
|
5464
6311
|
setDaemonServer(server);
|
|
5465
6312
|
server.on("error", (e) => {
|
|
5466
6313
|
if (e.code === "EADDRINUSE") {
|
|
5467
6314
|
try {
|
|
5468
|
-
if (
|
|
5469
|
-
const { pid } = JSON.parse(
|
|
6315
|
+
if (import_fs15.default.existsSync(DAEMON_PID_FILE)) {
|
|
6316
|
+
const { pid } = JSON.parse(import_fs15.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5470
6317
|
process.kill(pid, 0);
|
|
5471
6318
|
return process.exit(0);
|
|
5472
6319
|
}
|
|
5473
6320
|
} catch {
|
|
5474
6321
|
try {
|
|
5475
|
-
|
|
6322
|
+
import_fs15.default.unlinkSync(DAEMON_PID_FILE);
|
|
5476
6323
|
} catch {
|
|
5477
6324
|
}
|
|
5478
6325
|
server.listen(DAEMON_PORT, DAEMON_HOST);
|
|
@@ -5531,43 +6378,45 @@ data: ${JSON.stringify(item.data)}
|
|
|
5531
6378
|
}
|
|
5532
6379
|
startActivitySocket();
|
|
5533
6380
|
}
|
|
5534
|
-
var import_http,
|
|
6381
|
+
var import_http, import_fs15, import_path18, import_crypto6, import_child_process4, import_chalk2;
|
|
5535
6382
|
var init_server = __esm({
|
|
5536
6383
|
"src/daemon/server.ts"() {
|
|
5537
6384
|
"use strict";
|
|
5538
6385
|
import_http = __toESM(require("http"));
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
6386
|
+
import_fs15 = __toESM(require("fs"));
|
|
6387
|
+
import_path18 = __toESM(require("path"));
|
|
6388
|
+
import_crypto6 = require("crypto");
|
|
5542
6389
|
import_child_process4 = require("child_process");
|
|
5543
6390
|
import_chalk2 = __toESM(require("chalk"));
|
|
5544
6391
|
init_core();
|
|
5545
6392
|
init_shields();
|
|
5546
6393
|
init_ui2();
|
|
5547
6394
|
init_state2();
|
|
6395
|
+
init_patch();
|
|
6396
|
+
init_config_schema();
|
|
5548
6397
|
}
|
|
5549
6398
|
});
|
|
5550
6399
|
|
|
5551
6400
|
// src/daemon/index.ts
|
|
5552
6401
|
function stopDaemon() {
|
|
5553
|
-
if (!
|
|
6402
|
+
if (!import_fs16.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk3.default.yellow("Not running."));
|
|
5554
6403
|
try {
|
|
5555
|
-
const { pid } = JSON.parse(
|
|
6404
|
+
const { pid } = JSON.parse(import_fs16.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5556
6405
|
process.kill(pid, "SIGTERM");
|
|
5557
6406
|
console.log(import_chalk3.default.green("\u2705 Stopped."));
|
|
5558
6407
|
} catch {
|
|
5559
6408
|
console.log(import_chalk3.default.gray("Cleaned up stale PID file."));
|
|
5560
6409
|
} finally {
|
|
5561
6410
|
try {
|
|
5562
|
-
|
|
6411
|
+
import_fs16.default.unlinkSync(DAEMON_PID_FILE);
|
|
5563
6412
|
} catch {
|
|
5564
6413
|
}
|
|
5565
6414
|
}
|
|
5566
6415
|
}
|
|
5567
6416
|
function daemonStatus() {
|
|
5568
|
-
if (
|
|
6417
|
+
if (import_fs16.default.existsSync(DAEMON_PID_FILE)) {
|
|
5569
6418
|
try {
|
|
5570
|
-
const { pid } = JSON.parse(
|
|
6419
|
+
const { pid } = JSON.parse(import_fs16.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5571
6420
|
process.kill(pid, 0);
|
|
5572
6421
|
console.log(import_chalk3.default.green("Node9 daemon: running"));
|
|
5573
6422
|
return;
|
|
@@ -5586,11 +6435,11 @@ function daemonStatus() {
|
|
|
5586
6435
|
console.log(import_chalk3.default.yellow("Node9 daemon: not running"));
|
|
5587
6436
|
}
|
|
5588
6437
|
}
|
|
5589
|
-
var
|
|
6438
|
+
var import_fs16, import_chalk3, import_child_process5;
|
|
5590
6439
|
var init_daemon2 = __esm({
|
|
5591
6440
|
"src/daemon/index.ts"() {
|
|
5592
6441
|
"use strict";
|
|
5593
|
-
|
|
6442
|
+
import_fs16 = __toESM(require("fs"));
|
|
5594
6443
|
import_chalk3 = __toESM(require("chalk"));
|
|
5595
6444
|
import_child_process5 = require("child_process");
|
|
5596
6445
|
init_server();
|
|
@@ -5617,17 +6466,17 @@ function formatBase(activity) {
|
|
|
5617
6466
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
5618
6467
|
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
5619
6468
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
5620
|
-
return `${
|
|
6469
|
+
return `${import_chalk16.default.gray(time)} ${icon} ${import_chalk16.default.white.bold(toolName)} ${import_chalk16.default.dim(argsPreview)}`;
|
|
5621
6470
|
}
|
|
5622
6471
|
function renderResult(activity, result) {
|
|
5623
6472
|
const base = formatBase(activity);
|
|
5624
6473
|
let status;
|
|
5625
6474
|
if (result.status === "allow") {
|
|
5626
|
-
status =
|
|
6475
|
+
status = import_chalk16.default.green("\u2713 ALLOW");
|
|
5627
6476
|
} else if (result.status === "dlp") {
|
|
5628
|
-
status =
|
|
6477
|
+
status = import_chalk16.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
5629
6478
|
} else {
|
|
5630
|
-
status =
|
|
6479
|
+
status = import_chalk16.default.red("\u2717 BLOCK");
|
|
5631
6480
|
}
|
|
5632
6481
|
if (process.stdout.isTTY) {
|
|
5633
6482
|
import_readline3.default.clearLine(process.stdout, 0);
|
|
@@ -5637,16 +6486,16 @@ function renderResult(activity, result) {
|
|
|
5637
6486
|
}
|
|
5638
6487
|
function renderPending(activity) {
|
|
5639
6488
|
if (!process.stdout.isTTY) return;
|
|
5640
|
-
process.stdout.write(`${formatBase(activity)} ${
|
|
6489
|
+
process.stdout.write(`${formatBase(activity)} ${import_chalk16.default.yellow("\u25CF \u2026")}\r`);
|
|
5641
6490
|
}
|
|
5642
6491
|
async function ensureDaemon() {
|
|
5643
6492
|
let pidPort = null;
|
|
5644
|
-
if (
|
|
6493
|
+
if (import_fs24.default.existsSync(PID_FILE)) {
|
|
5645
6494
|
try {
|
|
5646
|
-
const { port } = JSON.parse(
|
|
6495
|
+
const { port } = JSON.parse(import_fs24.default.readFileSync(PID_FILE, "utf-8"));
|
|
5647
6496
|
pidPort = port;
|
|
5648
6497
|
} catch {
|
|
5649
|
-
console.error(
|
|
6498
|
+
console.error(import_chalk16.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
5650
6499
|
}
|
|
5651
6500
|
}
|
|
5652
6501
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -5657,7 +6506,7 @@ async function ensureDaemon() {
|
|
|
5657
6506
|
if (res.ok) return checkPort;
|
|
5658
6507
|
} catch {
|
|
5659
6508
|
}
|
|
5660
|
-
console.log(
|
|
6509
|
+
console.log(import_chalk16.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
5661
6510
|
const child = (0, import_child_process13.spawn)(process.execPath, [process.argv[1], "daemon"], {
|
|
5662
6511
|
detached: true,
|
|
5663
6512
|
stdio: "ignore",
|
|
@@ -5674,12 +6523,15 @@ async function ensureDaemon() {
|
|
|
5674
6523
|
} catch {
|
|
5675
6524
|
}
|
|
5676
6525
|
}
|
|
5677
|
-
console.error(
|
|
6526
|
+
console.error(import_chalk16.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
5678
6527
|
process.exit(1);
|
|
5679
6528
|
}
|
|
5680
|
-
function postDecisionHttp(id, decision, csrfToken, port) {
|
|
6529
|
+
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
5681
6530
|
return new Promise((resolve, reject) => {
|
|
5682
|
-
const
|
|
6531
|
+
const bodyObj = { decision, source: "terminal" };
|
|
6532
|
+
if (opts?.persist) bodyObj.persist = true;
|
|
6533
|
+
if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
|
|
6534
|
+
const body = JSON.stringify(bodyObj);
|
|
5683
6535
|
const req = import_http2.default.request(
|
|
5684
6536
|
{
|
|
5685
6537
|
hostname: "127.0.0.1",
|
|
@@ -5702,22 +6554,33 @@ function postDecisionHttp(id, decision, csrfToken, port) {
|
|
|
5702
6554
|
req.end(body);
|
|
5703
6555
|
});
|
|
5704
6556
|
}
|
|
5705
|
-
function buildCardLines(req) {
|
|
6557
|
+
function buildCardLines(req, localCount = 0) {
|
|
5706
6558
|
const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
|
|
5707
6559
|
const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
|
|
5708
6560
|
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`;
|
|
5709
6561
|
const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
|
|
5710
|
-
|
|
6562
|
+
const lines = [
|
|
5711
6563
|
``,
|
|
5712
6564
|
`${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
|
|
5713
6565
|
`${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
|
|
5714
|
-
`${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}
|
|
5715
|
-
|
|
6566
|
+
`${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`
|
|
6567
|
+
];
|
|
6568
|
+
if (req.riskMetadata?.ruleName && blockedBy.includes("Taint")) {
|
|
6569
|
+
lines.push(`${CYAN}\u2551${RESET} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET}`);
|
|
6570
|
+
}
|
|
6571
|
+
lines.push(`${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`);
|
|
6572
|
+
if (localCount >= 2) {
|
|
6573
|
+
lines.push(
|
|
6574
|
+
`${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
|
|
6575
|
+
);
|
|
6576
|
+
}
|
|
6577
|
+
lines.push(
|
|
5716
6578
|
`${CYAN}\u255A${RESET}`,
|
|
5717
6579
|
``,
|
|
5718
|
-
` ${BOLD}${GREEN}[
|
|
6580
|
+
` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${YELLOW}[a]${RESET} Always Allow ${BOLD}${CYAN}[t]${RESET} Trust 30m`,
|
|
5719
6581
|
``
|
|
5720
|
-
|
|
6582
|
+
);
|
|
6583
|
+
return lines;
|
|
5721
6584
|
}
|
|
5722
6585
|
async function startTail(options = {}) {
|
|
5723
6586
|
const port = await ensureDaemon();
|
|
@@ -5745,7 +6608,7 @@ async function startTail(options = {}) {
|
|
|
5745
6608
|
req2.end();
|
|
5746
6609
|
});
|
|
5747
6610
|
if (result.ok) {
|
|
5748
|
-
console.log(
|
|
6611
|
+
console.log(import_chalk16.default.green("\u2713 Flight Recorder buffer cleared."));
|
|
5749
6612
|
} else if (result.code === "ECONNREFUSED") {
|
|
5750
6613
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
5751
6614
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -5762,17 +6625,22 @@ async function startTail(options = {}) {
|
|
|
5762
6625
|
let cardActive = false;
|
|
5763
6626
|
let cardLineCount = 0;
|
|
5764
6627
|
let cancelActiveCard = null;
|
|
6628
|
+
const localAllowCounts = /* @__PURE__ */ new Map();
|
|
5765
6629
|
const canApprove = process.stdout.isTTY && process.stdin.isTTY;
|
|
5766
6630
|
if (canApprove) import_readline3.default.emitKeypressEvents(process.stdin);
|
|
5767
6631
|
function clearCard() {
|
|
5768
6632
|
if (cardLineCount > 0) {
|
|
5769
|
-
process.stdout
|
|
6633
|
+
import_readline3.default.moveCursor(process.stdout, 0, -cardLineCount);
|
|
6634
|
+
process.stdout.write(ERASE_DOWN);
|
|
5770
6635
|
cardLineCount = 0;
|
|
5771
6636
|
}
|
|
5772
6637
|
}
|
|
5773
6638
|
function printCard(req2) {
|
|
5774
|
-
process.stdout.write(HIDE_CURSOR
|
|
5775
|
-
const
|
|
6639
|
+
process.stdout.write(HIDE_CURSOR);
|
|
6640
|
+
const daemonPrior = req2.allowCount !== void 0 ? req2.allowCount - 1 : 0;
|
|
6641
|
+
const localPrior = localAllowCounts.get(req2.toolName) ?? 0;
|
|
6642
|
+
const priorCount = Math.max(daemonPrior, localPrior);
|
|
6643
|
+
const lines = buildCardLines(req2, priorCount);
|
|
5776
6644
|
for (const line of lines) process.stdout.write(line + "\n");
|
|
5777
6645
|
cardLineCount = lines.length;
|
|
5778
6646
|
}
|
|
@@ -5800,34 +6668,70 @@ async function startTail(options = {}) {
|
|
|
5800
6668
|
process.stdin.pause();
|
|
5801
6669
|
cancelActiveCard = null;
|
|
5802
6670
|
};
|
|
5803
|
-
const settle = (
|
|
6671
|
+
const settle = (action) => {
|
|
5804
6672
|
if (settled) return;
|
|
5805
6673
|
settled = true;
|
|
5806
6674
|
cleanup();
|
|
5807
6675
|
clearCard();
|
|
6676
|
+
const stampedLines = buildCardLines(
|
|
6677
|
+
req2,
|
|
6678
|
+
Math.max(
|
|
6679
|
+
req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
|
|
6680
|
+
localAllowCounts.get(req2.toolName) ?? 0
|
|
6681
|
+
)
|
|
6682
|
+
);
|
|
6683
|
+
const decisionStamp = action === "always-allow" ? import_chalk16.default.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? import_chalk16.default.cyan("\u23F1 TRUST 30m") : action === "allow" ? import_chalk16.default.green("\u2713 ALLOWED") : import_chalk16.default.red("\u2717 DENIED");
|
|
6684
|
+
stampedLines.push(` ${BOLD}\u2192${RESET} ${decisionStamp} ${GRAY}(terminal)${RESET}`, ``);
|
|
6685
|
+
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
5808
6686
|
process.stdout.write(SHOW_CURSOR);
|
|
5809
|
-
|
|
6687
|
+
cardLineCount = 0;
|
|
6688
|
+
if (action === "allow" || action === "always-allow" || action === "trust") {
|
|
6689
|
+
localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
|
|
6690
|
+
} else if (action === "deny") {
|
|
6691
|
+
localAllowCounts.delete(req2.toolName);
|
|
6692
|
+
}
|
|
6693
|
+
let httpDecision;
|
|
6694
|
+
let httpOpts;
|
|
6695
|
+
if (action === "always-allow") {
|
|
6696
|
+
httpDecision = "allow";
|
|
6697
|
+
httpOpts = { persist: true };
|
|
6698
|
+
} else if (action === "trust") {
|
|
6699
|
+
httpDecision = "trust";
|
|
6700
|
+
httpOpts = { trustDuration: "30m" };
|
|
6701
|
+
} else {
|
|
6702
|
+
httpDecision = action;
|
|
6703
|
+
}
|
|
6704
|
+
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
|
|
5810
6705
|
try {
|
|
5811
|
-
|
|
5812
|
-
|
|
6706
|
+
import_fs24.default.appendFileSync(
|
|
6707
|
+
import_path26.default.join(import_os21.default.homedir(), ".node9", "hook-debug.log"),
|
|
5813
6708
|
`[tail] POST /decision failed: ${String(err)}
|
|
5814
6709
|
`
|
|
5815
6710
|
);
|
|
5816
6711
|
} catch {
|
|
5817
6712
|
}
|
|
5818
6713
|
});
|
|
5819
|
-
const decisionLabel = decision === "allow" ? import_chalk15.default.green("\u2713 ALLOWED (terminal)") : import_chalk15.default.red("\u2717 DENIED (terminal)");
|
|
5820
|
-
console.log(`${import_chalk15.default.cyan("\u25C6")} ${import_chalk15.default.bold(req2.toolName.padEnd(16))} ${decisionLabel}`);
|
|
5821
6714
|
approvalQueue.shift();
|
|
5822
6715
|
cardActive = false;
|
|
5823
6716
|
showNextCard();
|
|
5824
6717
|
};
|
|
5825
|
-
cancelActiveCard = () => {
|
|
6718
|
+
cancelActiveCard = (externalDecision) => {
|
|
5826
6719
|
if (settled) return;
|
|
5827
6720
|
settled = true;
|
|
5828
6721
|
cleanup();
|
|
5829
6722
|
clearCard();
|
|
6723
|
+
const priorCount = Math.max(
|
|
6724
|
+
req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
|
|
6725
|
+
localAllowCounts.get(req2.toolName) ?? 0
|
|
6726
|
+
);
|
|
6727
|
+
const stampedLines = buildCardLines(req2, priorCount);
|
|
6728
|
+
if (externalDecision) {
|
|
6729
|
+
const source = externalDecision === "allow" ? import_chalk16.default.green("\u2713 ALLOWED") : import_chalk16.default.red("\u2717 DENIED");
|
|
6730
|
+
stampedLines.push(` ${BOLD}\u2192${RESET} ${source} ${GRAY}(external)${RESET}`, ``);
|
|
6731
|
+
}
|
|
6732
|
+
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
5830
6733
|
process.stdout.write(SHOW_CURSOR);
|
|
6734
|
+
cardLineCount = 0;
|
|
5831
6735
|
approvalQueue.shift();
|
|
5832
6736
|
cardActive = false;
|
|
5833
6737
|
showNextCard();
|
|
@@ -5835,10 +6739,14 @@ async function startTail(options = {}) {
|
|
|
5835
6739
|
process.stdin.resume();
|
|
5836
6740
|
onKeypress = (_str, key) => {
|
|
5837
6741
|
const name = key?.name ?? "";
|
|
5838
|
-
if (name === "
|
|
6742
|
+
if (name === "y" || name === "return") {
|
|
5839
6743
|
settle("allow");
|
|
5840
|
-
} else if (name === "
|
|
6744
|
+
} else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
|
|
5841
6745
|
settle("deny");
|
|
6746
|
+
} else if (name === "a") {
|
|
6747
|
+
settle("always-allow");
|
|
6748
|
+
} else if (name === "t") {
|
|
6749
|
+
settle("trust");
|
|
5842
6750
|
}
|
|
5843
6751
|
};
|
|
5844
6752
|
process.stdin.on("keypress", onKeypress);
|
|
@@ -5851,19 +6759,27 @@ async function startTail(options = {}) {
|
|
|
5851
6759
|
else if (process.platform === "win32")
|
|
5852
6760
|
(0, import_child_process13.execSync)(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
|
|
5853
6761
|
else (0, import_child_process13.execSync)(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
|
|
6762
|
+
const intToken = getInternalToken();
|
|
6763
|
+
fetch(`http://127.0.0.1:${port}/browser-opened`, {
|
|
6764
|
+
method: "POST",
|
|
6765
|
+
headers: intToken ? { "X-Node9-Internal": intToken } : {}
|
|
6766
|
+
}).catch(() => {
|
|
6767
|
+
});
|
|
5854
6768
|
}
|
|
5855
6769
|
} catch {
|
|
5856
6770
|
}
|
|
5857
|
-
console.log(
|
|
5858
|
-
\u{1F6F0}\uFE0F Node9 tail `) +
|
|
6771
|
+
console.log(import_chalk16.default.cyan.bold(`
|
|
6772
|
+
\u{1F6F0}\uFE0F Node9 tail `) + import_chalk16.default.dim(`\u2192 ${dashboardUrl}`));
|
|
5859
6773
|
if (canApprove) {
|
|
5860
|
-
console.log(
|
|
6774
|
+
console.log(
|
|
6775
|
+
import_chalk16.default.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
|
|
6776
|
+
);
|
|
5861
6777
|
}
|
|
5862
6778
|
if (options.history) {
|
|
5863
|
-
console.log(
|
|
6779
|
+
console.log(import_chalk16.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
5864
6780
|
} else {
|
|
5865
6781
|
console.log(
|
|
5866
|
-
|
|
6782
|
+
import_chalk16.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
5867
6783
|
);
|
|
5868
6784
|
}
|
|
5869
6785
|
process.on("SIGINT", () => {
|
|
@@ -5873,13 +6789,13 @@ async function startTail(options = {}) {
|
|
|
5873
6789
|
import_readline3.default.clearLine(process.stdout, 0);
|
|
5874
6790
|
import_readline3.default.cursorTo(process.stdout, 0);
|
|
5875
6791
|
}
|
|
5876
|
-
console.log(
|
|
6792
|
+
console.log(import_chalk16.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
5877
6793
|
process.exit(0);
|
|
5878
6794
|
});
|
|
5879
6795
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
5880
6796
|
const req = import_http2.default.get(sseUrl, (res) => {
|
|
5881
6797
|
if (res.statusCode !== 200) {
|
|
5882
|
-
console.error(
|
|
6798
|
+
console.error(import_chalk16.default.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
5883
6799
|
process.exit(1);
|
|
5884
6800
|
}
|
|
5885
6801
|
let currentEvent = "";
|
|
@@ -5909,7 +6825,7 @@ async function startTail(options = {}) {
|
|
|
5909
6825
|
import_readline3.default.clearLine(process.stdout, 0);
|
|
5910
6826
|
import_readline3.default.cursorTo(process.stdout, 0);
|
|
5911
6827
|
}
|
|
5912
|
-
console.log(
|
|
6828
|
+
console.log(import_chalk16.default.red("\n\u274C Daemon disconnected."));
|
|
5913
6829
|
process.exit(1);
|
|
5914
6830
|
});
|
|
5915
6831
|
});
|
|
@@ -5950,11 +6866,17 @@ async function startTail(options = {}) {
|
|
|
5950
6866
|
}
|
|
5951
6867
|
if (event === "remove") {
|
|
5952
6868
|
try {
|
|
5953
|
-
const { id } = JSON.parse(rawData);
|
|
6869
|
+
const { id, decision } = JSON.parse(rawData);
|
|
5954
6870
|
const idx = approvalQueue.findIndex((r) => r.id === id);
|
|
5955
6871
|
if (idx !== -1) {
|
|
5956
6872
|
if (idx === 0 && cardActive && cancelActiveCard) {
|
|
5957
|
-
|
|
6873
|
+
const toolName = approvalQueue[0].toolName;
|
|
6874
|
+
if (decision === "allow") {
|
|
6875
|
+
localAllowCounts.set(toolName, (localAllowCounts.get(toolName) ?? 0) + 1);
|
|
6876
|
+
} else if (decision === "deny") {
|
|
6877
|
+
localAllowCounts.delete(toolName);
|
|
6878
|
+
}
|
|
6879
|
+
cancelActiveCard(decision);
|
|
5958
6880
|
} else {
|
|
5959
6881
|
approvalQueue.splice(idx, 1);
|
|
5960
6882
|
}
|
|
@@ -5989,25 +6911,26 @@ async function startTail(options = {}) {
|
|
|
5989
6911
|
}
|
|
5990
6912
|
req.on("error", (err) => {
|
|
5991
6913
|
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
5992
|
-
console.error(
|
|
6914
|
+
console.error(import_chalk16.default.red(`
|
|
5993
6915
|
\u274C ${msg}`));
|
|
5994
6916
|
process.exit(1);
|
|
5995
6917
|
});
|
|
5996
6918
|
}
|
|
5997
|
-
var import_http2,
|
|
6919
|
+
var import_http2, import_chalk16, import_fs24, import_os21, import_path26, import_readline3, import_child_process13, PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN;
|
|
5998
6920
|
var init_tail = __esm({
|
|
5999
6921
|
"src/tui/tail.ts"() {
|
|
6000
6922
|
"use strict";
|
|
6001
6923
|
import_http2 = __toESM(require("http"));
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6924
|
+
import_chalk16 = __toESM(require("chalk"));
|
|
6925
|
+
import_fs24 = __toESM(require("fs"));
|
|
6926
|
+
import_os21 = __toESM(require("os"));
|
|
6927
|
+
import_path26 = __toESM(require("path"));
|
|
6006
6928
|
import_readline3 = __toESM(require("readline"));
|
|
6007
6929
|
import_child_process13 = require("child_process");
|
|
6008
6930
|
init_daemon2();
|
|
6931
|
+
init_daemon();
|
|
6009
6932
|
init_core();
|
|
6010
|
-
PID_FILE =
|
|
6933
|
+
PID_FILE = import_path26.default.join(import_os21.default.homedir(), ".node9", "daemon.pid");
|
|
6011
6934
|
ICONS = {
|
|
6012
6935
|
bash: "\u{1F4BB}",
|
|
6013
6936
|
shell: "\u{1F4BB}",
|
|
@@ -6035,8 +6958,6 @@ var init_tail = __esm({
|
|
|
6035
6958
|
HIDE_CURSOR = "\x1B[?25l";
|
|
6036
6959
|
SHOW_CURSOR = "\x1B[?25h";
|
|
6037
6960
|
ERASE_DOWN = "\x1B[J";
|
|
6038
|
-
SAVE_CURSOR = "\x1B7";
|
|
6039
|
-
RESTORE_CURSOR = "\x1B8";
|
|
6040
6961
|
}
|
|
6041
6962
|
});
|
|
6042
6963
|
|
|
@@ -6355,6 +7276,25 @@ async function setupGemini() {
|
|
|
6355
7276
|
printDaemonTip();
|
|
6356
7277
|
}
|
|
6357
7278
|
}
|
|
7279
|
+
function detectAgents(homeDir2 = import_os11.default.homedir()) {
|
|
7280
|
+
const exists = (p) => {
|
|
7281
|
+
try {
|
|
7282
|
+
return import_fs11.default.existsSync(p);
|
|
7283
|
+
} catch (err) {
|
|
7284
|
+
const code = err.code;
|
|
7285
|
+
if (code !== "ENOENT") {
|
|
7286
|
+
process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err)}
|
|
7287
|
+
`);
|
|
7288
|
+
}
|
|
7289
|
+
return false;
|
|
7290
|
+
}
|
|
7291
|
+
};
|
|
7292
|
+
return {
|
|
7293
|
+
claude: exists(import_path14.default.join(homeDir2, ".claude")) || exists(import_path14.default.join(homeDir2, ".claude.json")),
|
|
7294
|
+
gemini: exists(import_path14.default.join(homeDir2, ".gemini")),
|
|
7295
|
+
cursor: exists(import_path14.default.join(homeDir2, ".cursor"))
|
|
7296
|
+
};
|
|
7297
|
+
}
|
|
6358
7298
|
async function setupCursor() {
|
|
6359
7299
|
const homeDir2 = import_os11.default.homedir();
|
|
6360
7300
|
const mcpPath = import_path14.default.join(homeDir2, ".cursor", "mcp.json");
|
|
@@ -6413,10 +7353,10 @@ async function setupCursor() {
|
|
|
6413
7353
|
|
|
6414
7354
|
// src/cli.ts
|
|
6415
7355
|
init_daemon2();
|
|
6416
|
-
var
|
|
6417
|
-
var
|
|
6418
|
-
var
|
|
6419
|
-
var
|
|
7356
|
+
var import_chalk17 = __toESM(require("chalk"));
|
|
7357
|
+
var import_fs25 = __toESM(require("fs"));
|
|
7358
|
+
var import_path27 = __toESM(require("path"));
|
|
7359
|
+
var import_os22 = __toESM(require("os"));
|
|
6420
7360
|
var import_prompts3 = require("@inquirer/prompts");
|
|
6421
7361
|
|
|
6422
7362
|
// src/utils/duration.ts
|
|
@@ -6637,9 +7577,9 @@ async function autoStartDaemonAndWait() {
|
|
|
6637
7577
|
|
|
6638
7578
|
// src/cli/commands/check.ts
|
|
6639
7579
|
var import_chalk5 = __toESM(require("chalk"));
|
|
6640
|
-
var
|
|
6641
|
-
var
|
|
6642
|
-
var
|
|
7580
|
+
var import_fs18 = __toESM(require("fs"));
|
|
7581
|
+
var import_path20 = __toESM(require("path"));
|
|
7582
|
+
var import_os15 = __toESM(require("os"));
|
|
6643
7583
|
init_orchestrator();
|
|
6644
7584
|
init_daemon();
|
|
6645
7585
|
init_config();
|
|
@@ -6647,26 +7587,26 @@ init_policy();
|
|
|
6647
7587
|
|
|
6648
7588
|
// src/undo.ts
|
|
6649
7589
|
var import_child_process8 = require("child_process");
|
|
6650
|
-
var
|
|
6651
|
-
var
|
|
6652
|
-
var
|
|
6653
|
-
var
|
|
6654
|
-
var SNAPSHOT_STACK_PATH =
|
|
6655
|
-
var UNDO_LATEST_PATH =
|
|
7590
|
+
var import_crypto7 = __toESM(require("crypto"));
|
|
7591
|
+
var import_fs17 = __toESM(require("fs"));
|
|
7592
|
+
var import_path19 = __toESM(require("path"));
|
|
7593
|
+
var import_os14 = __toESM(require("os"));
|
|
7594
|
+
var SNAPSHOT_STACK_PATH = import_path19.default.join(import_os14.default.homedir(), ".node9", "snapshots.json");
|
|
7595
|
+
var UNDO_LATEST_PATH = import_path19.default.join(import_os14.default.homedir(), ".node9", "undo_latest.txt");
|
|
6656
7596
|
var MAX_SNAPSHOTS = 10;
|
|
6657
7597
|
var GIT_TIMEOUT = 15e3;
|
|
6658
7598
|
function readStack() {
|
|
6659
7599
|
try {
|
|
6660
|
-
if (
|
|
6661
|
-
return JSON.parse(
|
|
7600
|
+
if (import_fs17.default.existsSync(SNAPSHOT_STACK_PATH))
|
|
7601
|
+
return JSON.parse(import_fs17.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
6662
7602
|
} catch {
|
|
6663
7603
|
}
|
|
6664
7604
|
return [];
|
|
6665
7605
|
}
|
|
6666
7606
|
function writeStack(stack) {
|
|
6667
|
-
const dir =
|
|
6668
|
-
if (!
|
|
6669
|
-
|
|
7607
|
+
const dir = import_path19.default.dirname(SNAPSHOT_STACK_PATH);
|
|
7608
|
+
if (!import_fs17.default.existsSync(dir)) import_fs17.default.mkdirSync(dir, { recursive: true });
|
|
7609
|
+
import_fs17.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
6670
7610
|
}
|
|
6671
7611
|
function buildArgsSummary(tool, args) {
|
|
6672
7612
|
if (!args || typeof args !== "object") return "";
|
|
@@ -6682,7 +7622,7 @@ function buildArgsSummary(tool, args) {
|
|
|
6682
7622
|
function normalizeCwdForHash(cwd) {
|
|
6683
7623
|
let normalized;
|
|
6684
7624
|
try {
|
|
6685
|
-
normalized =
|
|
7625
|
+
normalized = import_fs17.default.realpathSync(cwd);
|
|
6686
7626
|
} catch {
|
|
6687
7627
|
normalized = cwd;
|
|
6688
7628
|
}
|
|
@@ -6691,17 +7631,17 @@ function normalizeCwdForHash(cwd) {
|
|
|
6691
7631
|
return normalized;
|
|
6692
7632
|
}
|
|
6693
7633
|
function getShadowRepoDir(cwd) {
|
|
6694
|
-
const hash =
|
|
6695
|
-
return
|
|
7634
|
+
const hash = import_crypto7.default.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
7635
|
+
return import_path19.default.join(import_os14.default.homedir(), ".node9", "snapshots", hash);
|
|
6696
7636
|
}
|
|
6697
7637
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
6698
7638
|
try {
|
|
6699
7639
|
const cutoff = Date.now() - 6e4;
|
|
6700
|
-
for (const f of
|
|
7640
|
+
for (const f of import_fs17.default.readdirSync(shadowDir)) {
|
|
6701
7641
|
if (f.startsWith("index_")) {
|
|
6702
|
-
const fp =
|
|
7642
|
+
const fp = import_path19.default.join(shadowDir, f);
|
|
6703
7643
|
try {
|
|
6704
|
-
if (
|
|
7644
|
+
if (import_fs17.default.statSync(fp).mtimeMs < cutoff) import_fs17.default.unlinkSync(fp);
|
|
6705
7645
|
} catch {
|
|
6706
7646
|
}
|
|
6707
7647
|
}
|
|
@@ -6713,7 +7653,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
6713
7653
|
const hardcoded = [".git", ".node9"];
|
|
6714
7654
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
6715
7655
|
try {
|
|
6716
|
-
|
|
7656
|
+
import_fs17.default.writeFileSync(import_path19.default.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
6717
7657
|
} catch {
|
|
6718
7658
|
}
|
|
6719
7659
|
}
|
|
@@ -6726,25 +7666,25 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6726
7666
|
timeout: 3e3
|
|
6727
7667
|
});
|
|
6728
7668
|
if (check.status === 0) {
|
|
6729
|
-
const ptPath =
|
|
7669
|
+
const ptPath = import_path19.default.join(shadowDir, "project-path.txt");
|
|
6730
7670
|
try {
|
|
6731
|
-
const stored =
|
|
7671
|
+
const stored = import_fs17.default.readFileSync(ptPath, "utf8").trim();
|
|
6732
7672
|
if (stored === normalizedCwd) return true;
|
|
6733
7673
|
if (process.env.NODE9_DEBUG === "1")
|
|
6734
7674
|
console.error(
|
|
6735
7675
|
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
6736
7676
|
);
|
|
6737
|
-
|
|
7677
|
+
import_fs17.default.rmSync(shadowDir, { recursive: true, force: true });
|
|
6738
7678
|
} catch {
|
|
6739
7679
|
try {
|
|
6740
|
-
|
|
7680
|
+
import_fs17.default.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
6741
7681
|
} catch {
|
|
6742
7682
|
}
|
|
6743
7683
|
return true;
|
|
6744
7684
|
}
|
|
6745
7685
|
}
|
|
6746
7686
|
try {
|
|
6747
|
-
|
|
7687
|
+
import_fs17.default.mkdirSync(shadowDir, { recursive: true });
|
|
6748
7688
|
} catch {
|
|
6749
7689
|
}
|
|
6750
7690
|
const init = (0, import_child_process8.spawnSync)("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
@@ -6753,7 +7693,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6753
7693
|
console.error("[Node9] git init --bare failed:", init.stderr?.toString());
|
|
6754
7694
|
return false;
|
|
6755
7695
|
}
|
|
6756
|
-
const configFile =
|
|
7696
|
+
const configFile = import_path19.default.join(shadowDir, "config");
|
|
6757
7697
|
(0, import_child_process8.spawnSync)("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
6758
7698
|
timeout: 3e3
|
|
6759
7699
|
});
|
|
@@ -6761,7 +7701,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6761
7701
|
timeout: 3e3
|
|
6762
7702
|
});
|
|
6763
7703
|
try {
|
|
6764
|
-
|
|
7704
|
+
import_fs17.default.writeFileSync(import_path19.default.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
6765
7705
|
} catch {
|
|
6766
7706
|
}
|
|
6767
7707
|
return true;
|
|
@@ -6784,7 +7724,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6784
7724
|
const shadowDir = getShadowRepoDir(cwd);
|
|
6785
7725
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
6786
7726
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
6787
|
-
indexFile =
|
|
7727
|
+
indexFile = import_path19.default.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
6788
7728
|
const shadowEnv = {
|
|
6789
7729
|
...process.env,
|
|
6790
7730
|
GIT_DIR: shadowDir,
|
|
@@ -6813,7 +7753,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6813
7753
|
const shouldGc = stack.length % 5 === 0;
|
|
6814
7754
|
if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
6815
7755
|
writeStack(stack);
|
|
6816
|
-
|
|
7756
|
+
import_fs17.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
6817
7757
|
if (shouldGc) {
|
|
6818
7758
|
(0, import_child_process8.spawn)("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
6819
7759
|
}
|
|
@@ -6824,7 +7764,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6824
7764
|
} finally {
|
|
6825
7765
|
if (indexFile) {
|
|
6826
7766
|
try {
|
|
6827
|
-
|
|
7767
|
+
import_fs17.default.unlinkSync(indexFile);
|
|
6828
7768
|
} catch {
|
|
6829
7769
|
}
|
|
6830
7770
|
}
|
|
@@ -6893,9 +7833,9 @@ function applyUndo(hash, cwd) {
|
|
|
6893
7833
|
timeout: GIT_TIMEOUT
|
|
6894
7834
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
6895
7835
|
for (const file of [...tracked, ...untracked]) {
|
|
6896
|
-
const fullPath =
|
|
6897
|
-
if (!snapshotFiles.has(file) &&
|
|
6898
|
-
|
|
7836
|
+
const fullPath = import_path19.default.join(dir, file);
|
|
7837
|
+
if (!snapshotFiles.has(file) && import_fs17.default.existsSync(fullPath)) {
|
|
7838
|
+
import_fs17.default.unlinkSync(fullPath);
|
|
6899
7839
|
}
|
|
6900
7840
|
}
|
|
6901
7841
|
return true;
|
|
@@ -6919,9 +7859,9 @@ function registerCheckCommand(program2) {
|
|
|
6919
7859
|
} catch (err) {
|
|
6920
7860
|
const tempConfig = getConfig();
|
|
6921
7861
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
6922
|
-
const logPath =
|
|
7862
|
+
const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
|
|
6923
7863
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6924
|
-
|
|
7864
|
+
import_fs18.default.appendFileSync(
|
|
6925
7865
|
logPath,
|
|
6926
7866
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
6927
7867
|
RAW: ${raw}
|
|
@@ -6932,10 +7872,10 @@ RAW: ${raw}
|
|
|
6932
7872
|
}
|
|
6933
7873
|
const config = getConfig(payload.cwd || void 0);
|
|
6934
7874
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
6935
|
-
const logPath =
|
|
6936
|
-
if (!
|
|
6937
|
-
|
|
6938
|
-
|
|
7875
|
+
const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
|
|
7876
|
+
if (!import_fs18.default.existsSync(import_path20.default.dirname(logPath)))
|
|
7877
|
+
import_fs18.default.mkdirSync(import_path20.default.dirname(logPath), { recursive: true });
|
|
7878
|
+
import_fs18.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
6939
7879
|
`);
|
|
6940
7880
|
}
|
|
6941
7881
|
const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
|
|
@@ -6948,8 +7888,8 @@ RAW: ${raw}
|
|
|
6948
7888
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
6949
7889
|
let ttyFd = null;
|
|
6950
7890
|
try {
|
|
6951
|
-
ttyFd =
|
|
6952
|
-
const writeTty = (line) =>
|
|
7891
|
+
ttyFd = import_fs18.default.openSync("/dev/tty", "w");
|
|
7892
|
+
const writeTty = (line) => import_fs18.default.writeSync(ttyFd, line + "\n");
|
|
6953
7893
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
6954
7894
|
writeTty(import_chalk5.default.bgRed.white.bold(`
|
|
6955
7895
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
@@ -6965,7 +7905,7 @@ RAW: ${raw}
|
|
|
6965
7905
|
} finally {
|
|
6966
7906
|
if (ttyFd !== null)
|
|
6967
7907
|
try {
|
|
6968
|
-
|
|
7908
|
+
import_fs18.default.closeSync(ttyFd);
|
|
6969
7909
|
} catch {
|
|
6970
7910
|
}
|
|
6971
7911
|
}
|
|
@@ -6996,7 +7936,7 @@ RAW: ${raw}
|
|
|
6996
7936
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
6997
7937
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
6998
7938
|
}
|
|
6999
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
7939
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && import_path20.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
7000
7940
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
7001
7941
|
cwd: safeCwdForAuth
|
|
7002
7942
|
});
|
|
@@ -7008,12 +7948,12 @@ RAW: ${raw}
|
|
|
7008
7948
|
}
|
|
7009
7949
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
7010
7950
|
try {
|
|
7011
|
-
const tty =
|
|
7012
|
-
|
|
7951
|
+
const tty = import_fs18.default.openSync("/dev/tty", "w");
|
|
7952
|
+
import_fs18.default.writeSync(
|
|
7013
7953
|
tty,
|
|
7014
7954
|
import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
7015
7955
|
);
|
|
7016
|
-
|
|
7956
|
+
import_fs18.default.closeSync(tty);
|
|
7017
7957
|
} catch {
|
|
7018
7958
|
}
|
|
7019
7959
|
const daemonReady = await autoStartDaemonAndWait();
|
|
@@ -7040,9 +7980,9 @@ RAW: ${raw}
|
|
|
7040
7980
|
});
|
|
7041
7981
|
} catch (err) {
|
|
7042
7982
|
if (process.env.NODE9_DEBUG === "1") {
|
|
7043
|
-
const logPath =
|
|
7983
|
+
const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
|
|
7044
7984
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
7045
|
-
|
|
7985
|
+
import_fs18.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
7046
7986
|
`);
|
|
7047
7987
|
}
|
|
7048
7988
|
process.exit(0);
|
|
@@ -7076,12 +8016,52 @@ RAW: ${raw}
|
|
|
7076
8016
|
}
|
|
7077
8017
|
|
|
7078
8018
|
// src/cli/commands/log.ts
|
|
7079
|
-
var
|
|
7080
|
-
var
|
|
7081
|
-
var
|
|
8019
|
+
var import_fs19 = __toESM(require("fs"));
|
|
8020
|
+
var import_path21 = __toESM(require("path"));
|
|
8021
|
+
var import_os16 = __toESM(require("os"));
|
|
7082
8022
|
init_audit();
|
|
7083
8023
|
init_config();
|
|
7084
8024
|
init_policy();
|
|
8025
|
+
init_daemon();
|
|
8026
|
+
|
|
8027
|
+
// src/utils/cp-mv-parser.ts
|
|
8028
|
+
function parseCpMvOp(command) {
|
|
8029
|
+
const trimmed = command.trim();
|
|
8030
|
+
const tokens = trimmed.split(/\s+/);
|
|
8031
|
+
if (tokens.length < 3) return null;
|
|
8032
|
+
const [cmd, ...rest] = tokens;
|
|
8033
|
+
const base = cmd.split("/").pop() ?? cmd;
|
|
8034
|
+
if (base !== "cp" && base !== "mv") return null;
|
|
8035
|
+
const args = [];
|
|
8036
|
+
for (const tok of rest) {
|
|
8037
|
+
if (tok === "--") {
|
|
8038
|
+
args.push(...rest.slice(rest.indexOf("--") + 1));
|
|
8039
|
+
break;
|
|
8040
|
+
}
|
|
8041
|
+
if (tok === "-t" || tok === "--target-directory") return null;
|
|
8042
|
+
if (tok.startsWith("--target-directory=")) return null;
|
|
8043
|
+
if (tok.startsWith("-") && !tok.startsWith("--")) {
|
|
8044
|
+
if (tok.includes("t")) return null;
|
|
8045
|
+
continue;
|
|
8046
|
+
}
|
|
8047
|
+
if (tok.startsWith("--")) {
|
|
8048
|
+
continue;
|
|
8049
|
+
}
|
|
8050
|
+
args.push(tok);
|
|
8051
|
+
}
|
|
8052
|
+
if (args.length !== 2) return null;
|
|
8053
|
+
const [src, dest] = args;
|
|
8054
|
+
if (!src || !dest) return null;
|
|
8055
|
+
if (containsShellMetachar(src) || containsShellMetachar(dest)) return null;
|
|
8056
|
+
return { src, dest, clearSource: base === "mv" };
|
|
8057
|
+
}
|
|
8058
|
+
function containsShellMetachar(token) {
|
|
8059
|
+
if (/[$`{;*?]/.test(token)) return true;
|
|
8060
|
+
if (token.includes("\0")) return true;
|
|
8061
|
+
return false;
|
|
8062
|
+
}
|
|
8063
|
+
|
|
8064
|
+
// src/cli/commands/log.ts
|
|
7085
8065
|
function sanitize3(value) {
|
|
7086
8066
|
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
7087
8067
|
}
|
|
@@ -7100,11 +8080,20 @@ function registerLogCommand(program2) {
|
|
|
7100
8080
|
decision: "allowed",
|
|
7101
8081
|
source: "post-hook"
|
|
7102
8082
|
};
|
|
7103
|
-
const logPath =
|
|
7104
|
-
if (!
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
8083
|
+
const logPath = import_path21.default.join(import_os16.default.homedir(), ".node9", "audit.log");
|
|
8084
|
+
if (!import_fs19.default.existsSync(import_path21.default.dirname(logPath)))
|
|
8085
|
+
import_fs19.default.mkdirSync(import_path21.default.dirname(logPath), { recursive: true });
|
|
8086
|
+
import_fs19.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
8087
|
+
if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
|
|
8088
|
+
const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
8089
|
+
if (command) {
|
|
8090
|
+
const op = parseCpMvOp(command);
|
|
8091
|
+
if (op) {
|
|
8092
|
+
await notifyTaintPropagate(op.src, op.dest, op.clearSource);
|
|
8093
|
+
}
|
|
8094
|
+
}
|
|
8095
|
+
}
|
|
8096
|
+
const safeCwd = typeof payload.cwd === "string" && import_path21.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
7108
8097
|
const config = getConfig(safeCwd);
|
|
7109
8098
|
if (shouldSnapshot(tool, {}, config)) {
|
|
7110
8099
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -7113,9 +8102,9 @@ function registerLogCommand(program2) {
|
|
|
7113
8102
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7114
8103
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
7115
8104
|
`);
|
|
7116
|
-
const debugPath =
|
|
8105
|
+
const debugPath = import_path21.default.join(import_os16.default.homedir(), ".node9", "hook-debug.log");
|
|
7117
8106
|
try {
|
|
7118
|
-
|
|
8107
|
+
import_fs19.default.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
7119
8108
|
`);
|
|
7120
8109
|
} catch {
|
|
7121
8110
|
}
|
|
@@ -7419,14 +8408,14 @@ function registerConfigShowCommand(program2) {
|
|
|
7419
8408
|
|
|
7420
8409
|
// src/cli/commands/doctor.ts
|
|
7421
8410
|
var import_chalk7 = __toESM(require("chalk"));
|
|
7422
|
-
var
|
|
7423
|
-
var
|
|
7424
|
-
var
|
|
8411
|
+
var import_fs20 = __toESM(require("fs"));
|
|
8412
|
+
var import_path22 = __toESM(require("path"));
|
|
8413
|
+
var import_os17 = __toESM(require("os"));
|
|
7425
8414
|
var import_child_process9 = require("child_process");
|
|
7426
8415
|
init_daemon();
|
|
7427
8416
|
function registerDoctorCommand(program2, version2) {
|
|
7428
8417
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
7429
|
-
const homeDir2 =
|
|
8418
|
+
const homeDir2 = import_os17.default.homedir();
|
|
7430
8419
|
let failures = 0;
|
|
7431
8420
|
function pass(msg) {
|
|
7432
8421
|
console.log(import_chalk7.default.green(" \u2705 ") + msg);
|
|
@@ -7475,10 +8464,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7475
8464
|
);
|
|
7476
8465
|
}
|
|
7477
8466
|
section("Configuration");
|
|
7478
|
-
const globalConfigPath =
|
|
7479
|
-
if (
|
|
8467
|
+
const globalConfigPath = import_path22.default.join(homeDir2, ".node9", "config.json");
|
|
8468
|
+
if (import_fs20.default.existsSync(globalConfigPath)) {
|
|
7480
8469
|
try {
|
|
7481
|
-
JSON.parse(
|
|
8470
|
+
JSON.parse(import_fs20.default.readFileSync(globalConfigPath, "utf-8"));
|
|
7482
8471
|
pass("~/.node9/config.json found and valid");
|
|
7483
8472
|
} catch {
|
|
7484
8473
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -7486,10 +8475,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7486
8475
|
} else {
|
|
7487
8476
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
7488
8477
|
}
|
|
7489
|
-
const projectConfigPath =
|
|
7490
|
-
if (
|
|
8478
|
+
const projectConfigPath = import_path22.default.join(process.cwd(), "node9.config.json");
|
|
8479
|
+
if (import_fs20.default.existsSync(projectConfigPath)) {
|
|
7491
8480
|
try {
|
|
7492
|
-
JSON.parse(
|
|
8481
|
+
JSON.parse(import_fs20.default.readFileSync(projectConfigPath, "utf-8"));
|
|
7493
8482
|
pass("node9.config.json found and valid (project)");
|
|
7494
8483
|
} catch {
|
|
7495
8484
|
fail(
|
|
@@ -7498,8 +8487,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7498
8487
|
);
|
|
7499
8488
|
}
|
|
7500
8489
|
}
|
|
7501
|
-
const credsPath =
|
|
7502
|
-
if (
|
|
8490
|
+
const credsPath = import_path22.default.join(homeDir2, ".node9", "credentials.json");
|
|
8491
|
+
if (import_fs20.default.existsSync(credsPath)) {
|
|
7503
8492
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
7504
8493
|
} else {
|
|
7505
8494
|
warn(
|
|
@@ -7508,10 +8497,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7508
8497
|
);
|
|
7509
8498
|
}
|
|
7510
8499
|
section("Agent Hooks");
|
|
7511
|
-
const claudeSettingsPath =
|
|
7512
|
-
if (
|
|
8500
|
+
const claudeSettingsPath = import_path22.default.join(homeDir2, ".claude", "settings.json");
|
|
8501
|
+
if (import_fs20.default.existsSync(claudeSettingsPath)) {
|
|
7513
8502
|
try {
|
|
7514
|
-
const cs = JSON.parse(
|
|
8503
|
+
const cs = JSON.parse(import_fs20.default.readFileSync(claudeSettingsPath, "utf-8"));
|
|
7515
8504
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
7516
8505
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
7517
8506
|
);
|
|
@@ -7527,10 +8516,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7527
8516
|
} else {
|
|
7528
8517
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
7529
8518
|
}
|
|
7530
|
-
const geminiSettingsPath =
|
|
7531
|
-
if (
|
|
8519
|
+
const geminiSettingsPath = import_path22.default.join(homeDir2, ".gemini", "settings.json");
|
|
8520
|
+
if (import_fs20.default.existsSync(geminiSettingsPath)) {
|
|
7532
8521
|
try {
|
|
7533
|
-
const gs = JSON.parse(
|
|
8522
|
+
const gs = JSON.parse(import_fs20.default.readFileSync(geminiSettingsPath, "utf-8"));
|
|
7534
8523
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
7535
8524
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
7536
8525
|
);
|
|
@@ -7546,10 +8535,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7546
8535
|
} else {
|
|
7547
8536
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
7548
8537
|
}
|
|
7549
|
-
const cursorHooksPath =
|
|
7550
|
-
if (
|
|
8538
|
+
const cursorHooksPath = import_path22.default.join(homeDir2, ".cursor", "hooks.json");
|
|
8539
|
+
if (import_fs20.default.existsSync(cursorHooksPath)) {
|
|
7551
8540
|
try {
|
|
7552
|
-
const cur = JSON.parse(
|
|
8541
|
+
const cur = JSON.parse(import_fs20.default.readFileSync(cursorHooksPath, "utf-8"));
|
|
7553
8542
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
7554
8543
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
7555
8544
|
);
|
|
@@ -7587,9 +8576,9 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7587
8576
|
|
|
7588
8577
|
// src/cli/commands/audit.ts
|
|
7589
8578
|
var import_chalk8 = __toESM(require("chalk"));
|
|
7590
|
-
var
|
|
7591
|
-
var
|
|
7592
|
-
var
|
|
8579
|
+
var import_fs21 = __toESM(require("fs"));
|
|
8580
|
+
var import_path23 = __toESM(require("path"));
|
|
8581
|
+
var import_os18 = __toESM(require("os"));
|
|
7593
8582
|
function formatRelativeTime(timestamp) {
|
|
7594
8583
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
7595
8584
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -7602,14 +8591,14 @@ function formatRelativeTime(timestamp) {
|
|
|
7602
8591
|
}
|
|
7603
8592
|
function registerAuditCommand(program2) {
|
|
7604
8593
|
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) => {
|
|
7605
|
-
const logPath =
|
|
7606
|
-
if (!
|
|
8594
|
+
const logPath = import_path23.default.join(import_os18.default.homedir(), ".node9", "audit.log");
|
|
8595
|
+
if (!import_fs21.default.existsSync(logPath)) {
|
|
7607
8596
|
console.log(
|
|
7608
8597
|
import_chalk8.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
7609
8598
|
);
|
|
7610
8599
|
return;
|
|
7611
8600
|
}
|
|
7612
|
-
const raw =
|
|
8601
|
+
const raw = import_fs21.default.readFileSync(logPath, "utf-8");
|
|
7613
8602
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
7614
8603
|
let entries = lines.flatMap((line) => {
|
|
7615
8604
|
try {
|
|
@@ -7727,11 +8716,44 @@ function registerDaemonCommand(program2) {
|
|
|
7727
8716
|
|
|
7728
8717
|
// src/cli/commands/status.ts
|
|
7729
8718
|
var import_chalk10 = __toESM(require("chalk"));
|
|
7730
|
-
var
|
|
7731
|
-
var
|
|
7732
|
-
var
|
|
8719
|
+
var import_fs22 = __toESM(require("fs"));
|
|
8720
|
+
var import_path24 = __toESM(require("path"));
|
|
8721
|
+
var import_os19 = __toESM(require("os"));
|
|
7733
8722
|
init_core();
|
|
7734
8723
|
init_daemon();
|
|
8724
|
+
function readJson2(filePath) {
|
|
8725
|
+
try {
|
|
8726
|
+
if (import_fs22.default.existsSync(filePath)) return JSON.parse(import_fs22.default.readFileSync(filePath, "utf-8"));
|
|
8727
|
+
} catch {
|
|
8728
|
+
}
|
|
8729
|
+
return null;
|
|
8730
|
+
}
|
|
8731
|
+
function isNode9Hook2(cmd) {
|
|
8732
|
+
if (!cmd) return false;
|
|
8733
|
+
return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
|
|
8734
|
+
}
|
|
8735
|
+
function wrappedMcpServers(servers) {
|
|
8736
|
+
if (!servers) return [];
|
|
8737
|
+
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(" ")}`);
|
|
8738
|
+
}
|
|
8739
|
+
function printAgentSection(label, hookPairs, wrapped) {
|
|
8740
|
+
console.log(import_chalk10.default.bold(` ${label}`));
|
|
8741
|
+
for (const { name, present } of hookPairs) {
|
|
8742
|
+
if (present) {
|
|
8743
|
+
console.log(import_chalk10.default.green(` \u2713 ${name}`));
|
|
8744
|
+
} else {
|
|
8745
|
+
console.log(import_chalk10.default.red(` \u2717 ${name}`) + import_chalk10.default.gray(" (not wired)"));
|
|
8746
|
+
}
|
|
8747
|
+
}
|
|
8748
|
+
if (wrapped.length > 0) {
|
|
8749
|
+
console.log(import_chalk10.default.cyan(` MCP proxied:`));
|
|
8750
|
+
for (const entry of wrapped) {
|
|
8751
|
+
console.log(import_chalk10.default.gray(` \u2022 ${entry}`));
|
|
8752
|
+
}
|
|
8753
|
+
} else {
|
|
8754
|
+
console.log(import_chalk10.default.gray(` MCP proxied: none`));
|
|
8755
|
+
}
|
|
8756
|
+
}
|
|
7735
8757
|
function registerStatusCommand(program2) {
|
|
7736
8758
|
program2.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
|
|
7737
8759
|
const creds = getCredentials();
|
|
@@ -7766,19 +8788,72 @@ function registerStatusCommand(program2) {
|
|
|
7766
8788
|
console.log("");
|
|
7767
8789
|
const modeLabel = settings.mode === "audit" ? import_chalk10.default.blue("audit") : settings.mode === "strict" ? import_chalk10.default.red("strict") : import_chalk10.default.white("standard");
|
|
7768
8790
|
console.log(` Mode: ${modeLabel}`);
|
|
7769
|
-
const projectConfig =
|
|
7770
|
-
const globalConfig =
|
|
8791
|
+
const projectConfig = import_path24.default.join(process.cwd(), "node9.config.json");
|
|
8792
|
+
const globalConfig = import_path24.default.join(import_os19.default.homedir(), ".node9", "config.json");
|
|
7771
8793
|
console.log(
|
|
7772
|
-
` Local: ${
|
|
8794
|
+
` Local: ${import_fs22.default.existsSync(projectConfig) ? import_chalk10.default.green("Active (node9.config.json)") : import_chalk10.default.gray("Not present")}`
|
|
7773
8795
|
);
|
|
7774
8796
|
console.log(
|
|
7775
|
-
` Global: ${
|
|
8797
|
+
` Global: ${import_fs22.default.existsSync(globalConfig) ? import_chalk10.default.green("Active (~/.node9/config.json)") : import_chalk10.default.gray("Not present")}`
|
|
7776
8798
|
);
|
|
7777
8799
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
7778
8800
|
console.log(
|
|
7779
8801
|
` Sandbox: ${import_chalk10.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
7780
8802
|
);
|
|
7781
8803
|
}
|
|
8804
|
+
const homeDir2 = import_os19.default.homedir();
|
|
8805
|
+
const claudeSettings = readJson2(
|
|
8806
|
+
import_path24.default.join(homeDir2, ".claude", "settings.json")
|
|
8807
|
+
);
|
|
8808
|
+
const claudeConfig = readJson2(import_path24.default.join(homeDir2, ".claude.json"));
|
|
8809
|
+
const geminiSettings = readJson2(
|
|
8810
|
+
import_path24.default.join(homeDir2, ".gemini", "settings.json")
|
|
8811
|
+
);
|
|
8812
|
+
const cursorConfig = readJson2(import_path24.default.join(homeDir2, ".cursor", "mcp.json"));
|
|
8813
|
+
const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
|
|
8814
|
+
if (agentFound) {
|
|
8815
|
+
console.log("");
|
|
8816
|
+
console.log(import_chalk10.default.bold(" Agent Wiring:"));
|
|
8817
|
+
console.log("");
|
|
8818
|
+
if (claudeSettings || claudeConfig) {
|
|
8819
|
+
const preHook = claudeSettings?.hooks?.PreToolUse?.some(
|
|
8820
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8821
|
+
) ?? false;
|
|
8822
|
+
const postHook = claudeSettings?.hooks?.PostToolUse?.some(
|
|
8823
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8824
|
+
) ?? false;
|
|
8825
|
+
printAgentSection(
|
|
8826
|
+
"Claude Code",
|
|
8827
|
+
[
|
|
8828
|
+
{ name: "PreToolUse (node9 check)", present: preHook },
|
|
8829
|
+
{ name: "PostToolUse (node9 log)", present: postHook }
|
|
8830
|
+
],
|
|
8831
|
+
wrappedMcpServers(claudeConfig?.mcpServers)
|
|
8832
|
+
);
|
|
8833
|
+
console.log("");
|
|
8834
|
+
}
|
|
8835
|
+
if (geminiSettings) {
|
|
8836
|
+
const beforeHook = geminiSettings.hooks?.BeforeTool?.some(
|
|
8837
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8838
|
+
) ?? false;
|
|
8839
|
+
const afterHook = geminiSettings.hooks?.AfterTool?.some(
|
|
8840
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8841
|
+
) ?? false;
|
|
8842
|
+
printAgentSection(
|
|
8843
|
+
"Gemini CLI",
|
|
8844
|
+
[
|
|
8845
|
+
{ name: "BeforeTool (node9 check)", present: beforeHook },
|
|
8846
|
+
{ name: "AfterTool (node9 log)", present: afterHook }
|
|
8847
|
+
],
|
|
8848
|
+
wrappedMcpServers(geminiSettings.mcpServers)
|
|
8849
|
+
);
|
|
8850
|
+
console.log("");
|
|
8851
|
+
}
|
|
8852
|
+
if (cursorConfig) {
|
|
8853
|
+
printAgentSection("Cursor", [], wrappedMcpServers(cursorConfig.mcpServers));
|
|
8854
|
+
console.log("");
|
|
8855
|
+
}
|
|
8856
|
+
}
|
|
7782
8857
|
const pauseState = checkPause();
|
|
7783
8858
|
if (pauseState.paused) {
|
|
7784
8859
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
@@ -7791,8 +8866,63 @@ function registerStatusCommand(program2) {
|
|
|
7791
8866
|
});
|
|
7792
8867
|
}
|
|
7793
8868
|
|
|
7794
|
-
// src/cli/commands/
|
|
8869
|
+
// src/cli/commands/init.ts
|
|
7795
8870
|
var import_chalk11 = __toESM(require("chalk"));
|
|
8871
|
+
var import_fs23 = __toESM(require("fs"));
|
|
8872
|
+
var import_path25 = __toESM(require("path"));
|
|
8873
|
+
var import_os20 = __toESM(require("os"));
|
|
8874
|
+
init_core();
|
|
8875
|
+
function registerInitCommand(program2) {
|
|
8876
|
+
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) => {
|
|
8877
|
+
console.log(import_chalk11.default.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
8878
|
+
const configPath = import_path25.default.join(import_os20.default.homedir(), ".node9", "config.json");
|
|
8879
|
+
if (import_fs23.default.existsSync(configPath) && !options.force) {
|
|
8880
|
+
console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
8881
|
+
} else {
|
|
8882
|
+
const requestedMode = options.mode.toLowerCase();
|
|
8883
|
+
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
8884
|
+
const configToSave = {
|
|
8885
|
+
...DEFAULT_CONFIG,
|
|
8886
|
+
settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
|
|
8887
|
+
};
|
|
8888
|
+
const dir = import_path25.default.dirname(configPath);
|
|
8889
|
+
if (!import_fs23.default.existsSync(dir)) import_fs23.default.mkdirSync(dir, { recursive: true });
|
|
8890
|
+
import_fs23.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
8891
|
+
console.log(import_chalk11.default.green(`\u2705 Config created: ${configPath}`));
|
|
8892
|
+
console.log(import_chalk11.default.gray(` Mode: ${safeMode}`));
|
|
8893
|
+
}
|
|
8894
|
+
if (options.skipSetup) return;
|
|
8895
|
+
console.log("");
|
|
8896
|
+
const detected = detectAgents();
|
|
8897
|
+
const found = Object.keys(detected).filter(
|
|
8898
|
+
(k) => detected[k]
|
|
8899
|
+
);
|
|
8900
|
+
if (found.length === 0) {
|
|
8901
|
+
console.log(
|
|
8902
|
+
import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, or Cursor")
|
|
8903
|
+
);
|
|
8904
|
+
console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor>"));
|
|
8905
|
+
return;
|
|
8906
|
+
}
|
|
8907
|
+
console.log(import_chalk11.default.bold("Detected agents:"));
|
|
8908
|
+
for (const agent of found) {
|
|
8909
|
+
console.log(import_chalk11.default.green(` \u2713 ${agent}`));
|
|
8910
|
+
}
|
|
8911
|
+
console.log("");
|
|
8912
|
+
for (const agent of found) {
|
|
8913
|
+
console.log(import_chalk11.default.bold(`Wiring ${agent}...`));
|
|
8914
|
+
if (agent === "claude") await setupClaude();
|
|
8915
|
+
else if (agent === "gemini") await setupGemini();
|
|
8916
|
+
else if (agent === "cursor") await setupCursor();
|
|
8917
|
+
console.log("");
|
|
8918
|
+
}
|
|
8919
|
+
console.log(import_chalk11.default.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
|
|
8920
|
+
console.log(import_chalk11.default.gray(" Run: node9 daemon start"));
|
|
8921
|
+
});
|
|
8922
|
+
}
|
|
8923
|
+
|
|
8924
|
+
// src/cli/commands/undo.ts
|
|
8925
|
+
var import_chalk12 = __toESM(require("chalk"));
|
|
7796
8926
|
var import_prompts2 = require("@inquirer/prompts");
|
|
7797
8927
|
function registerUndoCommand(program2) {
|
|
7798
8928
|
program2.command("undo").description(
|
|
@@ -7804,22 +8934,22 @@ function registerUndoCommand(program2) {
|
|
|
7804
8934
|
if (history.length === 0) {
|
|
7805
8935
|
if (!options.all && allHistory.length > 0) {
|
|
7806
8936
|
console.log(
|
|
7807
|
-
|
|
8937
|
+
import_chalk12.default.yellow(
|
|
7808
8938
|
`
|
|
7809
8939
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
7810
|
-
Run ${
|
|
8940
|
+
Run ${import_chalk12.default.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
7811
8941
|
`
|
|
7812
8942
|
)
|
|
7813
8943
|
);
|
|
7814
8944
|
} else {
|
|
7815
|
-
console.log(
|
|
8945
|
+
console.log(import_chalk12.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
7816
8946
|
}
|
|
7817
8947
|
return;
|
|
7818
8948
|
}
|
|
7819
8949
|
const idx = history.length - steps;
|
|
7820
8950
|
if (idx < 0) {
|
|
7821
8951
|
console.log(
|
|
7822
|
-
|
|
8952
|
+
import_chalk12.default.yellow(
|
|
7823
8953
|
`
|
|
7824
8954
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
7825
8955
|
`
|
|
@@ -7831,19 +8961,19 @@ function registerUndoCommand(program2) {
|
|
|
7831
8961
|
const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
|
|
7832
8962
|
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
|
|
7833
8963
|
console.log(
|
|
7834
|
-
|
|
8964
|
+
import_chalk12.default.magenta.bold(`
|
|
7835
8965
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
|
|
7836
8966
|
);
|
|
7837
8967
|
console.log(
|
|
7838
|
-
|
|
7839
|
-
` Tool: ${
|
|
8968
|
+
import_chalk12.default.white(
|
|
8969
|
+
` Tool: ${import_chalk12.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk12.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
7840
8970
|
)
|
|
7841
8971
|
);
|
|
7842
|
-
console.log(
|
|
7843
|
-
console.log(
|
|
8972
|
+
console.log(import_chalk12.default.white(` When: ${import_chalk12.default.gray(ageStr)}`));
|
|
8973
|
+
console.log(import_chalk12.default.white(` Dir: ${import_chalk12.default.gray(snapshot.cwd)}`));
|
|
7844
8974
|
if (steps > 1)
|
|
7845
8975
|
console.log(
|
|
7846
|
-
|
|
8976
|
+
import_chalk12.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
7847
8977
|
);
|
|
7848
8978
|
console.log("");
|
|
7849
8979
|
const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
@@ -7851,21 +8981,21 @@ function registerUndoCommand(program2) {
|
|
|
7851
8981
|
const lines = diff.split("\n");
|
|
7852
8982
|
for (const line of lines) {
|
|
7853
8983
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
7854
|
-
console.log(
|
|
8984
|
+
console.log(import_chalk12.default.bold(line));
|
|
7855
8985
|
} else if (line.startsWith("+")) {
|
|
7856
|
-
console.log(
|
|
8986
|
+
console.log(import_chalk12.default.green(line));
|
|
7857
8987
|
} else if (line.startsWith("-")) {
|
|
7858
|
-
console.log(
|
|
8988
|
+
console.log(import_chalk12.default.red(line));
|
|
7859
8989
|
} else if (line.startsWith("@@")) {
|
|
7860
|
-
console.log(
|
|
8990
|
+
console.log(import_chalk12.default.cyan(line));
|
|
7861
8991
|
} else {
|
|
7862
|
-
console.log(
|
|
8992
|
+
console.log(import_chalk12.default.gray(line));
|
|
7863
8993
|
}
|
|
7864
8994
|
}
|
|
7865
8995
|
console.log("");
|
|
7866
8996
|
} else {
|
|
7867
8997
|
console.log(
|
|
7868
|
-
|
|
8998
|
+
import_chalk12.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
7869
8999
|
);
|
|
7870
9000
|
}
|
|
7871
9001
|
const proceed = await (0, import_prompts2.confirm)({
|
|
@@ -7874,18 +9004,18 @@ function registerUndoCommand(program2) {
|
|
|
7874
9004
|
});
|
|
7875
9005
|
if (proceed) {
|
|
7876
9006
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
7877
|
-
console.log(
|
|
9007
|
+
console.log(import_chalk12.default.green("\n\u2705 Reverted successfully.\n"));
|
|
7878
9008
|
} else {
|
|
7879
|
-
console.error(
|
|
9009
|
+
console.error(import_chalk12.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
7880
9010
|
}
|
|
7881
9011
|
} else {
|
|
7882
|
-
console.log(
|
|
9012
|
+
console.log(import_chalk12.default.gray("\nCancelled.\n"));
|
|
7883
9013
|
}
|
|
7884
9014
|
});
|
|
7885
9015
|
}
|
|
7886
9016
|
|
|
7887
9017
|
// src/cli/commands/watch.ts
|
|
7888
|
-
var
|
|
9018
|
+
var import_chalk13 = __toESM(require("chalk"));
|
|
7889
9019
|
var import_child_process11 = require("child_process");
|
|
7890
9020
|
init_daemon();
|
|
7891
9021
|
function registerWatchCommand(program2) {
|
|
@@ -7902,7 +9032,7 @@ function registerWatchCommand(program2) {
|
|
|
7902
9032
|
throw new Error("not running");
|
|
7903
9033
|
}
|
|
7904
9034
|
} catch {
|
|
7905
|
-
console.error(
|
|
9035
|
+
console.error(import_chalk13.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
|
|
7906
9036
|
const child = (0, import_child_process11.spawn)(process.execPath, [process.argv[1], "daemon"], {
|
|
7907
9037
|
detached: true,
|
|
7908
9038
|
stdio: "ignore",
|
|
@@ -7924,12 +9054,12 @@ function registerWatchCommand(program2) {
|
|
|
7924
9054
|
}
|
|
7925
9055
|
}
|
|
7926
9056
|
if (!ready) {
|
|
7927
|
-
console.error(
|
|
9057
|
+
console.error(import_chalk13.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
7928
9058
|
process.exit(1);
|
|
7929
9059
|
}
|
|
7930
9060
|
}
|
|
7931
9061
|
console.error(
|
|
7932
|
-
|
|
9062
|
+
import_chalk13.default.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + import_chalk13.default.dim(` \u2192 localhost:${port}`) + import_chalk13.default.dim(
|
|
7933
9063
|
"\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
|
|
7934
9064
|
)
|
|
7935
9065
|
);
|
|
@@ -7938,7 +9068,7 @@ function registerWatchCommand(program2) {
|
|
|
7938
9068
|
env: { ...process.env, NODE9_WATCH_MODE: "1" }
|
|
7939
9069
|
});
|
|
7940
9070
|
if (result.error) {
|
|
7941
|
-
console.error(
|
|
9071
|
+
console.error(import_chalk13.default.red(`\u274C Failed to run command: ${result.error.message}`));
|
|
7942
9072
|
process.exit(1);
|
|
7943
9073
|
}
|
|
7944
9074
|
process.exit(result.status ?? 0);
|
|
@@ -7947,7 +9077,7 @@ function registerWatchCommand(program2) {
|
|
|
7947
9077
|
|
|
7948
9078
|
// src/mcp-gateway/index.ts
|
|
7949
9079
|
var import_readline2 = __toESM(require("readline"));
|
|
7950
|
-
var
|
|
9080
|
+
var import_chalk14 = __toESM(require("chalk"));
|
|
7951
9081
|
var import_child_process12 = require("child_process");
|
|
7952
9082
|
var import_execa3 = require("execa");
|
|
7953
9083
|
init_orchestrator();
|
|
@@ -8011,13 +9141,13 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
8011
9141
|
const prov = checkProvenance(executable);
|
|
8012
9142
|
if (prov.trustLevel === "suspect") {
|
|
8013
9143
|
console.error(
|
|
8014
|
-
|
|
9144
|
+
import_chalk14.default.red(
|
|
8015
9145
|
`\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
|
|
8016
9146
|
)
|
|
8017
9147
|
);
|
|
8018
|
-
console.error(
|
|
9148
|
+
console.error(import_chalk14.default.red(" Verify this binary is trusted before proceeding."));
|
|
8019
9149
|
}
|
|
8020
|
-
console.error(
|
|
9150
|
+
console.error(import_chalk14.default.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
|
|
8021
9151
|
const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
|
|
8022
9152
|
"NODE_OPTIONS",
|
|
8023
9153
|
"NODE_PATH",
|
|
@@ -8081,10 +9211,10 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
8081
9211
|
mcpServer
|
|
8082
9212
|
});
|
|
8083
9213
|
if (!result.approved) {
|
|
8084
|
-
console.error(
|
|
9214
|
+
console.error(import_chalk14.default.red(`
|
|
8085
9215
|
\u{1F6D1} Node9 MCP Gateway: Action Blocked`));
|
|
8086
|
-
console.error(
|
|
8087
|
-
console.error(
|
|
9216
|
+
console.error(import_chalk14.default.gray(` Tool: ${toolName}`));
|
|
9217
|
+
console.error(import_chalk14.default.gray(` Reason: ${result.reason ?? "Security Policy"}
|
|
8088
9218
|
`));
|
|
8089
9219
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
8090
9220
|
const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -8157,7 +9287,7 @@ function registerMcpGatewayCommand(program2) {
|
|
|
8157
9287
|
}
|
|
8158
9288
|
|
|
8159
9289
|
// src/cli/commands/trust.ts
|
|
8160
|
-
var
|
|
9290
|
+
var import_chalk15 = __toESM(require("chalk"));
|
|
8161
9291
|
init_trusted_hosts();
|
|
8162
9292
|
function isValidHost(host) {
|
|
8163
9293
|
return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
|
|
@@ -8168,44 +9298,44 @@ function registerTrustCommand(program2) {
|
|
|
8168
9298
|
const normalized = normalizeHost(host.trim());
|
|
8169
9299
|
if (!isValidHost(normalized)) {
|
|
8170
9300
|
console.error(
|
|
8171
|
-
|
|
9301
|
+
import_chalk15.default.red(`
|
|
8172
9302
|
\u274C Invalid host: "${host}"
|
|
8173
|
-
`) +
|
|
9303
|
+
`) + import_chalk15.default.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
|
|
8174
9304
|
);
|
|
8175
9305
|
process.exit(1);
|
|
8176
9306
|
}
|
|
8177
9307
|
addTrustedHost(normalized);
|
|
8178
|
-
console.log(
|
|
9308
|
+
console.log(import_chalk15.default.green(`
|
|
8179
9309
|
\u2705 ${normalized} added to trusted hosts.`));
|
|
8180
9310
|
console.log(
|
|
8181
|
-
|
|
9311
|
+
import_chalk15.default.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
|
|
8182
9312
|
);
|
|
8183
9313
|
});
|
|
8184
9314
|
trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
|
|
8185
9315
|
const normalized = normalizeHost(host.trim());
|
|
8186
9316
|
const removed = removeTrustedHost(normalized);
|
|
8187
9317
|
if (!removed) {
|
|
8188
|
-
console.error(
|
|
9318
|
+
console.error(import_chalk15.default.yellow(`
|
|
8189
9319
|
\u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
|
|
8190
9320
|
`));
|
|
8191
9321
|
process.exit(1);
|
|
8192
9322
|
}
|
|
8193
|
-
console.log(
|
|
9323
|
+
console.log(import_chalk15.default.green(`
|
|
8194
9324
|
\u2705 ${normalized} removed from trusted hosts.
|
|
8195
9325
|
`));
|
|
8196
9326
|
});
|
|
8197
9327
|
trustCmd.command("list").description("Show all trusted hosts").action(() => {
|
|
8198
9328
|
const hosts = readTrustedHosts();
|
|
8199
9329
|
if (hosts.length === 0) {
|
|
8200
|
-
console.log(
|
|
8201
|
-
console.log(` Add one: ${
|
|
9330
|
+
console.log(import_chalk15.default.gray("\n No trusted hosts configured.\n"));
|
|
9331
|
+
console.log(` Add one: ${import_chalk15.default.cyan("node9 trust add api.mycompany.com")}
|
|
8202
9332
|
`);
|
|
8203
9333
|
return;
|
|
8204
9334
|
}
|
|
8205
|
-
console.log(
|
|
9335
|
+
console.log(import_chalk15.default.bold("\n\u{1F513} Trusted Hosts\n"));
|
|
8206
9336
|
for (const entry of hosts) {
|
|
8207
9337
|
const date = new Date(entry.addedAt).toLocaleDateString();
|
|
8208
|
-
console.log(` ${
|
|
9338
|
+
console.log(` ${import_chalk15.default.cyan(entry.host.padEnd(40))} ${import_chalk15.default.gray(`added ${date}`)}`);
|
|
8209
9339
|
}
|
|
8210
9340
|
console.log("");
|
|
8211
9341
|
});
|
|
@@ -8213,20 +9343,20 @@ function registerTrustCommand(program2) {
|
|
|
8213
9343
|
|
|
8214
9344
|
// src/cli.ts
|
|
8215
9345
|
var { version } = JSON.parse(
|
|
8216
|
-
|
|
9346
|
+
import_fs25.default.readFileSync(import_path27.default.join(__dirname, "../package.json"), "utf-8")
|
|
8217
9347
|
);
|
|
8218
9348
|
var program = new import_commander.Command();
|
|
8219
9349
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
8220
9350
|
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) => {
|
|
8221
9351
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
8222
|
-
const credPath =
|
|
8223
|
-
if (!
|
|
8224
|
-
|
|
9352
|
+
const credPath = import_path27.default.join(import_os22.default.homedir(), ".node9", "credentials.json");
|
|
9353
|
+
if (!import_fs25.default.existsSync(import_path27.default.dirname(credPath)))
|
|
9354
|
+
import_fs25.default.mkdirSync(import_path27.default.dirname(credPath), { recursive: true });
|
|
8225
9355
|
const profileName = options.profile || "default";
|
|
8226
9356
|
let existingCreds = {};
|
|
8227
9357
|
try {
|
|
8228
|
-
if (
|
|
8229
|
-
const raw = JSON.parse(
|
|
9358
|
+
if (import_fs25.default.existsSync(credPath)) {
|
|
9359
|
+
const raw = JSON.parse(import_fs25.default.readFileSync(credPath, "utf-8"));
|
|
8230
9360
|
if (raw.apiKey) {
|
|
8231
9361
|
existingCreds = {
|
|
8232
9362
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -8238,13 +9368,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
8238
9368
|
} catch {
|
|
8239
9369
|
}
|
|
8240
9370
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
8241
|
-
|
|
9371
|
+
import_fs25.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
8242
9372
|
if (profileName === "default") {
|
|
8243
|
-
const configPath =
|
|
9373
|
+
const configPath = import_path27.default.join(import_os22.default.homedir(), ".node9", "config.json");
|
|
8244
9374
|
let config = {};
|
|
8245
9375
|
try {
|
|
8246
|
-
if (
|
|
8247
|
-
config = JSON.parse(
|
|
9376
|
+
if (import_fs25.default.existsSync(configPath))
|
|
9377
|
+
config = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
|
|
8248
9378
|
} catch {
|
|
8249
9379
|
}
|
|
8250
9380
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -8259,36 +9389,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
8259
9389
|
approvers.cloud = false;
|
|
8260
9390
|
}
|
|
8261
9391
|
s.approvers = approvers;
|
|
8262
|
-
if (!
|
|
8263
|
-
|
|
8264
|
-
|
|
9392
|
+
if (!import_fs25.default.existsSync(import_path27.default.dirname(configPath)))
|
|
9393
|
+
import_fs25.default.mkdirSync(import_path27.default.dirname(configPath), { recursive: true });
|
|
9394
|
+
import_fs25.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
8265
9395
|
}
|
|
8266
9396
|
if (options.profile && profileName !== "default") {
|
|
8267
|
-
console.log(
|
|
8268
|
-
console.log(
|
|
9397
|
+
console.log(import_chalk17.default.green(`\u2705 Profile "${profileName}" saved`));
|
|
9398
|
+
console.log(import_chalk17.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
8269
9399
|
} else if (options.local) {
|
|
8270
|
-
console.log(
|
|
8271
|
-
console.log(
|
|
9400
|
+
console.log(import_chalk17.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
9401
|
+
console.log(import_chalk17.default.gray(` All decisions stay on this machine.`));
|
|
8272
9402
|
} else {
|
|
8273
|
-
console.log(
|
|
8274
|
-
console.log(
|
|
9403
|
+
console.log(import_chalk17.default.green(`\u2705 Logged in \u2014 agent mode`));
|
|
9404
|
+
console.log(import_chalk17.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
8275
9405
|
}
|
|
8276
9406
|
});
|
|
8277
9407
|
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) => {
|
|
8278
9408
|
if (target === "gemini") return await setupGemini();
|
|
8279
9409
|
if (target === "claude") return await setupClaude();
|
|
8280
9410
|
if (target === "cursor") return await setupCursor();
|
|
8281
|
-
console.error(
|
|
9411
|
+
console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8282
9412
|
process.exit(1);
|
|
8283
9413
|
});
|
|
8284
9414
|
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) => {
|
|
8285
9415
|
if (!target) {
|
|
8286
|
-
console.log(
|
|
8287
|
-
console.log(" Usage: " +
|
|
9416
|
+
console.log(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
9417
|
+
console.log(" Usage: " + import_chalk17.default.white("node9 setup <target>") + "\n");
|
|
8288
9418
|
console.log(" Targets:");
|
|
8289
|
-
console.log(" " +
|
|
8290
|
-
console.log(" " +
|
|
8291
|
-
console.log(" " +
|
|
9419
|
+
console.log(" " + import_chalk17.default.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
9420
|
+
console.log(" " + import_chalk17.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
9421
|
+
console.log(" " + import_chalk17.default.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
8292
9422
|
console.log("");
|
|
8293
9423
|
return;
|
|
8294
9424
|
}
|
|
@@ -8296,7 +9426,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
8296
9426
|
if (t === "gemini") return await setupGemini();
|
|
8297
9427
|
if (t === "claude") return await setupClaude();
|
|
8298
9428
|
if (t === "cursor") return await setupCursor();
|
|
8299
|
-
console.error(
|
|
9429
|
+
console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8300
9430
|
process.exit(1);
|
|
8301
9431
|
});
|
|
8302
9432
|
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) => {
|
|
@@ -8305,30 +9435,30 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
|
|
|
8305
9435
|
else if (target === "gemini") fn = teardownGemini;
|
|
8306
9436
|
else if (target === "cursor") fn = teardownCursor;
|
|
8307
9437
|
else {
|
|
8308
|
-
console.error(
|
|
9438
|
+
console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8309
9439
|
process.exit(1);
|
|
8310
9440
|
}
|
|
8311
|
-
console.log(
|
|
9441
|
+
console.log(import_chalk17.default.cyan(`
|
|
8312
9442
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
8313
9443
|
`));
|
|
8314
9444
|
try {
|
|
8315
9445
|
fn();
|
|
8316
9446
|
} catch (err) {
|
|
8317
|
-
console.error(
|
|
9447
|
+
console.error(import_chalk17.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
8318
9448
|
process.exit(1);
|
|
8319
9449
|
}
|
|
8320
|
-
console.log(
|
|
9450
|
+
console.log(import_chalk17.default.gray("\n Restart the agent for changes to take effect."));
|
|
8321
9451
|
});
|
|
8322
9452
|
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) => {
|
|
8323
|
-
console.log(
|
|
8324
|
-
console.log(
|
|
9453
|
+
console.log(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
9454
|
+
console.log(import_chalk17.default.bold("Stopping daemon..."));
|
|
8325
9455
|
try {
|
|
8326
9456
|
stopDaemon();
|
|
8327
|
-
console.log(
|
|
9457
|
+
console.log(import_chalk17.default.green(" \u2705 Daemon stopped"));
|
|
8328
9458
|
} catch {
|
|
8329
|
-
console.log(
|
|
9459
|
+
console.log(import_chalk17.default.blue(" \u2139\uFE0F Daemon was not running"));
|
|
8330
9460
|
}
|
|
8331
|
-
console.log(
|
|
9461
|
+
console.log(import_chalk17.default.bold("\nRemoving hooks..."));
|
|
8332
9462
|
let teardownFailed = false;
|
|
8333
9463
|
for (const [label, fn] of [
|
|
8334
9464
|
["Claude", teardownClaude],
|
|
@@ -8340,45 +9470,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
8340
9470
|
} catch (err) {
|
|
8341
9471
|
teardownFailed = true;
|
|
8342
9472
|
console.error(
|
|
8343
|
-
|
|
9473
|
+
import_chalk17.default.red(
|
|
8344
9474
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
|
|
8345
9475
|
)
|
|
8346
9476
|
);
|
|
8347
9477
|
}
|
|
8348
9478
|
}
|
|
8349
9479
|
if (options.purge) {
|
|
8350
|
-
const node9Dir =
|
|
8351
|
-
if (
|
|
9480
|
+
const node9Dir = import_path27.default.join(import_os22.default.homedir(), ".node9");
|
|
9481
|
+
if (import_fs25.default.existsSync(node9Dir)) {
|
|
8352
9482
|
const confirmed = await (0, import_prompts3.confirm)({
|
|
8353
9483
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
8354
9484
|
default: false
|
|
8355
9485
|
});
|
|
8356
9486
|
if (confirmed) {
|
|
8357
|
-
|
|
8358
|
-
if (
|
|
9487
|
+
import_fs25.default.rmSync(node9Dir, { recursive: true });
|
|
9488
|
+
if (import_fs25.default.existsSync(node9Dir)) {
|
|
8359
9489
|
console.error(
|
|
8360
|
-
|
|
9490
|
+
import_chalk17.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
8361
9491
|
);
|
|
8362
9492
|
} else {
|
|
8363
|
-
console.log(
|
|
9493
|
+
console.log(import_chalk17.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
8364
9494
|
}
|
|
8365
9495
|
} else {
|
|
8366
|
-
console.log(
|
|
9496
|
+
console.log(import_chalk17.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
8367
9497
|
}
|
|
8368
9498
|
} else {
|
|
8369
|
-
console.log(
|
|
9499
|
+
console.log(import_chalk17.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
8370
9500
|
}
|
|
8371
9501
|
} else {
|
|
8372
9502
|
console.log(
|
|
8373
|
-
|
|
9503
|
+
import_chalk17.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
8374
9504
|
);
|
|
8375
9505
|
}
|
|
8376
9506
|
if (teardownFailed) {
|
|
8377
|
-
console.error(
|
|
9507
|
+
console.error(import_chalk17.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
8378
9508
|
process.exit(1);
|
|
8379
9509
|
}
|
|
8380
|
-
console.log(
|
|
8381
|
-
console.log(
|
|
9510
|
+
console.log(import_chalk17.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
|
|
9511
|
+
console.log(import_chalk17.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
8382
9512
|
});
|
|
8383
9513
|
registerDoctorCommand(program, version);
|
|
8384
9514
|
program.command("explain").description(
|
|
@@ -8391,7 +9521,7 @@ program.command("explain").description(
|
|
|
8391
9521
|
try {
|
|
8392
9522
|
args = JSON.parse(trimmed);
|
|
8393
9523
|
} catch {
|
|
8394
|
-
console.error(
|
|
9524
|
+
console.error(import_chalk17.default.red(`
|
|
8395
9525
|
\u274C Invalid JSON: ${trimmed}
|
|
8396
9526
|
`));
|
|
8397
9527
|
process.exit(1);
|
|
@@ -8402,83 +9532,59 @@ program.command("explain").description(
|
|
|
8402
9532
|
}
|
|
8403
9533
|
const result = await explainPolicy(tool, args);
|
|
8404
9534
|
console.log("");
|
|
8405
|
-
console.log(
|
|
9535
|
+
console.log(import_chalk17.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
8406
9536
|
console.log("");
|
|
8407
|
-
console.log(` ${
|
|
9537
|
+
console.log(` ${import_chalk17.default.bold("Tool:")} ${import_chalk17.default.white(result.tool)}`);
|
|
8408
9538
|
if (argsRaw) {
|
|
8409
9539
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
8410
|
-
console.log(` ${
|
|
9540
|
+
console.log(` ${import_chalk17.default.bold("Input:")} ${import_chalk17.default.gray(preview)}`);
|
|
8411
9541
|
}
|
|
8412
9542
|
console.log("");
|
|
8413
|
-
console.log(
|
|
9543
|
+
console.log(import_chalk17.default.bold("Config Sources (Waterfall):"));
|
|
8414
9544
|
for (const tier of result.waterfall) {
|
|
8415
|
-
const num =
|
|
9545
|
+
const num = import_chalk17.default.gray(` ${tier.tier}.`);
|
|
8416
9546
|
const label = tier.label.padEnd(16);
|
|
8417
9547
|
let statusStr;
|
|
8418
9548
|
if (tier.tier === 1) {
|
|
8419
|
-
statusStr =
|
|
9549
|
+
statusStr = import_chalk17.default.gray(tier.note ?? "");
|
|
8420
9550
|
} else if (tier.status === "active") {
|
|
8421
|
-
const loc = tier.path ?
|
|
8422
|
-
const note = tier.note ?
|
|
8423
|
-
statusStr =
|
|
9551
|
+
const loc = tier.path ? import_chalk17.default.gray(tier.path) : "";
|
|
9552
|
+
const note = tier.note ? import_chalk17.default.gray(`(${tier.note})`) : "";
|
|
9553
|
+
statusStr = import_chalk17.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
8424
9554
|
} else {
|
|
8425
|
-
statusStr =
|
|
9555
|
+
statusStr = import_chalk17.default.gray("\u25CB " + (tier.note ?? "not found"));
|
|
8426
9556
|
}
|
|
8427
|
-
console.log(`${num} ${
|
|
9557
|
+
console.log(`${num} ${import_chalk17.default.white(label)} ${statusStr}`);
|
|
8428
9558
|
}
|
|
8429
9559
|
console.log("");
|
|
8430
|
-
console.log(
|
|
9560
|
+
console.log(import_chalk17.default.bold("Policy Evaluation:"));
|
|
8431
9561
|
for (const step of result.steps) {
|
|
8432
9562
|
const isFinal = step.isFinal;
|
|
8433
9563
|
let icon;
|
|
8434
|
-
if (step.outcome === "allow") icon =
|
|
8435
|
-
else if (step.outcome === "review") icon =
|
|
8436
|
-
else if (step.outcome === "skip") icon =
|
|
8437
|
-
else icon =
|
|
9564
|
+
if (step.outcome === "allow") icon = import_chalk17.default.green(" \u2705");
|
|
9565
|
+
else if (step.outcome === "review") icon = import_chalk17.default.red(" \u{1F534}");
|
|
9566
|
+
else if (step.outcome === "skip") icon = import_chalk17.default.gray(" \u2500 ");
|
|
9567
|
+
else icon = import_chalk17.default.gray(" \u25CB ");
|
|
8438
9568
|
const name = step.name.padEnd(18);
|
|
8439
|
-
const nameStr = isFinal ?
|
|
8440
|
-
const detail = isFinal ?
|
|
8441
|
-
const arrow = isFinal ?
|
|
9569
|
+
const nameStr = isFinal ? import_chalk17.default.white.bold(name) : import_chalk17.default.white(name);
|
|
9570
|
+
const detail = isFinal ? import_chalk17.default.white(step.detail) : import_chalk17.default.gray(step.detail);
|
|
9571
|
+
const arrow = isFinal ? import_chalk17.default.yellow(" \u2190 STOP") : "";
|
|
8442
9572
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
8443
9573
|
}
|
|
8444
9574
|
console.log("");
|
|
8445
9575
|
if (result.decision === "allow") {
|
|
8446
|
-
console.log(
|
|
9576
|
+
console.log(import_chalk17.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk17.default.gray(" \u2014 no approval needed"));
|
|
8447
9577
|
} else {
|
|
8448
9578
|
console.log(
|
|
8449
|
-
|
|
9579
|
+
import_chalk17.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk17.default.gray(" \u2014 human approval required")
|
|
8450
9580
|
);
|
|
8451
9581
|
if (result.blockedByLabel) {
|
|
8452
|
-
console.log(
|
|
9582
|
+
console.log(import_chalk17.default.gray(` Reason: ${result.blockedByLabel}`));
|
|
8453
9583
|
}
|
|
8454
9584
|
}
|
|
8455
9585
|
console.log("");
|
|
8456
9586
|
});
|
|
8457
|
-
program
|
|
8458
|
-
const configPath = import_path24.default.join(import_os20.default.homedir(), ".node9", "config.json");
|
|
8459
|
-
if (import_fs22.default.existsSync(configPath) && !options.force) {
|
|
8460
|
-
console.log(import_chalk16.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
8461
|
-
console.log(import_chalk16.default.gray(` Run with --force to overwrite.`));
|
|
8462
|
-
return;
|
|
8463
|
-
}
|
|
8464
|
-
const requestedMode = options.mode.toLowerCase();
|
|
8465
|
-
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
8466
|
-
const configToSave = {
|
|
8467
|
-
...DEFAULT_CONFIG,
|
|
8468
|
-
settings: {
|
|
8469
|
-
...DEFAULT_CONFIG.settings,
|
|
8470
|
-
mode: safeMode
|
|
8471
|
-
}
|
|
8472
|
-
};
|
|
8473
|
-
const dir = import_path24.default.dirname(configPath);
|
|
8474
|
-
if (!import_fs22.default.existsSync(dir)) import_fs22.default.mkdirSync(dir, { recursive: true });
|
|
8475
|
-
import_fs22.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
8476
|
-
console.log(import_chalk16.default.green(`\u2705 Global config created: ${configPath}`));
|
|
8477
|
-
console.log(import_chalk16.default.cyan(` Mode set to: ${safeMode}`));
|
|
8478
|
-
console.log(
|
|
8479
|
-
import_chalk16.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
8480
|
-
);
|
|
8481
|
-
});
|
|
9587
|
+
registerInitCommand(program);
|
|
8482
9588
|
registerAuditCommand(program);
|
|
8483
9589
|
registerStatusCommand(program);
|
|
8484
9590
|
registerDaemonCommand(program);
|
|
@@ -8487,7 +9593,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
8487
9593
|
try {
|
|
8488
9594
|
await startTail2(options);
|
|
8489
9595
|
} catch (err) {
|
|
8490
|
-
console.error(
|
|
9596
|
+
console.error(import_chalk17.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
|
|
8491
9597
|
process.exit(1);
|
|
8492
9598
|
}
|
|
8493
9599
|
});
|
|
@@ -8499,7 +9605,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
8499
9605
|
const ms = parseDuration(options.duration);
|
|
8500
9606
|
if (ms === null) {
|
|
8501
9607
|
console.error(
|
|
8502
|
-
|
|
9608
|
+
import_chalk17.default.red(`
|
|
8503
9609
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
8504
9610
|
`)
|
|
8505
9611
|
);
|
|
@@ -8507,20 +9613,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
8507
9613
|
}
|
|
8508
9614
|
pauseNode9(ms, options.duration);
|
|
8509
9615
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
8510
|
-
console.log(
|
|
9616
|
+
console.log(import_chalk17.default.yellow(`
|
|
8511
9617
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
8512
|
-
console.log(
|
|
8513
|
-
console.log(
|
|
9618
|
+
console.log(import_chalk17.default.gray(` All tool calls will be allowed without review.`));
|
|
9619
|
+
console.log(import_chalk17.default.gray(` Run "node9 resume" to re-enable early.
|
|
8514
9620
|
`));
|
|
8515
9621
|
});
|
|
8516
9622
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
8517
9623
|
const { paused } = checkPause();
|
|
8518
9624
|
if (!paused) {
|
|
8519
|
-
console.log(
|
|
9625
|
+
console.log(import_chalk17.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
8520
9626
|
return;
|
|
8521
9627
|
}
|
|
8522
9628
|
resumeNode9();
|
|
8523
|
-
console.log(
|
|
9629
|
+
console.log(import_chalk17.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
8524
9630
|
});
|
|
8525
9631
|
var HOOK_BASED_AGENTS = {
|
|
8526
9632
|
claude: "claude",
|
|
@@ -8533,15 +9639,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8533
9639
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
8534
9640
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
8535
9641
|
console.error(
|
|
8536
|
-
|
|
9642
|
+
import_chalk17.default.yellow(`
|
|
8537
9643
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
8538
9644
|
);
|
|
8539
|
-
console.error(
|
|
9645
|
+
console.error(import_chalk17.default.white(`
|
|
8540
9646
|
"${target}" uses its own hook system. Use:`));
|
|
8541
9647
|
console.error(
|
|
8542
|
-
|
|
9648
|
+
import_chalk17.default.green(` node9 addto ${target} `) + import_chalk17.default.gray("# one-time setup")
|
|
8543
9649
|
);
|
|
8544
|
-
console.error(
|
|
9650
|
+
console.error(import_chalk17.default.green(` ${target} `) + import_chalk17.default.gray("# run normally"));
|
|
8545
9651
|
process.exit(1);
|
|
8546
9652
|
}
|
|
8547
9653
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -8558,7 +9664,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8558
9664
|
}
|
|
8559
9665
|
);
|
|
8560
9666
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
8561
|
-
console.error(
|
|
9667
|
+
console.error(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
8562
9668
|
const daemonReady = await autoStartDaemonAndWait();
|
|
8563
9669
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
8564
9670
|
}
|
|
@@ -8571,12 +9677,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8571
9677
|
}
|
|
8572
9678
|
if (!result.approved) {
|
|
8573
9679
|
console.error(
|
|
8574
|
-
|
|
9680
|
+
import_chalk17.default.red(`
|
|
8575
9681
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
8576
9682
|
);
|
|
8577
9683
|
process.exit(1);
|
|
8578
9684
|
}
|
|
8579
|
-
console.error(
|
|
9685
|
+
console.error(import_chalk17.default.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
8580
9686
|
await runProxy(fullCommand);
|
|
8581
9687
|
} else {
|
|
8582
9688
|
program.help();
|
|
@@ -8591,9 +9697,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
8591
9697
|
const isCheckHook = process.argv[2] === "check";
|
|
8592
9698
|
if (isCheckHook) {
|
|
8593
9699
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
8594
|
-
const logPath =
|
|
9700
|
+
const logPath = import_path27.default.join(import_os22.default.homedir(), ".node9", "hook-debug.log");
|
|
8595
9701
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
8596
|
-
|
|
9702
|
+
import_fs25.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
8597
9703
|
`);
|
|
8598
9704
|
}
|
|
8599
9705
|
process.exit(0);
|