@node9/proxy 0.2.1 → 1.0.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 +66 -319
- package/dist/cli.js +1417 -608
- package/dist/cli.mjs +1417 -608
- package/dist/index.js +722 -261
- package/dist/index.mjs +722 -261
- package/package.json +44 -8
package/dist/cli.js
CHANGED
|
@@ -27,13 +27,254 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
var import_commander = require("commander");
|
|
28
28
|
|
|
29
29
|
// src/core.ts
|
|
30
|
-
var
|
|
30
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
31
31
|
var import_prompts = require("@inquirer/prompts");
|
|
32
32
|
var import_fs = __toESM(require("fs"));
|
|
33
33
|
var import_path = __toESM(require("path"));
|
|
34
34
|
var import_os = __toESM(require("os"));
|
|
35
35
|
var import_picomatch = __toESM(require("picomatch"));
|
|
36
36
|
var import_sh_syntax = require("sh-syntax");
|
|
37
|
+
|
|
38
|
+
// src/ui/native.ts
|
|
39
|
+
var import_child_process = require("child_process");
|
|
40
|
+
var import_chalk = __toESM(require("chalk"));
|
|
41
|
+
var isTestEnv = () => {
|
|
42
|
+
return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
|
|
43
|
+
};
|
|
44
|
+
function smartTruncate(str, maxLen = 500) {
|
|
45
|
+
if (str.length <= maxLen) return str;
|
|
46
|
+
const edge = Math.floor(maxLen / 2) - 3;
|
|
47
|
+
return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
|
|
48
|
+
}
|
|
49
|
+
function formatArgs(args) {
|
|
50
|
+
if (args === null || args === void 0) return "(none)";
|
|
51
|
+
let parsed = args;
|
|
52
|
+
if (typeof args === "string") {
|
|
53
|
+
const trimmed = args.trim();
|
|
54
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(trimmed);
|
|
57
|
+
} catch {
|
|
58
|
+
parsed = args;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
return smartTruncate(args, 600);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
65
|
+
const obj = parsed;
|
|
66
|
+
const codeKeys = [
|
|
67
|
+
"command",
|
|
68
|
+
"cmd",
|
|
69
|
+
"shell_command",
|
|
70
|
+
"bash_command",
|
|
71
|
+
"script",
|
|
72
|
+
"code",
|
|
73
|
+
"input",
|
|
74
|
+
"sql",
|
|
75
|
+
"query",
|
|
76
|
+
"arguments",
|
|
77
|
+
"args",
|
|
78
|
+
"param",
|
|
79
|
+
"params",
|
|
80
|
+
"text"
|
|
81
|
+
];
|
|
82
|
+
const foundKey = Object.keys(obj).find((k) => codeKeys.includes(k.toLowerCase()));
|
|
83
|
+
if (foundKey) {
|
|
84
|
+
const val = obj[foundKey];
|
|
85
|
+
const str = typeof val === "string" ? val : JSON.stringify(val);
|
|
86
|
+
return `[${foundKey.toUpperCase()}]:
|
|
87
|
+
${smartTruncate(str, 500)}`;
|
|
88
|
+
}
|
|
89
|
+
return Object.entries(obj).slice(0, 5).map(
|
|
90
|
+
([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
|
|
91
|
+
).join("\n");
|
|
92
|
+
}
|
|
93
|
+
return smartTruncate(JSON.stringify(parsed), 200);
|
|
94
|
+
}
|
|
95
|
+
function sendDesktopNotification(title, body) {
|
|
96
|
+
if (isTestEnv()) return;
|
|
97
|
+
try {
|
|
98
|
+
if (process.platform === "darwin") {
|
|
99
|
+
const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
|
|
100
|
+
(0, import_child_process.spawn)("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
|
|
101
|
+
} else if (process.platform === "linux") {
|
|
102
|
+
(0, import_child_process.spawn)("notify-send", [title, body, "--icon=dialog-warning"], {
|
|
103
|
+
detached: true,
|
|
104
|
+
stdio: "ignore"
|
|
105
|
+
}).unref();
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
|
|
111
|
+
if (isTestEnv()) return "deny";
|
|
112
|
+
const formattedArgs = formatArgs(args);
|
|
113
|
+
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
|
|
114
|
+
let message = "";
|
|
115
|
+
if (locked) message += `\u26A0\uFE0F LOCKED BY ADMIN POLICY
|
|
116
|
+
`;
|
|
117
|
+
message += `Tool: ${toolName}
|
|
118
|
+
`;
|
|
119
|
+
message += `Agent: ${agent || "AI Agent"}
|
|
120
|
+
`;
|
|
121
|
+
message += `Rule: ${explainableLabel || "Security Policy"}
|
|
122
|
+
|
|
123
|
+
`;
|
|
124
|
+
message += `${formattedArgs}`;
|
|
125
|
+
process.stderr.write(import_chalk.default.yellow(`
|
|
126
|
+
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
127
|
+
`));
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
let childProcess = null;
|
|
130
|
+
const onAbort = () => {
|
|
131
|
+
if (childProcess && childProcess.pid) {
|
|
132
|
+
try {
|
|
133
|
+
process.kill(childProcess.pid, "SIGKILL");
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
resolve("deny");
|
|
138
|
+
};
|
|
139
|
+
if (signal) {
|
|
140
|
+
if (signal.aborted) return resolve("deny");
|
|
141
|
+
signal.addEventListener("abort", onAbort);
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
if (process.platform === "darwin") {
|
|
145
|
+
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block", "Always Allow", "Allow"} default button "Allow" cancel button "Block"`;
|
|
146
|
+
const script = `on run argv
|
|
147
|
+
tell application "System Events"
|
|
148
|
+
activate
|
|
149
|
+
display dialog (item 1 of argv) with title (item 2 of argv) ${buttons}
|
|
150
|
+
end tell
|
|
151
|
+
end run`;
|
|
152
|
+
childProcess = (0, import_child_process.spawn)("osascript", ["-e", script, "--", message, title]);
|
|
153
|
+
} else if (process.platform === "linux") {
|
|
154
|
+
const argsList = [
|
|
155
|
+
locked ? "--info" : "--question",
|
|
156
|
+
"--modal",
|
|
157
|
+
"--width=450",
|
|
158
|
+
"--title",
|
|
159
|
+
title,
|
|
160
|
+
"--text",
|
|
161
|
+
message,
|
|
162
|
+
"--ok-label",
|
|
163
|
+
locked ? "Waiting..." : "Allow",
|
|
164
|
+
"--timeout",
|
|
165
|
+
"300"
|
|
166
|
+
];
|
|
167
|
+
if (!locked) {
|
|
168
|
+
argsList.push("--cancel-label", "Block");
|
|
169
|
+
argsList.push("--extra-button", "Always Allow");
|
|
170
|
+
}
|
|
171
|
+
childProcess = (0, import_child_process.spawn)("zenity", argsList);
|
|
172
|
+
} else if (process.platform === "win32") {
|
|
173
|
+
const b64Msg = Buffer.from(message).toString("base64");
|
|
174
|
+
const b64Title = Buffer.from(title).toString("base64");
|
|
175
|
+
const ps = `Add-Type -AssemblyName PresentationFramework; $msg = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("${b64Msg}")); $title = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("${b64Title}")); $res = [System.Windows.MessageBox]::Show($msg, $title, "${locked ? "OK" : "YesNo"}", "Warning", "Button2", "DefaultDesktopOnly"); if ($res -eq "Yes") { exit 0 } else { exit 1 }`;
|
|
176
|
+
childProcess = (0, import_child_process.spawn)("powershell", ["-Command", ps]);
|
|
177
|
+
}
|
|
178
|
+
let output = "";
|
|
179
|
+
childProcess?.stdout?.on("data", (d) => output += d.toString());
|
|
180
|
+
childProcess?.on("close", (code) => {
|
|
181
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
182
|
+
if (locked) return resolve("deny");
|
|
183
|
+
if (output.includes("Always Allow")) return resolve("always_allow");
|
|
184
|
+
if (code === 0) return resolve("allow");
|
|
185
|
+
resolve("deny");
|
|
186
|
+
});
|
|
187
|
+
} catch {
|
|
188
|
+
resolve("deny");
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/core.ts
|
|
194
|
+
var PAUSED_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "PAUSED");
|
|
195
|
+
var TRUST_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "trust.json");
|
|
196
|
+
function checkPause() {
|
|
197
|
+
try {
|
|
198
|
+
if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
199
|
+
const state = JSON.parse(import_fs.default.readFileSync(PAUSED_FILE, "utf-8"));
|
|
200
|
+
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
201
|
+
try {
|
|
202
|
+
import_fs.default.unlinkSync(PAUSED_FILE);
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
return { paused: false };
|
|
206
|
+
}
|
|
207
|
+
return { paused: true, expiresAt: state.expiry, duration: state.duration };
|
|
208
|
+
} catch {
|
|
209
|
+
return { paused: false };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function atomicWriteSync(filePath, data, options) {
|
|
213
|
+
const dir = import_path.default.dirname(filePath);
|
|
214
|
+
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
215
|
+
const tmpPath = `${filePath}.${import_os.default.hostname()}.${process.pid}.tmp`;
|
|
216
|
+
import_fs.default.writeFileSync(tmpPath, data, options);
|
|
217
|
+
import_fs.default.renameSync(tmpPath, filePath);
|
|
218
|
+
}
|
|
219
|
+
function pauseNode9(durationMs, durationStr) {
|
|
220
|
+
const state = { expiry: Date.now() + durationMs, duration: durationStr };
|
|
221
|
+
atomicWriteSync(PAUSED_FILE, JSON.stringify(state, null, 2));
|
|
222
|
+
}
|
|
223
|
+
function resumeNode9() {
|
|
224
|
+
try {
|
|
225
|
+
if (import_fs.default.existsSync(PAUSED_FILE)) import_fs.default.unlinkSync(PAUSED_FILE);
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function getActiveTrustSession(toolName) {
|
|
230
|
+
try {
|
|
231
|
+
if (!import_fs.default.existsSync(TRUST_FILE)) return false;
|
|
232
|
+
const trust = JSON.parse(import_fs.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
233
|
+
const now = Date.now();
|
|
234
|
+
const active = trust.entries.filter((e) => e.expiry > now);
|
|
235
|
+
if (active.length !== trust.entries.length) {
|
|
236
|
+
import_fs.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
237
|
+
}
|
|
238
|
+
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function writeTrustSession(toolName, durationMs) {
|
|
244
|
+
try {
|
|
245
|
+
let trust = { entries: [] };
|
|
246
|
+
try {
|
|
247
|
+
if (import_fs.default.existsSync(TRUST_FILE)) {
|
|
248
|
+
trust = JSON.parse(import_fs.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > now);
|
|
254
|
+
trust.entries.push({ tool: toolName, expiry: now + durationMs });
|
|
255
|
+
atomicWriteSync(TRUST_FILE, JSON.stringify(trust, null, 2));
|
|
256
|
+
} catch (err) {
|
|
257
|
+
if (process.env.NODE9_DEBUG === "1") {
|
|
258
|
+
console.error("[Node9 Trust Error]:", err);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function appendAuditModeEntry(toolName, args) {
|
|
263
|
+
try {
|
|
264
|
+
const entry = JSON.stringify({
|
|
265
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
266
|
+
tool: toolName,
|
|
267
|
+
args,
|
|
268
|
+
decision: "would-have-blocked",
|
|
269
|
+
source: "audit-mode"
|
|
270
|
+
});
|
|
271
|
+
const logPath = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
272
|
+
const dir = import_path.default.dirname(logPath);
|
|
273
|
+
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
274
|
+
import_fs.default.appendFileSync(logPath, entry + "\n");
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
}
|
|
37
278
|
var DANGEROUS_WORDS = [
|
|
38
279
|
"delete",
|
|
39
280
|
"drop",
|
|
@@ -51,10 +292,6 @@ var DANGEROUS_WORDS = [
|
|
|
51
292
|
function tokenize(toolName) {
|
|
52
293
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
53
294
|
}
|
|
54
|
-
function containsDangerousWord(toolName, dangerousWords) {
|
|
55
|
-
const tokens = tokenize(toolName);
|
|
56
|
-
return dangerousWords.some((word) => tokens.includes(word.toLowerCase()));
|
|
57
|
-
}
|
|
58
295
|
function matchesPattern(text, patterns) {
|
|
59
296
|
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
60
297
|
if (p.length === 0) return false;
|
|
@@ -65,9 +302,9 @@ function matchesPattern(text, patterns) {
|
|
|
65
302
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
66
303
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
67
304
|
}
|
|
68
|
-
function getNestedValue(obj,
|
|
305
|
+
function getNestedValue(obj, path6) {
|
|
69
306
|
if (!obj || typeof obj !== "object") return null;
|
|
70
|
-
return
|
|
307
|
+
return path6.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
71
308
|
}
|
|
72
309
|
function extractShellCommand(toolName, args, toolInspection) {
|
|
73
310
|
const patterns = Object.keys(toolInspection);
|
|
@@ -159,8 +396,15 @@ function redactSecrets(text) {
|
|
|
159
396
|
return redacted;
|
|
160
397
|
}
|
|
161
398
|
var DEFAULT_CONFIG = {
|
|
162
|
-
settings: {
|
|
399
|
+
settings: {
|
|
400
|
+
mode: "standard",
|
|
401
|
+
autoStartDaemon: true,
|
|
402
|
+
enableUndo: false,
|
|
403
|
+
enableHookLogDebug: false,
|
|
404
|
+
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
405
|
+
},
|
|
163
406
|
policy: {
|
|
407
|
+
sandboxPaths: [],
|
|
164
408
|
dangerousWords: DANGEROUS_WORDS,
|
|
165
409
|
ignoredTools: [
|
|
166
410
|
"list_*",
|
|
@@ -168,34 +412,19 @@ var DEFAULT_CONFIG = {
|
|
|
168
412
|
"read_*",
|
|
169
413
|
"describe_*",
|
|
170
414
|
"read",
|
|
171
|
-
"write",
|
|
172
|
-
"edit",
|
|
173
|
-
"multiedit",
|
|
174
|
-
"glob",
|
|
175
415
|
"grep",
|
|
176
416
|
"ls",
|
|
177
|
-
"notebookread",
|
|
178
|
-
"notebookedit",
|
|
179
|
-
"todoread",
|
|
180
|
-
"todowrite",
|
|
181
|
-
"webfetch",
|
|
182
|
-
"websearch",
|
|
183
|
-
"exitplanmode",
|
|
184
417
|
"askuserquestion"
|
|
185
418
|
],
|
|
186
|
-
toolInspection: {
|
|
187
|
-
|
|
188
|
-
run_shell_command: "command",
|
|
189
|
-
shell: "command",
|
|
190
|
-
"terminal.execute": "command"
|
|
191
|
-
},
|
|
192
|
-
rules: [
|
|
193
|
-
{ action: "rm", allowPaths: ["**/node_modules/**", "dist/**", "build/**", ".DS_Store"] }
|
|
194
|
-
]
|
|
419
|
+
toolInspection: { bash: "command", shell: "command" },
|
|
420
|
+
rules: [{ action: "rm", allowPaths: ["**/node_modules/**", "dist/**", ".DS_Store"] }]
|
|
195
421
|
},
|
|
196
422
|
environments: {}
|
|
197
423
|
};
|
|
198
424
|
var cachedConfig = null;
|
|
425
|
+
function _resetConfigCache() {
|
|
426
|
+
cachedConfig = null;
|
|
427
|
+
}
|
|
199
428
|
function getGlobalSettings() {
|
|
200
429
|
try {
|
|
201
430
|
const globalConfigPath = import_path.default.join(import_os.default.homedir(), ".node9", "config.json");
|
|
@@ -206,18 +435,19 @@ function getGlobalSettings() {
|
|
|
206
435
|
mode: settings.mode || "standard",
|
|
207
436
|
autoStartDaemon: settings.autoStartDaemon !== false,
|
|
208
437
|
slackEnabled: settings.slackEnabled !== false,
|
|
209
|
-
|
|
210
|
-
|
|
438
|
+
enableTrustSessions: settings.enableTrustSessions === true,
|
|
439
|
+
allowGlobalPause: settings.allowGlobalPause !== false
|
|
211
440
|
};
|
|
212
441
|
}
|
|
213
442
|
} catch {
|
|
214
443
|
}
|
|
215
|
-
return {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
444
|
+
return {
|
|
445
|
+
mode: "standard",
|
|
446
|
+
autoStartDaemon: true,
|
|
447
|
+
slackEnabled: true,
|
|
448
|
+
enableTrustSessions: false,
|
|
449
|
+
allowGlobalPause: true
|
|
450
|
+
};
|
|
221
451
|
}
|
|
222
452
|
function getInternalToken() {
|
|
223
453
|
try {
|
|
@@ -230,51 +460,83 @@ function getInternalToken() {
|
|
|
230
460
|
return null;
|
|
231
461
|
}
|
|
232
462
|
}
|
|
233
|
-
async function evaluatePolicy(toolName, args) {
|
|
463
|
+
async function evaluatePolicy(toolName, args, agent) {
|
|
234
464
|
const config = getConfig();
|
|
235
|
-
if (matchesPattern(toolName, config.policy.ignoredTools)) return "allow";
|
|
465
|
+
if (matchesPattern(toolName, config.policy.ignoredTools)) return { decision: "allow" };
|
|
466
|
+
let allTokens = [];
|
|
467
|
+
let actionTokens = [];
|
|
468
|
+
let pathTokens = [];
|
|
236
469
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
237
470
|
if (shellCommand) {
|
|
238
|
-
const
|
|
471
|
+
const analyzed = await analyzeShellCommand(shellCommand);
|
|
472
|
+
allTokens = analyzed.allTokens;
|
|
473
|
+
actionTokens = analyzed.actions;
|
|
474
|
+
pathTokens = analyzed.paths;
|
|
239
475
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
240
|
-
if (INLINE_EXEC_PATTERN.test(shellCommand.trim()))
|
|
241
|
-
|
|
242
|
-
const basename = action.includes("/") ? action.split("/").pop() : action;
|
|
243
|
-
const rule = config.policy.rules.find(
|
|
244
|
-
(r) => r.action === action || matchesPattern(action, r.action) || basename && (r.action === basename || matchesPattern(basename, r.action))
|
|
245
|
-
);
|
|
246
|
-
if (rule) {
|
|
247
|
-
if (paths.length > 0) {
|
|
248
|
-
const anyBlocked = paths.some((p) => matchesPattern(p, rule.blockPaths || []));
|
|
249
|
-
if (anyBlocked) return "review";
|
|
250
|
-
const allAllowed = paths.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
251
|
-
if (allAllowed) return "allow";
|
|
252
|
-
}
|
|
253
|
-
return "review";
|
|
254
|
-
}
|
|
476
|
+
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
477
|
+
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)" };
|
|
255
478
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
479
|
+
} else {
|
|
480
|
+
allTokens = tokenize(toolName);
|
|
481
|
+
actionTokens = [toolName];
|
|
482
|
+
}
|
|
483
|
+
const isManual = agent === "Terminal";
|
|
484
|
+
if (isManual) {
|
|
485
|
+
const NUCLEAR_COMMANDS = [
|
|
486
|
+
"drop",
|
|
487
|
+
"destroy",
|
|
488
|
+
"purge",
|
|
489
|
+
"rmdir",
|
|
490
|
+
"format",
|
|
491
|
+
"truncate",
|
|
492
|
+
"alter",
|
|
493
|
+
"grant",
|
|
494
|
+
"revoke",
|
|
495
|
+
"docker"
|
|
496
|
+
];
|
|
497
|
+
const hasNuclear = allTokens.some((t) => NUCLEAR_COMMANDS.includes(t.toLowerCase()));
|
|
498
|
+
if (!hasNuclear) return { decision: "allow" };
|
|
499
|
+
}
|
|
500
|
+
if (pathTokens.length > 0 && config.policy.sandboxPaths.length > 0) {
|
|
501
|
+
const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
|
|
502
|
+
if (allInSandbox) return { decision: "allow" };
|
|
503
|
+
}
|
|
504
|
+
for (const action of actionTokens) {
|
|
505
|
+
const rule = config.policy.rules.find(
|
|
506
|
+
(r) => r.action === action || matchesPattern(action, r.action)
|
|
266
507
|
);
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
508
|
+
if (rule) {
|
|
509
|
+
if (pathTokens.length > 0) {
|
|
510
|
+
const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
|
|
511
|
+
if (anyBlocked)
|
|
512
|
+
return { decision: "review", blockedByLabel: "Project/Global Config (Rule Block)" };
|
|
513
|
+
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
514
|
+
if (allAllowed) return { decision: "allow" };
|
|
515
|
+
}
|
|
516
|
+
return { decision: "review", blockedByLabel: "Project/Global Config (Rule Default Block)" };
|
|
517
|
+
}
|
|
270
518
|
}
|
|
271
|
-
const isDangerous =
|
|
272
|
-
|
|
519
|
+
const isDangerous = allTokens.some(
|
|
520
|
+
(token) => config.policy.dangerousWords.some((word) => {
|
|
521
|
+
const w = word.toLowerCase();
|
|
522
|
+
if (token === w) return true;
|
|
523
|
+
try {
|
|
524
|
+
return new RegExp(`\\b${w}\\b`, "i").test(token);
|
|
525
|
+
} catch {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
})
|
|
529
|
+
);
|
|
530
|
+
if (isDangerous) {
|
|
531
|
+
const label = isManual ? "Manual Nuclear Protection" : "Project/Global Config (Dangerous Word)";
|
|
532
|
+
return { decision: "review", blockedByLabel: label };
|
|
533
|
+
}
|
|
534
|
+
if (config.settings.mode === "strict") {
|
|
273
535
|
const envConfig = getActiveEnvironment(config);
|
|
274
|
-
if (envConfig?.requireApproval === false) return "allow";
|
|
275
|
-
return "review";
|
|
536
|
+
if (envConfig?.requireApproval === false) return { decision: "allow" };
|
|
537
|
+
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)" };
|
|
276
538
|
}
|
|
277
|
-
return "allow";
|
|
539
|
+
return { decision: "allow" };
|
|
278
540
|
}
|
|
279
541
|
function isIgnoredTool(toolName) {
|
|
280
542
|
const config = getConfig();
|
|
@@ -305,22 +567,40 @@ function getPersistentDecision(toolName) {
|
|
|
305
567
|
}
|
|
306
568
|
return null;
|
|
307
569
|
}
|
|
308
|
-
async function askDaemon(toolName, args, meta) {
|
|
570
|
+
async function askDaemon(toolName, args, meta, signal) {
|
|
309
571
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
572
|
+
const checkCtrl = new AbortController();
|
|
573
|
+
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
574
|
+
const onAbort = () => checkCtrl.abort();
|
|
575
|
+
if (signal) signal.addEventListener("abort", onAbort);
|
|
576
|
+
try {
|
|
577
|
+
const checkRes = await fetch(`${base}/check`, {
|
|
578
|
+
method: "POST",
|
|
579
|
+
headers: { "Content-Type": "application/json" },
|
|
580
|
+
body: JSON.stringify({ toolName, args, agent: meta?.agent, mcpServer: meta?.mcpServer }),
|
|
581
|
+
signal: checkCtrl.signal
|
|
582
|
+
});
|
|
583
|
+
if (!checkRes.ok) throw new Error("Daemon fail");
|
|
584
|
+
const { id } = await checkRes.json();
|
|
585
|
+
const waitCtrl = new AbortController();
|
|
586
|
+
const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
|
|
587
|
+
const onWaitAbort = () => waitCtrl.abort();
|
|
588
|
+
if (signal) signal.addEventListener("abort", onWaitAbort);
|
|
589
|
+
try {
|
|
590
|
+
const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
|
|
591
|
+
if (!waitRes.ok) return "deny";
|
|
592
|
+
const { decision } = await waitRes.json();
|
|
593
|
+
if (decision === "allow") return "allow";
|
|
594
|
+
if (decision === "abandoned") return "abandoned";
|
|
595
|
+
return "deny";
|
|
596
|
+
} finally {
|
|
597
|
+
clearTimeout(waitTimer);
|
|
598
|
+
if (signal) signal.removeEventListener("abort", onWaitAbort);
|
|
599
|
+
}
|
|
600
|
+
} finally {
|
|
601
|
+
clearTimeout(checkTimer);
|
|
602
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
603
|
+
}
|
|
324
604
|
}
|
|
325
605
|
async function notifyDaemonViewer(toolName, args, meta) {
|
|
326
606
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
@@ -350,176 +630,353 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
350
630
|
});
|
|
351
631
|
}
|
|
352
632
|
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta) {
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
633
|
+
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
634
|
+
const pauseState = checkPause();
|
|
635
|
+
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
636
|
+
const creds = getCredentials();
|
|
637
|
+
const config = getConfig();
|
|
638
|
+
const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
|
|
639
|
+
const approvers = {
|
|
640
|
+
...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
|
|
641
|
+
};
|
|
642
|
+
if (isTestEnv2) {
|
|
643
|
+
approvers.native = false;
|
|
644
|
+
approvers.browser = false;
|
|
645
|
+
approvers.terminal = false;
|
|
646
|
+
}
|
|
647
|
+
const isManual = meta?.agent === "Terminal";
|
|
648
|
+
let explainableLabel = "Local Config";
|
|
649
|
+
if (config.settings.mode === "audit") {
|
|
650
|
+
if (!isIgnoredTool(toolName)) {
|
|
651
|
+
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
652
|
+
if (policyResult.decision === "review") {
|
|
653
|
+
appendAuditModeEntry(toolName, args);
|
|
654
|
+
sendDesktopNotification(
|
|
655
|
+
"Node9 Audit Mode",
|
|
656
|
+
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return { approved: true, checkedBy: "audit" };
|
|
661
|
+
}
|
|
662
|
+
if (!isIgnoredTool(toolName)) {
|
|
663
|
+
if (getActiveTrustSession(toolName)) {
|
|
664
|
+
if (creds?.apiKey) auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
665
|
+
return { approved: true, checkedBy: "trust" };
|
|
666
|
+
}
|
|
667
|
+
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
668
|
+
if (policyResult.decision === "allow") {
|
|
669
|
+
if (creds?.apiKey) auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
670
|
+
return { approved: true, checkedBy: "local-policy" };
|
|
671
|
+
}
|
|
672
|
+
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
359
673
|
const persistent = getPersistentDecision(toolName);
|
|
360
|
-
if (persistent === "allow")
|
|
361
|
-
|
|
674
|
+
if (persistent === "allow") {
|
|
675
|
+
if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
676
|
+
return { approved: true, checkedBy: "persistent" };
|
|
677
|
+
}
|
|
678
|
+
if (persistent === "deny") {
|
|
362
679
|
return {
|
|
363
680
|
approved: false,
|
|
364
|
-
reason: `
|
|
681
|
+
reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
|
|
365
682
|
blockedBy: "persistent-deny",
|
|
366
|
-
|
|
683
|
+
blockedByLabel: "Persistent User Rule"
|
|
367
684
|
};
|
|
368
|
-
}
|
|
369
|
-
if (cloudEnforced) {
|
|
370
|
-
const creds = getCredentials();
|
|
371
|
-
const envConfig = getActiveEnvironment(getConfig());
|
|
372
|
-
let viewerId = null;
|
|
373
|
-
const internalToken = getInternalToken();
|
|
374
|
-
if (isDaemonRunning() && internalToken) {
|
|
375
|
-
viewerId = await notifyDaemonViewer(toolName, args, meta).catch(() => null);
|
|
376
685
|
}
|
|
377
|
-
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
return {
|
|
382
|
-
approved,
|
|
383
|
-
checkedBy: approved ? "cloud" : void 0,
|
|
384
|
-
blockedBy: approved ? void 0 : "team-policy",
|
|
385
|
-
changeHint: approved ? void 0 : `Visit your Node9 dashboard \u2192 Policy Studio to change this rule`
|
|
386
|
-
};
|
|
686
|
+
} else {
|
|
687
|
+
if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
|
|
688
|
+
return { approved: true };
|
|
387
689
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
690
|
+
let cloudRequestId = null;
|
|
691
|
+
let isRemoteLocked = false;
|
|
692
|
+
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
693
|
+
if (cloudEnforced) {
|
|
392
694
|
try {
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
} else {
|
|
695
|
+
const envConfig = getActiveEnvironment(getConfig());
|
|
696
|
+
const initResult = await initNode9SaaS(toolName, args, creds, envConfig?.slackChannel, meta);
|
|
697
|
+
if (!initResult.pending) {
|
|
397
698
|
return {
|
|
398
|
-
approved:
|
|
399
|
-
reason:
|
|
400
|
-
checkedBy:
|
|
401
|
-
blockedBy:
|
|
402
|
-
|
|
699
|
+
approved: !!initResult.approved,
|
|
700
|
+
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
701
|
+
checkedBy: initResult.approved ? "cloud" : void 0,
|
|
702
|
+
blockedBy: initResult.approved ? void 0 : "team-policy",
|
|
703
|
+
blockedByLabel: "Organization Policy (SaaS)"
|
|
403
704
|
};
|
|
404
705
|
}
|
|
405
|
-
|
|
706
|
+
cloudRequestId = initResult.requestId || null;
|
|
707
|
+
isRemoteLocked = !!initResult.remoteApprovalOnly;
|
|
708
|
+
explainableLabel = "Organization Policy (SaaS)";
|
|
709
|
+
} catch (err) {
|
|
710
|
+
const error = err;
|
|
711
|
+
const isAuthError = error.message.includes("401") || error.message.includes("403");
|
|
712
|
+
const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
|
|
713
|
+
const reason = isAuthError ? "Invalid or missing API key. Run `node9 login` to generate a key (must start with n9_live_)." : isNetworkError ? "Could not reach the Node9 cloud. Check your network or API URL." : error.message;
|
|
714
|
+
console.error(
|
|
715
|
+
import_chalk2.default.yellow(`
|
|
716
|
+
\u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + import_chalk2.default.dim(`
|
|
717
|
+
Falling back to local rules...
|
|
718
|
+
`)
|
|
719
|
+
);
|
|
406
720
|
}
|
|
407
721
|
}
|
|
408
|
-
if (
|
|
409
|
-
console.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
console.
|
|
413
|
-
|
|
414
|
-
|
|
722
|
+
if (cloudEnforced && cloudRequestId) {
|
|
723
|
+
console.error(
|
|
724
|
+
import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
|
|
725
|
+
);
|
|
726
|
+
console.error(import_chalk2.default.cyan(" Dashboard \u2192 ") + import_chalk2.default.bold("Mission Control > Activity Feed\n"));
|
|
727
|
+
} else if (!cloudEnforced) {
|
|
728
|
+
const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
|
|
729
|
+
console.error(
|
|
730
|
+
import_chalk2.default.dim(`
|
|
731
|
+
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
732
|
+
`)
|
|
415
733
|
);
|
|
416
|
-
const controller = new AbortController();
|
|
417
|
-
const TIMEOUT_MS = 3e4;
|
|
418
|
-
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
419
|
-
try {
|
|
420
|
-
const approved = await (0, import_prompts.confirm)(
|
|
421
|
-
{ message: `Authorize? (auto-deny in ${TIMEOUT_MS / 1e3}s)`, default: false },
|
|
422
|
-
{ signal: controller.signal }
|
|
423
|
-
);
|
|
424
|
-
clearTimeout(timer);
|
|
425
|
-
return { approved };
|
|
426
|
-
} catch {
|
|
427
|
-
clearTimeout(timer);
|
|
428
|
-
console.error(import_chalk.default.yellow("\n\u23F1 Prompt timed out \u2014 action denied by default."));
|
|
429
|
-
return { approved: false };
|
|
430
|
-
}
|
|
431
734
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
735
|
+
const abortController = new AbortController();
|
|
736
|
+
const { signal } = abortController;
|
|
737
|
+
const racePromises = [];
|
|
738
|
+
let viewerId = null;
|
|
739
|
+
const internalToken = getInternalToken();
|
|
740
|
+
if (cloudEnforced && cloudRequestId) {
|
|
741
|
+
racePromises.push(
|
|
742
|
+
(async () => {
|
|
743
|
+
try {
|
|
744
|
+
if (isDaemonRunning() && internalToken) {
|
|
745
|
+
viewerId = await notifyDaemonViewer(toolName, args, meta).catch(() => null);
|
|
746
|
+
}
|
|
747
|
+
const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
|
|
748
|
+
return {
|
|
749
|
+
approved: cloudResult.approved,
|
|
750
|
+
reason: cloudResult.approved ? void 0 : cloudResult.reason || "Action rejected by organization administrator via Slack.",
|
|
751
|
+
checkedBy: cloudResult.approved ? "cloud" : void 0,
|
|
752
|
+
blockedBy: cloudResult.approved ? void 0 : "team-policy",
|
|
753
|
+
blockedByLabel: "Organization Policy (SaaS)"
|
|
754
|
+
};
|
|
755
|
+
} catch (err) {
|
|
756
|
+
const error = err;
|
|
757
|
+
if (error.name === "AbortError" || error.message?.includes("Aborted")) throw err;
|
|
758
|
+
throw err;
|
|
759
|
+
}
|
|
760
|
+
})()
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
if (approvers.native && !isManual) {
|
|
764
|
+
racePromises.push(
|
|
765
|
+
(async () => {
|
|
766
|
+
const decision = await askNativePopup(
|
|
767
|
+
toolName,
|
|
768
|
+
args,
|
|
769
|
+
meta?.agent,
|
|
770
|
+
explainableLabel,
|
|
771
|
+
isRemoteLocked,
|
|
772
|
+
signal
|
|
773
|
+
);
|
|
774
|
+
if (decision === "always_allow") {
|
|
775
|
+
writeTrustSession(toolName, 36e5);
|
|
776
|
+
return { approved: true, checkedBy: "trust" };
|
|
777
|
+
}
|
|
778
|
+
const isApproved = decision === "allow";
|
|
779
|
+
return {
|
|
780
|
+
approved: isApproved,
|
|
781
|
+
reason: isApproved ? void 0 : "The human user clicked 'Block' on the system dialog window.",
|
|
782
|
+
checkedBy: isApproved ? "daemon" : void 0,
|
|
783
|
+
blockedBy: isApproved ? void 0 : "local-decision",
|
|
784
|
+
blockedByLabel: "User Decision (Native)"
|
|
785
|
+
};
|
|
786
|
+
})()
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
if (approvers.browser && isDaemonRunning()) {
|
|
790
|
+
racePromises.push(
|
|
791
|
+
(async () => {
|
|
792
|
+
try {
|
|
793
|
+
if (!approvers.native && !cloudEnforced) {
|
|
794
|
+
console.error(
|
|
795
|
+
import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
|
|
796
|
+
);
|
|
797
|
+
console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
798
|
+
`));
|
|
799
|
+
}
|
|
800
|
+
const daemonDecision = await askDaemon(toolName, args, meta, signal);
|
|
801
|
+
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
802
|
+
const isApproved = daemonDecision === "allow";
|
|
803
|
+
return {
|
|
804
|
+
approved: isApproved,
|
|
805
|
+
reason: isApproved ? void 0 : "The human user rejected this action via the Node9 Browser Dashboard.",
|
|
806
|
+
checkedBy: isApproved ? "daemon" : void 0,
|
|
807
|
+
blockedBy: isApproved ? void 0 : "local-decision",
|
|
808
|
+
blockedByLabel: "User Decision (Browser)"
|
|
809
|
+
};
|
|
810
|
+
} catch (err) {
|
|
811
|
+
throw err;
|
|
812
|
+
}
|
|
813
|
+
})()
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
if (approvers.terminal && allowTerminalFallback && process.stdout.isTTY) {
|
|
817
|
+
racePromises.push(
|
|
818
|
+
(async () => {
|
|
819
|
+
try {
|
|
820
|
+
console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
|
|
821
|
+
console.log(`${import_chalk2.default.bold("Action:")} ${import_chalk2.default.red(toolName)}`);
|
|
822
|
+
console.log(`${import_chalk2.default.bold("Flagged By:")} ${import_chalk2.default.yellow(explainableLabel)}`);
|
|
823
|
+
if (isRemoteLocked) {
|
|
824
|
+
console.log(import_chalk2.default.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
|
|
825
|
+
`));
|
|
826
|
+
await new Promise((_, reject) => {
|
|
827
|
+
signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
const TIMEOUT_MS = 6e4;
|
|
831
|
+
let timer;
|
|
832
|
+
const result = await new Promise((resolve, reject) => {
|
|
833
|
+
timer = setTimeout(() => reject(new Error("Terminal Timeout")), TIMEOUT_MS);
|
|
834
|
+
(0, import_prompts.confirm)(
|
|
835
|
+
{ message: `Authorize? (auto-deny in ${TIMEOUT_MS / 1e3}s)`, default: false },
|
|
836
|
+
{ signal }
|
|
837
|
+
).then(resolve).catch(reject);
|
|
838
|
+
});
|
|
839
|
+
clearTimeout(timer);
|
|
840
|
+
return {
|
|
841
|
+
approved: result,
|
|
842
|
+
reason: result ? void 0 : "The human user typed 'N' in the terminal to reject this action.",
|
|
843
|
+
checkedBy: result ? "terminal" : void 0,
|
|
844
|
+
blockedBy: result ? void 0 : "local-decision",
|
|
845
|
+
blockedByLabel: "User Decision (Terminal)"
|
|
846
|
+
};
|
|
847
|
+
} catch (err) {
|
|
848
|
+
const error = err;
|
|
849
|
+
if (error.name === "AbortError" || error.message?.includes("Prompt was canceled") || error.message?.includes("Aborted by SaaS"))
|
|
850
|
+
throw err;
|
|
851
|
+
if (error.message === "Terminal Timeout") {
|
|
852
|
+
return {
|
|
853
|
+
approved: false,
|
|
854
|
+
reason: "The terminal prompt timed out without a human response.",
|
|
855
|
+
blockedBy: "local-decision"
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
throw err;
|
|
859
|
+
}
|
|
860
|
+
})()
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
if (racePromises.length === 0) {
|
|
864
|
+
return {
|
|
865
|
+
approved: false,
|
|
866
|
+
noApprovalMechanism: true,
|
|
867
|
+
reason: `NODE9 SECURITY INTERVENTION: Action blocked by automated policy [${explainableLabel}].
|
|
868
|
+
REASON: Action blocked because no approval channels are available. (Native/Browser UI is disabled in config, and this terminal is non-interactive).`,
|
|
869
|
+
blockedBy: "no-approval-mechanism",
|
|
870
|
+
blockedByLabel: explainableLabel
|
|
871
|
+
};
|
|
448
872
|
}
|
|
449
|
-
|
|
873
|
+
const finalResult = await new Promise((resolve) => {
|
|
874
|
+
let resolved = false;
|
|
875
|
+
let failures = 0;
|
|
876
|
+
const total = racePromises.length;
|
|
877
|
+
const finish = (res) => {
|
|
878
|
+
if (!resolved) {
|
|
879
|
+
resolved = true;
|
|
880
|
+
abortController.abort();
|
|
881
|
+
if (viewerId && internalToken) {
|
|
882
|
+
resolveViaDaemon(viewerId, res.approved ? "allow" : "deny", internalToken).catch(
|
|
883
|
+
() => null
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
resolve(res);
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
for (const p of racePromises) {
|
|
890
|
+
p.then(finish).catch((err) => {
|
|
891
|
+
if (err.name === "AbortError" || err.message?.includes("canceled") || err.message?.includes("Aborted"))
|
|
892
|
+
return;
|
|
893
|
+
if (err.message === "Abandoned") {
|
|
894
|
+
finish({
|
|
895
|
+
approved: false,
|
|
896
|
+
reason: "Browser dashboard closed without making a decision.",
|
|
897
|
+
blockedBy: "local-decision",
|
|
898
|
+
blockedByLabel: "Browser Dashboard (Abandoned)"
|
|
899
|
+
});
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
failures++;
|
|
903
|
+
if (failures === total && !resolved) {
|
|
904
|
+
finish({ approved: false, reason: "All approval channels failed or disconnected." });
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
910
|
+
await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
|
|
911
|
+
}
|
|
912
|
+
return finalResult;
|
|
450
913
|
}
|
|
451
914
|
function getConfig() {
|
|
452
915
|
if (cachedConfig) return cachedConfig;
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
916
|
+
const globalPath = import_path.default.join(import_os.default.homedir(), ".node9", "config.json");
|
|
917
|
+
const projectPath = import_path.default.join(process.cwd(), "node9.config.json");
|
|
918
|
+
const globalConfig = tryLoadConfig(globalPath);
|
|
919
|
+
const projectConfig = tryLoadConfig(projectPath);
|
|
920
|
+
const mergedSettings = {
|
|
921
|
+
...DEFAULT_CONFIG.settings,
|
|
922
|
+
approvers: { ...DEFAULT_CONFIG.settings.approvers }
|
|
923
|
+
};
|
|
924
|
+
const mergedPolicy = {
|
|
925
|
+
sandboxPaths: [...DEFAULT_CONFIG.policy.sandboxPaths],
|
|
926
|
+
dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
|
|
927
|
+
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
928
|
+
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
929
|
+
rules: [...DEFAULT_CONFIG.policy.rules]
|
|
930
|
+
};
|
|
931
|
+
const applyLayer = (source) => {
|
|
932
|
+
if (!source) return;
|
|
933
|
+
const s = source.settings || {};
|
|
934
|
+
const p = source.policy || {};
|
|
935
|
+
if (s.mode !== void 0) mergedSettings.mode = s.mode;
|
|
936
|
+
if (s.autoStartDaemon !== void 0) mergedSettings.autoStartDaemon = s.autoStartDaemon;
|
|
937
|
+
if (s.enableUndo !== void 0) mergedSettings.enableUndo = s.enableUndo;
|
|
938
|
+
if (s.enableHookLogDebug !== void 0)
|
|
939
|
+
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
940
|
+
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
941
|
+
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
942
|
+
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
943
|
+
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
944
|
+
if (p.toolInspection)
|
|
945
|
+
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
946
|
+
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
947
|
+
};
|
|
948
|
+
applyLayer(globalConfig);
|
|
949
|
+
applyLayer(projectConfig);
|
|
950
|
+
if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
|
|
951
|
+
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
952
|
+
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
953
|
+
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
954
|
+
cachedConfig = {
|
|
955
|
+
settings: mergedSettings,
|
|
956
|
+
policy: mergedPolicy,
|
|
957
|
+
environments: {}
|
|
958
|
+
};
|
|
464
959
|
return cachedConfig;
|
|
465
960
|
}
|
|
466
961
|
function tryLoadConfig(filePath) {
|
|
467
962
|
if (!import_fs.default.existsSync(filePath)) return null;
|
|
468
963
|
try {
|
|
469
|
-
|
|
470
|
-
validateConfig(config, filePath);
|
|
471
|
-
return config;
|
|
964
|
+
return JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
|
|
472
965
|
} catch {
|
|
473
966
|
return null;
|
|
474
967
|
}
|
|
475
968
|
}
|
|
476
|
-
function validateConfig(config, path5) {
|
|
477
|
-
const allowedTopLevel = ["version", "settings", "policy", "environments", "apiKey", "apiUrl"];
|
|
478
|
-
Object.keys(config).forEach((key) => {
|
|
479
|
-
if (!allowedTopLevel.includes(key))
|
|
480
|
-
console.warn(import_chalk.default.yellow(`\u26A0\uFE0F Node9: Unknown top-level key "${key}" in ${path5}`));
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
function buildConfig(parsed) {
|
|
484
|
-
const p = parsed.policy || {};
|
|
485
|
-
const s = parsed.settings || {};
|
|
486
|
-
return {
|
|
487
|
-
settings: {
|
|
488
|
-
mode: s.mode ?? DEFAULT_CONFIG.settings.mode,
|
|
489
|
-
autoStartDaemon: s.autoStartDaemon ?? DEFAULT_CONFIG.settings.autoStartDaemon
|
|
490
|
-
},
|
|
491
|
-
policy: {
|
|
492
|
-
dangerousWords: p.dangerousWords ?? DEFAULT_CONFIG.policy.dangerousWords,
|
|
493
|
-
ignoredTools: p.ignoredTools ?? DEFAULT_CONFIG.policy.ignoredTools,
|
|
494
|
-
toolInspection: p.toolInspection ?? DEFAULT_CONFIG.policy.toolInspection,
|
|
495
|
-
rules: p.rules ?? DEFAULT_CONFIG.policy.rules
|
|
496
|
-
},
|
|
497
|
-
environments: parsed.environments || {}
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
969
|
function getActiveEnvironment(config) {
|
|
501
970
|
const env = process.env.NODE_ENV || "development";
|
|
502
971
|
return config.environments[env] ?? null;
|
|
503
972
|
}
|
|
504
973
|
function getCredentials() {
|
|
505
974
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
506
|
-
if (process.env.NODE9_API_KEY)
|
|
975
|
+
if (process.env.NODE9_API_KEY) {
|
|
507
976
|
return {
|
|
508
977
|
apiKey: process.env.NODE9_API_KEY,
|
|
509
978
|
apiUrl: process.env.NODE9_API_URL || DEFAULT_API_URL
|
|
510
979
|
};
|
|
511
|
-
try {
|
|
512
|
-
const projectConfigPath = import_path.default.join(process.cwd(), "node9.config.json");
|
|
513
|
-
if (import_fs.default.existsSync(projectConfigPath)) {
|
|
514
|
-
const projectConfig = JSON.parse(import_fs.default.readFileSync(projectConfigPath, "utf-8"));
|
|
515
|
-
if (typeof projectConfig.apiKey === "string" && projectConfig.apiKey) {
|
|
516
|
-
return {
|
|
517
|
-
apiKey: projectConfig.apiKey,
|
|
518
|
-
apiUrl: typeof projectConfig.apiUrl === "string" && projectConfig.apiUrl || DEFAULT_API_URL
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
} catch {
|
|
523
980
|
}
|
|
524
981
|
try {
|
|
525
982
|
const credPath = import_path.default.join(import_os.default.homedir(), ".node9", "credentials.json");
|
|
@@ -544,14 +1001,32 @@ function getCredentials() {
|
|
|
544
1001
|
}
|
|
545
1002
|
return null;
|
|
546
1003
|
}
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
1004
|
+
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
1005
|
+
const controller = new AbortController();
|
|
1006
|
+
setTimeout(() => controller.abort(), 5e3);
|
|
1007
|
+
fetch(`${creds.apiUrl}/audit`, {
|
|
1008
|
+
method: "POST",
|
|
1009
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
1010
|
+
body: JSON.stringify({
|
|
1011
|
+
toolName,
|
|
1012
|
+
args,
|
|
1013
|
+
checkedBy,
|
|
1014
|
+
context: {
|
|
1015
|
+
agent: meta?.agent,
|
|
1016
|
+
mcpServer: meta?.mcpServer,
|
|
1017
|
+
hostname: import_os.default.hostname(),
|
|
1018
|
+
cwd: process.cwd(),
|
|
1019
|
+
platform: import_os.default.platform()
|
|
1020
|
+
}
|
|
1021
|
+
}),
|
|
1022
|
+
signal: controller.signal
|
|
1023
|
+
}).catch(() => {
|
|
1024
|
+
});
|
|
550
1025
|
}
|
|
551
|
-
async function
|
|
1026
|
+
async function initNode9SaaS(toolName, args, creds, slackChannel, meta) {
|
|
1027
|
+
const controller = new AbortController();
|
|
1028
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
552
1029
|
try {
|
|
553
|
-
const controller = new AbortController();
|
|
554
|
-
const timeout = setTimeout(() => controller.abort(), 35e3);
|
|
555
1030
|
const response = await fetch(creds.apiUrl, {
|
|
556
1031
|
method: "POST",
|
|
557
1032
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
@@ -569,46 +1044,55 @@ async function callNode9SaaS(toolName, args, creds, slackChannel, meta) {
|
|
|
569
1044
|
}),
|
|
570
1045
|
signal: controller.signal
|
|
571
1046
|
});
|
|
1047
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1048
|
+
return await response.json();
|
|
1049
|
+
} finally {
|
|
572
1050
|
clearTimeout(timeout);
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
return true;
|
|
600
|
-
}
|
|
601
|
-
if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
|
|
602
|
-
console.error(import_chalk.default.red("\u274C Denied \u2014 action blocked.\n"));
|
|
603
|
-
return false;
|
|
604
|
-
}
|
|
605
|
-
} catch {
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
async function pollNode9SaaS(requestId, creds, signal) {
|
|
1054
|
+
const statusUrl = `${creds.apiUrl}/status/${requestId}`;
|
|
1055
|
+
const POLL_INTERVAL_MS = 1e3;
|
|
1056
|
+
const POLL_DEADLINE = Date.now() + 10 * 60 * 1e3;
|
|
1057
|
+
while (Date.now() < POLL_DEADLINE) {
|
|
1058
|
+
if (signal.aborted) throw new Error("Aborted");
|
|
1059
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
1060
|
+
try {
|
|
1061
|
+
const pollCtrl = new AbortController();
|
|
1062
|
+
const pollTimer = setTimeout(() => pollCtrl.abort(), 5e3);
|
|
1063
|
+
const statusRes = await fetch(statusUrl, {
|
|
1064
|
+
headers: { Authorization: `Bearer ${creds.apiKey}` },
|
|
1065
|
+
signal: pollCtrl.signal
|
|
1066
|
+
});
|
|
1067
|
+
clearTimeout(pollTimer);
|
|
1068
|
+
if (!statusRes.ok) continue;
|
|
1069
|
+
const { status, reason } = await statusRes.json();
|
|
1070
|
+
if (status === "APPROVED") {
|
|
1071
|
+
console.error(import_chalk2.default.green("\u2705 Approved via Cloud.\n"));
|
|
1072
|
+
return { approved: true, reason };
|
|
1073
|
+
}
|
|
1074
|
+
if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
|
|
1075
|
+
console.error(import_chalk2.default.red("\u274C Denied via Cloud.\n"));
|
|
1076
|
+
return { approved: false, reason };
|
|
606
1077
|
}
|
|
1078
|
+
} catch {
|
|
607
1079
|
}
|
|
608
|
-
|
|
609
|
-
|
|
1080
|
+
}
|
|
1081
|
+
return { approved: false, reason: "Cloud approval timed out after 10 minutes." };
|
|
1082
|
+
}
|
|
1083
|
+
async function resolveNode9SaaS(requestId, creds, approved) {
|
|
1084
|
+
try {
|
|
1085
|
+
const resolveUrl = `${creds.apiUrl}/requests/${requestId}`;
|
|
1086
|
+
const ctrl = new AbortController();
|
|
1087
|
+
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
1088
|
+
await fetch(resolveUrl, {
|
|
1089
|
+
method: "PATCH",
|
|
1090
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
1091
|
+
body: JSON.stringify({ decision: approved ? "APPROVED" : "DENIED" }),
|
|
1092
|
+
signal: ctrl.signal
|
|
1093
|
+
});
|
|
1094
|
+
clearTimeout(timer);
|
|
610
1095
|
} catch {
|
|
611
|
-
return false;
|
|
612
1096
|
}
|
|
613
1097
|
}
|
|
614
1098
|
|
|
@@ -616,11 +1100,11 @@ async function callNode9SaaS(toolName, args, creds, slackChannel, meta) {
|
|
|
616
1100
|
var import_fs2 = __toESM(require("fs"));
|
|
617
1101
|
var import_path2 = __toESM(require("path"));
|
|
618
1102
|
var import_os2 = __toESM(require("os"));
|
|
619
|
-
var
|
|
1103
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
620
1104
|
var import_prompts2 = require("@inquirer/prompts");
|
|
621
1105
|
function printDaemonTip() {
|
|
622
1106
|
console.log(
|
|
623
|
-
|
|
1107
|
+
import_chalk3.default.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + import_chalk3.default.white("\n To view your history or manage persistent rules, run:") + import_chalk3.default.green("\n node9 daemon --openui")
|
|
624
1108
|
);
|
|
625
1109
|
}
|
|
626
1110
|
function fullPathCommand(subcommand) {
|
|
@@ -661,7 +1145,7 @@ async function setupClaude() {
|
|
|
661
1145
|
matcher: ".*",
|
|
662
1146
|
hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
|
|
663
1147
|
});
|
|
664
|
-
console.log(
|
|
1148
|
+
console.log(import_chalk3.default.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
|
|
665
1149
|
anythingChanged = true;
|
|
666
1150
|
}
|
|
667
1151
|
const hasPostHook = settings.hooks.PostToolUse?.some(
|
|
@@ -671,9 +1155,9 @@ async function setupClaude() {
|
|
|
671
1155
|
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
672
1156
|
settings.hooks.PostToolUse.push({
|
|
673
1157
|
matcher: ".*",
|
|
674
|
-
hooks: [{ type: "command", command: fullPathCommand("log") }]
|
|
1158
|
+
hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
|
|
675
1159
|
});
|
|
676
|
-
console.log(
|
|
1160
|
+
console.log(import_chalk3.default.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
|
|
677
1161
|
anythingChanged = true;
|
|
678
1162
|
}
|
|
679
1163
|
if (anythingChanged) {
|
|
@@ -687,10 +1171,10 @@ async function setupClaude() {
|
|
|
687
1171
|
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
688
1172
|
}
|
|
689
1173
|
if (serversToWrap.length > 0) {
|
|
690
|
-
console.log(
|
|
691
|
-
console.log(
|
|
1174
|
+
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
1175
|
+
console.log(import_chalk3.default.white(` ${mcpPath}`));
|
|
692
1176
|
for (const { name, originalCmd } of serversToWrap) {
|
|
693
|
-
console.log(
|
|
1177
|
+
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
694
1178
|
}
|
|
695
1179
|
console.log("");
|
|
696
1180
|
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
@@ -700,22 +1184,22 @@ async function setupClaude() {
|
|
|
700
1184
|
}
|
|
701
1185
|
claudeConfig.mcpServers = servers;
|
|
702
1186
|
writeJson(mcpPath, claudeConfig);
|
|
703
|
-
console.log(
|
|
1187
|
+
console.log(import_chalk3.default.green(`
|
|
704
1188
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
705
1189
|
anythingChanged = true;
|
|
706
1190
|
} else {
|
|
707
|
-
console.log(
|
|
1191
|
+
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
708
1192
|
}
|
|
709
1193
|
console.log("");
|
|
710
1194
|
}
|
|
711
1195
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
712
|
-
console.log(
|
|
1196
|
+
console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
|
|
713
1197
|
printDaemonTip();
|
|
714
1198
|
return;
|
|
715
1199
|
}
|
|
716
1200
|
if (anythingChanged) {
|
|
717
|
-
console.log(
|
|
718
|
-
console.log(
|
|
1201
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
1202
|
+
console.log(import_chalk3.default.gray(" Restart Claude Code for changes to take effect."));
|
|
719
1203
|
printDaemonTip();
|
|
720
1204
|
}
|
|
721
1205
|
}
|
|
@@ -735,10 +1219,15 @@ async function setupGemini() {
|
|
|
735
1219
|
settings.hooks.BeforeTool.push({
|
|
736
1220
|
matcher: ".*",
|
|
737
1221
|
hooks: [
|
|
738
|
-
{
|
|
1222
|
+
{
|
|
1223
|
+
name: "node9-check",
|
|
1224
|
+
type: "command",
|
|
1225
|
+
command: fullPathCommand("check"),
|
|
1226
|
+
timeout: 6e5
|
|
1227
|
+
}
|
|
739
1228
|
]
|
|
740
1229
|
});
|
|
741
|
-
console.log(
|
|
1230
|
+
console.log(import_chalk3.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
742
1231
|
anythingChanged = true;
|
|
743
1232
|
}
|
|
744
1233
|
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
@@ -751,7 +1240,7 @@ async function setupGemini() {
|
|
|
751
1240
|
matcher: ".*",
|
|
752
1241
|
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
753
1242
|
});
|
|
754
|
-
console.log(
|
|
1243
|
+
console.log(import_chalk3.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
755
1244
|
anythingChanged = true;
|
|
756
1245
|
}
|
|
757
1246
|
if (anythingChanged) {
|
|
@@ -765,10 +1254,10 @@ async function setupGemini() {
|
|
|
765
1254
|
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
766
1255
|
}
|
|
767
1256
|
if (serversToWrap.length > 0) {
|
|
768
|
-
console.log(
|
|
769
|
-
console.log(
|
|
1257
|
+
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
1258
|
+
console.log(import_chalk3.default.white(` ${settingsPath} (mcpServers)`));
|
|
770
1259
|
for (const { name, originalCmd } of serversToWrap) {
|
|
771
|
-
console.log(
|
|
1260
|
+
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
772
1261
|
}
|
|
773
1262
|
console.log("");
|
|
774
1263
|
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
@@ -778,22 +1267,22 @@ async function setupGemini() {
|
|
|
778
1267
|
}
|
|
779
1268
|
settings.mcpServers = servers;
|
|
780
1269
|
writeJson(settingsPath, settings);
|
|
781
|
-
console.log(
|
|
1270
|
+
console.log(import_chalk3.default.green(`
|
|
782
1271
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
783
1272
|
anythingChanged = true;
|
|
784
1273
|
} else {
|
|
785
|
-
console.log(
|
|
1274
|
+
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
786
1275
|
}
|
|
787
1276
|
console.log("");
|
|
788
1277
|
}
|
|
789
1278
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
790
|
-
console.log(
|
|
1279
|
+
console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
|
|
791
1280
|
printDaemonTip();
|
|
792
1281
|
return;
|
|
793
1282
|
}
|
|
794
1283
|
if (anythingChanged) {
|
|
795
|
-
console.log(
|
|
796
|
-
console.log(
|
|
1284
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
|
|
1285
|
+
console.log(import_chalk3.default.gray(" Restart Gemini CLI for changes to take effect."));
|
|
797
1286
|
printDaemonTip();
|
|
798
1287
|
}
|
|
799
1288
|
}
|
|
@@ -812,7 +1301,7 @@ async function setupCursor() {
|
|
|
812
1301
|
if (!hasPreHook) {
|
|
813
1302
|
if (!hooksFile.hooks.preToolUse) hooksFile.hooks.preToolUse = [];
|
|
814
1303
|
hooksFile.hooks.preToolUse.push({ command: fullPathCommand("check") });
|
|
815
|
-
console.log(
|
|
1304
|
+
console.log(import_chalk3.default.green(" \u2705 preToolUse hook added \u2192 node9 check"));
|
|
816
1305
|
anythingChanged = true;
|
|
817
1306
|
}
|
|
818
1307
|
const hasPostHook = hooksFile.hooks.postToolUse?.some(
|
|
@@ -821,7 +1310,7 @@ async function setupCursor() {
|
|
|
821
1310
|
if (!hasPostHook) {
|
|
822
1311
|
if (!hooksFile.hooks.postToolUse) hooksFile.hooks.postToolUse = [];
|
|
823
1312
|
hooksFile.hooks.postToolUse.push({ command: fullPathCommand("log") });
|
|
824
|
-
console.log(
|
|
1313
|
+
console.log(import_chalk3.default.green(" \u2705 postToolUse hook added \u2192 node9 log"));
|
|
825
1314
|
anythingChanged = true;
|
|
826
1315
|
}
|
|
827
1316
|
if (anythingChanged) {
|
|
@@ -835,10 +1324,10 @@ async function setupCursor() {
|
|
|
835
1324
|
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
836
1325
|
}
|
|
837
1326
|
if (serversToWrap.length > 0) {
|
|
838
|
-
console.log(
|
|
839
|
-
console.log(
|
|
1327
|
+
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
1328
|
+
console.log(import_chalk3.default.white(` ${mcpPath}`));
|
|
840
1329
|
for (const { name, originalCmd } of serversToWrap) {
|
|
841
|
-
console.log(
|
|
1330
|
+
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
842
1331
|
}
|
|
843
1332
|
console.log("");
|
|
844
1333
|
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
@@ -848,22 +1337,22 @@ async function setupCursor() {
|
|
|
848
1337
|
}
|
|
849
1338
|
mcpConfig.mcpServers = servers;
|
|
850
1339
|
writeJson(mcpPath, mcpConfig);
|
|
851
|
-
console.log(
|
|
1340
|
+
console.log(import_chalk3.default.green(`
|
|
852
1341
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
853
1342
|
anythingChanged = true;
|
|
854
1343
|
} else {
|
|
855
|
-
console.log(
|
|
1344
|
+
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
856
1345
|
}
|
|
857
1346
|
console.log("");
|
|
858
1347
|
}
|
|
859
1348
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
860
|
-
console.log(
|
|
1349
|
+
console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Cursor."));
|
|
861
1350
|
printDaemonTip();
|
|
862
1351
|
return;
|
|
863
1352
|
}
|
|
864
1353
|
if (anythingChanged) {
|
|
865
|
-
console.log(
|
|
866
|
-
console.log(
|
|
1354
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor!"));
|
|
1355
|
+
console.log(import_chalk3.default.gray(" Restart Cursor for changes to take effect."));
|
|
867
1356
|
printDaemonTip();
|
|
868
1357
|
}
|
|
869
1358
|
}
|
|
@@ -1132,6 +1621,27 @@ var ui_default = `<!doctype html>
|
|
|
1132
1621
|
font-size: 12px;
|
|
1133
1622
|
font-weight: 500;
|
|
1134
1623
|
}
|
|
1624
|
+
.btn-trust {
|
|
1625
|
+
background: rgba(240, 136, 62, 0.1);
|
|
1626
|
+
border: 1px solid rgba(240, 136, 62, 0.35);
|
|
1627
|
+
color: var(--primary);
|
|
1628
|
+
font-size: 12px;
|
|
1629
|
+
font-weight: 600;
|
|
1630
|
+
}
|
|
1631
|
+
.btn-trust:hover:not(:disabled) {
|
|
1632
|
+
background: rgba(240, 136, 62, 0.2);
|
|
1633
|
+
filter: none;
|
|
1634
|
+
transform: translateY(-1px);
|
|
1635
|
+
}
|
|
1636
|
+
.trust-row {
|
|
1637
|
+
display: none;
|
|
1638
|
+
grid-column: span 2;
|
|
1639
|
+
grid-template-columns: 1fr 1fr;
|
|
1640
|
+
gap: 8px;
|
|
1641
|
+
}
|
|
1642
|
+
.trust-row.show {
|
|
1643
|
+
display: grid;
|
|
1644
|
+
}
|
|
1135
1645
|
button:hover:not(:disabled) {
|
|
1136
1646
|
filter: brightness(1.15);
|
|
1137
1647
|
transform: translateY(-1px);
|
|
@@ -1435,15 +1945,31 @@ var ui_default = `<!doctype html>
|
|
|
1435
1945
|
<span class="slider"></span>
|
|
1436
1946
|
</label>
|
|
1437
1947
|
</div>
|
|
1948
|
+
<div class="setting-row">
|
|
1949
|
+
<div class="setting-text">
|
|
1950
|
+
<div class="setting-label">Trust Sessions</div>
|
|
1951
|
+
<div class="setting-desc">
|
|
1952
|
+
Show "Trust 30m / 1h" buttons \u2014 allow a tool without interruption for a set time.
|
|
1953
|
+
</div>
|
|
1954
|
+
</div>
|
|
1955
|
+
<label class="toggle">
|
|
1956
|
+
<input
|
|
1957
|
+
type="checkbox"
|
|
1958
|
+
id="trustSessionsToggle"
|
|
1959
|
+
onchange="onTrustToggle(this.checked)"
|
|
1960
|
+
/>
|
|
1961
|
+
<span class="slider"></span>
|
|
1962
|
+
</label>
|
|
1963
|
+
</div>
|
|
1438
1964
|
</div>
|
|
1439
1965
|
|
|
1440
1966
|
<div class="panel">
|
|
1441
|
-
<div class="panel-title">\u{1F4AC}
|
|
1967
|
+
<div class="panel-title">\u{1F4AC} Cloud Approvals</div>
|
|
1442
1968
|
<div class="setting-row">
|
|
1443
1969
|
<div class="setting-text">
|
|
1444
|
-
<div class="setting-label">Enable
|
|
1970
|
+
<div class="setting-label">Enable Cloud</div>
|
|
1445
1971
|
<div class="setting-desc">
|
|
1446
|
-
Use Slack as the approval authority when a key is saved.
|
|
1972
|
+
Use Cloud/Slack as the approval authority when a key is saved.
|
|
1447
1973
|
</div>
|
|
1448
1974
|
</div>
|
|
1449
1975
|
<label class="toggle">
|
|
@@ -1497,6 +2023,7 @@ var ui_default = `<!doctype html>
|
|
|
1497
2023
|
const requests = new Set();
|
|
1498
2024
|
let orgName = null;
|
|
1499
2025
|
let autoDenyMs = 120000;
|
|
2026
|
+
let trustEnabled = false;
|
|
1500
2027
|
|
|
1501
2028
|
function highlightSyntax(code) {
|
|
1502
2029
|
if (typeof code !== 'string') return esc(code);
|
|
@@ -1549,6 +2076,21 @@ var ui_default = `<!doctype html>
|
|
|
1549
2076
|
}, 200);
|
|
1550
2077
|
}
|
|
1551
2078
|
|
|
2079
|
+
function sendTrust(id, duration) {
|
|
2080
|
+
const card = document.getElementById('c-' + id);
|
|
2081
|
+
if (card) card.style.opacity = '0.5';
|
|
2082
|
+
fetch('/decision/' + id, {
|
|
2083
|
+
method: 'POST',
|
|
2084
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
2085
|
+
body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
|
|
2086
|
+
});
|
|
2087
|
+
setTimeout(() => {
|
|
2088
|
+
card?.remove();
|
|
2089
|
+
requests.delete(id);
|
|
2090
|
+
refresh();
|
|
2091
|
+
}, 200);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
1552
2094
|
function addCard(req) {
|
|
1553
2095
|
if (requests.has(req.id)) return;
|
|
1554
2096
|
requests.add(req.id);
|
|
@@ -1568,6 +2110,7 @@ var ui_default = `<!doctype html>
|
|
|
1568
2110
|
card.id = 'c-' + req.id;
|
|
1569
2111
|
const agentLabel = req.agent ? esc(req.agent) : 'AI Agent';
|
|
1570
2112
|
const mcpLabel = req.mcpServer ? esc(req.mcpServer) : null;
|
|
2113
|
+
const dis = isSlack ? 'disabled' : '';
|
|
1571
2114
|
card.innerHTML = \`
|
|
1572
2115
|
<div class="source-row">
|
|
1573
2116
|
<span class="agent-badge">\${agentLabel}</span>
|
|
@@ -1577,11 +2120,15 @@ var ui_default = `<!doctype html>
|
|
|
1577
2120
|
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Slack approval \u2014 view only</div>' : ''}
|
|
1578
2121
|
<span class="label">Input Payload</span>
|
|
1579
2122
|
<pre>\${cmd}</pre>
|
|
1580
|
-
<div class="actions">
|
|
1581
|
-
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${
|
|
1582
|
-
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${
|
|
1583
|
-
<
|
|
1584
|
-
|
|
2123
|
+
<div class="actions" id="act-\${req.id}">
|
|
2124
|
+
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>Approve Execution</button>
|
|
2125
|
+
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>Block Action</button>
|
|
2126
|
+
<div class="trust-row\${trustEnabled ? ' show' : ''}" id="tr-\${req.id}">
|
|
2127
|
+
<button class="btn-trust" onclick="sendTrust('\${req.id}','30m')" \${dis}>\u23F1 Trust 30m</button>
|
|
2128
|
+
<button class="btn-trust" onclick="sendTrust('\${req.id}','1h')" \${dis}>\u23F1 Trust 1h</button>
|
|
2129
|
+
</div>
|
|
2130
|
+
<button class="btn-secondary" onclick="sendDecision('\${req.id}','allow',true)" \${dis}>Always Allow</button>
|
|
2131
|
+
<button class="btn-secondary" onclick="sendDecision('\${req.id}','deny',true)" \${dis}>Always Deny</button>
|
|
1585
2132
|
</div>
|
|
1586
2133
|
\`;
|
|
1587
2134
|
list.appendChild(card);
|
|
@@ -1613,8 +2160,10 @@ var ui_default = `<!doctype html>
|
|
|
1613
2160
|
autoDenyMs = data.autoDenyMs;
|
|
1614
2161
|
if (orgName) {
|
|
1615
2162
|
const b = document.getElementById('cloudBadge');
|
|
1616
|
-
b
|
|
1617
|
-
|
|
2163
|
+
if (b) {
|
|
2164
|
+
b.innerText = orgName;
|
|
2165
|
+
b.classList.add('online');
|
|
2166
|
+
}
|
|
1618
2167
|
}
|
|
1619
2168
|
data.requests.forEach(addCard);
|
|
1620
2169
|
});
|
|
@@ -1644,6 +2193,14 @@ var ui_default = `<!doctype html>
|
|
|
1644
2193
|
}).catch(() => {});
|
|
1645
2194
|
}
|
|
1646
2195
|
|
|
2196
|
+
function onTrustToggle(checked) {
|
|
2197
|
+
trustEnabled = checked;
|
|
2198
|
+
saveSetting('enableTrustSessions', checked);
|
|
2199
|
+
document.querySelectorAll('[id^="tr-"]').forEach((el) => {
|
|
2200
|
+
el.classList.toggle('show', checked);
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
|
|
1647
2204
|
fetch('/settings')
|
|
1648
2205
|
.then((r) => r.json())
|
|
1649
2206
|
.then((s) => {
|
|
@@ -1656,6 +2213,13 @@ var ui_default = `<!doctype html>
|
|
|
1656
2213
|
if (!s.autoStartDaemon && !s.autoStarted) {
|
|
1657
2214
|
document.getElementById('warnBanner').classList.add('show');
|
|
1658
2215
|
}
|
|
2216
|
+
trustEnabled = !!s.enableTrustSessions;
|
|
2217
|
+
const trustTog = document.getElementById('trustSessionsToggle');
|
|
2218
|
+
if (trustTog) trustTog.checked = trustEnabled;
|
|
2219
|
+
// Show/hide trust rows on any cards already rendered
|
|
2220
|
+
document.querySelectorAll('[id^="tr-"]').forEach((el) => {
|
|
2221
|
+
el.classList.toggle('show', trustEnabled);
|
|
2222
|
+
});
|
|
1659
2223
|
})
|
|
1660
2224
|
.catch(() => {});
|
|
1661
2225
|
|
|
@@ -1765,9 +2329,9 @@ var import_http = __toESM(require("http"));
|
|
|
1765
2329
|
var import_fs3 = __toESM(require("fs"));
|
|
1766
2330
|
var import_path3 = __toESM(require("path"));
|
|
1767
2331
|
var import_os3 = __toESM(require("os"));
|
|
1768
|
-
var
|
|
2332
|
+
var import_child_process2 = require("child_process");
|
|
1769
2333
|
var import_crypto = require("crypto");
|
|
1770
|
-
var
|
|
2334
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
1771
2335
|
var DAEMON_PORT2 = 7391;
|
|
1772
2336
|
var DAEMON_HOST2 = "127.0.0.1";
|
|
1773
2337
|
var homeDir = import_os3.default.homedir();
|
|
@@ -1776,6 +2340,33 @@ var DECISIONS_FILE = import_path3.default.join(homeDir, ".node9", "decisions.jso
|
|
|
1776
2340
|
var GLOBAL_CONFIG_FILE = import_path3.default.join(homeDir, ".node9", "config.json");
|
|
1777
2341
|
var CREDENTIALS_FILE = import_path3.default.join(homeDir, ".node9", "credentials.json");
|
|
1778
2342
|
var AUDIT_LOG_FILE = import_path3.default.join(homeDir, ".node9", "audit.log");
|
|
2343
|
+
var TRUST_FILE2 = import_path3.default.join(homeDir, ".node9", "trust.json");
|
|
2344
|
+
function atomicWriteSync2(filePath, data, options) {
|
|
2345
|
+
const dir = import_path3.default.dirname(filePath);
|
|
2346
|
+
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
2347
|
+
const tmpPath = `${filePath}.${(0, import_crypto.randomUUID)()}.tmp`;
|
|
2348
|
+
import_fs3.default.writeFileSync(tmpPath, data, options);
|
|
2349
|
+
import_fs3.default.renameSync(tmpPath, filePath);
|
|
2350
|
+
}
|
|
2351
|
+
function writeTrustEntry(toolName, durationMs) {
|
|
2352
|
+
try {
|
|
2353
|
+
let trust = { entries: [] };
|
|
2354
|
+
try {
|
|
2355
|
+
if (import_fs3.default.existsSync(TRUST_FILE2))
|
|
2356
|
+
trust = JSON.parse(import_fs3.default.readFileSync(TRUST_FILE2, "utf-8"));
|
|
2357
|
+
} catch {
|
|
2358
|
+
}
|
|
2359
|
+
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
|
|
2360
|
+
trust.entries.push({ tool: toolName, expiry: Date.now() + durationMs });
|
|
2361
|
+
atomicWriteSync2(TRUST_FILE2, JSON.stringify(trust, null, 2));
|
|
2362
|
+
} catch {
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
var TRUST_DURATIONS = {
|
|
2366
|
+
"30m": 30 * 6e4,
|
|
2367
|
+
"1h": 60 * 6e4,
|
|
2368
|
+
"2h": 2 * 60 * 6e4
|
|
2369
|
+
};
|
|
1779
2370
|
var SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
1780
2371
|
function redactArgs(value) {
|
|
1781
2372
|
if (!value || typeof value !== "object") return value;
|
|
@@ -1788,10 +2379,16 @@ function redactArgs(value) {
|
|
|
1788
2379
|
}
|
|
1789
2380
|
function appendAuditLog(data) {
|
|
1790
2381
|
try {
|
|
1791
|
-
const entry =
|
|
2382
|
+
const entry = {
|
|
2383
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2384
|
+
tool: data.toolName,
|
|
2385
|
+
args: redactArgs(data.args),
|
|
2386
|
+
decision: data.decision,
|
|
2387
|
+
source: "daemon"
|
|
2388
|
+
};
|
|
1792
2389
|
const dir = import_path3.default.dirname(AUDIT_LOG_FILE);
|
|
1793
2390
|
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
1794
|
-
import_fs3.default.appendFileSync(AUDIT_LOG_FILE, entry);
|
|
2391
|
+
import_fs3.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
1795
2392
|
} catch {
|
|
1796
2393
|
}
|
|
1797
2394
|
}
|
|
@@ -1816,21 +2413,6 @@ function getOrgName() {
|
|
|
1816
2413
|
return null;
|
|
1817
2414
|
}
|
|
1818
2415
|
var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
1819
|
-
function readGlobalSettings() {
|
|
1820
|
-
try {
|
|
1821
|
-
if (import_fs3.default.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
1822
|
-
const config = JSON.parse(import_fs3.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
|
|
1823
|
-
const s = config?.settings ?? {};
|
|
1824
|
-
return {
|
|
1825
|
-
autoStartDaemon: s.autoStartDaemon !== false,
|
|
1826
|
-
slackEnabled: s.slackEnabled !== false,
|
|
1827
|
-
agentMode: s.agentMode === true
|
|
1828
|
-
};
|
|
1829
|
-
}
|
|
1830
|
-
} catch {
|
|
1831
|
-
}
|
|
1832
|
-
return { autoStartDaemon: true, slackEnabled: true, agentMode: false };
|
|
1833
|
-
}
|
|
1834
2416
|
function hasStoredSlackKey() {
|
|
1835
2417
|
return import_fs3.default.existsSync(CREDENTIALS_FILE);
|
|
1836
2418
|
}
|
|
@@ -1844,14 +2426,13 @@ function writeGlobalSetting(key, value) {
|
|
|
1844
2426
|
}
|
|
1845
2427
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
1846
2428
|
config.settings[key] = value;
|
|
1847
|
-
|
|
1848
|
-
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
1849
|
-
import_fs3.default.writeFileSync(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
2429
|
+
atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
1850
2430
|
}
|
|
1851
2431
|
var pending = /* @__PURE__ */ new Map();
|
|
1852
2432
|
var sseClients = /* @__PURE__ */ new Set();
|
|
1853
2433
|
var abandonTimer = null;
|
|
1854
2434
|
var daemonServer = null;
|
|
2435
|
+
var hadBrowserClient = false;
|
|
1855
2436
|
function abandonPending() {
|
|
1856
2437
|
abandonTimer = null;
|
|
1857
2438
|
pending.forEach((entry, id) => {
|
|
@@ -1887,10 +2468,8 @@ data: ${JSON.stringify(data)}
|
|
|
1887
2468
|
}
|
|
1888
2469
|
function openBrowser(url) {
|
|
1889
2470
|
try {
|
|
1890
|
-
const
|
|
1891
|
-
|
|
1892
|
-
else if (process.platform === "win32") (0, import_child_process.execSync)(`cmd /c start "" "${url}"`, opts);
|
|
1893
|
-
else (0, import_child_process.execSync)(`xdg-open "${url}"`, opts);
|
|
2471
|
+
const args = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
|
|
2472
|
+
(0, import_child_process2.spawn)(args[0], args.slice(1), { detached: true, stdio: "ignore" }).unref();
|
|
1894
2473
|
} catch {
|
|
1895
2474
|
}
|
|
1896
2475
|
}
|
|
@@ -1912,11 +2491,9 @@ function readPersistentDecisions() {
|
|
|
1912
2491
|
}
|
|
1913
2492
|
function writePersistentDecision(toolName, decision) {
|
|
1914
2493
|
try {
|
|
1915
|
-
const dir = import_path3.default.dirname(DECISIONS_FILE);
|
|
1916
|
-
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
1917
2494
|
const decisions = readPersistentDecisions();
|
|
1918
2495
|
decisions[toolName] = decision;
|
|
1919
|
-
|
|
2496
|
+
atomicWriteSync2(DECISIONS_FILE, JSON.stringify(decisions, null, 2));
|
|
1920
2497
|
broadcast("decisions", decisions);
|
|
1921
2498
|
} catch {
|
|
1922
2499
|
}
|
|
@@ -1926,6 +2503,22 @@ function startDaemon() {
|
|
|
1926
2503
|
const internalToken = (0, import_crypto.randomUUID)();
|
|
1927
2504
|
const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
|
|
1928
2505
|
const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
|
|
2506
|
+
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
2507
|
+
let idleTimer;
|
|
2508
|
+
function resetIdleTimer() {
|
|
2509
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
2510
|
+
idleTimer = setTimeout(() => {
|
|
2511
|
+
if (autoStarted) {
|
|
2512
|
+
try {
|
|
2513
|
+
import_fs3.default.unlinkSync(DAEMON_PID_FILE);
|
|
2514
|
+
} catch {
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
process.exit(0);
|
|
2518
|
+
}, IDLE_TIMEOUT_MS);
|
|
2519
|
+
idleTimer.unref();
|
|
2520
|
+
}
|
|
2521
|
+
resetIdleTimer();
|
|
1929
2522
|
const server = import_http.default.createServer(async (req, res) => {
|
|
1930
2523
|
const { pathname } = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
1931
2524
|
if (req.method === "GET" && pathname === "/") {
|
|
@@ -1942,6 +2535,7 @@ function startDaemon() {
|
|
|
1942
2535
|
clearTimeout(abandonTimer);
|
|
1943
2536
|
abandonTimer = null;
|
|
1944
2537
|
}
|
|
2538
|
+
hadBrowserClient = true;
|
|
1945
2539
|
sseClients.add(res);
|
|
1946
2540
|
res.write(
|
|
1947
2541
|
`event: init
|
|
@@ -1968,12 +2562,13 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
1968
2562
|
return req.on("close", () => {
|
|
1969
2563
|
sseClients.delete(res);
|
|
1970
2564
|
if (sseClients.size === 0 && pending.size > 0) {
|
|
1971
|
-
abandonTimer = setTimeout(abandonPending,
|
|
2565
|
+
abandonTimer = setTimeout(abandonPending, hadBrowserClient ? 1e4 : 15e3);
|
|
1972
2566
|
}
|
|
1973
2567
|
});
|
|
1974
2568
|
}
|
|
1975
2569
|
if (req.method === "POST" && pathname === "/check") {
|
|
1976
2570
|
try {
|
|
2571
|
+
resetIdleTimer();
|
|
1977
2572
|
const body = await readBody(req);
|
|
1978
2573
|
if (body.length > 65536) return res.writeHead(413).end();
|
|
1979
2574
|
const { toolName, args, slackDelegated = false, agent, mcpServer } = JSON.parse(body);
|
|
@@ -1994,8 +2589,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
1994
2589
|
appendAuditLog({
|
|
1995
2590
|
toolName: e.toolName,
|
|
1996
2591
|
args: e.args,
|
|
1997
|
-
decision: "auto-deny"
|
|
1998
|
-
timestamp: Date.now()
|
|
2592
|
+
decision: "auto-deny"
|
|
1999
2593
|
});
|
|
2000
2594
|
if (e.waiter) e.waiter("deny");
|
|
2001
2595
|
else e.earlyDecision = "deny";
|
|
@@ -2013,7 +2607,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
2013
2607
|
agent: entry.agent,
|
|
2014
2608
|
mcpServer: entry.mcpServer
|
|
2015
2609
|
});
|
|
2016
|
-
if (sseClients.size === 0) openBrowser(`http://127.0.0.1:${DAEMON_PORT2}/`);
|
|
2610
|
+
if (sseClients.size === 0 && !autoStarted) openBrowser(`http://127.0.0.1:${DAEMON_PORT2}/`);
|
|
2017
2611
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2018
2612
|
return res.end(JSON.stringify({ id }));
|
|
2019
2613
|
} catch {
|
|
@@ -2040,17 +2634,33 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
2040
2634
|
const id = pathname.split("/").pop();
|
|
2041
2635
|
const entry = pending.get(id);
|
|
2042
2636
|
if (!entry) return res.writeHead(404).end();
|
|
2043
|
-
const { decision, persist } = JSON.parse(await readBody(req));
|
|
2044
|
-
if (
|
|
2637
|
+
const { decision, persist, trustDuration } = JSON.parse(await readBody(req));
|
|
2638
|
+
if (decision === "trust" && trustDuration) {
|
|
2639
|
+
const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
|
|
2640
|
+
writeTrustEntry(entry.toolName, ms);
|
|
2641
|
+
appendAuditLog({
|
|
2642
|
+
toolName: entry.toolName,
|
|
2643
|
+
args: entry.args,
|
|
2644
|
+
decision: `trust:${trustDuration}`
|
|
2645
|
+
});
|
|
2646
|
+
clearTimeout(entry.timer);
|
|
2647
|
+
if (entry.waiter) entry.waiter("allow");
|
|
2648
|
+
else entry.earlyDecision = "allow";
|
|
2649
|
+
pending.delete(id);
|
|
2650
|
+
broadcast("remove", { id });
|
|
2651
|
+
res.writeHead(200);
|
|
2652
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
2653
|
+
}
|
|
2654
|
+
const resolvedDecision = decision === "allow" || decision === "deny" ? decision : "deny";
|
|
2655
|
+
if (persist) writePersistentDecision(entry.toolName, resolvedDecision);
|
|
2045
2656
|
appendAuditLog({
|
|
2046
2657
|
toolName: entry.toolName,
|
|
2047
2658
|
args: entry.args,
|
|
2048
|
-
decision
|
|
2049
|
-
timestamp: Date.now()
|
|
2659
|
+
decision: resolvedDecision
|
|
2050
2660
|
});
|
|
2051
2661
|
clearTimeout(entry.timer);
|
|
2052
|
-
if (entry.waiter) entry.waiter(
|
|
2053
|
-
else entry.earlyDecision =
|
|
2662
|
+
if (entry.waiter) entry.waiter(resolvedDecision);
|
|
2663
|
+
else entry.earlyDecision = resolvedDecision;
|
|
2054
2664
|
pending.delete(id);
|
|
2055
2665
|
broadcast("remove", { id });
|
|
2056
2666
|
res.writeHead(200);
|
|
@@ -2060,7 +2670,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
2060
2670
|
}
|
|
2061
2671
|
}
|
|
2062
2672
|
if (req.method === "GET" && pathname === "/settings") {
|
|
2063
|
-
const s =
|
|
2673
|
+
const s = getGlobalSettings();
|
|
2064
2674
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2065
2675
|
return res.end(JSON.stringify({ ...s, autoStarted }));
|
|
2066
2676
|
}
|
|
@@ -2072,7 +2682,12 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
2072
2682
|
if (data.autoStartDaemon !== void 0)
|
|
2073
2683
|
writeGlobalSetting("autoStartDaemon", data.autoStartDaemon);
|
|
2074
2684
|
if (data.slackEnabled !== void 0) writeGlobalSetting("slackEnabled", data.slackEnabled);
|
|
2075
|
-
if (data.
|
|
2685
|
+
if (data.enableTrustSessions !== void 0)
|
|
2686
|
+
writeGlobalSetting("enableTrustSessions", data.enableTrustSessions);
|
|
2687
|
+
if (data.enableUndo !== void 0) writeGlobalSetting("enableUndo", data.enableUndo);
|
|
2688
|
+
if (data.enableHookLogDebug !== void 0)
|
|
2689
|
+
writeGlobalSetting("enableHookLogDebug", data.enableHookLogDebug);
|
|
2690
|
+
if (data.approvers !== void 0) writeGlobalSetting("approvers", data.approvers);
|
|
2076
2691
|
res.writeHead(200);
|
|
2077
2692
|
return res.end(JSON.stringify({ ok: true }));
|
|
2078
2693
|
} catch {
|
|
@@ -2080,7 +2695,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
2080
2695
|
}
|
|
2081
2696
|
}
|
|
2082
2697
|
if (req.method === "GET" && pathname === "/slack-status") {
|
|
2083
|
-
const s =
|
|
2698
|
+
const s = getGlobalSettings();
|
|
2084
2699
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2085
2700
|
return res.end(JSON.stringify({ hasKey: hasStoredSlackKey(), enabled: s.slackEnabled }));
|
|
2086
2701
|
}
|
|
@@ -2088,14 +2703,12 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
2088
2703
|
if (!validToken(req)) return res.writeHead(403).end();
|
|
2089
2704
|
try {
|
|
2090
2705
|
const { apiKey } = JSON.parse(await readBody(req));
|
|
2091
|
-
|
|
2092
|
-
import_fs3.default.mkdirSync(import_path3.default.dirname(CREDENTIALS_FILE), { recursive: true });
|
|
2093
|
-
import_fs3.default.writeFileSync(
|
|
2706
|
+
atomicWriteSync2(
|
|
2094
2707
|
CREDENTIALS_FILE,
|
|
2095
2708
|
JSON.stringify({ apiKey, apiUrl: "https://api.node9.ai/api/v1/intercept" }, null, 2),
|
|
2096
2709
|
{ mode: 384 }
|
|
2097
2710
|
);
|
|
2098
|
-
broadcast("slack-status", { hasKey: true, enabled:
|
|
2711
|
+
broadcast("slack-status", { hasKey: true, enabled: getGlobalSettings().slackEnabled });
|
|
2099
2712
|
res.writeHead(200);
|
|
2100
2713
|
return res.end(JSON.stringify({ ok: true }));
|
|
2101
2714
|
} catch {
|
|
@@ -2108,7 +2721,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
2108
2721
|
const toolName = decodeURIComponent(pathname.split("/").pop());
|
|
2109
2722
|
const decisions = readPersistentDecisions();
|
|
2110
2723
|
delete decisions[toolName];
|
|
2111
|
-
|
|
2724
|
+
atomicWriteSync2(DECISIONS_FILE, JSON.stringify(decisions, null, 2));
|
|
2112
2725
|
broadcast("decisions", decisions);
|
|
2113
2726
|
res.writeHead(200);
|
|
2114
2727
|
return res.end(JSON.stringify({ ok: true }));
|
|
@@ -2127,8 +2740,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
2127
2740
|
appendAuditLog({
|
|
2128
2741
|
toolName: entry.toolName,
|
|
2129
2742
|
args: entry.args,
|
|
2130
|
-
decision
|
|
2131
|
-
timestamp: Date.now()
|
|
2743
|
+
decision
|
|
2132
2744
|
});
|
|
2133
2745
|
clearTimeout(entry.timer);
|
|
2134
2746
|
if (entry.waiter) entry.waiter(decision);
|
|
@@ -2148,25 +2760,43 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
2148
2760
|
res.writeHead(404).end();
|
|
2149
2761
|
});
|
|
2150
2762
|
daemonServer = server;
|
|
2763
|
+
server.on("error", (e) => {
|
|
2764
|
+
if (e.code === "EADDRINUSE") {
|
|
2765
|
+
try {
|
|
2766
|
+
if (import_fs3.default.existsSync(DAEMON_PID_FILE)) {
|
|
2767
|
+
const { pid } = JSON.parse(import_fs3.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
2768
|
+
process.kill(pid, 0);
|
|
2769
|
+
return process.exit(0);
|
|
2770
|
+
}
|
|
2771
|
+
} catch {
|
|
2772
|
+
try {
|
|
2773
|
+
import_fs3.default.unlinkSync(DAEMON_PID_FILE);
|
|
2774
|
+
} catch {
|
|
2775
|
+
}
|
|
2776
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
console.error(import_chalk4.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
|
|
2781
|
+
process.exit(1);
|
|
2782
|
+
});
|
|
2151
2783
|
server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
|
|
2152
|
-
|
|
2153
|
-
import_fs3.default.mkdirSync(import_path3.default.dirname(DAEMON_PID_FILE), { recursive: true });
|
|
2154
|
-
import_fs3.default.writeFileSync(
|
|
2784
|
+
atomicWriteSync2(
|
|
2155
2785
|
DAEMON_PID_FILE,
|
|
2156
2786
|
JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
|
|
2157
2787
|
{ mode: 384 }
|
|
2158
2788
|
);
|
|
2159
|
-
console.log(
|
|
2789
|
+
console.log(import_chalk4.default.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
|
|
2160
2790
|
});
|
|
2161
2791
|
}
|
|
2162
2792
|
function stopDaemon() {
|
|
2163
|
-
if (!import_fs3.default.existsSync(DAEMON_PID_FILE)) return console.log(
|
|
2793
|
+
if (!import_fs3.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
|
|
2164
2794
|
try {
|
|
2165
2795
|
const { pid } = JSON.parse(import_fs3.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
2166
2796
|
process.kill(pid, "SIGTERM");
|
|
2167
|
-
console.log(
|
|
2797
|
+
console.log(import_chalk4.default.green("\u2705 Stopped."));
|
|
2168
2798
|
} catch {
|
|
2169
|
-
console.log(
|
|
2799
|
+
console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
|
|
2170
2800
|
} finally {
|
|
2171
2801
|
try {
|
|
2172
2802
|
import_fs3.default.unlinkSync(DAEMON_PID_FILE);
|
|
@@ -2176,28 +2806,108 @@ function stopDaemon() {
|
|
|
2176
2806
|
}
|
|
2177
2807
|
function daemonStatus() {
|
|
2178
2808
|
if (!import_fs3.default.existsSync(DAEMON_PID_FILE))
|
|
2179
|
-
return console.log(
|
|
2809
|
+
return console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
|
|
2180
2810
|
try {
|
|
2181
2811
|
const { pid } = JSON.parse(import_fs3.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
2182
2812
|
process.kill(pid, 0);
|
|
2183
|
-
console.log(
|
|
2813
|
+
console.log(import_chalk4.default.green("Node9 daemon: running"));
|
|
2184
2814
|
} catch {
|
|
2185
|
-
console.log(
|
|
2815
|
+
console.log(import_chalk4.default.yellow("Node9 daemon: not running (stale PID)"));
|
|
2186
2816
|
}
|
|
2187
2817
|
}
|
|
2188
2818
|
|
|
2189
2819
|
// src/cli.ts
|
|
2190
|
-
var
|
|
2820
|
+
var import_child_process4 = require("child_process");
|
|
2191
2821
|
var import_execa = require("execa");
|
|
2192
2822
|
var import_execa2 = require("execa");
|
|
2193
|
-
var
|
|
2823
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
2194
2824
|
var import_readline = __toESM(require("readline"));
|
|
2825
|
+
var import_fs5 = __toESM(require("fs"));
|
|
2826
|
+
var import_path5 = __toESM(require("path"));
|
|
2827
|
+
var import_os5 = __toESM(require("os"));
|
|
2828
|
+
|
|
2829
|
+
// src/undo.ts
|
|
2830
|
+
var import_child_process3 = require("child_process");
|
|
2195
2831
|
var import_fs4 = __toESM(require("fs"));
|
|
2196
2832
|
var import_path4 = __toESM(require("path"));
|
|
2197
2833
|
var import_os4 = __toESM(require("os"));
|
|
2834
|
+
var UNDO_LATEST_PATH = import_path4.default.join(import_os4.default.homedir(), ".node9", "undo_latest.txt");
|
|
2835
|
+
async function createShadowSnapshot() {
|
|
2836
|
+
try {
|
|
2837
|
+
const cwd = process.cwd();
|
|
2838
|
+
if (!import_fs4.default.existsSync(import_path4.default.join(cwd, ".git"))) return null;
|
|
2839
|
+
const tempIndex = import_path4.default.join(cwd, ".git", `node9_index_${Date.now()}`);
|
|
2840
|
+
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
2841
|
+
(0, import_child_process3.spawnSync)("git", ["add", "-A"], { env });
|
|
2842
|
+
const treeRes = (0, import_child_process3.spawnSync)("git", ["write-tree"], { env });
|
|
2843
|
+
const treeHash = treeRes.stdout.toString().trim();
|
|
2844
|
+
if (import_fs4.default.existsSync(tempIndex)) import_fs4.default.unlinkSync(tempIndex);
|
|
2845
|
+
if (!treeHash || treeRes.status !== 0) return null;
|
|
2846
|
+
const commitRes = (0, import_child_process3.spawnSync)("git", [
|
|
2847
|
+
"commit-tree",
|
|
2848
|
+
treeHash,
|
|
2849
|
+
"-m",
|
|
2850
|
+
`Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
2851
|
+
]);
|
|
2852
|
+
const commitHash = commitRes.stdout.toString().trim();
|
|
2853
|
+
if (commitHash && commitRes.status === 0) {
|
|
2854
|
+
const dir = import_path4.default.dirname(UNDO_LATEST_PATH);
|
|
2855
|
+
if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
|
|
2856
|
+
import_fs4.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
2857
|
+
return commitHash;
|
|
2858
|
+
}
|
|
2859
|
+
} catch (err) {
|
|
2860
|
+
if (process.env.NODE9_DEBUG === "1") {
|
|
2861
|
+
console.error("[Node9 Undo Engine Error]:", err);
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
return null;
|
|
2865
|
+
}
|
|
2866
|
+
function applyUndo(hash) {
|
|
2867
|
+
try {
|
|
2868
|
+
const restore = (0, import_child_process3.spawnSync)("git", ["restore", "--source", hash, "--staged", "--worktree", "."]);
|
|
2869
|
+
if (restore.status !== 0) return false;
|
|
2870
|
+
const lsTree = (0, import_child_process3.spawnSync)("git", ["ls-tree", "-r", "--name-only", hash]);
|
|
2871
|
+
const snapshotFiles = new Set(lsTree.stdout.toString().trim().split("\n").filter(Boolean));
|
|
2872
|
+
const tracked = (0, import_child_process3.spawnSync)("git", ["ls-files"]).stdout.toString().trim().split("\n").filter(Boolean);
|
|
2873
|
+
const untracked = (0, import_child_process3.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"]).stdout.toString().trim().split("\n").filter(Boolean);
|
|
2874
|
+
for (const file of [...tracked, ...untracked]) {
|
|
2875
|
+
if (!snapshotFiles.has(file) && import_fs4.default.existsSync(file)) {
|
|
2876
|
+
import_fs4.default.unlinkSync(file);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
return true;
|
|
2880
|
+
} catch {
|
|
2881
|
+
return false;
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
function getLatestSnapshotHash() {
|
|
2885
|
+
if (!import_fs4.default.existsSync(UNDO_LATEST_PATH)) return null;
|
|
2886
|
+
return import_fs4.default.readFileSync(UNDO_LATEST_PATH, "utf-8").trim();
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
// src/cli.ts
|
|
2890
|
+
var import_prompts3 = require("@inquirer/prompts");
|
|
2198
2891
|
var { version } = JSON.parse(
|
|
2199
|
-
|
|
2892
|
+
import_fs5.default.readFileSync(import_path5.default.join(__dirname, "../package.json"), "utf-8")
|
|
2200
2893
|
);
|
|
2894
|
+
function parseDuration(str) {
|
|
2895
|
+
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
2896
|
+
if (!m) return null;
|
|
2897
|
+
const n = parseFloat(m[1]);
|
|
2898
|
+
switch ((m[2] ?? "m").toLowerCase()) {
|
|
2899
|
+
case "s":
|
|
2900
|
+
return Math.round(n * 1e3);
|
|
2901
|
+
case "m":
|
|
2902
|
+
return Math.round(n * 6e4);
|
|
2903
|
+
case "h":
|
|
2904
|
+
return Math.round(n * 36e5);
|
|
2905
|
+
case "d":
|
|
2906
|
+
return Math.round(n * 864e5);
|
|
2907
|
+
default:
|
|
2908
|
+
return null;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2201
2911
|
function sanitize(value) {
|
|
2202
2912
|
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
2203
2913
|
}
|
|
@@ -2205,23 +2915,33 @@ function openBrowserLocal() {
|
|
|
2205
2915
|
const url = `http://${DAEMON_HOST2}:${DAEMON_PORT2}/`;
|
|
2206
2916
|
try {
|
|
2207
2917
|
const opts = { stdio: "ignore" };
|
|
2208
|
-
if (process.platform === "darwin") (0,
|
|
2209
|
-
else if (process.platform === "win32") (0,
|
|
2210
|
-
else (0,
|
|
2918
|
+
if (process.platform === "darwin") (0, import_child_process4.execSync)(`open "${url}"`, opts);
|
|
2919
|
+
else if (process.platform === "win32") (0, import_child_process4.execSync)(`cmd /c start "" "${url}"`, opts);
|
|
2920
|
+
else (0, import_child_process4.execSync)(`xdg-open "${url}"`, opts);
|
|
2211
2921
|
} catch {
|
|
2212
2922
|
}
|
|
2213
2923
|
}
|
|
2214
2924
|
async function autoStartDaemonAndWait() {
|
|
2215
2925
|
try {
|
|
2216
|
-
const child = (0,
|
|
2926
|
+
const child = (0, import_child_process4.spawn)("node9", ["daemon"], {
|
|
2217
2927
|
detached: true,
|
|
2218
2928
|
stdio: "ignore",
|
|
2219
2929
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
2220
2930
|
});
|
|
2221
2931
|
child.unref();
|
|
2222
|
-
for (let i = 0; i <
|
|
2932
|
+
for (let i = 0; i < 20; i++) {
|
|
2223
2933
|
await new Promise((r) => setTimeout(r, 250));
|
|
2224
|
-
if (isDaemonRunning())
|
|
2934
|
+
if (!isDaemonRunning()) continue;
|
|
2935
|
+
try {
|
|
2936
|
+
const res = await fetch("http://127.0.0.1:7391/settings", {
|
|
2937
|
+
signal: AbortSignal.timeout(500)
|
|
2938
|
+
});
|
|
2939
|
+
if (res.ok) {
|
|
2940
|
+
openBrowserLocal();
|
|
2941
|
+
return true;
|
|
2942
|
+
}
|
|
2943
|
+
} catch {
|
|
2944
|
+
}
|
|
2225
2945
|
}
|
|
2226
2946
|
} catch {
|
|
2227
2947
|
}
|
|
@@ -2239,48 +2959,72 @@ async function runProxy(targetCommand) {
|
|
|
2239
2959
|
if (stdout) executable = stdout.trim();
|
|
2240
2960
|
} catch {
|
|
2241
2961
|
}
|
|
2242
|
-
console.log(
|
|
2243
|
-
const child = (0,
|
|
2962
|
+
console.log(import_chalk5.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
|
|
2963
|
+
const child = (0, import_child_process4.spawn)(executable, args, {
|
|
2244
2964
|
stdio: ["pipe", "pipe", "inherit"],
|
|
2965
|
+
// We control STDIN and STDOUT
|
|
2245
2966
|
shell: true,
|
|
2246
|
-
env: { ...process.env, FORCE_COLOR: "1"
|
|
2967
|
+
env: { ...process.env, FORCE_COLOR: "1" }
|
|
2247
2968
|
});
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2969
|
+
const agentIn = import_readline.default.createInterface({ input: process.stdin, terminal: false });
|
|
2970
|
+
agentIn.on("line", async (line) => {
|
|
2971
|
+
let message;
|
|
2251
2972
|
try {
|
|
2252
|
-
|
|
2253
|
-
|
|
2973
|
+
message = JSON.parse(line);
|
|
2974
|
+
} catch {
|
|
2975
|
+
child.stdin.write(line + "\n");
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
if (message.method === "call_tool" || message.method === "tools/call" || message.method === "use_tool") {
|
|
2979
|
+
agentIn.pause();
|
|
2980
|
+
try {
|
|
2254
2981
|
const name = message.params?.name || message.params?.tool_name || "unknown";
|
|
2255
2982
|
const toolArgs = message.params?.arguments || message.params?.tool_input || {};
|
|
2256
|
-
const
|
|
2257
|
-
|
|
2983
|
+
const result = await authorizeHeadless(sanitize(name), toolArgs, true, {
|
|
2984
|
+
agent: "Proxy/MCP"
|
|
2985
|
+
});
|
|
2986
|
+
if (!result.approved) {
|
|
2258
2987
|
const errorResponse = {
|
|
2259
2988
|
jsonrpc: "2.0",
|
|
2260
2989
|
id: message.id,
|
|
2261
|
-
error: {
|
|
2990
|
+
error: {
|
|
2991
|
+
code: -32e3,
|
|
2992
|
+
message: `Node9: Action denied. ${result.reason || ""}`
|
|
2993
|
+
}
|
|
2262
2994
|
};
|
|
2263
|
-
|
|
2995
|
+
process.stdout.write(JSON.stringify(errorResponse) + "\n");
|
|
2264
2996
|
return;
|
|
2265
2997
|
}
|
|
2998
|
+
} catch {
|
|
2999
|
+
const errorResponse = {
|
|
3000
|
+
jsonrpc: "2.0",
|
|
3001
|
+
id: message.id,
|
|
3002
|
+
error: {
|
|
3003
|
+
code: -32e3,
|
|
3004
|
+
message: `Node9: Security engine encountered an error. Action blocked for safety.`
|
|
3005
|
+
}
|
|
3006
|
+
};
|
|
3007
|
+
process.stdout.write(JSON.stringify(errorResponse) + "\n");
|
|
3008
|
+
return;
|
|
3009
|
+
} finally {
|
|
3010
|
+
agentIn.resume();
|
|
2266
3011
|
}
|
|
2267
|
-
process.stdout.write(line + "\n");
|
|
2268
|
-
} catch {
|
|
2269
|
-
process.stdout.write(line + "\n");
|
|
2270
3012
|
}
|
|
3013
|
+
child.stdin.write(line + "\n");
|
|
2271
3014
|
});
|
|
3015
|
+
child.stdout.pipe(process.stdout);
|
|
2272
3016
|
child.on("exit", (code) => process.exit(code || 0));
|
|
2273
3017
|
}
|
|
2274
3018
|
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) => {
|
|
2275
3019
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
2276
|
-
const credPath =
|
|
2277
|
-
if (!
|
|
2278
|
-
|
|
3020
|
+
const credPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "credentials.json");
|
|
3021
|
+
if (!import_fs5.default.existsSync(import_path5.default.dirname(credPath)))
|
|
3022
|
+
import_fs5.default.mkdirSync(import_path5.default.dirname(credPath), { recursive: true });
|
|
2279
3023
|
const profileName = options.profile || "default";
|
|
2280
3024
|
let existingCreds = {};
|
|
2281
3025
|
try {
|
|
2282
|
-
if (
|
|
2283
|
-
const raw = JSON.parse(
|
|
3026
|
+
if (import_fs5.default.existsSync(credPath)) {
|
|
3027
|
+
const raw = JSON.parse(import_fs5.default.readFileSync(credPath, "utf-8"));
|
|
2284
3028
|
if (raw.apiKey) {
|
|
2285
3029
|
existingCreds = {
|
|
2286
3030
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -2292,60 +3036,65 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
2292
3036
|
} catch {
|
|
2293
3037
|
}
|
|
2294
3038
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
2295
|
-
|
|
3039
|
+
import_fs5.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
2296
3040
|
if (profileName === "default") {
|
|
2297
|
-
const configPath =
|
|
3041
|
+
const configPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
2298
3042
|
let config = {};
|
|
2299
3043
|
try {
|
|
2300
|
-
if (
|
|
2301
|
-
config = JSON.parse(
|
|
3044
|
+
if (import_fs5.default.existsSync(configPath))
|
|
3045
|
+
config = JSON.parse(import_fs5.default.readFileSync(configPath, "utf-8"));
|
|
2302
3046
|
} catch {
|
|
2303
3047
|
}
|
|
2304
3048
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
3049
|
+
const s = config.settings;
|
|
3050
|
+
const approvers = s.approvers || {
|
|
3051
|
+
native: true,
|
|
3052
|
+
browser: true,
|
|
3053
|
+
cloud: true,
|
|
3054
|
+
terminal: true
|
|
3055
|
+
};
|
|
3056
|
+
approvers.cloud = !options.local;
|
|
3057
|
+
s.approvers = approvers;
|
|
3058
|
+
if (!import_fs5.default.existsSync(import_path5.default.dirname(configPath)))
|
|
3059
|
+
import_fs5.default.mkdirSync(import_path5.default.dirname(configPath), { recursive: true });
|
|
3060
|
+
import_fs5.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
2309
3061
|
}
|
|
2310
3062
|
if (options.profile && profileName !== "default") {
|
|
2311
|
-
console.log(
|
|
2312
|
-
console.log(
|
|
2313
|
-
console.log(
|
|
2314
|
-
import_chalk4.default.gray(
|
|
2315
|
-
` Or lock a project to it: add "apiKey": "<your-api-key>" to node9.config.json`
|
|
2316
|
-
)
|
|
2317
|
-
);
|
|
3063
|
+
console.log(import_chalk5.default.green(`\u2705 Profile "${profileName}" saved`));
|
|
3064
|
+
console.log(import_chalk5.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
2318
3065
|
} else if (options.local) {
|
|
2319
|
-
console.log(
|
|
2320
|
-
console.log(
|
|
2321
|
-
console.log(
|
|
2322
|
-
import_chalk4.default.gray(` No data is sent to the cloud. Local config is the only authority.`)
|
|
2323
|
-
);
|
|
2324
|
-
console.log(import_chalk4.default.gray(` To enable cloud enforcement: node9 login <apiKey>`));
|
|
3066
|
+
console.log(import_chalk5.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
3067
|
+
console.log(import_chalk5.default.gray(` All decisions stay on this machine.`));
|
|
2325
3068
|
} else {
|
|
2326
|
-
console.log(
|
|
2327
|
-
console.log(
|
|
2328
|
-
console.log(import_chalk4.default.gray(` To keep local control only: node9 login <apiKey> --local`));
|
|
3069
|
+
console.log(import_chalk5.default.green(`\u2705 Logged in \u2014 agent mode`));
|
|
3070
|
+
console.log(import_chalk5.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
2329
3071
|
}
|
|
2330
3072
|
});
|
|
2331
3073
|
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) => {
|
|
2332
3074
|
if (target === "gemini") return await setupGemini();
|
|
2333
3075
|
if (target === "claude") return await setupClaude();
|
|
2334
3076
|
if (target === "cursor") return await setupCursor();
|
|
2335
|
-
console.error(
|
|
3077
|
+
console.error(import_chalk5.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
2336
3078
|
process.exit(1);
|
|
2337
3079
|
});
|
|
2338
3080
|
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").action((options) => {
|
|
2339
|
-
const configPath =
|
|
2340
|
-
if (
|
|
2341
|
-
console.log(
|
|
2342
|
-
console.log(
|
|
3081
|
+
const configPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
3082
|
+
if (import_fs5.default.existsSync(configPath) && !options.force) {
|
|
3083
|
+
console.log(import_chalk5.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
3084
|
+
console.log(import_chalk5.default.gray(` Run with --force to overwrite.`));
|
|
2343
3085
|
return;
|
|
2344
3086
|
}
|
|
2345
3087
|
const defaultConfig = {
|
|
2346
3088
|
version: "1.0",
|
|
2347
|
-
settings: {
|
|
3089
|
+
settings: {
|
|
3090
|
+
mode: "standard",
|
|
3091
|
+
autoStartDaemon: true,
|
|
3092
|
+
enableUndo: true,
|
|
3093
|
+
enableHookLogDebug: false,
|
|
3094
|
+
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
3095
|
+
},
|
|
2348
3096
|
policy: {
|
|
3097
|
+
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
2349
3098
|
dangerousWords: DANGEROUS_WORDS,
|
|
2350
3099
|
ignoredTools: [
|
|
2351
3100
|
"list_*",
|
|
@@ -2355,161 +3104,136 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
2355
3104
|
"read",
|
|
2356
3105
|
"write",
|
|
2357
3106
|
"edit",
|
|
2358
|
-
"multiedit",
|
|
2359
3107
|
"glob",
|
|
2360
3108
|
"grep",
|
|
2361
3109
|
"ls",
|
|
2362
3110
|
"notebookread",
|
|
2363
3111
|
"notebookedit",
|
|
2364
|
-
"todoread",
|
|
2365
|
-
"todowrite",
|
|
2366
3112
|
"webfetch",
|
|
2367
3113
|
"websearch",
|
|
2368
3114
|
"exitplanmode",
|
|
2369
|
-
"askuserquestion"
|
|
3115
|
+
"askuserquestion",
|
|
3116
|
+
"agent",
|
|
3117
|
+
"task*"
|
|
2370
3118
|
],
|
|
2371
3119
|
toolInspection: {
|
|
2372
3120
|
bash: "command",
|
|
2373
3121
|
shell: "command",
|
|
2374
3122
|
run_shell_command: "command",
|
|
2375
|
-
"terminal.execute": "command"
|
|
3123
|
+
"terminal.execute": "command",
|
|
3124
|
+
"postgres:query": "sql"
|
|
2376
3125
|
},
|
|
2377
3126
|
rules: [
|
|
2378
3127
|
{
|
|
2379
3128
|
action: "rm",
|
|
2380
|
-
allowPaths: [
|
|
3129
|
+
allowPaths: [
|
|
3130
|
+
"**/node_modules/**",
|
|
3131
|
+
"dist/**",
|
|
3132
|
+
"build/**",
|
|
3133
|
+
".next/**",
|
|
3134
|
+
"coverage/**",
|
|
3135
|
+
".cache/**",
|
|
3136
|
+
"tmp/**",
|
|
3137
|
+
"temp/**",
|
|
3138
|
+
".DS_Store"
|
|
3139
|
+
]
|
|
2381
3140
|
}
|
|
2382
3141
|
]
|
|
2383
3142
|
}
|
|
2384
3143
|
};
|
|
2385
|
-
if (!
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
console.log(
|
|
2389
|
-
console.log(
|
|
3144
|
+
if (!import_fs5.default.existsSync(import_path5.default.dirname(configPath)))
|
|
3145
|
+
import_fs5.default.mkdirSync(import_path5.default.dirname(configPath), { recursive: true });
|
|
3146
|
+
import_fs5.default.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
|
|
3147
|
+
console.log(import_chalk5.default.green(`\u2705 Global config created: ${configPath}`));
|
|
3148
|
+
console.log(import_chalk5.default.gray(` Edit this file to add custom tool inspection or security rules.`));
|
|
2390
3149
|
});
|
|
2391
3150
|
program.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
|
|
2392
3151
|
const creds = getCredentials();
|
|
2393
3152
|
const daemonRunning = isDaemonRunning();
|
|
2394
|
-
const
|
|
3153
|
+
const mergedConfig = getConfig();
|
|
3154
|
+
const settings = mergedConfig.settings;
|
|
2395
3155
|
console.log("");
|
|
2396
|
-
if (creds && settings.
|
|
2397
|
-
console.log(
|
|
2398
|
-
|
|
2399
|
-
console.log(import_chalk4.default.gray(" Switch to local control: node9 login <apiKey> --local"));
|
|
2400
|
-
} else if (creds && !settings.agentMode) {
|
|
3156
|
+
if (creds && settings.approvers.cloud) {
|
|
3157
|
+
console.log(import_chalk5.default.green(" \u25CF Agent mode") + import_chalk5.default.gray(" \u2014 cloud team policy enforced"));
|
|
3158
|
+
} else if (creds && !settings.approvers.cloud) {
|
|
2401
3159
|
console.log(
|
|
2402
|
-
|
|
3160
|
+
import_chalk5.default.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + import_chalk5.default.gray(" \u2014 all decisions stay on this machine")
|
|
2403
3161
|
);
|
|
3162
|
+
} else {
|
|
2404
3163
|
console.log(
|
|
2405
|
-
|
|
3164
|
+
import_chalk5.default.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + import_chalk5.default.gray(" \u2014 no API key (Local rules only)")
|
|
2406
3165
|
);
|
|
2407
|
-
console.log(import_chalk4.default.gray(" Enable cloud enforcement: node9 login <apiKey>"));
|
|
2408
|
-
} else {
|
|
2409
|
-
console.log(import_chalk4.default.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + import_chalk4.default.gray(" \u2014 no API key"));
|
|
2410
|
-
console.log(import_chalk4.default.gray(" All decisions stay on this machine."));
|
|
2411
|
-
console.log(import_chalk4.default.gray(" Connect to your team: node9 login <apiKey>"));
|
|
2412
3166
|
}
|
|
2413
3167
|
console.log("");
|
|
2414
3168
|
if (daemonRunning) {
|
|
2415
3169
|
console.log(
|
|
2416
|
-
|
|
3170
|
+
import_chalk5.default.green(" \u25CF Daemon running") + import_chalk5.default.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
|
|
2417
3171
|
);
|
|
2418
3172
|
} else {
|
|
2419
|
-
console.log(
|
|
2420
|
-
console.log(import_chalk4.default.gray(" Start: node9 daemon --background"));
|
|
3173
|
+
console.log(import_chalk5.default.gray(" \u25CB Daemon stopped"));
|
|
2421
3174
|
}
|
|
2422
|
-
|
|
2423
|
-
console.log(` Mode: ${import_chalk4.default.white(settings.mode)}`);
|
|
2424
|
-
const projectConfig = import_path4.default.join(process.cwd(), "node9.config.json");
|
|
2425
|
-
const globalConfig = import_path4.default.join(import_os4.default.homedir(), ".node9", "config.json");
|
|
2426
|
-
const configSource = import_fs4.default.existsSync(projectConfig) ? projectConfig : import_fs4.default.existsSync(globalConfig) ? globalConfig : import_chalk4.default.gray("none (built-in defaults)");
|
|
2427
|
-
console.log(` Config: ${import_chalk4.default.gray(configSource)}`);
|
|
2428
|
-
const profiles = listCredentialProfiles();
|
|
2429
|
-
if (profiles.length > 1) {
|
|
2430
|
-
const activeProfile = process.env.NODE9_PROFILE || "default";
|
|
2431
|
-
console.log("");
|
|
2432
|
-
console.log(` Active profile: ${import_chalk4.default.white(activeProfile)}`);
|
|
3175
|
+
if (settings.enableUndo) {
|
|
2433
3176
|
console.log(
|
|
2434
|
-
|
|
3177
|
+
import_chalk5.default.magenta(" \u25CF Undo Engine") + import_chalk5.default.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
|
|
2435
3178
|
);
|
|
2436
|
-
console.log(import_chalk4.default.gray(` Switch: NODE9_PROFILE=<name> claude`));
|
|
2437
|
-
}
|
|
2438
|
-
const decisionsFile = import_path4.default.join(import_os4.default.homedir(), ".node9", "decisions.json");
|
|
2439
|
-
let decisions = {};
|
|
2440
|
-
try {
|
|
2441
|
-
if (import_fs4.default.existsSync(decisionsFile))
|
|
2442
|
-
decisions = JSON.parse(import_fs4.default.readFileSync(decisionsFile, "utf-8"));
|
|
2443
|
-
} catch {
|
|
2444
3179
|
}
|
|
2445
|
-
const keys = Object.keys(decisions);
|
|
2446
3180
|
console.log("");
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
3181
|
+
const modeLabel = settings.mode === "audit" ? import_chalk5.default.blue("audit") : settings.mode === "strict" ? import_chalk5.default.red("strict") : import_chalk5.default.white("standard");
|
|
3182
|
+
console.log(` Mode: ${modeLabel}`);
|
|
3183
|
+
const projectConfig = import_path5.default.join(process.cwd(), "node9.config.json");
|
|
3184
|
+
const globalConfig = import_path5.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
3185
|
+
console.log(
|
|
3186
|
+
` Local: ${import_fs5.default.existsSync(projectConfig) ? import_chalk5.default.green("Active (node9.config.json)") : import_chalk5.default.gray("Not present")}`
|
|
3187
|
+
);
|
|
3188
|
+
console.log(
|
|
3189
|
+
` Global: ${import_fs5.default.existsSync(globalConfig) ? import_chalk5.default.green("Active (~/.node9/config.json)") : import_chalk5.default.gray("Not present")}`
|
|
3190
|
+
);
|
|
3191
|
+
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
3192
|
+
console.log(
|
|
3193
|
+
` Sandbox: ${import_chalk5.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
3194
|
+
);
|
|
2457
3195
|
}
|
|
2458
|
-
const
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
);
|
|
2466
|
-
}
|
|
2467
|
-
} catch {
|
|
3196
|
+
const pauseState = checkPause();
|
|
3197
|
+
if (pauseState.paused) {
|
|
3198
|
+
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
3199
|
+
console.log("");
|
|
3200
|
+
console.log(
|
|
3201
|
+
import_chalk5.default.yellow(` \u23F8 PAUSED until ${expiresAt}`) + import_chalk5.default.gray(" \u2014 all tool calls allowed")
|
|
3202
|
+
);
|
|
2468
3203
|
}
|
|
2469
3204
|
console.log("");
|
|
2470
3205
|
});
|
|
2471
|
-
program.command("daemon").description("Run the local approval server (
|
|
2472
|
-
"after",
|
|
2473
|
-
"\n Subcommands: start (default), stop, status\n Options:\n --background (-b) start detached, no second terminal needed\n --openui (-o) start in background and open the browser (or just open if already running)\n Example: node9 daemon --background"
|
|
2474
|
-
).argument("[action]", "start | stop | status (default: start)").option("-b, --background", "Start the daemon in the background (detached)").option(
|
|
2475
|
-
"-o, --openui",
|
|
2476
|
-
"Start in background and open browser (or just open browser if already running)"
|
|
2477
|
-
).action(
|
|
3206
|
+
program.command("daemon").description("Run the local approval server").argument("[action]", "start | stop | status (default: start)").option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").action(
|
|
2478
3207
|
async (action, options) => {
|
|
2479
3208
|
const cmd = (action ?? "start").toLowerCase();
|
|
2480
3209
|
if (cmd === "stop") return stopDaemon();
|
|
2481
3210
|
if (cmd === "status") return daemonStatus();
|
|
2482
3211
|
if (cmd !== "start" && action !== void 0) {
|
|
2483
|
-
console.error(
|
|
3212
|
+
console.error(import_chalk5.default.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
|
|
2484
3213
|
process.exit(1);
|
|
2485
3214
|
}
|
|
2486
3215
|
if (options.openui) {
|
|
2487
3216
|
if (isDaemonRunning()) {
|
|
2488
3217
|
openBrowserLocal();
|
|
2489
|
-
console.log(
|
|
3218
|
+
console.log(import_chalk5.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
2490
3219
|
process.exit(0);
|
|
2491
3220
|
}
|
|
2492
|
-
const child = (0,
|
|
3221
|
+
const child = (0, import_child_process4.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
2493
3222
|
child.unref();
|
|
2494
3223
|
for (let i = 0; i < 12; i++) {
|
|
2495
3224
|
await new Promise((r) => setTimeout(r, 250));
|
|
2496
3225
|
if (isDaemonRunning()) break;
|
|
2497
3226
|
}
|
|
2498
3227
|
openBrowserLocal();
|
|
2499
|
-
console.log(
|
|
3228
|
+
console.log(import_chalk5.default.green(`
|
|
2500
3229
|
\u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
|
|
2501
|
-
console.log(import_chalk4.default.gray(` http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
2502
3230
|
process.exit(0);
|
|
2503
3231
|
}
|
|
2504
3232
|
if (options.background) {
|
|
2505
|
-
const child = (0,
|
|
3233
|
+
const child = (0, import_child_process4.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
2506
3234
|
child.unref();
|
|
2507
|
-
console.log(
|
|
3235
|
+
console.log(import_chalk5.default.green(`
|
|
2508
3236
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
2509
|
-
console.log(import_chalk4.default.gray(` http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
2510
|
-
console.log(import_chalk4.default.gray(` node9 daemon status \u2014 check if running`));
|
|
2511
|
-
console.log(import_chalk4.default.gray(` node9 daemon stop \u2014 stop it
|
|
2512
|
-
`));
|
|
2513
3237
|
process.exit(0);
|
|
2514
3238
|
}
|
|
2515
3239
|
startDaemon();
|
|
@@ -2519,53 +3243,81 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
2519
3243
|
const processPayload = async (raw) => {
|
|
2520
3244
|
try {
|
|
2521
3245
|
if (!raw || raw.trim() === "") process.exit(0);
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
3246
|
+
let payload = JSON.parse(raw);
|
|
3247
|
+
try {
|
|
3248
|
+
payload = JSON.parse(raw);
|
|
3249
|
+
} catch (err) {
|
|
3250
|
+
const tempConfig = getConfig();
|
|
3251
|
+
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
3252
|
+
const logPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
3253
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3254
|
+
import_fs5.default.appendFileSync(
|
|
3255
|
+
logPath,
|
|
3256
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
3257
|
+
RAW: ${raw}
|
|
2531
3258
|
`
|
|
2532
|
-
|
|
3259
|
+
);
|
|
3260
|
+
}
|
|
3261
|
+
process.exit(0);
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
if (payload.cwd) {
|
|
3265
|
+
try {
|
|
3266
|
+
process.chdir(payload.cwd);
|
|
3267
|
+
_resetConfigCache();
|
|
3268
|
+
} catch {
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
const config = getConfig();
|
|
3272
|
+
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
3273
|
+
const logPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
3274
|
+
if (!import_fs5.default.existsSync(import_path5.default.dirname(logPath)))
|
|
3275
|
+
import_fs5.default.mkdirSync(import_path5.default.dirname(logPath), { recursive: true });
|
|
3276
|
+
import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
3277
|
+
`);
|
|
2533
3278
|
}
|
|
2534
|
-
const payload = JSON.parse(raw);
|
|
2535
3279
|
const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
|
|
2536
3280
|
const toolInput = payload.tool_input ?? payload.args ?? {};
|
|
2537
|
-
const agent = payload.
|
|
3281
|
+
const agent = payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0 ? "Claude Code" : payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0 ? "Gemini CLI" : payload.tool_name !== void 0 || payload.name !== void 0 ? "Unknown Agent" : "Terminal";
|
|
2538
3282
|
const mcpMatch = toolName.match(/^mcp__([^_](?:[^_]|_(?!_))*?)__/i);
|
|
2539
3283
|
const mcpServer = mcpMatch?.[1];
|
|
2540
3284
|
const sendBlock = (msg, result2) => {
|
|
2541
|
-
const
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
"local-config": "your local config (dangerousWords / rules)",
|
|
2545
|
-
"local-decision": "you denied it in the browser",
|
|
2546
|
-
"no-approval-mechanism": "no approval method is configured"
|
|
2547
|
-
};
|
|
2548
|
-
console.error(import_chalk4.default.red(`
|
|
3285
|
+
const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
|
|
3286
|
+
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
3287
|
+
console.error(import_chalk5.default.red(`
|
|
2549
3288
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
import_chalk4.default.gray(
|
|
2553
|
-
` Blocked by: ${BLOCKED_BY_LABELS[result2.blockedBy] ?? result2.blockedBy}`
|
|
2554
|
-
)
|
|
2555
|
-
);
|
|
2556
|
-
}
|
|
2557
|
-
if (result2?.changeHint) {
|
|
2558
|
-
console.error(import_chalk4.default.cyan(` To change: ${result2.changeHint}`));
|
|
2559
|
-
}
|
|
3289
|
+
console.error(import_chalk5.default.gray(` Triggered by: ${blockedByContext}`));
|
|
3290
|
+
if (result2?.changeHint) console.error(import_chalk5.default.cyan(` To change: ${result2.changeHint}`));
|
|
2560
3291
|
console.error("");
|
|
3292
|
+
let aiFeedbackMessage = "";
|
|
3293
|
+
if (isHumanDecision) {
|
|
3294
|
+
aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: The human user specifically REJECTED this action.
|
|
3295
|
+
REASON: ${msg || "No specific reason provided by user."}
|
|
3296
|
+
|
|
3297
|
+
INSTRUCTIONS FOR AI AGENT:
|
|
3298
|
+
- Do NOT retry this exact command immediately.
|
|
3299
|
+
- Explain to the user that you understand they blocked the action.
|
|
3300
|
+
- Ask the user if there is an alternative approach they would prefer, or if they intended to block this action entirely.
|
|
3301
|
+
- If you believe this action is critical, explain your reasoning to the user and ask them to run 'node9 pause 15m' to allow you to proceed.`;
|
|
3302
|
+
} else {
|
|
3303
|
+
aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: Action blocked by automated policy [${blockedByContext}].
|
|
3304
|
+
REASON: ${msg}
|
|
3305
|
+
|
|
3306
|
+
INSTRUCTIONS FOR AI AGENT:
|
|
3307
|
+
- This command violates the current security configuration.
|
|
3308
|
+
- Do NOT attempt to bypass this rule with bash syntax tricks; it will be blocked again.
|
|
3309
|
+
- Pivot to a non-destructive or read-only alternative.
|
|
3310
|
+
- Inform the user which security rule was triggered.`;
|
|
3311
|
+
}
|
|
2561
3312
|
process.stdout.write(
|
|
2562
3313
|
JSON.stringify({
|
|
2563
3314
|
decision: "block",
|
|
2564
|
-
reason:
|
|
3315
|
+
reason: aiFeedbackMessage,
|
|
3316
|
+
// This is the core instruction
|
|
2565
3317
|
hookSpecificOutput: {
|
|
2566
3318
|
hookEventName: "PreToolUse",
|
|
2567
3319
|
permissionDecision: "deny",
|
|
2568
|
-
permissionDecisionReason:
|
|
3320
|
+
permissionDecisionReason: aiFeedbackMessage
|
|
2569
3321
|
}
|
|
2570
3322
|
}) + "\n"
|
|
2571
3323
|
);
|
|
@@ -2576,36 +3328,53 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
2576
3328
|
return;
|
|
2577
3329
|
}
|
|
2578
3330
|
const meta = { agent, mcpServer };
|
|
3331
|
+
const STATE_CHANGING_TOOLS_PRE = [
|
|
3332
|
+
"bash",
|
|
3333
|
+
"shell",
|
|
3334
|
+
"write_file",
|
|
3335
|
+
"edit_file",
|
|
3336
|
+
"replace",
|
|
3337
|
+
"terminal.execute",
|
|
3338
|
+
"str_replace_based_edit_tool",
|
|
3339
|
+
"create_file"
|
|
3340
|
+
];
|
|
3341
|
+
if (config.settings.enableUndo && STATE_CHANGING_TOOLS_PRE.includes(toolName.toLowerCase())) {
|
|
3342
|
+
await createShadowSnapshot();
|
|
3343
|
+
}
|
|
2579
3344
|
const result = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
2580
3345
|
if (result.approved) {
|
|
2581
|
-
if (result.checkedBy)
|
|
3346
|
+
if (result.checkedBy)
|
|
2582
3347
|
process.stderr.write(`\u2713 node9 [${result.checkedBy}]: "${toolName}" allowed
|
|
2583
3348
|
`);
|
|
2584
|
-
}
|
|
2585
3349
|
process.exit(0);
|
|
2586
3350
|
}
|
|
2587
|
-
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY &&
|
|
2588
|
-
console.error(
|
|
3351
|
+
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
3352
|
+
console.error(import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
2589
3353
|
const daemonReady = await autoStartDaemonAndWait();
|
|
2590
3354
|
if (daemonReady) {
|
|
2591
3355
|
const retry = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
2592
3356
|
if (retry.approved) {
|
|
2593
|
-
if (retry.checkedBy)
|
|
3357
|
+
if (retry.checkedBy)
|
|
2594
3358
|
process.stderr.write(`\u2713 node9 [${retry.checkedBy}]: "${toolName}" allowed
|
|
2595
3359
|
`);
|
|
2596
|
-
}
|
|
2597
3360
|
process.exit(0);
|
|
2598
3361
|
}
|
|
2599
|
-
sendBlock(retry.reason ?? `Node9 blocked "${toolName}".`,
|
|
3362
|
+
sendBlock(retry.reason ?? `Node9 blocked "${toolName}".`, {
|
|
3363
|
+
...retry,
|
|
3364
|
+
blockedByLabel: retry.blockedByLabel
|
|
3365
|
+
});
|
|
2600
3366
|
return;
|
|
2601
3367
|
}
|
|
2602
3368
|
}
|
|
2603
|
-
sendBlock(result.reason ?? `Node9 blocked "${toolName}".`,
|
|
3369
|
+
sendBlock(result.reason ?? `Node9 blocked "${toolName}".`, {
|
|
3370
|
+
...result,
|
|
3371
|
+
blockedByLabel: result.blockedByLabel
|
|
3372
|
+
});
|
|
2604
3373
|
} catch (err) {
|
|
2605
3374
|
if (process.env.NODE9_DEBUG === "1") {
|
|
2606
|
-
const logPath =
|
|
3375
|
+
const logPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
2607
3376
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2608
|
-
|
|
3377
|
+
import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
2609
3378
|
`);
|
|
2610
3379
|
}
|
|
2611
3380
|
process.exit(0);
|
|
@@ -2616,48 +3385,101 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
2616
3385
|
} else {
|
|
2617
3386
|
let raw = "";
|
|
2618
3387
|
let processed = false;
|
|
3388
|
+
let inactivityTimer = null;
|
|
2619
3389
|
const done = async () => {
|
|
2620
3390
|
if (processed) return;
|
|
2621
3391
|
processed = true;
|
|
3392
|
+
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
2622
3393
|
if (!raw.trim()) return process.exit(0);
|
|
2623
3394
|
await processPayload(raw);
|
|
2624
3395
|
};
|
|
2625
3396
|
process.stdin.setEncoding("utf-8");
|
|
2626
|
-
process.stdin.on("data", (chunk) =>
|
|
2627
|
-
|
|
2628
|
-
|
|
3397
|
+
process.stdin.on("data", (chunk) => {
|
|
3398
|
+
raw += chunk;
|
|
3399
|
+
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
3400
|
+
inactivityTimer = setTimeout(() => void done(), 2e3);
|
|
3401
|
+
});
|
|
3402
|
+
process.stdin.on("end", () => {
|
|
3403
|
+
void done();
|
|
3404
|
+
});
|
|
3405
|
+
inactivityTimer = setTimeout(() => void done(), 5e3);
|
|
2629
3406
|
}
|
|
2630
3407
|
});
|
|
2631
3408
|
program.command("log").description("PostToolUse hook \u2014 records executed tool calls").argument("[data]", "JSON string of the tool call").action(async (data) => {
|
|
2632
|
-
const logPayload = (raw) => {
|
|
3409
|
+
const logPayload = async (raw) => {
|
|
2633
3410
|
try {
|
|
2634
3411
|
if (!raw || raw.trim() === "") process.exit(0);
|
|
2635
3412
|
const payload = JSON.parse(raw);
|
|
3413
|
+
const tool = sanitize(payload.tool_name ?? payload.name ?? "unknown");
|
|
3414
|
+
const rawInput = payload.tool_input ?? payload.args ?? {};
|
|
2636
3415
|
const entry = {
|
|
2637
3416
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2638
|
-
tool
|
|
2639
|
-
|
|
3417
|
+
tool,
|
|
3418
|
+
args: JSON.parse(redactSecrets(JSON.stringify(rawInput))),
|
|
3419
|
+
decision: "allowed",
|
|
3420
|
+
source: "post-hook"
|
|
2640
3421
|
};
|
|
2641
|
-
const logPath =
|
|
2642
|
-
if (!
|
|
2643
|
-
|
|
2644
|
-
|
|
3422
|
+
const logPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "audit.log");
|
|
3423
|
+
if (!import_fs5.default.existsSync(import_path5.default.dirname(logPath)))
|
|
3424
|
+
import_fs5.default.mkdirSync(import_path5.default.dirname(logPath), { recursive: true });
|
|
3425
|
+
import_fs5.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
3426
|
+
const config = getConfig();
|
|
3427
|
+
const STATE_CHANGING_TOOLS = [
|
|
3428
|
+
"bash",
|
|
3429
|
+
"shell",
|
|
3430
|
+
"write_file",
|
|
3431
|
+
"edit_file",
|
|
3432
|
+
"replace",
|
|
3433
|
+
"terminal.execute"
|
|
3434
|
+
];
|
|
3435
|
+
if (config.settings.enableUndo && STATE_CHANGING_TOOLS.includes(tool.toLowerCase())) {
|
|
3436
|
+
await createShadowSnapshot();
|
|
3437
|
+
}
|
|
2645
3438
|
} catch {
|
|
2646
3439
|
}
|
|
2647
3440
|
process.exit(0);
|
|
2648
3441
|
};
|
|
2649
3442
|
if (data) {
|
|
2650
|
-
logPayload(data);
|
|
3443
|
+
await logPayload(data);
|
|
2651
3444
|
} else {
|
|
2652
3445
|
let raw = "";
|
|
2653
3446
|
process.stdin.setEncoding("utf-8");
|
|
2654
3447
|
process.stdin.on("data", (chunk) => raw += chunk);
|
|
2655
|
-
process.stdin.on("end", () =>
|
|
3448
|
+
process.stdin.on("end", () => {
|
|
3449
|
+
void logPayload(raw);
|
|
3450
|
+
});
|
|
2656
3451
|
setTimeout(() => {
|
|
2657
3452
|
if (!raw) process.exit(0);
|
|
2658
3453
|
}, 500);
|
|
2659
3454
|
}
|
|
2660
3455
|
});
|
|
3456
|
+
program.command("pause").description("Temporarily disable Node9 protection for a set duration").option("-d, --duration <duration>", "How long to pause (e.g. 15m, 1h, 30s)", "15m").action((options) => {
|
|
3457
|
+
const ms = parseDuration(options.duration);
|
|
3458
|
+
if (ms === null) {
|
|
3459
|
+
console.error(
|
|
3460
|
+
import_chalk5.default.red(`
|
|
3461
|
+
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
3462
|
+
`)
|
|
3463
|
+
);
|
|
3464
|
+
process.exit(1);
|
|
3465
|
+
}
|
|
3466
|
+
pauseNode9(ms, options.duration);
|
|
3467
|
+
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
3468
|
+
console.log(import_chalk5.default.yellow(`
|
|
3469
|
+
\u23F8 Node9 paused until ${expiresAt}`));
|
|
3470
|
+
console.log(import_chalk5.default.gray(` All tool calls will be allowed without review.`));
|
|
3471
|
+
console.log(import_chalk5.default.gray(` Run "node9 resume" to re-enable early.
|
|
3472
|
+
`));
|
|
3473
|
+
});
|
|
3474
|
+
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
3475
|
+
const { paused } = checkPause();
|
|
3476
|
+
if (!paused) {
|
|
3477
|
+
console.log(import_chalk5.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
3478
|
+
return;
|
|
3479
|
+
}
|
|
3480
|
+
resumeNode9();
|
|
3481
|
+
console.log(import_chalk5.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
3482
|
+
});
|
|
2661
3483
|
var HOOK_BASED_AGENTS = {
|
|
2662
3484
|
claude: "claude",
|
|
2663
3485
|
gemini: "gemini",
|
|
@@ -2669,39 +3491,21 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
2669
3491
|
if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
|
|
2670
3492
|
const target = HOOK_BASED_AGENTS[firstArg];
|
|
2671
3493
|
console.error(
|
|
2672
|
-
|
|
3494
|
+
import_chalk5.default.yellow(`
|
|
2673
3495
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
2674
3496
|
);
|
|
3497
|
+
console.error(import_chalk5.default.white(`
|
|
3498
|
+
"${target}" uses its own hook system. Use:`));
|
|
2675
3499
|
console.error(
|
|
2676
|
-
|
|
2677
|
-
"${target}" is an interactive terminal app \u2014 it needs a real`)
|
|
2678
|
-
);
|
|
2679
|
-
console.error(
|
|
2680
|
-
import_chalk4.default.white(` TTY and communicates via its own hook system, not JSON-RPC.
|
|
2681
|
-
`)
|
|
2682
|
-
);
|
|
2683
|
-
console.error(import_chalk4.default.bold(` Use the hook-based integration instead:
|
|
2684
|
-
`));
|
|
2685
|
-
console.error(
|
|
2686
|
-
import_chalk4.default.green(` node9 addto ${target} `) + import_chalk4.default.gray("# one-time setup")
|
|
2687
|
-
);
|
|
2688
|
-
console.error(
|
|
2689
|
-
import_chalk4.default.green(` ${target} `) + import_chalk4.default.gray("# run normally \u2014 Node9 hooks fire automatically")
|
|
2690
|
-
);
|
|
2691
|
-
console.error(import_chalk4.default.white(`
|
|
2692
|
-
For browser approval popups (no API key required):`));
|
|
2693
|
-
console.error(
|
|
2694
|
-
import_chalk4.default.green(` node9 daemon --background`) + import_chalk4.default.gray("# start (no second terminal needed)")
|
|
2695
|
-
);
|
|
2696
|
-
console.error(
|
|
2697
|
-
import_chalk4.default.green(` ${target} `) + import_chalk4.default.gray("# Node9 will open browser on dangerous actions\n")
|
|
3500
|
+
import_chalk5.default.green(` node9 addto ${target} `) + import_chalk5.default.gray("# one-time setup")
|
|
2698
3501
|
);
|
|
3502
|
+
console.error(import_chalk5.default.green(` ${target} `) + import_chalk5.default.gray("# run normally"));
|
|
2699
3503
|
process.exit(1);
|
|
2700
3504
|
}
|
|
2701
3505
|
const fullCommand = commandArgs.join(" ");
|
|
2702
3506
|
let result = await authorizeHeadless("shell", { command: fullCommand });
|
|
2703
|
-
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON &&
|
|
2704
|
-
console.error(
|
|
3507
|
+
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
3508
|
+
console.error(import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
2705
3509
|
const daemonReady = await autoStartDaemonAndWait();
|
|
2706
3510
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
2707
3511
|
}
|
|
@@ -2710,39 +3514,44 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
2710
3514
|
}
|
|
2711
3515
|
if (!result.approved) {
|
|
2712
3516
|
console.error(
|
|
2713
|
-
|
|
3517
|
+
import_chalk5.default.red(`
|
|
2714
3518
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
2715
3519
|
);
|
|
2716
|
-
if (result.blockedBy) {
|
|
2717
|
-
const BLOCKED_BY_LABELS = {
|
|
2718
|
-
"team-policy": "Team policy (Node9 cloud)",
|
|
2719
|
-
"persistent-deny": "Persistent deny rule",
|
|
2720
|
-
"local-config": "Local config",
|
|
2721
|
-
"local-decision": "Browser UI decision",
|
|
2722
|
-
"no-approval-mechanism": "No approval mechanism available"
|
|
2723
|
-
};
|
|
2724
|
-
console.error(
|
|
2725
|
-
import_chalk4.default.gray(` Blocked by: ${BLOCKED_BY_LABELS[result.blockedBy] ?? result.blockedBy}`)
|
|
2726
|
-
);
|
|
2727
|
-
}
|
|
2728
|
-
if (result.changeHint) {
|
|
2729
|
-
console.error(import_chalk4.default.cyan(` To change: ${result.changeHint}`));
|
|
2730
|
-
}
|
|
2731
3520
|
process.exit(1);
|
|
2732
3521
|
}
|
|
2733
|
-
console.error(
|
|
3522
|
+
console.error(import_chalk5.default.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
2734
3523
|
await runProxy(fullCommand);
|
|
2735
3524
|
} else {
|
|
2736
3525
|
program.help();
|
|
2737
3526
|
}
|
|
2738
3527
|
});
|
|
3528
|
+
program.command("undo").description("Revert the project to the state before the last AI action").action(async () => {
|
|
3529
|
+
const hash = getLatestSnapshotHash();
|
|
3530
|
+
if (!hash) {
|
|
3531
|
+
console.log(import_chalk5.default.yellow("\n\u2139\uFE0F No Undo snapshot found for this machine.\n"));
|
|
3532
|
+
return;
|
|
3533
|
+
}
|
|
3534
|
+
console.log(import_chalk5.default.magenta.bold("\n\u23EA NODE9 UNDO ENGINE"));
|
|
3535
|
+
console.log(import_chalk5.default.white(`Target Snapshot: ${import_chalk5.default.gray(hash.slice(0, 7))}`));
|
|
3536
|
+
const proceed = await (0, import_prompts3.confirm)({
|
|
3537
|
+
message: "Revert all files to the state before the last AI action?",
|
|
3538
|
+
default: false
|
|
3539
|
+
});
|
|
3540
|
+
if (proceed) {
|
|
3541
|
+
if (applyUndo(hash)) {
|
|
3542
|
+
console.log(import_chalk5.default.green("\u2705 Project reverted successfully.\n"));
|
|
3543
|
+
} else {
|
|
3544
|
+
console.error(import_chalk5.default.red("\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
});
|
|
2739
3548
|
process.on("unhandledRejection", (reason) => {
|
|
2740
3549
|
const isCheckHook = process.argv[2] === "check";
|
|
2741
3550
|
if (isCheckHook) {
|
|
2742
|
-
if (process.env.NODE9_DEBUG === "1") {
|
|
2743
|
-
const logPath =
|
|
3551
|
+
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
3552
|
+
const logPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
2744
3553
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
2745
|
-
|
|
3554
|
+
import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
2746
3555
|
`);
|
|
2747
3556
|
}
|
|
2748
3557
|
process.exit(0);
|