@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/index.js
CHANGED
|
@@ -35,7 +35,7 @@ __export(src_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(src_exports);
|
|
36
36
|
|
|
37
37
|
// src/core.ts
|
|
38
|
-
var
|
|
38
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
39
39
|
var import_prompts = require("@inquirer/prompts");
|
|
40
40
|
var import_fs = __toESM(require("fs"));
|
|
41
41
|
var import_path = __toESM(require("path"));
|
|
@@ -45,19 +45,69 @@ var import_sh_syntax = require("sh-syntax");
|
|
|
45
45
|
|
|
46
46
|
// src/ui/native.ts
|
|
47
47
|
var import_child_process = require("child_process");
|
|
48
|
+
var import_chalk = __toESM(require("chalk"));
|
|
48
49
|
var isTestEnv = () => {
|
|
49
50
|
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";
|
|
50
51
|
};
|
|
52
|
+
function smartTruncate(str, maxLen = 500) {
|
|
53
|
+
if (str.length <= maxLen) return str;
|
|
54
|
+
const edge = Math.floor(maxLen / 2) - 3;
|
|
55
|
+
return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
|
|
56
|
+
}
|
|
57
|
+
function formatArgs(args) {
|
|
58
|
+
if (args === null || args === void 0) return "(none)";
|
|
59
|
+
let parsed = args;
|
|
60
|
+
if (typeof args === "string") {
|
|
61
|
+
const trimmed = args.trim();
|
|
62
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(trimmed);
|
|
65
|
+
} catch {
|
|
66
|
+
parsed = args;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
return smartTruncate(args, 600);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
73
|
+
const obj = parsed;
|
|
74
|
+
const codeKeys = [
|
|
75
|
+
"command",
|
|
76
|
+
"cmd",
|
|
77
|
+
"shell_command",
|
|
78
|
+
"bash_command",
|
|
79
|
+
"script",
|
|
80
|
+
"code",
|
|
81
|
+
"input",
|
|
82
|
+
"sql",
|
|
83
|
+
"query",
|
|
84
|
+
"arguments",
|
|
85
|
+
"args",
|
|
86
|
+
"param",
|
|
87
|
+
"params",
|
|
88
|
+
"text"
|
|
89
|
+
];
|
|
90
|
+
const foundKey = Object.keys(obj).find((k) => codeKeys.includes(k.toLowerCase()));
|
|
91
|
+
if (foundKey) {
|
|
92
|
+
const val = obj[foundKey];
|
|
93
|
+
const str = typeof val === "string" ? val : JSON.stringify(val);
|
|
94
|
+
return `[${foundKey.toUpperCase()}]:
|
|
95
|
+
${smartTruncate(str, 500)}`;
|
|
96
|
+
}
|
|
97
|
+
return Object.entries(obj).slice(0, 5).map(
|
|
98
|
+
([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
|
|
99
|
+
).join("\n");
|
|
100
|
+
}
|
|
101
|
+
return smartTruncate(JSON.stringify(parsed), 200);
|
|
102
|
+
}
|
|
51
103
|
function sendDesktopNotification(title, body) {
|
|
52
104
|
if (isTestEnv()) return;
|
|
53
105
|
try {
|
|
54
|
-
const safeTitle = title.replace(/"/g, '\\"');
|
|
55
|
-
const safeBody = body.replace(/"/g, '\\"');
|
|
56
106
|
if (process.platform === "darwin") {
|
|
57
|
-
const script = `display notification "${
|
|
107
|
+
const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
|
|
58
108
|
(0, import_child_process.spawn)("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
|
|
59
109
|
} else if (process.platform === "linux") {
|
|
60
|
-
(0, import_child_process.spawn)("notify-send", [
|
|
110
|
+
(0, import_child_process.spawn)("notify-send", [title, body, "--icon=dialog-warning"], {
|
|
61
111
|
detached: true,
|
|
62
112
|
stdio: "ignore"
|
|
63
113
|
}).unref();
|
|
@@ -65,69 +115,54 @@ function sendDesktopNotification(title, body) {
|
|
|
65
115
|
} catch {
|
|
66
116
|
}
|
|
67
117
|
}
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const str = typeof val === "string" ? val : JSON.stringify(val);
|
|
82
|
-
const truncated = str.length > MAX_VALUE_LEN ? str.slice(0, MAX_VALUE_LEN) + "\u2026" : str;
|
|
83
|
-
return ` ${key}: ${truncated}`;
|
|
84
|
-
});
|
|
85
|
-
if (entries.length > MAX_FIELDS) {
|
|
86
|
-
lines.push(` \u2026 and ${entries.length - MAX_FIELDS} more field(s)`);
|
|
118
|
+
function escapePango(text) {
|
|
119
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
120
|
+
}
|
|
121
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
122
|
+
const lines = [];
|
|
123
|
+
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
124
|
+
lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
|
|
125
|
+
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
126
|
+
lines.push("");
|
|
127
|
+
lines.push(formattedArgs);
|
|
128
|
+
if (!locked) {
|
|
129
|
+
lines.push("");
|
|
130
|
+
lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
|
|
87
131
|
}
|
|
88
132
|
return lines.join("\n");
|
|
89
133
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (process.env.NODE9_DEBUG === "1" || process.env.VITEST) {
|
|
93
|
-
console.log(`[DEBUG Native] askNativePopup called for: ${toolName}`);
|
|
94
|
-
console.log(`[DEBUG Native] isTestEnv check:`, {
|
|
95
|
-
VITEST: process.env.VITEST,
|
|
96
|
-
NODE_ENV: process.env.NODE_ENV,
|
|
97
|
-
CI: process.env.CI,
|
|
98
|
-
isTest: isTestEnv()
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
const title = locked ? `\u26A1 Node9 \u2014 Locked by Admin Policy` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Requires Approval`;
|
|
102
|
-
let message = "";
|
|
134
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
135
|
+
const lines = [];
|
|
103
136
|
if (locked) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
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
|
|
107
|
-
`;
|
|
108
|
-
}
|
|
109
|
-
message += `Tool: ${toolName}
|
|
110
|
-
`;
|
|
111
|
-
message += `Agent: ${agent || "AI Agent"}
|
|
112
|
-
`;
|
|
113
|
-
if (explainableLabel) {
|
|
114
|
-
message += `Reason: ${explainableLabel}
|
|
115
|
-
`;
|
|
137
|
+
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
138
|
+
lines.push("");
|
|
116
139
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
140
|
+
lines.push(
|
|
141
|
+
`<b>\u{1F916} ${escapePango(agent || "AI Agent")}</b> | <b>\u{1F527} <tt>${escapePango(toolName)}</tt></b>`
|
|
142
|
+
);
|
|
143
|
+
lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
|
|
144
|
+
lines.push("");
|
|
145
|
+
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
120
146
|
if (!locked) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
Enter = Allow
|
|
147
|
+
lines.push("");
|
|
148
|
+
lines.push(
|
|
149
|
+
'<small>\u21B5 Enter = <b>Allow \u21B5</b> | \u238B Esc = <b>Block \u238B</b> | "Always Allow" = never ask again</small>'
|
|
150
|
+
);
|
|
124
151
|
}
|
|
125
|
-
|
|
126
|
-
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
|
|
155
|
+
if (isTestEnv()) return "deny";
|
|
156
|
+
const formattedArgs = formatArgs(args);
|
|
157
|
+
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
|
|
158
|
+
const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
|
|
159
|
+
process.stderr.write(import_chalk.default.yellow(`
|
|
160
|
+
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
161
|
+
`));
|
|
127
162
|
return new Promise((resolve) => {
|
|
128
163
|
let childProcess = null;
|
|
129
164
|
const onAbort = () => {
|
|
130
|
-
if (childProcess) {
|
|
165
|
+
if (childProcess && childProcess.pid) {
|
|
131
166
|
try {
|
|
132
167
|
process.kill(childProcess.pid, "SIGKILL");
|
|
133
168
|
} catch {
|
|
@@ -139,83 +174,58 @@ Enter = Allow | Click "Block" to deny`;
|
|
|
139
174
|
if (signal.aborted) return resolve("deny");
|
|
140
175
|
signal.addEventListener("abort", onAbort);
|
|
141
176
|
}
|
|
142
|
-
const cleanup = () => {
|
|
143
|
-
if (signal) signal.removeEventListener("abort", onAbort);
|
|
144
|
-
};
|
|
145
177
|
try {
|
|
146
178
|
if (process.platform === "darwin") {
|
|
147
|
-
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block", "Always Allow", "Allow"} default button "Allow" cancel button "Block"`;
|
|
148
|
-
const script = `
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
childProcess.stdout?.on("data", (d) => output += d.toString());
|
|
156
|
-
childProcess.on("close", (code) => {
|
|
157
|
-
cleanup();
|
|
158
|
-
if (locked) return resolve("deny");
|
|
159
|
-
if (code === 0) {
|
|
160
|
-
if (output.includes("Always Allow")) return resolve("always_allow");
|
|
161
|
-
if (output.includes("Allow")) return resolve("allow");
|
|
162
|
-
}
|
|
163
|
-
resolve("deny");
|
|
164
|
-
});
|
|
179
|
+
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"`;
|
|
180
|
+
const script = `on run argv
|
|
181
|
+
tell application "System Events"
|
|
182
|
+
activate
|
|
183
|
+
display dialog (item 1 of argv) with title (item 2 of argv) ${buttons}
|
|
184
|
+
end tell
|
|
185
|
+
end run`;
|
|
186
|
+
childProcess = (0, import_child_process.spawn)("osascript", ["-e", script, "--", message, title]);
|
|
165
187
|
} else if (process.platform === "linux") {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
"--
|
|
175
|
-
"
|
|
176
|
-
|
|
177
|
-
"--question",
|
|
188
|
+
const pangoMessage = buildPangoMessage(
|
|
189
|
+
toolName,
|
|
190
|
+
formattedArgs,
|
|
191
|
+
agent,
|
|
192
|
+
explainableLabel,
|
|
193
|
+
locked
|
|
194
|
+
);
|
|
195
|
+
const argsList = [
|
|
196
|
+
locked ? "--info" : "--question",
|
|
197
|
+
"--modal",
|
|
198
|
+
"--width=480",
|
|
178
199
|
"--title",
|
|
179
200
|
title,
|
|
180
201
|
"--text",
|
|
181
|
-
|
|
202
|
+
pangoMessage,
|
|
182
203
|
"--ok-label",
|
|
183
|
-
"Allow",
|
|
184
|
-
"--cancel-label",
|
|
185
|
-
"Block",
|
|
186
|
-
"--extra-button",
|
|
187
|
-
"Always Allow",
|
|
204
|
+
locked ? "Waiting..." : "Allow \u21B5",
|
|
188
205
|
"--timeout",
|
|
189
206
|
"300"
|
|
190
207
|
];
|
|
208
|
+
if (!locked) {
|
|
209
|
+
argsList.push("--cancel-label", "Block \u238B");
|
|
210
|
+
argsList.push("--extra-button", "Always Allow");
|
|
211
|
+
}
|
|
191
212
|
childProcess = (0, import_child_process.spawn)("zenity", argsList);
|
|
192
|
-
let output = "";
|
|
193
|
-
childProcess.stdout?.on("data", (d) => output += d.toString());
|
|
194
|
-
childProcess.on("close", (code) => {
|
|
195
|
-
cleanup();
|
|
196
|
-
if (locked) return resolve("deny");
|
|
197
|
-
if (output.trim() === "Always Allow") return resolve("always_allow");
|
|
198
|
-
if (code === 0) return resolve("allow");
|
|
199
|
-
resolve("deny");
|
|
200
|
-
});
|
|
201
213
|
} else if (process.platform === "win32") {
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
$res = [System.Windows.MessageBox]::Show("${safeMessage}", "${safeTitle}", "${buttonType}", "Warning", "Button2", "DefaultDesktopOnly");
|
|
206
|
-
if ($res -eq "Yes") { exit 0 } else { exit 1 }`;
|
|
214
|
+
const b64Msg = Buffer.from(message).toString("base64");
|
|
215
|
+
const b64Title = Buffer.from(title).toString("base64");
|
|
216
|
+
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 }`;
|
|
207
217
|
childProcess = (0, import_child_process.spawn)("powershell", ["-Command", ps]);
|
|
208
|
-
childProcess.on("close", (code) => {
|
|
209
|
-
cleanup();
|
|
210
|
-
if (locked) return resolve("deny");
|
|
211
|
-
resolve(code === 0 ? "allow" : "deny");
|
|
212
|
-
});
|
|
213
|
-
} else {
|
|
214
|
-
cleanup();
|
|
215
|
-
resolve("deny");
|
|
216
218
|
}
|
|
219
|
+
let output = "";
|
|
220
|
+
childProcess?.stdout?.on("data", (d) => output += d.toString());
|
|
221
|
+
childProcess?.on("close", (code) => {
|
|
222
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
223
|
+
if (locked) return resolve("deny");
|
|
224
|
+
if (output.includes("Always Allow")) return resolve("always_allow");
|
|
225
|
+
if (code === 0) return resolve("allow");
|
|
226
|
+
resolve("deny");
|
|
227
|
+
});
|
|
217
228
|
} catch {
|
|
218
|
-
cleanup();
|
|
219
229
|
resolve("deny");
|
|
220
230
|
}
|
|
221
231
|
});
|
|
@@ -224,6 +234,8 @@ Enter = Allow | Click "Block" to deny`;
|
|
|
224
234
|
// src/core.ts
|
|
225
235
|
var PAUSED_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "PAUSED");
|
|
226
236
|
var TRUST_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "trust.json");
|
|
237
|
+
var LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
238
|
+
var HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
|
|
227
239
|
function checkPause() {
|
|
228
240
|
try {
|
|
229
241
|
if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -280,36 +292,39 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
280
292
|
}
|
|
281
293
|
}
|
|
282
294
|
}
|
|
283
|
-
function
|
|
295
|
+
function appendToLog(logPath, entry) {
|
|
284
296
|
try {
|
|
285
|
-
const entry = JSON.stringify({
|
|
286
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
287
|
-
tool: toolName,
|
|
288
|
-
args,
|
|
289
|
-
decision: "would-have-blocked",
|
|
290
|
-
source: "audit-mode"
|
|
291
|
-
});
|
|
292
|
-
const logPath = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
293
297
|
const dir = import_path.default.dirname(logPath);
|
|
294
298
|
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
295
|
-
import_fs.default.appendFileSync(logPath, entry + "\n");
|
|
299
|
+
import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
296
300
|
} catch {
|
|
297
301
|
}
|
|
298
302
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
303
|
+
function appendHookDebug(toolName, args, meta) {
|
|
304
|
+
const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
|
|
305
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
306
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
307
|
+
tool: toolName,
|
|
308
|
+
args: safeArgs,
|
|
309
|
+
agent: meta?.agent,
|
|
310
|
+
mcpServer: meta?.mcpServer,
|
|
311
|
+
hostname: import_os.default.hostname(),
|
|
312
|
+
cwd: process.cwd()
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
316
|
+
const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
|
|
317
|
+
appendToLog(LOCAL_AUDIT_LOG, {
|
|
318
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
319
|
+
tool: toolName,
|
|
320
|
+
args: safeArgs,
|
|
321
|
+
decision,
|
|
322
|
+
checkedBy,
|
|
323
|
+
agent: meta?.agent,
|
|
324
|
+
mcpServer: meta?.mcpServer,
|
|
325
|
+
hostname: import_os.default.hostname()
|
|
326
|
+
});
|
|
327
|
+
}
|
|
313
328
|
function tokenize(toolName) {
|
|
314
329
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
315
330
|
}
|
|
@@ -403,16 +418,41 @@ async function analyzeShellCommand(command) {
|
|
|
403
418
|
}
|
|
404
419
|
return { actions, paths, allTokens };
|
|
405
420
|
}
|
|
421
|
+
function redactSecrets(text) {
|
|
422
|
+
if (!text) return text;
|
|
423
|
+
let redacted = text;
|
|
424
|
+
redacted = redacted.replace(
|
|
425
|
+
/(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._\-\/\\=]+/gi,
|
|
426
|
+
"$1********"
|
|
427
|
+
);
|
|
428
|
+
redacted = redacted.replace(
|
|
429
|
+
/(api[_-]?key|secret|password|token)([:=]\s*['"]?)[a-zA-Z0-9._\-]{8,}/gi,
|
|
430
|
+
"$1$2********"
|
|
431
|
+
);
|
|
432
|
+
return redacted;
|
|
433
|
+
}
|
|
434
|
+
var DANGEROUS_WORDS = [
|
|
435
|
+
"drop",
|
|
436
|
+
"truncate",
|
|
437
|
+
"purge",
|
|
438
|
+
"format",
|
|
439
|
+
"destroy",
|
|
440
|
+
"terminate",
|
|
441
|
+
"revoke",
|
|
442
|
+
"docker",
|
|
443
|
+
"psql"
|
|
444
|
+
];
|
|
406
445
|
var DEFAULT_CONFIG = {
|
|
407
446
|
settings: {
|
|
408
447
|
mode: "standard",
|
|
409
448
|
autoStartDaemon: true,
|
|
410
|
-
enableUndo:
|
|
449
|
+
enableUndo: true,
|
|
450
|
+
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
411
451
|
enableHookLogDebug: false,
|
|
412
452
|
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
413
453
|
},
|
|
414
454
|
policy: {
|
|
415
|
-
sandboxPaths: [],
|
|
455
|
+
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
416
456
|
dangerousWords: DANGEROUS_WORDS,
|
|
417
457
|
ignoredTools: [
|
|
418
458
|
"list_*",
|
|
@@ -420,12 +460,44 @@ var DEFAULT_CONFIG = {
|
|
|
420
460
|
"read_*",
|
|
421
461
|
"describe_*",
|
|
422
462
|
"read",
|
|
463
|
+
"glob",
|
|
423
464
|
"grep",
|
|
424
465
|
"ls",
|
|
425
|
-
"
|
|
466
|
+
"notebookread",
|
|
467
|
+
"notebookedit",
|
|
468
|
+
"webfetch",
|
|
469
|
+
"websearch",
|
|
470
|
+
"exitplanmode",
|
|
471
|
+
"askuserquestion",
|
|
472
|
+
"agent",
|
|
473
|
+
"task*",
|
|
474
|
+
"toolsearch",
|
|
475
|
+
"mcp__ide__*",
|
|
476
|
+
"getDiagnostics"
|
|
426
477
|
],
|
|
427
|
-
toolInspection: {
|
|
428
|
-
|
|
478
|
+
toolInspection: {
|
|
479
|
+
bash: "command",
|
|
480
|
+
shell: "command",
|
|
481
|
+
run_shell_command: "command",
|
|
482
|
+
"terminal.execute": "command",
|
|
483
|
+
"postgres:query": "sql"
|
|
484
|
+
},
|
|
485
|
+
rules: [
|
|
486
|
+
{
|
|
487
|
+
action: "rm",
|
|
488
|
+
allowPaths: [
|
|
489
|
+
"**/node_modules/**",
|
|
490
|
+
"dist/**",
|
|
491
|
+
"build/**",
|
|
492
|
+
".next/**",
|
|
493
|
+
"coverage/**",
|
|
494
|
+
".cache/**",
|
|
495
|
+
"tmp/**",
|
|
496
|
+
"temp/**",
|
|
497
|
+
".DS_Store"
|
|
498
|
+
]
|
|
499
|
+
}
|
|
500
|
+
]
|
|
429
501
|
},
|
|
430
502
|
environments: {}
|
|
431
503
|
};
|
|
@@ -463,20 +535,15 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
463
535
|
}
|
|
464
536
|
const isManual = agent === "Terminal";
|
|
465
537
|
if (isManual) {
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
"
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
"revoke",
|
|
476
|
-
"docker"
|
|
477
|
-
];
|
|
478
|
-
const hasNuclear = allTokens.some((t) => NUCLEAR_COMMANDS.includes(t.toLowerCase()));
|
|
479
|
-
if (!hasNuclear) return { decision: "allow" };
|
|
538
|
+
const SYSTEM_DISASTER_COMMANDS = ["mkfs", "shred", "dd", "drop", "truncate", "purge"];
|
|
539
|
+
const hasSystemDisaster = allTokens.some(
|
|
540
|
+
(t) => SYSTEM_DISASTER_COMMANDS.includes(t.toLowerCase())
|
|
541
|
+
);
|
|
542
|
+
const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
|
|
543
|
+
if (hasSystemDisaster || isRootWipe) {
|
|
544
|
+
return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
|
|
545
|
+
}
|
|
546
|
+
return { decision: "allow" };
|
|
480
547
|
}
|
|
481
548
|
if (pathTokens.length > 0 && config.policy.sandboxPaths.length > 0) {
|
|
482
549
|
const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
|
|
@@ -490,27 +557,39 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
490
557
|
if (pathTokens.length > 0) {
|
|
491
558
|
const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
|
|
492
559
|
if (anyBlocked)
|
|
493
|
-
return {
|
|
560
|
+
return {
|
|
561
|
+
decision: "review",
|
|
562
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
|
|
563
|
+
};
|
|
494
564
|
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
495
565
|
if (allAllowed) return { decision: "allow" };
|
|
496
566
|
}
|
|
497
|
-
return {
|
|
567
|
+
return {
|
|
568
|
+
decision: "review",
|
|
569
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
|
|
570
|
+
};
|
|
498
571
|
}
|
|
499
572
|
}
|
|
573
|
+
let matchedDangerousWord;
|
|
500
574
|
const isDangerous = allTokens.some(
|
|
501
575
|
(token) => config.policy.dangerousWords.some((word) => {
|
|
502
576
|
const w = word.toLowerCase();
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
577
|
+
const hit = token === w || (() => {
|
|
578
|
+
try {
|
|
579
|
+
return new RegExp(`\\b${w}\\b`, "i").test(token);
|
|
580
|
+
} catch {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
})();
|
|
584
|
+
if (hit && !matchedDangerousWord) matchedDangerousWord = word;
|
|
585
|
+
return hit;
|
|
509
586
|
})
|
|
510
587
|
);
|
|
511
588
|
if (isDangerous) {
|
|
512
|
-
|
|
513
|
-
|
|
589
|
+
return {
|
|
590
|
+
decision: "review",
|
|
591
|
+
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`
|
|
592
|
+
};
|
|
514
593
|
}
|
|
515
594
|
if (config.settings.mode === "strict") {
|
|
516
595
|
const envConfig = getActiveEnvironment(config);
|
|
@@ -625,13 +704,16 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
625
704
|
approvers.browser = false;
|
|
626
705
|
approvers.terminal = false;
|
|
627
706
|
}
|
|
707
|
+
if (config.settings.enableHookLogDebug && !isTestEnv2) {
|
|
708
|
+
appendHookDebug(toolName, args, meta);
|
|
709
|
+
}
|
|
628
710
|
const isManual = meta?.agent === "Terminal";
|
|
629
711
|
let explainableLabel = "Local Config";
|
|
630
712
|
if (config.settings.mode === "audit") {
|
|
631
713
|
if (!isIgnoredTool(toolName)) {
|
|
632
714
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
633
715
|
if (policyResult.decision === "review") {
|
|
634
|
-
|
|
716
|
+
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
635
717
|
sendDesktopNotification(
|
|
636
718
|
"Node9 Audit Mode",
|
|
637
719
|
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
@@ -643,20 +725,24 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
643
725
|
if (!isIgnoredTool(toolName)) {
|
|
644
726
|
if (getActiveTrustSession(toolName)) {
|
|
645
727
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
728
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
646
729
|
return { approved: true, checkedBy: "trust" };
|
|
647
730
|
}
|
|
648
731
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
649
732
|
if (policyResult.decision === "allow") {
|
|
650
733
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
734
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
651
735
|
return { approved: true, checkedBy: "local-policy" };
|
|
652
736
|
}
|
|
653
737
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
654
738
|
const persistent = getPersistentDecision(toolName);
|
|
655
739
|
if (persistent === "allow") {
|
|
656
740
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
741
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
657
742
|
return { approved: true, checkedBy: "persistent" };
|
|
658
743
|
}
|
|
659
744
|
if (persistent === "deny") {
|
|
745
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "persistent-deny", meta);
|
|
660
746
|
return {
|
|
661
747
|
approved: false,
|
|
662
748
|
reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
|
|
@@ -666,6 +752,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
666
752
|
}
|
|
667
753
|
} else {
|
|
668
754
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
|
|
755
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
669
756
|
return { approved: true };
|
|
670
757
|
}
|
|
671
758
|
let cloudRequestId = null;
|
|
@@ -693,8 +780,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
693
780
|
const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
|
|
694
781
|
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;
|
|
695
782
|
console.error(
|
|
696
|
-
|
|
697
|
-
\u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) +
|
|
783
|
+
import_chalk2.default.yellow(`
|
|
784
|
+
\u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + import_chalk2.default.dim(`
|
|
698
785
|
Falling back to local rules...
|
|
699
786
|
`)
|
|
700
787
|
);
|
|
@@ -702,13 +789,13 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
702
789
|
}
|
|
703
790
|
if (cloudEnforced && cloudRequestId) {
|
|
704
791
|
console.error(
|
|
705
|
-
|
|
792
|
+
import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
|
|
706
793
|
);
|
|
707
|
-
console.error(
|
|
794
|
+
console.error(import_chalk2.default.cyan(" Dashboard \u2192 ") + import_chalk2.default.bold("Mission Control > Activity Feed\n"));
|
|
708
795
|
} else if (!cloudEnforced) {
|
|
709
796
|
const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
|
|
710
797
|
console.error(
|
|
711
|
-
|
|
798
|
+
import_chalk2.default.dim(`
|
|
712
799
|
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
713
800
|
`)
|
|
714
801
|
);
|
|
@@ -773,9 +860,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
773
860
|
try {
|
|
774
861
|
if (!approvers.native && !cloudEnforced) {
|
|
775
862
|
console.error(
|
|
776
|
-
|
|
863
|
+
import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
|
|
777
864
|
);
|
|
778
|
-
console.error(
|
|
865
|
+
console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
779
866
|
`));
|
|
780
867
|
}
|
|
781
868
|
const daemonDecision = await askDaemon(toolName, args, meta, signal);
|
|
@@ -798,11 +885,11 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
798
885
|
racePromises.push(
|
|
799
886
|
(async () => {
|
|
800
887
|
try {
|
|
801
|
-
console.log(
|
|
802
|
-
console.log(`${
|
|
803
|
-
console.log(`${
|
|
888
|
+
console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
|
|
889
|
+
console.log(`${import_chalk2.default.bold("Action:")} ${import_chalk2.default.red(toolName)}`);
|
|
890
|
+
console.log(`${import_chalk2.default.bold("Flagged By:")} ${import_chalk2.default.yellow(explainableLabel)}`);
|
|
804
891
|
if (isRemoteLocked) {
|
|
805
|
-
console.log(
|
|
892
|
+
console.log(import_chalk2.default.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
|
|
806
893
|
`));
|
|
807
894
|
await new Promise((_, reject) => {
|
|
808
895
|
signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
|
|
@@ -890,6 +977,15 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
890
977
|
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
891
978
|
await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
|
|
892
979
|
}
|
|
980
|
+
if (!isManual) {
|
|
981
|
+
appendLocalAudit(
|
|
982
|
+
toolName,
|
|
983
|
+
args,
|
|
984
|
+
finalResult.approved ? "allow" : "deny",
|
|
985
|
+
finalResult.checkedBy || finalResult.blockedBy || "unknown",
|
|
986
|
+
meta
|
|
987
|
+
);
|
|
988
|
+
}
|
|
893
989
|
return finalResult;
|
|
894
990
|
}
|
|
895
991
|
function getConfig() {
|
|
@@ -920,8 +1016,8 @@ function getConfig() {
|
|
|
920
1016
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
921
1017
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
922
1018
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
923
|
-
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
924
1019
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
1020
|
+
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
925
1021
|
if (p.toolInspection)
|
|
926
1022
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
927
1023
|
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
@@ -1053,11 +1149,11 @@ async function pollNode9SaaS(requestId, creds, signal) {
|
|
|
1053
1149
|
if (!statusRes.ok) continue;
|
|
1054
1150
|
const { status, reason } = await statusRes.json();
|
|
1055
1151
|
if (status === "APPROVED") {
|
|
1056
|
-
console.error(
|
|
1152
|
+
console.error(import_chalk2.default.green("\u2705 Approved via Cloud.\n"));
|
|
1057
1153
|
return { approved: true, reason };
|
|
1058
1154
|
}
|
|
1059
1155
|
if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
|
|
1060
|
-
console.error(
|
|
1156
|
+
console.error(import_chalk2.default.red("\u274C Denied via Cloud.\n"));
|
|
1061
1157
|
return { approved: false, reason };
|
|
1062
1158
|
}
|
|
1063
1159
|
} catch {
|