@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.js
CHANGED
|
@@ -27,7 +27,7 @@ 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"));
|
|
@@ -37,19 +37,69 @@ var import_sh_syntax = require("sh-syntax");
|
|
|
37
37
|
|
|
38
38
|
// src/ui/native.ts
|
|
39
39
|
var import_child_process = require("child_process");
|
|
40
|
+
var import_chalk = __toESM(require("chalk"));
|
|
40
41
|
var isTestEnv = () => {
|
|
41
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";
|
|
42
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
|
+
}
|
|
43
95
|
function sendDesktopNotification(title, body) {
|
|
44
96
|
if (isTestEnv()) return;
|
|
45
97
|
try {
|
|
46
|
-
const safeTitle = title.replace(/"/g, '\\"');
|
|
47
|
-
const safeBody = body.replace(/"/g, '\\"');
|
|
48
98
|
if (process.platform === "darwin") {
|
|
49
|
-
const script = `display notification "${
|
|
99
|
+
const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
|
|
50
100
|
(0, import_child_process.spawn)("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
|
|
51
101
|
} else if (process.platform === "linux") {
|
|
52
|
-
(0, import_child_process.spawn)("notify-send", [
|
|
102
|
+
(0, import_child_process.spawn)("notify-send", [title, body, "--icon=dialog-warning"], {
|
|
53
103
|
detached: true,
|
|
54
104
|
stdio: "ignore"
|
|
55
105
|
}).unref();
|
|
@@ -57,69 +107,54 @@ function sendDesktopNotification(title, body) {
|
|
|
57
107
|
} catch {
|
|
58
108
|
}
|
|
59
109
|
}
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const str = typeof val === "string" ? val : JSON.stringify(val);
|
|
74
|
-
const truncated = str.length > MAX_VALUE_LEN ? str.slice(0, MAX_VALUE_LEN) + "\u2026" : str;
|
|
75
|
-
return ` ${key}: ${truncated}`;
|
|
76
|
-
});
|
|
77
|
-
if (entries.length > MAX_FIELDS) {
|
|
78
|
-
lines.push(` \u2026 and ${entries.length - MAX_FIELDS} more field(s)`);
|
|
110
|
+
function escapePango(text) {
|
|
111
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
112
|
+
}
|
|
113
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
114
|
+
const lines = [];
|
|
115
|
+
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
116
|
+
lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
|
|
117
|
+
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
118
|
+
lines.push("");
|
|
119
|
+
lines.push(formattedArgs);
|
|
120
|
+
if (!locked) {
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
|
|
79
123
|
}
|
|
80
124
|
return lines.join("\n");
|
|
81
125
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (process.env.NODE9_DEBUG === "1" || process.env.VITEST) {
|
|
85
|
-
console.log(`[DEBUG Native] askNativePopup called for: ${toolName}`);
|
|
86
|
-
console.log(`[DEBUG Native] isTestEnv check:`, {
|
|
87
|
-
VITEST: process.env.VITEST,
|
|
88
|
-
NODE_ENV: process.env.NODE_ENV,
|
|
89
|
-
CI: process.env.CI,
|
|
90
|
-
isTest: isTestEnv()
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
const title = locked ? `\u26A1 Node9 \u2014 Locked by Admin Policy` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Requires Approval`;
|
|
94
|
-
let message = "";
|
|
126
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
127
|
+
const lines = [];
|
|
95
128
|
if (locked) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
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
|
|
99
|
-
`;
|
|
100
|
-
}
|
|
101
|
-
message += `Tool: ${toolName}
|
|
102
|
-
`;
|
|
103
|
-
message += `Agent: ${agent || "AI Agent"}
|
|
104
|
-
`;
|
|
105
|
-
if (explainableLabel) {
|
|
106
|
-
message += `Reason: ${explainableLabel}
|
|
107
|
-
`;
|
|
129
|
+
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
130
|
+
lines.push("");
|
|
108
131
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
lines.push(
|
|
133
|
+
`<b>\u{1F916} ${escapePango(agent || "AI Agent")}</b> | <b>\u{1F527} <tt>${escapePango(toolName)}</tt></b>`
|
|
134
|
+
);
|
|
135
|
+
lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
|
|
136
|
+
lines.push("");
|
|
137
|
+
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
112
138
|
if (!locked) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
Enter = Allow
|
|
139
|
+
lines.push("");
|
|
140
|
+
lines.push(
|
|
141
|
+
'<small>\u21B5 Enter = <b>Allow \u21B5</b> | \u238B Esc = <b>Block \u238B</b> | "Always Allow" = never ask again</small>'
|
|
142
|
+
);
|
|
116
143
|
}
|
|
117
|
-
|
|
118
|
-
|
|
144
|
+
return lines.join("\n");
|
|
145
|
+
}
|
|
146
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
|
|
147
|
+
if (isTestEnv()) return "deny";
|
|
148
|
+
const formattedArgs = formatArgs(args);
|
|
149
|
+
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
|
|
150
|
+
const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
|
|
151
|
+
process.stderr.write(import_chalk.default.yellow(`
|
|
152
|
+
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
153
|
+
`));
|
|
119
154
|
return new Promise((resolve) => {
|
|
120
155
|
let childProcess = null;
|
|
121
156
|
const onAbort = () => {
|
|
122
|
-
if (childProcess) {
|
|
157
|
+
if (childProcess && childProcess.pid) {
|
|
123
158
|
try {
|
|
124
159
|
process.kill(childProcess.pid, "SIGKILL");
|
|
125
160
|
} catch {
|
|
@@ -131,83 +166,58 @@ Enter = Allow | Click "Block" to deny`;
|
|
|
131
166
|
if (signal.aborted) return resolve("deny");
|
|
132
167
|
signal.addEventListener("abort", onAbort);
|
|
133
168
|
}
|
|
134
|
-
const cleanup = () => {
|
|
135
|
-
if (signal) signal.removeEventListener("abort", onAbort);
|
|
136
|
-
};
|
|
137
169
|
try {
|
|
138
170
|
if (process.platform === "darwin") {
|
|
139
|
-
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block", "Always Allow", "Allow"} default button "Allow" cancel button "Block"`;
|
|
140
|
-
const script = `
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
childProcess.stdout?.on("data", (d) => output += d.toString());
|
|
148
|
-
childProcess.on("close", (code) => {
|
|
149
|
-
cleanup();
|
|
150
|
-
if (locked) return resolve("deny");
|
|
151
|
-
if (code === 0) {
|
|
152
|
-
if (output.includes("Always Allow")) return resolve("always_allow");
|
|
153
|
-
if (output.includes("Allow")) return resolve("allow");
|
|
154
|
-
}
|
|
155
|
-
resolve("deny");
|
|
156
|
-
});
|
|
171
|
+
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"`;
|
|
172
|
+
const script = `on run argv
|
|
173
|
+
tell application "System Events"
|
|
174
|
+
activate
|
|
175
|
+
display dialog (item 1 of argv) with title (item 2 of argv) ${buttons}
|
|
176
|
+
end tell
|
|
177
|
+
end run`;
|
|
178
|
+
childProcess = (0, import_child_process.spawn)("osascript", ["-e", script, "--", message, title]);
|
|
157
179
|
} else if (process.platform === "linux") {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"--
|
|
167
|
-
"
|
|
168
|
-
|
|
169
|
-
"--question",
|
|
180
|
+
const pangoMessage = buildPangoMessage(
|
|
181
|
+
toolName,
|
|
182
|
+
formattedArgs,
|
|
183
|
+
agent,
|
|
184
|
+
explainableLabel,
|
|
185
|
+
locked
|
|
186
|
+
);
|
|
187
|
+
const argsList = [
|
|
188
|
+
locked ? "--info" : "--question",
|
|
189
|
+
"--modal",
|
|
190
|
+
"--width=480",
|
|
170
191
|
"--title",
|
|
171
192
|
title,
|
|
172
193
|
"--text",
|
|
173
|
-
|
|
194
|
+
pangoMessage,
|
|
174
195
|
"--ok-label",
|
|
175
|
-
"Allow",
|
|
176
|
-
"--cancel-label",
|
|
177
|
-
"Block",
|
|
178
|
-
"--extra-button",
|
|
179
|
-
"Always Allow",
|
|
196
|
+
locked ? "Waiting..." : "Allow \u21B5",
|
|
180
197
|
"--timeout",
|
|
181
198
|
"300"
|
|
182
199
|
];
|
|
200
|
+
if (!locked) {
|
|
201
|
+
argsList.push("--cancel-label", "Block \u238B");
|
|
202
|
+
argsList.push("--extra-button", "Always Allow");
|
|
203
|
+
}
|
|
183
204
|
childProcess = (0, import_child_process.spawn)("zenity", argsList);
|
|
184
|
-
let output = "";
|
|
185
|
-
childProcess.stdout?.on("data", (d) => output += d.toString());
|
|
186
|
-
childProcess.on("close", (code) => {
|
|
187
|
-
cleanup();
|
|
188
|
-
if (locked) return resolve("deny");
|
|
189
|
-
if (output.trim() === "Always Allow") return resolve("always_allow");
|
|
190
|
-
if (code === 0) return resolve("allow");
|
|
191
|
-
resolve("deny");
|
|
192
|
-
});
|
|
193
205
|
} else if (process.platform === "win32") {
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
$res = [System.Windows.MessageBox]::Show("${safeMessage}", "${safeTitle}", "${buttonType}", "Warning", "Button2", "DefaultDesktopOnly");
|
|
198
|
-
if ($res -eq "Yes") { exit 0 } else { exit 1 }`;
|
|
206
|
+
const b64Msg = Buffer.from(message).toString("base64");
|
|
207
|
+
const b64Title = Buffer.from(title).toString("base64");
|
|
208
|
+
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 }`;
|
|
199
209
|
childProcess = (0, import_child_process.spawn)("powershell", ["-Command", ps]);
|
|
200
|
-
childProcess.on("close", (code) => {
|
|
201
|
-
cleanup();
|
|
202
|
-
if (locked) return resolve("deny");
|
|
203
|
-
resolve(code === 0 ? "allow" : "deny");
|
|
204
|
-
});
|
|
205
|
-
} else {
|
|
206
|
-
cleanup();
|
|
207
|
-
resolve("deny");
|
|
208
210
|
}
|
|
211
|
+
let output = "";
|
|
212
|
+
childProcess?.stdout?.on("data", (d) => output += d.toString());
|
|
213
|
+
childProcess?.on("close", (code) => {
|
|
214
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
215
|
+
if (locked) return resolve("deny");
|
|
216
|
+
if (output.includes("Always Allow")) return resolve("always_allow");
|
|
217
|
+
if (code === 0) return resolve("allow");
|
|
218
|
+
resolve("deny");
|
|
219
|
+
});
|
|
209
220
|
} catch {
|
|
210
|
-
cleanup();
|
|
211
221
|
resolve("deny");
|
|
212
222
|
}
|
|
213
223
|
});
|
|
@@ -216,6 +226,8 @@ Enter = Allow | Click "Block" to deny`;
|
|
|
216
226
|
// src/core.ts
|
|
217
227
|
var PAUSED_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "PAUSED");
|
|
218
228
|
var TRUST_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "trust.json");
|
|
229
|
+
var LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
230
|
+
var HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
|
|
219
231
|
function checkPause() {
|
|
220
232
|
try {
|
|
221
233
|
if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -282,36 +294,39 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
282
294
|
}
|
|
283
295
|
}
|
|
284
296
|
}
|
|
285
|
-
function
|
|
297
|
+
function appendToLog(logPath, entry) {
|
|
286
298
|
try {
|
|
287
|
-
const entry = JSON.stringify({
|
|
288
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
289
|
-
tool: toolName,
|
|
290
|
-
args,
|
|
291
|
-
decision: "would-have-blocked",
|
|
292
|
-
source: "audit-mode"
|
|
293
|
-
});
|
|
294
|
-
const logPath = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
295
299
|
const dir = import_path.default.dirname(logPath);
|
|
296
300
|
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
297
|
-
import_fs.default.appendFileSync(logPath, entry + "\n");
|
|
301
|
+
import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
298
302
|
} catch {
|
|
299
303
|
}
|
|
300
304
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
305
|
+
function appendHookDebug(toolName, args, meta) {
|
|
306
|
+
const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
|
|
307
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
308
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
309
|
+
tool: toolName,
|
|
310
|
+
args: safeArgs,
|
|
311
|
+
agent: meta?.agent,
|
|
312
|
+
mcpServer: meta?.mcpServer,
|
|
313
|
+
hostname: import_os.default.hostname(),
|
|
314
|
+
cwd: process.cwd()
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
318
|
+
const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
|
|
319
|
+
appendToLog(LOCAL_AUDIT_LOG, {
|
|
320
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
321
|
+
tool: toolName,
|
|
322
|
+
args: safeArgs,
|
|
323
|
+
decision,
|
|
324
|
+
checkedBy,
|
|
325
|
+
agent: meta?.agent,
|
|
326
|
+
mcpServer: meta?.mcpServer,
|
|
327
|
+
hostname: import_os.default.hostname()
|
|
328
|
+
});
|
|
329
|
+
}
|
|
315
330
|
function tokenize(toolName) {
|
|
316
331
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
317
332
|
}
|
|
@@ -418,16 +433,28 @@ function redactSecrets(text) {
|
|
|
418
433
|
);
|
|
419
434
|
return redacted;
|
|
420
435
|
}
|
|
436
|
+
var DANGEROUS_WORDS = [
|
|
437
|
+
"drop",
|
|
438
|
+
"truncate",
|
|
439
|
+
"purge",
|
|
440
|
+
"format",
|
|
441
|
+
"destroy",
|
|
442
|
+
"terminate",
|
|
443
|
+
"revoke",
|
|
444
|
+
"docker",
|
|
445
|
+
"psql"
|
|
446
|
+
];
|
|
421
447
|
var DEFAULT_CONFIG = {
|
|
422
448
|
settings: {
|
|
423
449
|
mode: "standard",
|
|
424
450
|
autoStartDaemon: true,
|
|
425
|
-
enableUndo:
|
|
451
|
+
enableUndo: true,
|
|
452
|
+
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
426
453
|
enableHookLogDebug: false,
|
|
427
454
|
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
428
455
|
},
|
|
429
456
|
policy: {
|
|
430
|
-
sandboxPaths: [],
|
|
457
|
+
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
431
458
|
dangerousWords: DANGEROUS_WORDS,
|
|
432
459
|
ignoredTools: [
|
|
433
460
|
"list_*",
|
|
@@ -435,12 +462,44 @@ var DEFAULT_CONFIG = {
|
|
|
435
462
|
"read_*",
|
|
436
463
|
"describe_*",
|
|
437
464
|
"read",
|
|
465
|
+
"glob",
|
|
438
466
|
"grep",
|
|
439
467
|
"ls",
|
|
440
|
-
"
|
|
468
|
+
"notebookread",
|
|
469
|
+
"notebookedit",
|
|
470
|
+
"webfetch",
|
|
471
|
+
"websearch",
|
|
472
|
+
"exitplanmode",
|
|
473
|
+
"askuserquestion",
|
|
474
|
+
"agent",
|
|
475
|
+
"task*",
|
|
476
|
+
"toolsearch",
|
|
477
|
+
"mcp__ide__*",
|
|
478
|
+
"getDiagnostics"
|
|
441
479
|
],
|
|
442
|
-
toolInspection: {
|
|
443
|
-
|
|
480
|
+
toolInspection: {
|
|
481
|
+
bash: "command",
|
|
482
|
+
shell: "command",
|
|
483
|
+
run_shell_command: "command",
|
|
484
|
+
"terminal.execute": "command",
|
|
485
|
+
"postgres:query": "sql"
|
|
486
|
+
},
|
|
487
|
+
rules: [
|
|
488
|
+
{
|
|
489
|
+
action: "rm",
|
|
490
|
+
allowPaths: [
|
|
491
|
+
"**/node_modules/**",
|
|
492
|
+
"dist/**",
|
|
493
|
+
"build/**",
|
|
494
|
+
".next/**",
|
|
495
|
+
"coverage/**",
|
|
496
|
+
".cache/**",
|
|
497
|
+
"tmp/**",
|
|
498
|
+
"temp/**",
|
|
499
|
+
".DS_Store"
|
|
500
|
+
]
|
|
501
|
+
}
|
|
502
|
+
]
|
|
444
503
|
},
|
|
445
504
|
environments: {}
|
|
446
505
|
};
|
|
@@ -505,20 +564,15 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
505
564
|
}
|
|
506
565
|
const isManual = agent === "Terminal";
|
|
507
566
|
if (isManual) {
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
"
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
"revoke",
|
|
518
|
-
"docker"
|
|
519
|
-
];
|
|
520
|
-
const hasNuclear = allTokens.some((t) => NUCLEAR_COMMANDS.includes(t.toLowerCase()));
|
|
521
|
-
if (!hasNuclear) return { decision: "allow" };
|
|
567
|
+
const SYSTEM_DISASTER_COMMANDS = ["mkfs", "shred", "dd", "drop", "truncate", "purge"];
|
|
568
|
+
const hasSystemDisaster = allTokens.some(
|
|
569
|
+
(t) => SYSTEM_DISASTER_COMMANDS.includes(t.toLowerCase())
|
|
570
|
+
);
|
|
571
|
+
const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
|
|
572
|
+
if (hasSystemDisaster || isRootWipe) {
|
|
573
|
+
return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
|
|
574
|
+
}
|
|
575
|
+
return { decision: "allow" };
|
|
522
576
|
}
|
|
523
577
|
if (pathTokens.length > 0 && config.policy.sandboxPaths.length > 0) {
|
|
524
578
|
const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
|
|
@@ -532,27 +586,39 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
532
586
|
if (pathTokens.length > 0) {
|
|
533
587
|
const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
|
|
534
588
|
if (anyBlocked)
|
|
535
|
-
return {
|
|
589
|
+
return {
|
|
590
|
+
decision: "review",
|
|
591
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
|
|
592
|
+
};
|
|
536
593
|
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
537
594
|
if (allAllowed) return { decision: "allow" };
|
|
538
595
|
}
|
|
539
|
-
return {
|
|
596
|
+
return {
|
|
597
|
+
decision: "review",
|
|
598
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
|
|
599
|
+
};
|
|
540
600
|
}
|
|
541
601
|
}
|
|
602
|
+
let matchedDangerousWord;
|
|
542
603
|
const isDangerous = allTokens.some(
|
|
543
604
|
(token) => config.policy.dangerousWords.some((word) => {
|
|
544
605
|
const w = word.toLowerCase();
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
606
|
+
const hit = token === w || (() => {
|
|
607
|
+
try {
|
|
608
|
+
return new RegExp(`\\b${w}\\b`, "i").test(token);
|
|
609
|
+
} catch {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
})();
|
|
613
|
+
if (hit && !matchedDangerousWord) matchedDangerousWord = word;
|
|
614
|
+
return hit;
|
|
551
615
|
})
|
|
552
616
|
);
|
|
553
617
|
if (isDangerous) {
|
|
554
|
-
|
|
555
|
-
|
|
618
|
+
return {
|
|
619
|
+
decision: "review",
|
|
620
|
+
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`
|
|
621
|
+
};
|
|
556
622
|
}
|
|
557
623
|
if (config.settings.mode === "strict") {
|
|
558
624
|
const envConfig = getActiveEnvironment(config);
|
|
@@ -667,13 +733,16 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
667
733
|
approvers.browser = false;
|
|
668
734
|
approvers.terminal = false;
|
|
669
735
|
}
|
|
736
|
+
if (config.settings.enableHookLogDebug && !isTestEnv2) {
|
|
737
|
+
appendHookDebug(toolName, args, meta);
|
|
738
|
+
}
|
|
670
739
|
const isManual = meta?.agent === "Terminal";
|
|
671
740
|
let explainableLabel = "Local Config";
|
|
672
741
|
if (config.settings.mode === "audit") {
|
|
673
742
|
if (!isIgnoredTool(toolName)) {
|
|
674
743
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
675
744
|
if (policyResult.decision === "review") {
|
|
676
|
-
|
|
745
|
+
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
677
746
|
sendDesktopNotification(
|
|
678
747
|
"Node9 Audit Mode",
|
|
679
748
|
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
@@ -685,20 +754,24 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
685
754
|
if (!isIgnoredTool(toolName)) {
|
|
686
755
|
if (getActiveTrustSession(toolName)) {
|
|
687
756
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
757
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
688
758
|
return { approved: true, checkedBy: "trust" };
|
|
689
759
|
}
|
|
690
760
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
691
761
|
if (policyResult.decision === "allow") {
|
|
692
762
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
763
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
693
764
|
return { approved: true, checkedBy: "local-policy" };
|
|
694
765
|
}
|
|
695
766
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
696
767
|
const persistent = getPersistentDecision(toolName);
|
|
697
768
|
if (persistent === "allow") {
|
|
698
769
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
770
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
699
771
|
return { approved: true, checkedBy: "persistent" };
|
|
700
772
|
}
|
|
701
773
|
if (persistent === "deny") {
|
|
774
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "persistent-deny", meta);
|
|
702
775
|
return {
|
|
703
776
|
approved: false,
|
|
704
777
|
reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
|
|
@@ -708,6 +781,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
708
781
|
}
|
|
709
782
|
} else {
|
|
710
783
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
|
|
784
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
711
785
|
return { approved: true };
|
|
712
786
|
}
|
|
713
787
|
let cloudRequestId = null;
|
|
@@ -735,8 +809,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
735
809
|
const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
|
|
736
810
|
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;
|
|
737
811
|
console.error(
|
|
738
|
-
|
|
739
|
-
\u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) +
|
|
812
|
+
import_chalk2.default.yellow(`
|
|
813
|
+
\u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + import_chalk2.default.dim(`
|
|
740
814
|
Falling back to local rules...
|
|
741
815
|
`)
|
|
742
816
|
);
|
|
@@ -744,13 +818,13 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
744
818
|
}
|
|
745
819
|
if (cloudEnforced && cloudRequestId) {
|
|
746
820
|
console.error(
|
|
747
|
-
|
|
821
|
+
import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
|
|
748
822
|
);
|
|
749
|
-
console.error(
|
|
823
|
+
console.error(import_chalk2.default.cyan(" Dashboard \u2192 ") + import_chalk2.default.bold("Mission Control > Activity Feed\n"));
|
|
750
824
|
} else if (!cloudEnforced) {
|
|
751
825
|
const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
|
|
752
826
|
console.error(
|
|
753
|
-
|
|
827
|
+
import_chalk2.default.dim(`
|
|
754
828
|
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
755
829
|
`)
|
|
756
830
|
);
|
|
@@ -815,9 +889,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
815
889
|
try {
|
|
816
890
|
if (!approvers.native && !cloudEnforced) {
|
|
817
891
|
console.error(
|
|
818
|
-
|
|
892
|
+
import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
|
|
819
893
|
);
|
|
820
|
-
console.error(
|
|
894
|
+
console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
821
895
|
`));
|
|
822
896
|
}
|
|
823
897
|
const daemonDecision = await askDaemon(toolName, args, meta, signal);
|
|
@@ -840,11 +914,11 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
840
914
|
racePromises.push(
|
|
841
915
|
(async () => {
|
|
842
916
|
try {
|
|
843
|
-
console.log(
|
|
844
|
-
console.log(`${
|
|
845
|
-
console.log(`${
|
|
917
|
+
console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
|
|
918
|
+
console.log(`${import_chalk2.default.bold("Action:")} ${import_chalk2.default.red(toolName)}`);
|
|
919
|
+
console.log(`${import_chalk2.default.bold("Flagged By:")} ${import_chalk2.default.yellow(explainableLabel)}`);
|
|
846
920
|
if (isRemoteLocked) {
|
|
847
|
-
console.log(
|
|
921
|
+
console.log(import_chalk2.default.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
|
|
848
922
|
`));
|
|
849
923
|
await new Promise((_, reject) => {
|
|
850
924
|
signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
|
|
@@ -932,6 +1006,15 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
932
1006
|
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
933
1007
|
await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
|
|
934
1008
|
}
|
|
1009
|
+
if (!isManual) {
|
|
1010
|
+
appendLocalAudit(
|
|
1011
|
+
toolName,
|
|
1012
|
+
args,
|
|
1013
|
+
finalResult.approved ? "allow" : "deny",
|
|
1014
|
+
finalResult.checkedBy || finalResult.blockedBy || "unknown",
|
|
1015
|
+
meta
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
935
1018
|
return finalResult;
|
|
936
1019
|
}
|
|
937
1020
|
function getConfig() {
|
|
@@ -962,8 +1045,8 @@ function getConfig() {
|
|
|
962
1045
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
963
1046
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
964
1047
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
965
|
-
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
966
1048
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
1049
|
+
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
967
1050
|
if (p.toolInspection)
|
|
968
1051
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
969
1052
|
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
@@ -1091,11 +1174,11 @@ async function pollNode9SaaS(requestId, creds, signal) {
|
|
|
1091
1174
|
if (!statusRes.ok) continue;
|
|
1092
1175
|
const { status, reason } = await statusRes.json();
|
|
1093
1176
|
if (status === "APPROVED") {
|
|
1094
|
-
console.error(
|
|
1177
|
+
console.error(import_chalk2.default.green("\u2705 Approved via Cloud.\n"));
|
|
1095
1178
|
return { approved: true, reason };
|
|
1096
1179
|
}
|
|
1097
1180
|
if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
|
|
1098
|
-
console.error(
|
|
1181
|
+
console.error(import_chalk2.default.red("\u274C Denied via Cloud.\n"));
|
|
1099
1182
|
return { approved: false, reason };
|
|
1100
1183
|
}
|
|
1101
1184
|
} catch {
|
|
@@ -1123,11 +1206,11 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
1123
1206
|
var import_fs2 = __toESM(require("fs"));
|
|
1124
1207
|
var import_path2 = __toESM(require("path"));
|
|
1125
1208
|
var import_os2 = __toESM(require("os"));
|
|
1126
|
-
var
|
|
1209
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
1127
1210
|
var import_prompts2 = require("@inquirer/prompts");
|
|
1128
1211
|
function printDaemonTip() {
|
|
1129
1212
|
console.log(
|
|
1130
|
-
|
|
1213
|
+
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")
|
|
1131
1214
|
);
|
|
1132
1215
|
}
|
|
1133
1216
|
function fullPathCommand(subcommand) {
|
|
@@ -1168,7 +1251,7 @@ async function setupClaude() {
|
|
|
1168
1251
|
matcher: ".*",
|
|
1169
1252
|
hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
|
|
1170
1253
|
});
|
|
1171
|
-
console.log(
|
|
1254
|
+
console.log(import_chalk3.default.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
|
|
1172
1255
|
anythingChanged = true;
|
|
1173
1256
|
}
|
|
1174
1257
|
const hasPostHook = settings.hooks.PostToolUse?.some(
|
|
@@ -1180,7 +1263,7 @@ async function setupClaude() {
|
|
|
1180
1263
|
matcher: ".*",
|
|
1181
1264
|
hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
|
|
1182
1265
|
});
|
|
1183
|
-
console.log(
|
|
1266
|
+
console.log(import_chalk3.default.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
|
|
1184
1267
|
anythingChanged = true;
|
|
1185
1268
|
}
|
|
1186
1269
|
if (anythingChanged) {
|
|
@@ -1194,10 +1277,10 @@ async function setupClaude() {
|
|
|
1194
1277
|
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
1195
1278
|
}
|
|
1196
1279
|
if (serversToWrap.length > 0) {
|
|
1197
|
-
console.log(
|
|
1198
|
-
console.log(
|
|
1280
|
+
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
1281
|
+
console.log(import_chalk3.default.white(` ${mcpPath}`));
|
|
1199
1282
|
for (const { name, originalCmd } of serversToWrap) {
|
|
1200
|
-
console.log(
|
|
1283
|
+
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
1201
1284
|
}
|
|
1202
1285
|
console.log("");
|
|
1203
1286
|
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
@@ -1207,22 +1290,22 @@ async function setupClaude() {
|
|
|
1207
1290
|
}
|
|
1208
1291
|
claudeConfig.mcpServers = servers;
|
|
1209
1292
|
writeJson(mcpPath, claudeConfig);
|
|
1210
|
-
console.log(
|
|
1293
|
+
console.log(import_chalk3.default.green(`
|
|
1211
1294
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
1212
1295
|
anythingChanged = true;
|
|
1213
1296
|
} else {
|
|
1214
|
-
console.log(
|
|
1297
|
+
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
1215
1298
|
}
|
|
1216
1299
|
console.log("");
|
|
1217
1300
|
}
|
|
1218
1301
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
1219
|
-
console.log(
|
|
1302
|
+
console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
|
|
1220
1303
|
printDaemonTip();
|
|
1221
1304
|
return;
|
|
1222
1305
|
}
|
|
1223
1306
|
if (anythingChanged) {
|
|
1224
|
-
console.log(
|
|
1225
|
-
console.log(
|
|
1307
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
1308
|
+
console.log(import_chalk3.default.gray(" Restart Claude Code for changes to take effect."));
|
|
1226
1309
|
printDaemonTip();
|
|
1227
1310
|
}
|
|
1228
1311
|
}
|
|
@@ -1250,7 +1333,7 @@ async function setupGemini() {
|
|
|
1250
1333
|
}
|
|
1251
1334
|
]
|
|
1252
1335
|
});
|
|
1253
|
-
console.log(
|
|
1336
|
+
console.log(import_chalk3.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
1254
1337
|
anythingChanged = true;
|
|
1255
1338
|
}
|
|
1256
1339
|
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
@@ -1263,7 +1346,7 @@ async function setupGemini() {
|
|
|
1263
1346
|
matcher: ".*",
|
|
1264
1347
|
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
1265
1348
|
});
|
|
1266
|
-
console.log(
|
|
1349
|
+
console.log(import_chalk3.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
1267
1350
|
anythingChanged = true;
|
|
1268
1351
|
}
|
|
1269
1352
|
if (anythingChanged) {
|
|
@@ -1277,10 +1360,10 @@ async function setupGemini() {
|
|
|
1277
1360
|
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
1278
1361
|
}
|
|
1279
1362
|
if (serversToWrap.length > 0) {
|
|
1280
|
-
console.log(
|
|
1281
|
-
console.log(
|
|
1363
|
+
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
1364
|
+
console.log(import_chalk3.default.white(` ${settingsPath} (mcpServers)`));
|
|
1282
1365
|
for (const { name, originalCmd } of serversToWrap) {
|
|
1283
|
-
console.log(
|
|
1366
|
+
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
1284
1367
|
}
|
|
1285
1368
|
console.log("");
|
|
1286
1369
|
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
@@ -1290,22 +1373,22 @@ async function setupGemini() {
|
|
|
1290
1373
|
}
|
|
1291
1374
|
settings.mcpServers = servers;
|
|
1292
1375
|
writeJson(settingsPath, settings);
|
|
1293
|
-
console.log(
|
|
1376
|
+
console.log(import_chalk3.default.green(`
|
|
1294
1377
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
1295
1378
|
anythingChanged = true;
|
|
1296
1379
|
} else {
|
|
1297
|
-
console.log(
|
|
1380
|
+
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
1298
1381
|
}
|
|
1299
1382
|
console.log("");
|
|
1300
1383
|
}
|
|
1301
1384
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
1302
|
-
console.log(
|
|
1385
|
+
console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
|
|
1303
1386
|
printDaemonTip();
|
|
1304
1387
|
return;
|
|
1305
1388
|
}
|
|
1306
1389
|
if (anythingChanged) {
|
|
1307
|
-
console.log(
|
|
1308
|
-
console.log(
|
|
1390
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
|
|
1391
|
+
console.log(import_chalk3.default.gray(" Restart Gemini CLI for changes to take effect."));
|
|
1309
1392
|
printDaemonTip();
|
|
1310
1393
|
}
|
|
1311
1394
|
}
|
|
@@ -1324,7 +1407,7 @@ async function setupCursor() {
|
|
|
1324
1407
|
if (!hasPreHook) {
|
|
1325
1408
|
if (!hooksFile.hooks.preToolUse) hooksFile.hooks.preToolUse = [];
|
|
1326
1409
|
hooksFile.hooks.preToolUse.push({ command: fullPathCommand("check") });
|
|
1327
|
-
console.log(
|
|
1410
|
+
console.log(import_chalk3.default.green(" \u2705 preToolUse hook added \u2192 node9 check"));
|
|
1328
1411
|
anythingChanged = true;
|
|
1329
1412
|
}
|
|
1330
1413
|
const hasPostHook = hooksFile.hooks.postToolUse?.some(
|
|
@@ -1333,7 +1416,7 @@ async function setupCursor() {
|
|
|
1333
1416
|
if (!hasPostHook) {
|
|
1334
1417
|
if (!hooksFile.hooks.postToolUse) hooksFile.hooks.postToolUse = [];
|
|
1335
1418
|
hooksFile.hooks.postToolUse.push({ command: fullPathCommand("log") });
|
|
1336
|
-
console.log(
|
|
1419
|
+
console.log(import_chalk3.default.green(" \u2705 postToolUse hook added \u2192 node9 log"));
|
|
1337
1420
|
anythingChanged = true;
|
|
1338
1421
|
}
|
|
1339
1422
|
if (anythingChanged) {
|
|
@@ -1347,10 +1430,10 @@ async function setupCursor() {
|
|
|
1347
1430
|
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
1348
1431
|
}
|
|
1349
1432
|
if (serversToWrap.length > 0) {
|
|
1350
|
-
console.log(
|
|
1351
|
-
console.log(
|
|
1433
|
+
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
1434
|
+
console.log(import_chalk3.default.white(` ${mcpPath}`));
|
|
1352
1435
|
for (const { name, originalCmd } of serversToWrap) {
|
|
1353
|
-
console.log(
|
|
1436
|
+
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
1354
1437
|
}
|
|
1355
1438
|
console.log("");
|
|
1356
1439
|
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
@@ -1360,22 +1443,22 @@ async function setupCursor() {
|
|
|
1360
1443
|
}
|
|
1361
1444
|
mcpConfig.mcpServers = servers;
|
|
1362
1445
|
writeJson(mcpPath, mcpConfig);
|
|
1363
|
-
console.log(
|
|
1446
|
+
console.log(import_chalk3.default.green(`
|
|
1364
1447
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
1365
1448
|
anythingChanged = true;
|
|
1366
1449
|
} else {
|
|
1367
|
-
console.log(
|
|
1450
|
+
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
1368
1451
|
}
|
|
1369
1452
|
console.log("");
|
|
1370
1453
|
}
|
|
1371
1454
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
1372
|
-
console.log(
|
|
1455
|
+
console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Cursor."));
|
|
1373
1456
|
printDaemonTip();
|
|
1374
1457
|
return;
|
|
1375
1458
|
}
|
|
1376
1459
|
if (anythingChanged) {
|
|
1377
|
-
console.log(
|
|
1378
|
-
console.log(
|
|
1460
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor!"));
|
|
1461
|
+
console.log(import_chalk3.default.gray(" Restart Cursor for changes to take effect."));
|
|
1379
1462
|
printDaemonTip();
|
|
1380
1463
|
}
|
|
1381
1464
|
}
|
|
@@ -2354,7 +2437,7 @@ var import_path3 = __toESM(require("path"));
|
|
|
2354
2437
|
var import_os3 = __toESM(require("os"));
|
|
2355
2438
|
var import_child_process2 = require("child_process");
|
|
2356
2439
|
var import_crypto = require("crypto");
|
|
2357
|
-
var
|
|
2440
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
2358
2441
|
var DAEMON_PORT2 = 7391;
|
|
2359
2442
|
var DAEMON_HOST2 = "127.0.0.1";
|
|
2360
2443
|
var homeDir = import_os3.default.homedir();
|
|
@@ -2800,7 +2883,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
2800
2883
|
return;
|
|
2801
2884
|
}
|
|
2802
2885
|
}
|
|
2803
|
-
console.error(
|
|
2886
|
+
console.error(import_chalk4.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
|
|
2804
2887
|
process.exit(1);
|
|
2805
2888
|
});
|
|
2806
2889
|
server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
|
|
@@ -2809,17 +2892,17 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
2809
2892
|
JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
|
|
2810
2893
|
{ mode: 384 }
|
|
2811
2894
|
);
|
|
2812
|
-
console.log(
|
|
2895
|
+
console.log(import_chalk4.default.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
|
|
2813
2896
|
});
|
|
2814
2897
|
}
|
|
2815
2898
|
function stopDaemon() {
|
|
2816
|
-
if (!import_fs3.default.existsSync(DAEMON_PID_FILE)) return console.log(
|
|
2899
|
+
if (!import_fs3.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
|
|
2817
2900
|
try {
|
|
2818
2901
|
const { pid } = JSON.parse(import_fs3.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
2819
2902
|
process.kill(pid, "SIGTERM");
|
|
2820
|
-
console.log(
|
|
2903
|
+
console.log(import_chalk4.default.green("\u2705 Stopped."));
|
|
2821
2904
|
} catch {
|
|
2822
|
-
console.log(
|
|
2905
|
+
console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
|
|
2823
2906
|
} finally {
|
|
2824
2907
|
try {
|
|
2825
2908
|
import_fs3.default.unlinkSync(DAEMON_PID_FILE);
|
|
@@ -2829,13 +2912,13 @@ function stopDaemon() {
|
|
|
2829
2912
|
}
|
|
2830
2913
|
function daemonStatus() {
|
|
2831
2914
|
if (!import_fs3.default.existsSync(DAEMON_PID_FILE))
|
|
2832
|
-
return console.log(
|
|
2915
|
+
return console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
|
|
2833
2916
|
try {
|
|
2834
2917
|
const { pid } = JSON.parse(import_fs3.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
2835
2918
|
process.kill(pid, 0);
|
|
2836
|
-
console.log(
|
|
2919
|
+
console.log(import_chalk4.default.green("Node9 daemon: running"));
|
|
2837
2920
|
} catch {
|
|
2838
|
-
console.log(
|
|
2921
|
+
console.log(import_chalk4.default.yellow("Node9 daemon: not running (stale PID)"));
|
|
2839
2922
|
}
|
|
2840
2923
|
}
|
|
2841
2924
|
|
|
@@ -2843,7 +2926,7 @@ function daemonStatus() {
|
|
|
2843
2926
|
var import_child_process4 = require("child_process");
|
|
2844
2927
|
var import_execa = require("execa");
|
|
2845
2928
|
var import_execa2 = require("execa");
|
|
2846
|
-
var
|
|
2929
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
2847
2930
|
var import_readline = __toESM(require("readline"));
|
|
2848
2931
|
var import_fs5 = __toESM(require("fs"));
|
|
2849
2932
|
var import_path5 = __toESM(require("path"));
|
|
@@ -2982,7 +3065,7 @@ async function runProxy(targetCommand) {
|
|
|
2982
3065
|
if (stdout) executable = stdout.trim();
|
|
2983
3066
|
} catch {
|
|
2984
3067
|
}
|
|
2985
|
-
console.log(
|
|
3068
|
+
console.log(import_chalk5.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
|
|
2986
3069
|
const child = (0, import_child_process4.spawn)(executable, args, {
|
|
2987
3070
|
stdio: ["pipe", "pipe", "inherit"],
|
|
2988
3071
|
// We control STDIN and STDOUT
|
|
@@ -3083,92 +3166,47 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3083
3166
|
import_fs5.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3084
3167
|
}
|
|
3085
3168
|
if (options.profile && profileName !== "default") {
|
|
3086
|
-
console.log(
|
|
3087
|
-
console.log(
|
|
3169
|
+
console.log(import_chalk5.default.green(`\u2705 Profile "${profileName}" saved`));
|
|
3170
|
+
console.log(import_chalk5.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
3088
3171
|
} else if (options.local) {
|
|
3089
|
-
console.log(
|
|
3090
|
-
console.log(
|
|
3172
|
+
console.log(import_chalk5.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
3173
|
+
console.log(import_chalk5.default.gray(` All decisions stay on this machine.`));
|
|
3091
3174
|
} else {
|
|
3092
|
-
console.log(
|
|
3093
|
-
console.log(
|
|
3175
|
+
console.log(import_chalk5.default.green(`\u2705 Logged in \u2014 agent mode`));
|
|
3176
|
+
console.log(import_chalk5.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
3094
3177
|
}
|
|
3095
3178
|
});
|
|
3096
3179
|
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) => {
|
|
3097
3180
|
if (target === "gemini") return await setupGemini();
|
|
3098
3181
|
if (target === "claude") return await setupClaude();
|
|
3099
3182
|
if (target === "cursor") return await setupCursor();
|
|
3100
|
-
console.error(
|
|
3183
|
+
console.error(import_chalk5.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
3101
3184
|
process.exit(1);
|
|
3102
3185
|
});
|
|
3103
|
-
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").action((options) => {
|
|
3186
|
+
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) => {
|
|
3104
3187
|
const configPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
3105
3188
|
if (import_fs5.default.existsSync(configPath) && !options.force) {
|
|
3106
|
-
console.log(
|
|
3107
|
-
console.log(
|
|
3189
|
+
console.log(import_chalk5.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
3190
|
+
console.log(import_chalk5.default.gray(` Run with --force to overwrite.`));
|
|
3108
3191
|
return;
|
|
3109
3192
|
}
|
|
3110
|
-
const
|
|
3111
|
-
|
|
3193
|
+
const requestedMode = options.mode.toLowerCase();
|
|
3194
|
+
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
3195
|
+
const configToSave = {
|
|
3196
|
+
...DEFAULT_CONFIG,
|
|
3112
3197
|
settings: {
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
enableUndo: true,
|
|
3116
|
-
enableHookLogDebug: false,
|
|
3117
|
-
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
3118
|
-
},
|
|
3119
|
-
policy: {
|
|
3120
|
-
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
3121
|
-
dangerousWords: DANGEROUS_WORDS,
|
|
3122
|
-
ignoredTools: [
|
|
3123
|
-
"list_*",
|
|
3124
|
-
"get_*",
|
|
3125
|
-
"read_*",
|
|
3126
|
-
"describe_*",
|
|
3127
|
-
"read",
|
|
3128
|
-
"write",
|
|
3129
|
-
"edit",
|
|
3130
|
-
"glob",
|
|
3131
|
-
"grep",
|
|
3132
|
-
"ls",
|
|
3133
|
-
"notebookread",
|
|
3134
|
-
"notebookedit",
|
|
3135
|
-
"webfetch",
|
|
3136
|
-
"websearch",
|
|
3137
|
-
"exitplanmode",
|
|
3138
|
-
"askuserquestion",
|
|
3139
|
-
"agent",
|
|
3140
|
-
"task*"
|
|
3141
|
-
],
|
|
3142
|
-
toolInspection: {
|
|
3143
|
-
bash: "command",
|
|
3144
|
-
shell: "command",
|
|
3145
|
-
run_shell_command: "command",
|
|
3146
|
-
"terminal.execute": "command",
|
|
3147
|
-
"postgres:query": "sql"
|
|
3148
|
-
},
|
|
3149
|
-
rules: [
|
|
3150
|
-
{
|
|
3151
|
-
action: "rm",
|
|
3152
|
-
allowPaths: [
|
|
3153
|
-
"**/node_modules/**",
|
|
3154
|
-
"dist/**",
|
|
3155
|
-
"build/**",
|
|
3156
|
-
".next/**",
|
|
3157
|
-
"coverage/**",
|
|
3158
|
-
".cache/**",
|
|
3159
|
-
"tmp/**",
|
|
3160
|
-
"temp/**",
|
|
3161
|
-
".DS_Store"
|
|
3162
|
-
]
|
|
3163
|
-
}
|
|
3164
|
-
]
|
|
3198
|
+
...DEFAULT_CONFIG.settings,
|
|
3199
|
+
mode: safeMode
|
|
3165
3200
|
}
|
|
3166
3201
|
};
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
import_fs5.default.writeFileSync(configPath, JSON.stringify(
|
|
3170
|
-
console.log(
|
|
3171
|
-
console.log(
|
|
3202
|
+
const dir = import_path5.default.dirname(configPath);
|
|
3203
|
+
if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
|
|
3204
|
+
import_fs5.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
3205
|
+
console.log(import_chalk5.default.green(`\u2705 Global config created: ${configPath}`));
|
|
3206
|
+
console.log(import_chalk5.default.cyan(` Mode set to: ${safeMode}`));
|
|
3207
|
+
console.log(
|
|
3208
|
+
import_chalk5.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
3209
|
+
);
|
|
3172
3210
|
});
|
|
3173
3211
|
program.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
|
|
3174
3212
|
const creds = getCredentials();
|
|
@@ -3177,43 +3215,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
3177
3215
|
const settings = mergedConfig.settings;
|
|
3178
3216
|
console.log("");
|
|
3179
3217
|
if (creds && settings.approvers.cloud) {
|
|
3180
|
-
console.log(
|
|
3218
|
+
console.log(import_chalk5.default.green(" \u25CF Agent mode") + import_chalk5.default.gray(" \u2014 cloud team policy enforced"));
|
|
3181
3219
|
} else if (creds && !settings.approvers.cloud) {
|
|
3182
3220
|
console.log(
|
|
3183
|
-
|
|
3221
|
+
import_chalk5.default.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + import_chalk5.default.gray(" \u2014 all decisions stay on this machine")
|
|
3184
3222
|
);
|
|
3185
3223
|
} else {
|
|
3186
3224
|
console.log(
|
|
3187
|
-
|
|
3225
|
+
import_chalk5.default.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + import_chalk5.default.gray(" \u2014 no API key (Local rules only)")
|
|
3188
3226
|
);
|
|
3189
3227
|
}
|
|
3190
3228
|
console.log("");
|
|
3191
3229
|
if (daemonRunning) {
|
|
3192
3230
|
console.log(
|
|
3193
|
-
|
|
3231
|
+
import_chalk5.default.green(" \u25CF Daemon running") + import_chalk5.default.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
|
|
3194
3232
|
);
|
|
3195
3233
|
} else {
|
|
3196
|
-
console.log(
|
|
3234
|
+
console.log(import_chalk5.default.gray(" \u25CB Daemon stopped"));
|
|
3197
3235
|
}
|
|
3198
3236
|
if (settings.enableUndo) {
|
|
3199
3237
|
console.log(
|
|
3200
|
-
|
|
3238
|
+
import_chalk5.default.magenta(" \u25CF Undo Engine") + import_chalk5.default.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
|
|
3201
3239
|
);
|
|
3202
3240
|
}
|
|
3203
3241
|
console.log("");
|
|
3204
|
-
const modeLabel = settings.mode === "audit" ?
|
|
3242
|
+
const modeLabel = settings.mode === "audit" ? import_chalk5.default.blue("audit") : settings.mode === "strict" ? import_chalk5.default.red("strict") : import_chalk5.default.white("standard");
|
|
3205
3243
|
console.log(` Mode: ${modeLabel}`);
|
|
3206
3244
|
const projectConfig = import_path5.default.join(process.cwd(), "node9.config.json");
|
|
3207
3245
|
const globalConfig = import_path5.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
3208
3246
|
console.log(
|
|
3209
|
-
` Local: ${import_fs5.default.existsSync(projectConfig) ?
|
|
3247
|
+
` Local: ${import_fs5.default.existsSync(projectConfig) ? import_chalk5.default.green("Active (node9.config.json)") : import_chalk5.default.gray("Not present")}`
|
|
3210
3248
|
);
|
|
3211
3249
|
console.log(
|
|
3212
|
-
` Global: ${import_fs5.default.existsSync(globalConfig) ?
|
|
3250
|
+
` Global: ${import_fs5.default.existsSync(globalConfig) ? import_chalk5.default.green("Active (~/.node9/config.json)") : import_chalk5.default.gray("Not present")}`
|
|
3213
3251
|
);
|
|
3214
3252
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
3215
3253
|
console.log(
|
|
3216
|
-
` Sandbox: ${
|
|
3254
|
+
` Sandbox: ${import_chalk5.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
3217
3255
|
);
|
|
3218
3256
|
}
|
|
3219
3257
|
const pauseState = checkPause();
|
|
@@ -3221,7 +3259,7 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
3221
3259
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
3222
3260
|
console.log("");
|
|
3223
3261
|
console.log(
|
|
3224
|
-
|
|
3262
|
+
import_chalk5.default.yellow(` \u23F8 PAUSED until ${expiresAt}`) + import_chalk5.default.gray(" \u2014 all tool calls allowed")
|
|
3225
3263
|
);
|
|
3226
3264
|
}
|
|
3227
3265
|
console.log("");
|
|
@@ -3232,13 +3270,13 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
3232
3270
|
if (cmd === "stop") return stopDaemon();
|
|
3233
3271
|
if (cmd === "status") return daemonStatus();
|
|
3234
3272
|
if (cmd !== "start" && action !== void 0) {
|
|
3235
|
-
console.error(
|
|
3273
|
+
console.error(import_chalk5.default.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
|
|
3236
3274
|
process.exit(1);
|
|
3237
3275
|
}
|
|
3238
3276
|
if (options.openui) {
|
|
3239
3277
|
if (isDaemonRunning()) {
|
|
3240
3278
|
openBrowserLocal();
|
|
3241
|
-
console.log(
|
|
3279
|
+
console.log(import_chalk5.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
3242
3280
|
process.exit(0);
|
|
3243
3281
|
}
|
|
3244
3282
|
const child = (0, import_child_process4.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
@@ -3248,14 +3286,14 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
3248
3286
|
if (isDaemonRunning()) break;
|
|
3249
3287
|
}
|
|
3250
3288
|
openBrowserLocal();
|
|
3251
|
-
console.log(
|
|
3289
|
+
console.log(import_chalk5.default.green(`
|
|
3252
3290
|
\u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
|
|
3253
3291
|
process.exit(0);
|
|
3254
3292
|
}
|
|
3255
3293
|
if (options.background) {
|
|
3256
3294
|
const child = (0, import_child_process4.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
3257
3295
|
child.unref();
|
|
3258
|
-
console.log(
|
|
3296
|
+
console.log(import_chalk5.default.green(`
|
|
3259
3297
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
3260
3298
|
process.exit(0);
|
|
3261
3299
|
}
|
|
@@ -3307,31 +3345,32 @@ RAW: ${raw}
|
|
|
3307
3345
|
const sendBlock = (msg, result2) => {
|
|
3308
3346
|
const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
|
|
3309
3347
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
3310
|
-
console.error(
|
|
3348
|
+
console.error(import_chalk5.default.red(`
|
|
3311
3349
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
3312
|
-
console.error(
|
|
3313
|
-
if (result2?.changeHint) console.error(
|
|
3350
|
+
console.error(import_chalk5.default.gray(` Triggered by: ${blockedByContext}`));
|
|
3351
|
+
if (result2?.changeHint) console.error(import_chalk5.default.cyan(` To change: ${result2.changeHint}`));
|
|
3314
3352
|
console.error("");
|
|
3315
3353
|
let aiFeedbackMessage = "";
|
|
3316
3354
|
if (isHumanDecision) {
|
|
3317
3355
|
aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: The human user specifically REJECTED this action.
|
|
3318
|
-
|
|
3356
|
+
REASON: ${msg || "No specific reason provided by user."}
|
|
3319
3357
|
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3358
|
+
INSTRUCTIONS FOR AI AGENT:
|
|
3359
|
+
- Do NOT retry this exact command immediately.
|
|
3360
|
+
- Explain to the user that you understand they blocked the action.
|
|
3361
|
+
- Ask the user if there is an alternative approach they would prefer, or if they intended to block this action entirely.
|
|
3362
|
+
- 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.`;
|
|
3325
3363
|
} else {
|
|
3326
3364
|
aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: Action blocked by automated policy [${blockedByContext}].
|
|
3327
|
-
|
|
3365
|
+
REASON: ${msg}
|
|
3328
3366
|
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3367
|
+
INSTRUCTIONS FOR AI AGENT:
|
|
3368
|
+
- This command violates the current security configuration.
|
|
3369
|
+
- Do NOT attempt to bypass this rule with bash syntax tricks; it will be blocked again.
|
|
3370
|
+
- Pivot to a non-destructive or read-only alternative.
|
|
3371
|
+
- Inform the user which security rule was triggered.`;
|
|
3334
3372
|
}
|
|
3373
|
+
console.error(import_chalk5.default.dim(` (Detailed instructions sent to AI agent)`));
|
|
3335
3374
|
process.stdout.write(
|
|
3336
3375
|
JSON.stringify({
|
|
3337
3376
|
decision: "block",
|
|
@@ -3372,7 +3411,7 @@ RAW: ${raw}
|
|
|
3372
3411
|
process.exit(0);
|
|
3373
3412
|
}
|
|
3374
3413
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
3375
|
-
console.error(
|
|
3414
|
+
console.error(import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
3376
3415
|
const daemonReady = await autoStartDaemonAndWait();
|
|
3377
3416
|
if (daemonReady) {
|
|
3378
3417
|
const retry = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
@@ -3480,7 +3519,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
3480
3519
|
const ms = parseDuration(options.duration);
|
|
3481
3520
|
if (ms === null) {
|
|
3482
3521
|
console.error(
|
|
3483
|
-
|
|
3522
|
+
import_chalk5.default.red(`
|
|
3484
3523
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
3485
3524
|
`)
|
|
3486
3525
|
);
|
|
@@ -3488,20 +3527,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
3488
3527
|
}
|
|
3489
3528
|
pauseNode9(ms, options.duration);
|
|
3490
3529
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
3491
|
-
console.log(
|
|
3530
|
+
console.log(import_chalk5.default.yellow(`
|
|
3492
3531
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
3493
|
-
console.log(
|
|
3494
|
-
console.log(
|
|
3532
|
+
console.log(import_chalk5.default.gray(` All tool calls will be allowed without review.`));
|
|
3533
|
+
console.log(import_chalk5.default.gray(` Run "node9 resume" to re-enable early.
|
|
3495
3534
|
`));
|
|
3496
3535
|
});
|
|
3497
3536
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
3498
3537
|
const { paused } = checkPause();
|
|
3499
3538
|
if (!paused) {
|
|
3500
|
-
console.log(
|
|
3539
|
+
console.log(import_chalk5.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
3501
3540
|
return;
|
|
3502
3541
|
}
|
|
3503
3542
|
resumeNode9();
|
|
3504
|
-
console.log(
|
|
3543
|
+
console.log(import_chalk5.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
3505
3544
|
});
|
|
3506
3545
|
var HOOK_BASED_AGENTS = {
|
|
3507
3546
|
claude: "claude",
|
|
@@ -3514,21 +3553,23 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
3514
3553
|
if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
|
|
3515
3554
|
const target = HOOK_BASED_AGENTS[firstArg];
|
|
3516
3555
|
console.error(
|
|
3517
|
-
|
|
3556
|
+
import_chalk5.default.yellow(`
|
|
3518
3557
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
3519
3558
|
);
|
|
3520
|
-
console.error(
|
|
3559
|
+
console.error(import_chalk5.default.white(`
|
|
3521
3560
|
"${target}" uses its own hook system. Use:`));
|
|
3522
3561
|
console.error(
|
|
3523
|
-
|
|
3562
|
+
import_chalk5.default.green(` node9 addto ${target} `) + import_chalk5.default.gray("# one-time setup")
|
|
3524
3563
|
);
|
|
3525
|
-
console.error(
|
|
3564
|
+
console.error(import_chalk5.default.green(` ${target} `) + import_chalk5.default.gray("# run normally"));
|
|
3526
3565
|
process.exit(1);
|
|
3527
3566
|
}
|
|
3528
3567
|
const fullCommand = commandArgs.join(" ");
|
|
3529
|
-
let result = await authorizeHeadless("shell", { command: fullCommand }
|
|
3568
|
+
let result = await authorizeHeadless("shell", { command: fullCommand }, true, {
|
|
3569
|
+
agent: "Terminal"
|
|
3570
|
+
});
|
|
3530
3571
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
3531
|
-
console.error(
|
|
3572
|
+
console.error(import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
3532
3573
|
const daemonReady = await autoStartDaemonAndWait();
|
|
3533
3574
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
3534
3575
|
}
|
|
@@ -3537,12 +3578,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
3537
3578
|
}
|
|
3538
3579
|
if (!result.approved) {
|
|
3539
3580
|
console.error(
|
|
3540
|
-
|
|
3581
|
+
import_chalk5.default.red(`
|
|
3541
3582
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
3542
3583
|
);
|
|
3543
3584
|
process.exit(1);
|
|
3544
3585
|
}
|
|
3545
|
-
console.error(
|
|
3586
|
+
console.error(import_chalk5.default.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
3546
3587
|
await runProxy(fullCommand);
|
|
3547
3588
|
} else {
|
|
3548
3589
|
program.help();
|
|
@@ -3551,20 +3592,20 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
3551
3592
|
program.command("undo").description("Revert the project to the state before the last AI action").action(async () => {
|
|
3552
3593
|
const hash = getLatestSnapshotHash();
|
|
3553
3594
|
if (!hash) {
|
|
3554
|
-
console.log(
|
|
3595
|
+
console.log(import_chalk5.default.yellow("\n\u2139\uFE0F No Undo snapshot found for this machine.\n"));
|
|
3555
3596
|
return;
|
|
3556
3597
|
}
|
|
3557
|
-
console.log(
|
|
3558
|
-
console.log(
|
|
3598
|
+
console.log(import_chalk5.default.magenta.bold("\n\u23EA NODE9 UNDO ENGINE"));
|
|
3599
|
+
console.log(import_chalk5.default.white(`Target Snapshot: ${import_chalk5.default.gray(hash.slice(0, 7))}`));
|
|
3559
3600
|
const proceed = await (0, import_prompts3.confirm)({
|
|
3560
3601
|
message: "Revert all files to the state before the last AI action?",
|
|
3561
3602
|
default: false
|
|
3562
3603
|
});
|
|
3563
3604
|
if (proceed) {
|
|
3564
3605
|
if (applyUndo(hash)) {
|
|
3565
|
-
console.log(
|
|
3606
|
+
console.log(import_chalk5.default.green("\u2705 Project reverted successfully.\n"));
|
|
3566
3607
|
} else {
|
|
3567
|
-
console.error(
|
|
3608
|
+
console.error(import_chalk5.default.red("\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
3568
3609
|
}
|
|
3569
3610
|
}
|
|
3570
3611
|
});
|