@node9/proxy 1.0.0 → 1.0.2
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 +28 -4
- package/dist/cli.js +391 -350
- package/dist/cli.mjs +391 -350
- package/dist/index.js +284 -188
- package/dist/index.mjs +284 -188
- package/package.json +30 -1
package/dist/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/core.ts
|
|
7
|
-
import
|
|
7
|
+
import chalk2 from "chalk";
|
|
8
8
|
import { confirm } from "@inquirer/prompts";
|
|
9
9
|
import fs from "fs";
|
|
10
10
|
import path from "path";
|
|
@@ -14,19 +14,69 @@ import { parse } from "sh-syntax";
|
|
|
14
14
|
|
|
15
15
|
// src/ui/native.ts
|
|
16
16
|
import { spawn } from "child_process";
|
|
17
|
+
import chalk from "chalk";
|
|
17
18
|
var isTestEnv = () => {
|
|
18
19
|
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";
|
|
19
20
|
};
|
|
21
|
+
function smartTruncate(str, maxLen = 500) {
|
|
22
|
+
if (str.length <= maxLen) return str;
|
|
23
|
+
const edge = Math.floor(maxLen / 2) - 3;
|
|
24
|
+
return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
|
|
25
|
+
}
|
|
26
|
+
function formatArgs(args) {
|
|
27
|
+
if (args === null || args === void 0) return "(none)";
|
|
28
|
+
let parsed = args;
|
|
29
|
+
if (typeof args === "string") {
|
|
30
|
+
const trimmed = args.trim();
|
|
31
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
32
|
+
try {
|
|
33
|
+
parsed = JSON.parse(trimmed);
|
|
34
|
+
} catch {
|
|
35
|
+
parsed = args;
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
return smartTruncate(args, 600);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
42
|
+
const obj = parsed;
|
|
43
|
+
const codeKeys = [
|
|
44
|
+
"command",
|
|
45
|
+
"cmd",
|
|
46
|
+
"shell_command",
|
|
47
|
+
"bash_command",
|
|
48
|
+
"script",
|
|
49
|
+
"code",
|
|
50
|
+
"input",
|
|
51
|
+
"sql",
|
|
52
|
+
"query",
|
|
53
|
+
"arguments",
|
|
54
|
+
"args",
|
|
55
|
+
"param",
|
|
56
|
+
"params",
|
|
57
|
+
"text"
|
|
58
|
+
];
|
|
59
|
+
const foundKey = Object.keys(obj).find((k) => codeKeys.includes(k.toLowerCase()));
|
|
60
|
+
if (foundKey) {
|
|
61
|
+
const val = obj[foundKey];
|
|
62
|
+
const str = typeof val === "string" ? val : JSON.stringify(val);
|
|
63
|
+
return `[${foundKey.toUpperCase()}]:
|
|
64
|
+
${smartTruncate(str, 500)}`;
|
|
65
|
+
}
|
|
66
|
+
return Object.entries(obj).slice(0, 5).map(
|
|
67
|
+
([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
|
|
68
|
+
).join("\n");
|
|
69
|
+
}
|
|
70
|
+
return smartTruncate(JSON.stringify(parsed), 200);
|
|
71
|
+
}
|
|
20
72
|
function sendDesktopNotification(title, body) {
|
|
21
73
|
if (isTestEnv()) return;
|
|
22
74
|
try {
|
|
23
|
-
const safeTitle = title.replace(/"/g, '\\"');
|
|
24
|
-
const safeBody = body.replace(/"/g, '\\"');
|
|
25
75
|
if (process.platform === "darwin") {
|
|
26
|
-
const script = `display notification "${
|
|
76
|
+
const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
|
|
27
77
|
spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
|
|
28
78
|
} else if (process.platform === "linux") {
|
|
29
|
-
spawn("notify-send", [
|
|
79
|
+
spawn("notify-send", [title, body, "--icon=dialog-warning"], {
|
|
30
80
|
detached: true,
|
|
31
81
|
stdio: "ignore"
|
|
32
82
|
}).unref();
|
|
@@ -34,69 +84,54 @@ function sendDesktopNotification(title, body) {
|
|
|
34
84
|
} catch {
|
|
35
85
|
}
|
|
36
86
|
}
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const str = typeof val === "string" ? val : JSON.stringify(val);
|
|
51
|
-
const truncated = str.length > MAX_VALUE_LEN ? str.slice(0, MAX_VALUE_LEN) + "\u2026" : str;
|
|
52
|
-
return ` ${key}: ${truncated}`;
|
|
53
|
-
});
|
|
54
|
-
if (entries.length > MAX_FIELDS) {
|
|
55
|
-
lines.push(` \u2026 and ${entries.length - MAX_FIELDS} more field(s)`);
|
|
87
|
+
function escapePango(text) {
|
|
88
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
89
|
+
}
|
|
90
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
91
|
+
const lines = [];
|
|
92
|
+
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
93
|
+
lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
|
|
94
|
+
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
95
|
+
lines.push("");
|
|
96
|
+
lines.push(formattedArgs);
|
|
97
|
+
if (!locked) {
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
|
|
56
100
|
}
|
|
57
101
|
return lines.join("\n");
|
|
58
102
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (process.env.NODE9_DEBUG === "1" || process.env.VITEST) {
|
|
62
|
-
console.log(`[DEBUG Native] askNativePopup called for: ${toolName}`);
|
|
63
|
-
console.log(`[DEBUG Native] isTestEnv check:`, {
|
|
64
|
-
VITEST: process.env.VITEST,
|
|
65
|
-
NODE_ENV: process.env.NODE_ENV,
|
|
66
|
-
CI: process.env.CI,
|
|
67
|
-
isTest: isTestEnv()
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
const title = locked ? `\u26A1 Node9 \u2014 Locked by Admin Policy` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Requires Approval`;
|
|
71
|
-
let message = "";
|
|
103
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
104
|
+
const lines = [];
|
|
72
105
|
if (locked) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
message += `\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
76
|
-
`;
|
|
77
|
-
}
|
|
78
|
-
message += `Tool: ${toolName}
|
|
79
|
-
`;
|
|
80
|
-
message += `Agent: ${agent || "AI Agent"}
|
|
81
|
-
`;
|
|
82
|
-
if (explainableLabel) {
|
|
83
|
-
message += `Reason: ${explainableLabel}
|
|
84
|
-
`;
|
|
106
|
+
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
107
|
+
lines.push("");
|
|
85
108
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
109
|
+
lines.push(
|
|
110
|
+
`<b>\u{1F916} ${escapePango(agent || "AI Agent")}</b> | <b>\u{1F527} <tt>${escapePango(toolName)}</tt></b>`
|
|
111
|
+
);
|
|
112
|
+
lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
|
|
113
|
+
lines.push("");
|
|
114
|
+
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
89
115
|
if (!locked) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
Enter = Allow
|
|
116
|
+
lines.push("");
|
|
117
|
+
lines.push(
|
|
118
|
+
'<small>\u21B5 Enter = <b>Allow \u21B5</b> | \u238B Esc = <b>Block \u238B</b> | "Always Allow" = never ask again</small>'
|
|
119
|
+
);
|
|
93
120
|
}
|
|
94
|
-
|
|
95
|
-
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
123
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
|
|
124
|
+
if (isTestEnv()) return "deny";
|
|
125
|
+
const formattedArgs = formatArgs(args);
|
|
126
|
+
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
|
|
127
|
+
const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
|
|
128
|
+
process.stderr.write(chalk.yellow(`
|
|
129
|
+
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
130
|
+
`));
|
|
96
131
|
return new Promise((resolve) => {
|
|
97
132
|
let childProcess = null;
|
|
98
133
|
const onAbort = () => {
|
|
99
|
-
if (childProcess) {
|
|
134
|
+
if (childProcess && childProcess.pid) {
|
|
100
135
|
try {
|
|
101
136
|
process.kill(childProcess.pid, "SIGKILL");
|
|
102
137
|
} catch {
|
|
@@ -108,83 +143,58 @@ Enter = Allow | Click "Block" to deny`;
|
|
|
108
143
|
if (signal.aborted) return resolve("deny");
|
|
109
144
|
signal.addEventListener("abort", onAbort);
|
|
110
145
|
}
|
|
111
|
-
const cleanup = () => {
|
|
112
|
-
if (signal) signal.removeEventListener("abort", onAbort);
|
|
113
|
-
};
|
|
114
146
|
try {
|
|
115
147
|
if (process.platform === "darwin") {
|
|
116
|
-
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block", "Always Allow", "Allow"} default button "Allow" cancel button "Block"`;
|
|
117
|
-
const script = `
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
childProcess.stdout?.on("data", (d) => output += d.toString());
|
|
125
|
-
childProcess.on("close", (code) => {
|
|
126
|
-
cleanup();
|
|
127
|
-
if (locked) return resolve("deny");
|
|
128
|
-
if (code === 0) {
|
|
129
|
-
if (output.includes("Always Allow")) return resolve("always_allow");
|
|
130
|
-
if (output.includes("Allow")) return resolve("allow");
|
|
131
|
-
}
|
|
132
|
-
resolve("deny");
|
|
133
|
-
});
|
|
148
|
+
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block \u238B", "Always Allow", "Allow \u21B5"} default button "Allow \u21B5" cancel button "Block \u238B"`;
|
|
149
|
+
const script = `on run argv
|
|
150
|
+
tell application "System Events"
|
|
151
|
+
activate
|
|
152
|
+
display dialog (item 1 of argv) with title (item 2 of argv) ${buttons}
|
|
153
|
+
end tell
|
|
154
|
+
end run`;
|
|
155
|
+
childProcess = spawn("osascript", ["-e", script, "--", message, title]);
|
|
134
156
|
} else if (process.platform === "linux") {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
"--
|
|
144
|
-
"
|
|
145
|
-
|
|
146
|
-
"--question",
|
|
157
|
+
const pangoMessage = buildPangoMessage(
|
|
158
|
+
toolName,
|
|
159
|
+
formattedArgs,
|
|
160
|
+
agent,
|
|
161
|
+
explainableLabel,
|
|
162
|
+
locked
|
|
163
|
+
);
|
|
164
|
+
const argsList = [
|
|
165
|
+
locked ? "--info" : "--question",
|
|
166
|
+
"--modal",
|
|
167
|
+
"--width=480",
|
|
147
168
|
"--title",
|
|
148
169
|
title,
|
|
149
170
|
"--text",
|
|
150
|
-
|
|
171
|
+
pangoMessage,
|
|
151
172
|
"--ok-label",
|
|
152
|
-
"Allow",
|
|
153
|
-
"--cancel-label",
|
|
154
|
-
"Block",
|
|
155
|
-
"--extra-button",
|
|
156
|
-
"Always Allow",
|
|
173
|
+
locked ? "Waiting..." : "Allow \u21B5",
|
|
157
174
|
"--timeout",
|
|
158
175
|
"300"
|
|
159
176
|
];
|
|
177
|
+
if (!locked) {
|
|
178
|
+
argsList.push("--cancel-label", "Block \u238B");
|
|
179
|
+
argsList.push("--extra-button", "Always Allow");
|
|
180
|
+
}
|
|
160
181
|
childProcess = spawn("zenity", argsList);
|
|
161
|
-
let output = "";
|
|
162
|
-
childProcess.stdout?.on("data", (d) => output += d.toString());
|
|
163
|
-
childProcess.on("close", (code) => {
|
|
164
|
-
cleanup();
|
|
165
|
-
if (locked) return resolve("deny");
|
|
166
|
-
if (output.trim() === "Always Allow") return resolve("always_allow");
|
|
167
|
-
if (code === 0) return resolve("allow");
|
|
168
|
-
resolve("deny");
|
|
169
|
-
});
|
|
170
182
|
} else if (process.platform === "win32") {
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
$res = [System.Windows.MessageBox]::Show("${safeMessage}", "${safeTitle}", "${buttonType}", "Warning", "Button2", "DefaultDesktopOnly");
|
|
175
|
-
if ($res -eq "Yes") { exit 0 } else { exit 1 }`;
|
|
183
|
+
const b64Msg = Buffer.from(message).toString("base64");
|
|
184
|
+
const b64Title = Buffer.from(title).toString("base64");
|
|
185
|
+
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
186
|
childProcess = spawn("powershell", ["-Command", ps]);
|
|
177
|
-
childProcess.on("close", (code) => {
|
|
178
|
-
cleanup();
|
|
179
|
-
if (locked) return resolve("deny");
|
|
180
|
-
resolve(code === 0 ? "allow" : "deny");
|
|
181
|
-
});
|
|
182
|
-
} else {
|
|
183
|
-
cleanup();
|
|
184
|
-
resolve("deny");
|
|
185
187
|
}
|
|
188
|
+
let output = "";
|
|
189
|
+
childProcess?.stdout?.on("data", (d) => output += d.toString());
|
|
190
|
+
childProcess?.on("close", (code) => {
|
|
191
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
192
|
+
if (locked) return resolve("deny");
|
|
193
|
+
if (output.includes("Always Allow")) return resolve("always_allow");
|
|
194
|
+
if (code === 0) return resolve("allow");
|
|
195
|
+
resolve("deny");
|
|
196
|
+
});
|
|
186
197
|
} catch {
|
|
187
|
-
cleanup();
|
|
188
198
|
resolve("deny");
|
|
189
199
|
}
|
|
190
200
|
});
|
|
@@ -193,6 +203,8 @@ Enter = Allow | Click "Block" to deny`;
|
|
|
193
203
|
// src/core.ts
|
|
194
204
|
var PAUSED_FILE = path.join(os.homedir(), ".node9", "PAUSED");
|
|
195
205
|
var TRUST_FILE = path.join(os.homedir(), ".node9", "trust.json");
|
|
206
|
+
var LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
|
|
207
|
+
var HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
|
|
196
208
|
function checkPause() {
|
|
197
209
|
try {
|
|
198
210
|
if (!fs.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -259,36 +271,39 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
259
271
|
}
|
|
260
272
|
}
|
|
261
273
|
}
|
|
262
|
-
function
|
|
274
|
+
function appendToLog(logPath, entry) {
|
|
263
275
|
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 = path.join(os.homedir(), ".node9", "audit.log");
|
|
272
276
|
const dir = path.dirname(logPath);
|
|
273
277
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
274
|
-
fs.appendFileSync(logPath, entry + "\n");
|
|
278
|
+
fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
275
279
|
} catch {
|
|
276
280
|
}
|
|
277
281
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
282
|
+
function appendHookDebug(toolName, args, meta) {
|
|
283
|
+
const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
|
|
284
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
285
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
286
|
+
tool: toolName,
|
|
287
|
+
args: safeArgs,
|
|
288
|
+
agent: meta?.agent,
|
|
289
|
+
mcpServer: meta?.mcpServer,
|
|
290
|
+
hostname: os.hostname(),
|
|
291
|
+
cwd: process.cwd()
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
295
|
+
const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
|
|
296
|
+
appendToLog(LOCAL_AUDIT_LOG, {
|
|
297
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
298
|
+
tool: toolName,
|
|
299
|
+
args: safeArgs,
|
|
300
|
+
decision,
|
|
301
|
+
checkedBy,
|
|
302
|
+
agent: meta?.agent,
|
|
303
|
+
mcpServer: meta?.mcpServer,
|
|
304
|
+
hostname: os.hostname()
|
|
305
|
+
});
|
|
306
|
+
}
|
|
292
307
|
function tokenize(toolName) {
|
|
293
308
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
294
309
|
}
|
|
@@ -395,16 +410,28 @@ function redactSecrets(text) {
|
|
|
395
410
|
);
|
|
396
411
|
return redacted;
|
|
397
412
|
}
|
|
413
|
+
var DANGEROUS_WORDS = [
|
|
414
|
+
"drop",
|
|
415
|
+
"truncate",
|
|
416
|
+
"purge",
|
|
417
|
+
"format",
|
|
418
|
+
"destroy",
|
|
419
|
+
"terminate",
|
|
420
|
+
"revoke",
|
|
421
|
+
"docker",
|
|
422
|
+
"psql"
|
|
423
|
+
];
|
|
398
424
|
var DEFAULT_CONFIG = {
|
|
399
425
|
settings: {
|
|
400
426
|
mode: "standard",
|
|
401
427
|
autoStartDaemon: true,
|
|
402
|
-
enableUndo:
|
|
428
|
+
enableUndo: true,
|
|
429
|
+
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
403
430
|
enableHookLogDebug: false,
|
|
404
431
|
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
405
432
|
},
|
|
406
433
|
policy: {
|
|
407
|
-
sandboxPaths: [],
|
|
434
|
+
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
408
435
|
dangerousWords: DANGEROUS_WORDS,
|
|
409
436
|
ignoredTools: [
|
|
410
437
|
"list_*",
|
|
@@ -412,12 +439,44 @@ var DEFAULT_CONFIG = {
|
|
|
412
439
|
"read_*",
|
|
413
440
|
"describe_*",
|
|
414
441
|
"read",
|
|
442
|
+
"glob",
|
|
415
443
|
"grep",
|
|
416
444
|
"ls",
|
|
417
|
-
"
|
|
445
|
+
"notebookread",
|
|
446
|
+
"notebookedit",
|
|
447
|
+
"webfetch",
|
|
448
|
+
"websearch",
|
|
449
|
+
"exitplanmode",
|
|
450
|
+
"askuserquestion",
|
|
451
|
+
"agent",
|
|
452
|
+
"task*",
|
|
453
|
+
"toolsearch",
|
|
454
|
+
"mcp__ide__*",
|
|
455
|
+
"getDiagnostics"
|
|
418
456
|
],
|
|
419
|
-
toolInspection: {
|
|
420
|
-
|
|
457
|
+
toolInspection: {
|
|
458
|
+
bash: "command",
|
|
459
|
+
shell: "command",
|
|
460
|
+
run_shell_command: "command",
|
|
461
|
+
"terminal.execute": "command",
|
|
462
|
+
"postgres:query": "sql"
|
|
463
|
+
},
|
|
464
|
+
rules: [
|
|
465
|
+
{
|
|
466
|
+
action: "rm",
|
|
467
|
+
allowPaths: [
|
|
468
|
+
"**/node_modules/**",
|
|
469
|
+
"dist/**",
|
|
470
|
+
"build/**",
|
|
471
|
+
".next/**",
|
|
472
|
+
"coverage/**",
|
|
473
|
+
".cache/**",
|
|
474
|
+
"tmp/**",
|
|
475
|
+
"temp/**",
|
|
476
|
+
".DS_Store"
|
|
477
|
+
]
|
|
478
|
+
}
|
|
479
|
+
]
|
|
421
480
|
},
|
|
422
481
|
environments: {}
|
|
423
482
|
};
|
|
@@ -482,20 +541,15 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
482
541
|
}
|
|
483
542
|
const isManual = agent === "Terminal";
|
|
484
543
|
if (isManual) {
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
"
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
"revoke",
|
|
495
|
-
"docker"
|
|
496
|
-
];
|
|
497
|
-
const hasNuclear = allTokens.some((t) => NUCLEAR_COMMANDS.includes(t.toLowerCase()));
|
|
498
|
-
if (!hasNuclear) return { decision: "allow" };
|
|
544
|
+
const SYSTEM_DISASTER_COMMANDS = ["mkfs", "shred", "dd", "drop", "truncate", "purge"];
|
|
545
|
+
const hasSystemDisaster = allTokens.some(
|
|
546
|
+
(t) => SYSTEM_DISASTER_COMMANDS.includes(t.toLowerCase())
|
|
547
|
+
);
|
|
548
|
+
const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
|
|
549
|
+
if (hasSystemDisaster || isRootWipe) {
|
|
550
|
+
return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
|
|
551
|
+
}
|
|
552
|
+
return { decision: "allow" };
|
|
499
553
|
}
|
|
500
554
|
if (pathTokens.length > 0 && config.policy.sandboxPaths.length > 0) {
|
|
501
555
|
const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
|
|
@@ -509,27 +563,39 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
509
563
|
if (pathTokens.length > 0) {
|
|
510
564
|
const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
|
|
511
565
|
if (anyBlocked)
|
|
512
|
-
return {
|
|
566
|
+
return {
|
|
567
|
+
decision: "review",
|
|
568
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
|
|
569
|
+
};
|
|
513
570
|
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
514
571
|
if (allAllowed) return { decision: "allow" };
|
|
515
572
|
}
|
|
516
|
-
return {
|
|
573
|
+
return {
|
|
574
|
+
decision: "review",
|
|
575
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
|
|
576
|
+
};
|
|
517
577
|
}
|
|
518
578
|
}
|
|
579
|
+
let matchedDangerousWord;
|
|
519
580
|
const isDangerous = allTokens.some(
|
|
520
581
|
(token) => config.policy.dangerousWords.some((word) => {
|
|
521
582
|
const w = word.toLowerCase();
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
583
|
+
const hit = token === w || (() => {
|
|
584
|
+
try {
|
|
585
|
+
return new RegExp(`\\b${w}\\b`, "i").test(token);
|
|
586
|
+
} catch {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
})();
|
|
590
|
+
if (hit && !matchedDangerousWord) matchedDangerousWord = word;
|
|
591
|
+
return hit;
|
|
528
592
|
})
|
|
529
593
|
);
|
|
530
594
|
if (isDangerous) {
|
|
531
|
-
|
|
532
|
-
|
|
595
|
+
return {
|
|
596
|
+
decision: "review",
|
|
597
|
+
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`
|
|
598
|
+
};
|
|
533
599
|
}
|
|
534
600
|
if (config.settings.mode === "strict") {
|
|
535
601
|
const envConfig = getActiveEnvironment(config);
|
|
@@ -644,13 +710,16 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
644
710
|
approvers.browser = false;
|
|
645
711
|
approvers.terminal = false;
|
|
646
712
|
}
|
|
713
|
+
if (config.settings.enableHookLogDebug && !isTestEnv2) {
|
|
714
|
+
appendHookDebug(toolName, args, meta);
|
|
715
|
+
}
|
|
647
716
|
const isManual = meta?.agent === "Terminal";
|
|
648
717
|
let explainableLabel = "Local Config";
|
|
649
718
|
if (config.settings.mode === "audit") {
|
|
650
719
|
if (!isIgnoredTool(toolName)) {
|
|
651
720
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
652
721
|
if (policyResult.decision === "review") {
|
|
653
|
-
|
|
722
|
+
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
654
723
|
sendDesktopNotification(
|
|
655
724
|
"Node9 Audit Mode",
|
|
656
725
|
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
@@ -662,20 +731,24 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
662
731
|
if (!isIgnoredTool(toolName)) {
|
|
663
732
|
if (getActiveTrustSession(toolName)) {
|
|
664
733
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
734
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
665
735
|
return { approved: true, checkedBy: "trust" };
|
|
666
736
|
}
|
|
667
737
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
668
738
|
if (policyResult.decision === "allow") {
|
|
669
739
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
740
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
670
741
|
return { approved: true, checkedBy: "local-policy" };
|
|
671
742
|
}
|
|
672
743
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
673
744
|
const persistent = getPersistentDecision(toolName);
|
|
674
745
|
if (persistent === "allow") {
|
|
675
746
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
747
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
676
748
|
return { approved: true, checkedBy: "persistent" };
|
|
677
749
|
}
|
|
678
750
|
if (persistent === "deny") {
|
|
751
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "persistent-deny", meta);
|
|
679
752
|
return {
|
|
680
753
|
approved: false,
|
|
681
754
|
reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
|
|
@@ -685,6 +758,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
685
758
|
}
|
|
686
759
|
} else {
|
|
687
760
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
|
|
761
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
688
762
|
return { approved: true };
|
|
689
763
|
}
|
|
690
764
|
let cloudRequestId = null;
|
|
@@ -712,8 +786,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
712
786
|
const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
|
|
713
787
|
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
788
|
console.error(
|
|
715
|
-
|
|
716
|
-
\u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) +
|
|
789
|
+
chalk2.yellow(`
|
|
790
|
+
\u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + chalk2.dim(`
|
|
717
791
|
Falling back to local rules...
|
|
718
792
|
`)
|
|
719
793
|
);
|
|
@@ -721,13 +795,13 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
721
795
|
}
|
|
722
796
|
if (cloudEnforced && cloudRequestId) {
|
|
723
797
|
console.error(
|
|
724
|
-
|
|
798
|
+
chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
|
|
725
799
|
);
|
|
726
|
-
console.error(
|
|
800
|
+
console.error(chalk2.cyan(" Dashboard \u2192 ") + chalk2.bold("Mission Control > Activity Feed\n"));
|
|
727
801
|
} else if (!cloudEnforced) {
|
|
728
802
|
const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
|
|
729
803
|
console.error(
|
|
730
|
-
|
|
804
|
+
chalk2.dim(`
|
|
731
805
|
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
732
806
|
`)
|
|
733
807
|
);
|
|
@@ -792,9 +866,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
792
866
|
try {
|
|
793
867
|
if (!approvers.native && !cloudEnforced) {
|
|
794
868
|
console.error(
|
|
795
|
-
|
|
869
|
+
chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
|
|
796
870
|
);
|
|
797
|
-
console.error(
|
|
871
|
+
console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
798
872
|
`));
|
|
799
873
|
}
|
|
800
874
|
const daemonDecision = await askDaemon(toolName, args, meta, signal);
|
|
@@ -817,11 +891,11 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
817
891
|
racePromises.push(
|
|
818
892
|
(async () => {
|
|
819
893
|
try {
|
|
820
|
-
console.log(
|
|
821
|
-
console.log(`${
|
|
822
|
-
console.log(`${
|
|
894
|
+
console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
|
|
895
|
+
console.log(`${chalk2.bold("Action:")} ${chalk2.red(toolName)}`);
|
|
896
|
+
console.log(`${chalk2.bold("Flagged By:")} ${chalk2.yellow(explainableLabel)}`);
|
|
823
897
|
if (isRemoteLocked) {
|
|
824
|
-
console.log(
|
|
898
|
+
console.log(chalk2.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
|
|
825
899
|
`));
|
|
826
900
|
await new Promise((_, reject) => {
|
|
827
901
|
signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
|
|
@@ -909,6 +983,15 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
909
983
|
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
910
984
|
await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
|
|
911
985
|
}
|
|
986
|
+
if (!isManual) {
|
|
987
|
+
appendLocalAudit(
|
|
988
|
+
toolName,
|
|
989
|
+
args,
|
|
990
|
+
finalResult.approved ? "allow" : "deny",
|
|
991
|
+
finalResult.checkedBy || finalResult.blockedBy || "unknown",
|
|
992
|
+
meta
|
|
993
|
+
);
|
|
994
|
+
}
|
|
912
995
|
return finalResult;
|
|
913
996
|
}
|
|
914
997
|
function getConfig() {
|
|
@@ -939,8 +1022,8 @@ function getConfig() {
|
|
|
939
1022
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
940
1023
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
941
1024
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
942
|
-
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
943
1025
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
1026
|
+
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
944
1027
|
if (p.toolInspection)
|
|
945
1028
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
946
1029
|
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
@@ -1068,11 +1151,11 @@ async function pollNode9SaaS(requestId, creds, signal) {
|
|
|
1068
1151
|
if (!statusRes.ok) continue;
|
|
1069
1152
|
const { status, reason } = await statusRes.json();
|
|
1070
1153
|
if (status === "APPROVED") {
|
|
1071
|
-
console.error(
|
|
1154
|
+
console.error(chalk2.green("\u2705 Approved via Cloud.\n"));
|
|
1072
1155
|
return { approved: true, reason };
|
|
1073
1156
|
}
|
|
1074
1157
|
if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
|
|
1075
|
-
console.error(
|
|
1158
|
+
console.error(chalk2.red("\u274C Denied via Cloud.\n"));
|
|
1076
1159
|
return { approved: false, reason };
|
|
1077
1160
|
}
|
|
1078
1161
|
} catch {
|
|
@@ -1100,11 +1183,11 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
1100
1183
|
import fs2 from "fs";
|
|
1101
1184
|
import path2 from "path";
|
|
1102
1185
|
import os2 from "os";
|
|
1103
|
-
import
|
|
1186
|
+
import chalk3 from "chalk";
|
|
1104
1187
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
1105
1188
|
function printDaemonTip() {
|
|
1106
1189
|
console.log(
|
|
1107
|
-
|
|
1190
|
+
chalk3.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk3.white("\n To view your history or manage persistent rules, run:") + chalk3.green("\n node9 daemon --openui")
|
|
1108
1191
|
);
|
|
1109
1192
|
}
|
|
1110
1193
|
function fullPathCommand(subcommand) {
|
|
@@ -1145,7 +1228,7 @@ async function setupClaude() {
|
|
|
1145
1228
|
matcher: ".*",
|
|
1146
1229
|
hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
|
|
1147
1230
|
});
|
|
1148
|
-
console.log(
|
|
1231
|
+
console.log(chalk3.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
|
|
1149
1232
|
anythingChanged = true;
|
|
1150
1233
|
}
|
|
1151
1234
|
const hasPostHook = settings.hooks.PostToolUse?.some(
|
|
@@ -1157,7 +1240,7 @@ async function setupClaude() {
|
|
|
1157
1240
|
matcher: ".*",
|
|
1158
1241
|
hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
|
|
1159
1242
|
});
|
|
1160
|
-
console.log(
|
|
1243
|
+
console.log(chalk3.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
|
|
1161
1244
|
anythingChanged = true;
|
|
1162
1245
|
}
|
|
1163
1246
|
if (anythingChanged) {
|
|
@@ -1171,10 +1254,10 @@ async function setupClaude() {
|
|
|
1171
1254
|
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
1172
1255
|
}
|
|
1173
1256
|
if (serversToWrap.length > 0) {
|
|
1174
|
-
console.log(
|
|
1175
|
-
console.log(
|
|
1257
|
+
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
1258
|
+
console.log(chalk3.white(` ${mcpPath}`));
|
|
1176
1259
|
for (const { name, originalCmd } of serversToWrap) {
|
|
1177
|
-
console.log(
|
|
1260
|
+
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
1178
1261
|
}
|
|
1179
1262
|
console.log("");
|
|
1180
1263
|
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
@@ -1184,22 +1267,22 @@ async function setupClaude() {
|
|
|
1184
1267
|
}
|
|
1185
1268
|
claudeConfig.mcpServers = servers;
|
|
1186
1269
|
writeJson(mcpPath, claudeConfig);
|
|
1187
|
-
console.log(
|
|
1270
|
+
console.log(chalk3.green(`
|
|
1188
1271
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
1189
1272
|
anythingChanged = true;
|
|
1190
1273
|
} else {
|
|
1191
|
-
console.log(
|
|
1274
|
+
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
1192
1275
|
}
|
|
1193
1276
|
console.log("");
|
|
1194
1277
|
}
|
|
1195
1278
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
1196
|
-
console.log(
|
|
1279
|
+
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
|
|
1197
1280
|
printDaemonTip();
|
|
1198
1281
|
return;
|
|
1199
1282
|
}
|
|
1200
1283
|
if (anythingChanged) {
|
|
1201
|
-
console.log(
|
|
1202
|
-
console.log(
|
|
1284
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
1285
|
+
console.log(chalk3.gray(" Restart Claude Code for changes to take effect."));
|
|
1203
1286
|
printDaemonTip();
|
|
1204
1287
|
}
|
|
1205
1288
|
}
|
|
@@ -1227,7 +1310,7 @@ async function setupGemini() {
|
|
|
1227
1310
|
}
|
|
1228
1311
|
]
|
|
1229
1312
|
});
|
|
1230
|
-
console.log(
|
|
1313
|
+
console.log(chalk3.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
1231
1314
|
anythingChanged = true;
|
|
1232
1315
|
}
|
|
1233
1316
|
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
@@ -1240,7 +1323,7 @@ async function setupGemini() {
|
|
|
1240
1323
|
matcher: ".*",
|
|
1241
1324
|
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
1242
1325
|
});
|
|
1243
|
-
console.log(
|
|
1326
|
+
console.log(chalk3.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
1244
1327
|
anythingChanged = true;
|
|
1245
1328
|
}
|
|
1246
1329
|
if (anythingChanged) {
|
|
@@ -1254,10 +1337,10 @@ async function setupGemini() {
|
|
|
1254
1337
|
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
1255
1338
|
}
|
|
1256
1339
|
if (serversToWrap.length > 0) {
|
|
1257
|
-
console.log(
|
|
1258
|
-
console.log(
|
|
1340
|
+
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
1341
|
+
console.log(chalk3.white(` ${settingsPath} (mcpServers)`));
|
|
1259
1342
|
for (const { name, originalCmd } of serversToWrap) {
|
|
1260
|
-
console.log(
|
|
1343
|
+
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
1261
1344
|
}
|
|
1262
1345
|
console.log("");
|
|
1263
1346
|
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
@@ -1267,22 +1350,22 @@ async function setupGemini() {
|
|
|
1267
1350
|
}
|
|
1268
1351
|
settings.mcpServers = servers;
|
|
1269
1352
|
writeJson(settingsPath, settings);
|
|
1270
|
-
console.log(
|
|
1353
|
+
console.log(chalk3.green(`
|
|
1271
1354
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
1272
1355
|
anythingChanged = true;
|
|
1273
1356
|
} else {
|
|
1274
|
-
console.log(
|
|
1357
|
+
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
1275
1358
|
}
|
|
1276
1359
|
console.log("");
|
|
1277
1360
|
}
|
|
1278
1361
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
1279
|
-
console.log(
|
|
1362
|
+
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
|
|
1280
1363
|
printDaemonTip();
|
|
1281
1364
|
return;
|
|
1282
1365
|
}
|
|
1283
1366
|
if (anythingChanged) {
|
|
1284
|
-
console.log(
|
|
1285
|
-
console.log(
|
|
1367
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
|
|
1368
|
+
console.log(chalk3.gray(" Restart Gemini CLI for changes to take effect."));
|
|
1286
1369
|
printDaemonTip();
|
|
1287
1370
|
}
|
|
1288
1371
|
}
|
|
@@ -1301,7 +1384,7 @@ async function setupCursor() {
|
|
|
1301
1384
|
if (!hasPreHook) {
|
|
1302
1385
|
if (!hooksFile.hooks.preToolUse) hooksFile.hooks.preToolUse = [];
|
|
1303
1386
|
hooksFile.hooks.preToolUse.push({ command: fullPathCommand("check") });
|
|
1304
|
-
console.log(
|
|
1387
|
+
console.log(chalk3.green(" \u2705 preToolUse hook added \u2192 node9 check"));
|
|
1305
1388
|
anythingChanged = true;
|
|
1306
1389
|
}
|
|
1307
1390
|
const hasPostHook = hooksFile.hooks.postToolUse?.some(
|
|
@@ -1310,7 +1393,7 @@ async function setupCursor() {
|
|
|
1310
1393
|
if (!hasPostHook) {
|
|
1311
1394
|
if (!hooksFile.hooks.postToolUse) hooksFile.hooks.postToolUse = [];
|
|
1312
1395
|
hooksFile.hooks.postToolUse.push({ command: fullPathCommand("log") });
|
|
1313
|
-
console.log(
|
|
1396
|
+
console.log(chalk3.green(" \u2705 postToolUse hook added \u2192 node9 log"));
|
|
1314
1397
|
anythingChanged = true;
|
|
1315
1398
|
}
|
|
1316
1399
|
if (anythingChanged) {
|
|
@@ -1324,10 +1407,10 @@ async function setupCursor() {
|
|
|
1324
1407
|
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
1325
1408
|
}
|
|
1326
1409
|
if (serversToWrap.length > 0) {
|
|
1327
|
-
console.log(
|
|
1328
|
-
console.log(
|
|
1410
|
+
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
1411
|
+
console.log(chalk3.white(` ${mcpPath}`));
|
|
1329
1412
|
for (const { name, originalCmd } of serversToWrap) {
|
|
1330
|
-
console.log(
|
|
1413
|
+
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
1331
1414
|
}
|
|
1332
1415
|
console.log("");
|
|
1333
1416
|
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
@@ -1337,22 +1420,22 @@ async function setupCursor() {
|
|
|
1337
1420
|
}
|
|
1338
1421
|
mcpConfig.mcpServers = servers;
|
|
1339
1422
|
writeJson(mcpPath, mcpConfig);
|
|
1340
|
-
console.log(
|
|
1423
|
+
console.log(chalk3.green(`
|
|
1341
1424
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
1342
1425
|
anythingChanged = true;
|
|
1343
1426
|
} else {
|
|
1344
|
-
console.log(
|
|
1427
|
+
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
1345
1428
|
}
|
|
1346
1429
|
console.log("");
|
|
1347
1430
|
}
|
|
1348
1431
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
1349
|
-
console.log(
|
|
1432
|
+
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Cursor."));
|
|
1350
1433
|
printDaemonTip();
|
|
1351
1434
|
return;
|
|
1352
1435
|
}
|
|
1353
1436
|
if (anythingChanged) {
|
|
1354
|
-
console.log(
|
|
1355
|
-
console.log(
|
|
1437
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor!"));
|
|
1438
|
+
console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
|
|
1356
1439
|
printDaemonTip();
|
|
1357
1440
|
}
|
|
1358
1441
|
}
|
|
@@ -2331,7 +2414,7 @@ import path3 from "path";
|
|
|
2331
2414
|
import os3 from "os";
|
|
2332
2415
|
import { spawn as spawn2 } from "child_process";
|
|
2333
2416
|
import { randomUUID } from "crypto";
|
|
2334
|
-
import
|
|
2417
|
+
import chalk4 from "chalk";
|
|
2335
2418
|
var DAEMON_PORT2 = 7391;
|
|
2336
2419
|
var DAEMON_HOST2 = "127.0.0.1";
|
|
2337
2420
|
var homeDir = os3.homedir();
|
|
@@ -2777,7 +2860,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
2777
2860
|
return;
|
|
2778
2861
|
}
|
|
2779
2862
|
}
|
|
2780
|
-
console.error(
|
|
2863
|
+
console.error(chalk4.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
|
|
2781
2864
|
process.exit(1);
|
|
2782
2865
|
});
|
|
2783
2866
|
server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
|
|
@@ -2786,17 +2869,17 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
2786
2869
|
JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
|
|
2787
2870
|
{ mode: 384 }
|
|
2788
2871
|
);
|
|
2789
|
-
console.log(
|
|
2872
|
+
console.log(chalk4.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
|
|
2790
2873
|
});
|
|
2791
2874
|
}
|
|
2792
2875
|
function stopDaemon() {
|
|
2793
|
-
if (!fs3.existsSync(DAEMON_PID_FILE)) return console.log(
|
|
2876
|
+
if (!fs3.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
|
|
2794
2877
|
try {
|
|
2795
2878
|
const { pid } = JSON.parse(fs3.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
2796
2879
|
process.kill(pid, "SIGTERM");
|
|
2797
|
-
console.log(
|
|
2880
|
+
console.log(chalk4.green("\u2705 Stopped."));
|
|
2798
2881
|
} catch {
|
|
2799
|
-
console.log(
|
|
2882
|
+
console.log(chalk4.gray("Cleaned up stale PID file."));
|
|
2800
2883
|
} finally {
|
|
2801
2884
|
try {
|
|
2802
2885
|
fs3.unlinkSync(DAEMON_PID_FILE);
|
|
@@ -2806,13 +2889,13 @@ function stopDaemon() {
|
|
|
2806
2889
|
}
|
|
2807
2890
|
function daemonStatus() {
|
|
2808
2891
|
if (!fs3.existsSync(DAEMON_PID_FILE))
|
|
2809
|
-
return console.log(
|
|
2892
|
+
return console.log(chalk4.yellow("Node9 daemon: not running"));
|
|
2810
2893
|
try {
|
|
2811
2894
|
const { pid } = JSON.parse(fs3.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
2812
2895
|
process.kill(pid, 0);
|
|
2813
|
-
console.log(
|
|
2896
|
+
console.log(chalk4.green("Node9 daemon: running"));
|
|
2814
2897
|
} catch {
|
|
2815
|
-
console.log(
|
|
2898
|
+
console.log(chalk4.yellow("Node9 daemon: not running (stale PID)"));
|
|
2816
2899
|
}
|
|
2817
2900
|
}
|
|
2818
2901
|
|
|
@@ -2820,7 +2903,7 @@ function daemonStatus() {
|
|
|
2820
2903
|
import { spawn as spawn3, execSync } from "child_process";
|
|
2821
2904
|
import { parseCommandString } from "execa";
|
|
2822
2905
|
import { execa } from "execa";
|
|
2823
|
-
import
|
|
2906
|
+
import chalk5 from "chalk";
|
|
2824
2907
|
import readline from "readline";
|
|
2825
2908
|
import fs5 from "fs";
|
|
2826
2909
|
import path5 from "path";
|
|
@@ -2959,7 +3042,7 @@ async function runProxy(targetCommand) {
|
|
|
2959
3042
|
if (stdout) executable = stdout.trim();
|
|
2960
3043
|
} catch {
|
|
2961
3044
|
}
|
|
2962
|
-
console.log(
|
|
3045
|
+
console.log(chalk5.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
|
|
2963
3046
|
const child = spawn3(executable, args, {
|
|
2964
3047
|
stdio: ["pipe", "pipe", "inherit"],
|
|
2965
3048
|
// We control STDIN and STDOUT
|
|
@@ -3060,92 +3143,47 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3060
3143
|
fs5.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3061
3144
|
}
|
|
3062
3145
|
if (options.profile && profileName !== "default") {
|
|
3063
|
-
console.log(
|
|
3064
|
-
console.log(
|
|
3146
|
+
console.log(chalk5.green(`\u2705 Profile "${profileName}" saved`));
|
|
3147
|
+
console.log(chalk5.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
3065
3148
|
} else if (options.local) {
|
|
3066
|
-
console.log(
|
|
3067
|
-
console.log(
|
|
3149
|
+
console.log(chalk5.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
3150
|
+
console.log(chalk5.gray(` All decisions stay on this machine.`));
|
|
3068
3151
|
} else {
|
|
3069
|
-
console.log(
|
|
3070
|
-
console.log(
|
|
3152
|
+
console.log(chalk5.green(`\u2705 Logged in \u2014 agent mode`));
|
|
3153
|
+
console.log(chalk5.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
3071
3154
|
}
|
|
3072
3155
|
});
|
|
3073
3156
|
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) => {
|
|
3074
3157
|
if (target === "gemini") return await setupGemini();
|
|
3075
3158
|
if (target === "claude") return await setupClaude();
|
|
3076
3159
|
if (target === "cursor") return await setupCursor();
|
|
3077
|
-
console.error(
|
|
3160
|
+
console.error(chalk5.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
3078
3161
|
process.exit(1);
|
|
3079
3162
|
});
|
|
3080
|
-
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").action((options) => {
|
|
3163
|
+
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
|
|
3081
3164
|
const configPath = path5.join(os5.homedir(), ".node9", "config.json");
|
|
3082
3165
|
if (fs5.existsSync(configPath) && !options.force) {
|
|
3083
|
-
console.log(
|
|
3084
|
-
console.log(
|
|
3166
|
+
console.log(chalk5.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
3167
|
+
console.log(chalk5.gray(` Run with --force to overwrite.`));
|
|
3085
3168
|
return;
|
|
3086
3169
|
}
|
|
3087
|
-
const
|
|
3088
|
-
|
|
3170
|
+
const requestedMode = options.mode.toLowerCase();
|
|
3171
|
+
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
3172
|
+
const configToSave = {
|
|
3173
|
+
...DEFAULT_CONFIG,
|
|
3089
3174
|
settings: {
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
enableUndo: true,
|
|
3093
|
-
enableHookLogDebug: false,
|
|
3094
|
-
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
3095
|
-
},
|
|
3096
|
-
policy: {
|
|
3097
|
-
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
3098
|
-
dangerousWords: DANGEROUS_WORDS,
|
|
3099
|
-
ignoredTools: [
|
|
3100
|
-
"list_*",
|
|
3101
|
-
"get_*",
|
|
3102
|
-
"read_*",
|
|
3103
|
-
"describe_*",
|
|
3104
|
-
"read",
|
|
3105
|
-
"write",
|
|
3106
|
-
"edit",
|
|
3107
|
-
"glob",
|
|
3108
|
-
"grep",
|
|
3109
|
-
"ls",
|
|
3110
|
-
"notebookread",
|
|
3111
|
-
"notebookedit",
|
|
3112
|
-
"webfetch",
|
|
3113
|
-
"websearch",
|
|
3114
|
-
"exitplanmode",
|
|
3115
|
-
"askuserquestion",
|
|
3116
|
-
"agent",
|
|
3117
|
-
"task*"
|
|
3118
|
-
],
|
|
3119
|
-
toolInspection: {
|
|
3120
|
-
bash: "command",
|
|
3121
|
-
shell: "command",
|
|
3122
|
-
run_shell_command: "command",
|
|
3123
|
-
"terminal.execute": "command",
|
|
3124
|
-
"postgres:query": "sql"
|
|
3125
|
-
},
|
|
3126
|
-
rules: [
|
|
3127
|
-
{
|
|
3128
|
-
action: "rm",
|
|
3129
|
-
allowPaths: [
|
|
3130
|
-
"**/node_modules/**",
|
|
3131
|
-
"dist/**",
|
|
3132
|
-
"build/**",
|
|
3133
|
-
".next/**",
|
|
3134
|
-
"coverage/**",
|
|
3135
|
-
".cache/**",
|
|
3136
|
-
"tmp/**",
|
|
3137
|
-
"temp/**",
|
|
3138
|
-
".DS_Store"
|
|
3139
|
-
]
|
|
3140
|
-
}
|
|
3141
|
-
]
|
|
3175
|
+
...DEFAULT_CONFIG.settings,
|
|
3176
|
+
mode: safeMode
|
|
3142
3177
|
}
|
|
3143
3178
|
};
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
fs5.writeFileSync(configPath, JSON.stringify(
|
|
3147
|
-
console.log(
|
|
3148
|
-
console.log(
|
|
3179
|
+
const dir = path5.dirname(configPath);
|
|
3180
|
+
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
3181
|
+
fs5.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
3182
|
+
console.log(chalk5.green(`\u2705 Global config created: ${configPath}`));
|
|
3183
|
+
console.log(chalk5.cyan(` Mode set to: ${safeMode}`));
|
|
3184
|
+
console.log(
|
|
3185
|
+
chalk5.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
3186
|
+
);
|
|
3149
3187
|
});
|
|
3150
3188
|
program.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
|
|
3151
3189
|
const creds = getCredentials();
|
|
@@ -3154,43 +3192,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
3154
3192
|
const settings = mergedConfig.settings;
|
|
3155
3193
|
console.log("");
|
|
3156
3194
|
if (creds && settings.approvers.cloud) {
|
|
3157
|
-
console.log(
|
|
3195
|
+
console.log(chalk5.green(" \u25CF Agent mode") + chalk5.gray(" \u2014 cloud team policy enforced"));
|
|
3158
3196
|
} else if (creds && !settings.approvers.cloud) {
|
|
3159
3197
|
console.log(
|
|
3160
|
-
|
|
3198
|
+
chalk5.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk5.gray(" \u2014 all decisions stay on this machine")
|
|
3161
3199
|
);
|
|
3162
3200
|
} else {
|
|
3163
3201
|
console.log(
|
|
3164
|
-
|
|
3202
|
+
chalk5.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk5.gray(" \u2014 no API key (Local rules only)")
|
|
3165
3203
|
);
|
|
3166
3204
|
}
|
|
3167
3205
|
console.log("");
|
|
3168
3206
|
if (daemonRunning) {
|
|
3169
3207
|
console.log(
|
|
3170
|
-
|
|
3208
|
+
chalk5.green(" \u25CF Daemon running") + chalk5.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
|
|
3171
3209
|
);
|
|
3172
3210
|
} else {
|
|
3173
|
-
console.log(
|
|
3211
|
+
console.log(chalk5.gray(" \u25CB Daemon stopped"));
|
|
3174
3212
|
}
|
|
3175
3213
|
if (settings.enableUndo) {
|
|
3176
3214
|
console.log(
|
|
3177
|
-
|
|
3215
|
+
chalk5.magenta(" \u25CF Undo Engine") + chalk5.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
|
|
3178
3216
|
);
|
|
3179
3217
|
}
|
|
3180
3218
|
console.log("");
|
|
3181
|
-
const modeLabel = settings.mode === "audit" ?
|
|
3219
|
+
const modeLabel = settings.mode === "audit" ? chalk5.blue("audit") : settings.mode === "strict" ? chalk5.red("strict") : chalk5.white("standard");
|
|
3182
3220
|
console.log(` Mode: ${modeLabel}`);
|
|
3183
3221
|
const projectConfig = path5.join(process.cwd(), "node9.config.json");
|
|
3184
3222
|
const globalConfig = path5.join(os5.homedir(), ".node9", "config.json");
|
|
3185
3223
|
console.log(
|
|
3186
|
-
` Local: ${fs5.existsSync(projectConfig) ?
|
|
3224
|
+
` Local: ${fs5.existsSync(projectConfig) ? chalk5.green("Active (node9.config.json)") : chalk5.gray("Not present")}`
|
|
3187
3225
|
);
|
|
3188
3226
|
console.log(
|
|
3189
|
-
` Global: ${fs5.existsSync(globalConfig) ?
|
|
3227
|
+
` Global: ${fs5.existsSync(globalConfig) ? chalk5.green("Active (~/.node9/config.json)") : chalk5.gray("Not present")}`
|
|
3190
3228
|
);
|
|
3191
3229
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
3192
3230
|
console.log(
|
|
3193
|
-
` Sandbox: ${
|
|
3231
|
+
` Sandbox: ${chalk5.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
3194
3232
|
);
|
|
3195
3233
|
}
|
|
3196
3234
|
const pauseState = checkPause();
|
|
@@ -3198,7 +3236,7 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
3198
3236
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
3199
3237
|
console.log("");
|
|
3200
3238
|
console.log(
|
|
3201
|
-
|
|
3239
|
+
chalk5.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk5.gray(" \u2014 all tool calls allowed")
|
|
3202
3240
|
);
|
|
3203
3241
|
}
|
|
3204
3242
|
console.log("");
|
|
@@ -3209,13 +3247,13 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
3209
3247
|
if (cmd === "stop") return stopDaemon();
|
|
3210
3248
|
if (cmd === "status") return daemonStatus();
|
|
3211
3249
|
if (cmd !== "start" && action !== void 0) {
|
|
3212
|
-
console.error(
|
|
3250
|
+
console.error(chalk5.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
|
|
3213
3251
|
process.exit(1);
|
|
3214
3252
|
}
|
|
3215
3253
|
if (options.openui) {
|
|
3216
3254
|
if (isDaemonRunning()) {
|
|
3217
3255
|
openBrowserLocal();
|
|
3218
|
-
console.log(
|
|
3256
|
+
console.log(chalk5.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
3219
3257
|
process.exit(0);
|
|
3220
3258
|
}
|
|
3221
3259
|
const child = spawn3("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
@@ -3225,14 +3263,14 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
3225
3263
|
if (isDaemonRunning()) break;
|
|
3226
3264
|
}
|
|
3227
3265
|
openBrowserLocal();
|
|
3228
|
-
console.log(
|
|
3266
|
+
console.log(chalk5.green(`
|
|
3229
3267
|
\u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
|
|
3230
3268
|
process.exit(0);
|
|
3231
3269
|
}
|
|
3232
3270
|
if (options.background) {
|
|
3233
3271
|
const child = spawn3("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
3234
3272
|
child.unref();
|
|
3235
|
-
console.log(
|
|
3273
|
+
console.log(chalk5.green(`
|
|
3236
3274
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
3237
3275
|
process.exit(0);
|
|
3238
3276
|
}
|
|
@@ -3284,31 +3322,32 @@ RAW: ${raw}
|
|
|
3284
3322
|
const sendBlock = (msg, result2) => {
|
|
3285
3323
|
const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
|
|
3286
3324
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
3287
|
-
console.error(
|
|
3325
|
+
console.error(chalk5.red(`
|
|
3288
3326
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
3289
|
-
console.error(
|
|
3290
|
-
if (result2?.changeHint) console.error(
|
|
3327
|
+
console.error(chalk5.gray(` Triggered by: ${blockedByContext}`));
|
|
3328
|
+
if (result2?.changeHint) console.error(chalk5.cyan(` To change: ${result2.changeHint}`));
|
|
3291
3329
|
console.error("");
|
|
3292
3330
|
let aiFeedbackMessage = "";
|
|
3293
3331
|
if (isHumanDecision) {
|
|
3294
3332
|
aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: The human user specifically REJECTED this action.
|
|
3295
|
-
|
|
3333
|
+
REASON: ${msg || "No specific reason provided by user."}
|
|
3296
3334
|
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3335
|
+
INSTRUCTIONS FOR AI AGENT:
|
|
3336
|
+
- Do NOT retry this exact command immediately.
|
|
3337
|
+
- Explain to the user that you understand they blocked the action.
|
|
3338
|
+
- Ask the user if there is an alternative approach they would prefer, or if they intended to block this action entirely.
|
|
3339
|
+
- 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
3340
|
} else {
|
|
3303
3341
|
aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: Action blocked by automated policy [${blockedByContext}].
|
|
3304
|
-
|
|
3342
|
+
REASON: ${msg}
|
|
3305
3343
|
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3344
|
+
INSTRUCTIONS FOR AI AGENT:
|
|
3345
|
+
- This command violates the current security configuration.
|
|
3346
|
+
- Do NOT attempt to bypass this rule with bash syntax tricks; it will be blocked again.
|
|
3347
|
+
- Pivot to a non-destructive or read-only alternative.
|
|
3348
|
+
- Inform the user which security rule was triggered.`;
|
|
3311
3349
|
}
|
|
3350
|
+
console.error(chalk5.dim(` (Detailed instructions sent to AI agent)`));
|
|
3312
3351
|
process.stdout.write(
|
|
3313
3352
|
JSON.stringify({
|
|
3314
3353
|
decision: "block",
|
|
@@ -3349,7 +3388,7 @@ RAW: ${raw}
|
|
|
3349
3388
|
process.exit(0);
|
|
3350
3389
|
}
|
|
3351
3390
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
3352
|
-
console.error(
|
|
3391
|
+
console.error(chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
3353
3392
|
const daemonReady = await autoStartDaemonAndWait();
|
|
3354
3393
|
if (daemonReady) {
|
|
3355
3394
|
const retry = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
@@ -3457,7 +3496,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
3457
3496
|
const ms = parseDuration(options.duration);
|
|
3458
3497
|
if (ms === null) {
|
|
3459
3498
|
console.error(
|
|
3460
|
-
|
|
3499
|
+
chalk5.red(`
|
|
3461
3500
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
3462
3501
|
`)
|
|
3463
3502
|
);
|
|
@@ -3465,20 +3504,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
3465
3504
|
}
|
|
3466
3505
|
pauseNode9(ms, options.duration);
|
|
3467
3506
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
3468
|
-
console.log(
|
|
3507
|
+
console.log(chalk5.yellow(`
|
|
3469
3508
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
3470
|
-
console.log(
|
|
3471
|
-
console.log(
|
|
3509
|
+
console.log(chalk5.gray(` All tool calls will be allowed without review.`));
|
|
3510
|
+
console.log(chalk5.gray(` Run "node9 resume" to re-enable early.
|
|
3472
3511
|
`));
|
|
3473
3512
|
});
|
|
3474
3513
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
3475
3514
|
const { paused } = checkPause();
|
|
3476
3515
|
if (!paused) {
|
|
3477
|
-
console.log(
|
|
3516
|
+
console.log(chalk5.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
3478
3517
|
return;
|
|
3479
3518
|
}
|
|
3480
3519
|
resumeNode9();
|
|
3481
|
-
console.log(
|
|
3520
|
+
console.log(chalk5.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
3482
3521
|
});
|
|
3483
3522
|
var HOOK_BASED_AGENTS = {
|
|
3484
3523
|
claude: "claude",
|
|
@@ -3491,21 +3530,23 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
3491
3530
|
if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
|
|
3492
3531
|
const target = HOOK_BASED_AGENTS[firstArg];
|
|
3493
3532
|
console.error(
|
|
3494
|
-
|
|
3533
|
+
chalk5.yellow(`
|
|
3495
3534
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
3496
3535
|
);
|
|
3497
|
-
console.error(
|
|
3536
|
+
console.error(chalk5.white(`
|
|
3498
3537
|
"${target}" uses its own hook system. Use:`));
|
|
3499
3538
|
console.error(
|
|
3500
|
-
|
|
3539
|
+
chalk5.green(` node9 addto ${target} `) + chalk5.gray("# one-time setup")
|
|
3501
3540
|
);
|
|
3502
|
-
console.error(
|
|
3541
|
+
console.error(chalk5.green(` ${target} `) + chalk5.gray("# run normally"));
|
|
3503
3542
|
process.exit(1);
|
|
3504
3543
|
}
|
|
3505
3544
|
const fullCommand = commandArgs.join(" ");
|
|
3506
|
-
let result = await authorizeHeadless("shell", { command: fullCommand }
|
|
3545
|
+
let result = await authorizeHeadless("shell", { command: fullCommand }, true, {
|
|
3546
|
+
agent: "Terminal"
|
|
3547
|
+
});
|
|
3507
3548
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
3508
|
-
console.error(
|
|
3549
|
+
console.error(chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
3509
3550
|
const daemonReady = await autoStartDaemonAndWait();
|
|
3510
3551
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
3511
3552
|
}
|
|
@@ -3514,12 +3555,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
3514
3555
|
}
|
|
3515
3556
|
if (!result.approved) {
|
|
3516
3557
|
console.error(
|
|
3517
|
-
|
|
3558
|
+
chalk5.red(`
|
|
3518
3559
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
3519
3560
|
);
|
|
3520
3561
|
process.exit(1);
|
|
3521
3562
|
}
|
|
3522
|
-
console.error(
|
|
3563
|
+
console.error(chalk5.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
3523
3564
|
await runProxy(fullCommand);
|
|
3524
3565
|
} else {
|
|
3525
3566
|
program.help();
|
|
@@ -3528,20 +3569,20 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
3528
3569
|
program.command("undo").description("Revert the project to the state before the last AI action").action(async () => {
|
|
3529
3570
|
const hash = getLatestSnapshotHash();
|
|
3530
3571
|
if (!hash) {
|
|
3531
|
-
console.log(
|
|
3572
|
+
console.log(chalk5.yellow("\n\u2139\uFE0F No Undo snapshot found for this machine.\n"));
|
|
3532
3573
|
return;
|
|
3533
3574
|
}
|
|
3534
|
-
console.log(
|
|
3535
|
-
console.log(
|
|
3575
|
+
console.log(chalk5.magenta.bold("\n\u23EA NODE9 UNDO ENGINE"));
|
|
3576
|
+
console.log(chalk5.white(`Target Snapshot: ${chalk5.gray(hash.slice(0, 7))}`));
|
|
3536
3577
|
const proceed = await confirm3({
|
|
3537
3578
|
message: "Revert all files to the state before the last AI action?",
|
|
3538
3579
|
default: false
|
|
3539
3580
|
});
|
|
3540
3581
|
if (proceed) {
|
|
3541
3582
|
if (applyUndo(hash)) {
|
|
3542
|
-
console.log(
|
|
3583
|
+
console.log(chalk5.green("\u2705 Project reverted successfully.\n"));
|
|
3543
3584
|
} else {
|
|
3544
|
-
console.error(
|
|
3585
|
+
console.error(chalk5.red("\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
3545
3586
|
}
|
|
3546
3587
|
}
|
|
3547
3588
|
});
|