@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/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 import_chalk = __toESM(require("chalk"));
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 "${safeBody}" with title "${safeTitle}"`;
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", [safeTitle, safeBody, "--icon=dialog-warning"], {
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 formatArgs(args) {
69
- if (args === null || args === void 0) return "(none)";
70
- if (typeof args !== "object" || Array.isArray(args)) {
71
- const str = typeof args === "string" ? args : JSON.stringify(args);
72
- return str.length > 200 ? str.slice(0, 200) + "\u2026" : str;
73
- }
74
- const entries = Object.entries(args).filter(
75
- ([, v]) => v !== null && v !== void 0 && v !== ""
76
- );
77
- if (entries.length === 0) return "(none)";
78
- const MAX_FIELDS = 5;
79
- const MAX_VALUE_LEN = 120;
80
- const lines = entries.slice(0, MAX_FIELDS).map(([key, val]) => {
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
91
- if (isTestEnv()) return "deny";
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
- message += `\u26A1 Awaiting remote approval via Slack. Local override is disabled.
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
- message += `
118
- Arguments:
119
- ${formatArgs(args)}`;
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
- message += `
122
-
123
- Enter = Allow | Click "Block" to deny`;
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
- const safeMessage = message.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, "'");
126
- const safeTitle = title.replace(/"/g, '\\"');
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
- tell application "System Events"
150
- activate
151
- display dialog "${safeMessage}" with title "${safeTitle}" ${buttons}
152
- end tell`;
153
- childProcess = (0, import_child_process.spawn)("osascript", ["-e", script]);
154
- let output = "";
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 argsList = locked ? [
167
- "--info",
168
- "--title",
169
- title,
170
- "--text",
171
- safeMessage,
172
- "--ok-label",
173
- "Waiting for Slack\u2026",
174
- "--timeout",
175
- "300"
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
- safeMessage,
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 buttonType = locked ? "OK" : "YesNo";
203
- const ps = `
204
- Add-Type -AssemblyName PresentationFramework;
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 appendAuditModeEntry(toolName, args) {
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
- var DANGEROUS_WORDS = [
300
- "delete",
301
- "drop",
302
- "remove",
303
- "terminate",
304
- "refund",
305
- "write",
306
- "update",
307
- "destroy",
308
- "rm",
309
- "rmdir",
310
- "purge",
311
- "format"
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: false,
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
- "askuserquestion"
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: { bash: "command", shell: "command" },
428
- rules: [{ action: "rm", allowPaths: ["**/node_modules/**", "dist/**", ".DS_Store"] }]
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 NUCLEAR_COMMANDS = [
467
- "drop",
468
- "destroy",
469
- "purge",
470
- "rmdir",
471
- "format",
472
- "truncate",
473
- "alter",
474
- "grant",
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 { decision: "review", blockedByLabel: "Project/Global Config (Rule Block)" };
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 { decision: "review", blockedByLabel: "Project/Global Config (Rule Default Block)" };
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
- if (token === w) return true;
504
- try {
505
- return new RegExp(`\\b${w}\\b`, "i").test(token);
506
- } catch {
507
- return false;
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
- const label = isManual ? "Manual Nuclear Protection" : "Project/Global Config (Dangerous Word)";
513
- return { decision: "review", blockedByLabel: label };
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
- appendAuditModeEntry(toolName, args);
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
- import_chalk.default.yellow(`
697
- \u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + import_chalk.default.dim(`
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
- import_chalk.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
792
+ import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
706
793
  );
707
- console.error(import_chalk.default.cyan(" Dashboard \u2192 ") + import_chalk.default.bold("Mission Control > Activity Feed\n"));
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
- import_chalk.default.dim(`
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
- import_chalk.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
863
+ import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
777
864
  );
778
- console.error(import_chalk.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
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(import_chalk.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
802
- console.log(`${import_chalk.default.bold("Action:")} ${import_chalk.default.red(toolName)}`);
803
- console.log(`${import_chalk.default.bold("Flagged By:")} ${import_chalk.default.yellow(explainableLabel)}`);
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(import_chalk.default.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
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(import_chalk.default.green("\u2705 Approved via Cloud.\n"));
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(import_chalk.default.red("\u274C Denied via Cloud.\n"));
1156
+ console.error(import_chalk2.default.red("\u274C Denied via Cloud.\n"));
1061
1157
  return { approved: false, reason };
1062
1158
  }
1063
1159
  } catch {