@node9/proxy 1.0.1 → 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
@@ -115,21 +115,47 @@ function sendDesktopNotification(title, body) {
115
115
  } catch {
116
116
  }
117
117
  }
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');
131
+ }
132
+ return lines.join("\n");
133
+ }
134
+ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
135
+ const lines = [];
136
+ if (locked) {
137
+ lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
138
+ lines.push("");
139
+ }
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>`);
146
+ if (!locked) {
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
+ );
151
+ }
152
+ return lines.join("\n");
153
+ }
118
154
  async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
119
155
  if (isTestEnv()) return "deny";
120
156
  const formattedArgs = formatArgs(args);
121
157
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
122
- let message = "";
123
- if (locked) message += `\u26A0\uFE0F LOCKED BY ADMIN POLICY
124
- `;
125
- message += `Tool: ${toolName}
126
- `;
127
- message += `Agent: ${agent || "AI Agent"}
128
- `;
129
- message += `Rule: ${explainableLabel || "Security Policy"}
130
-
131
- `;
132
- message += `${formattedArgs}`;
158
+ const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
133
159
  process.stderr.write(import_chalk.default.yellow(`
134
160
  \u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
135
161
  `));
@@ -150,7 +176,7 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
150
176
  }
151
177
  try {
152
178
  if (process.platform === "darwin") {
153
- const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block", "Always Allow", "Allow"} default button "Allow" cancel button "Block"`;
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"`;
154
180
  const script = `on run argv
155
181
  tell application "System Events"
156
182
  activate
@@ -159,21 +185,28 @@ end tell
159
185
  end run`;
160
186
  childProcess = (0, import_child_process.spawn)("osascript", ["-e", script, "--", message, title]);
161
187
  } else if (process.platform === "linux") {
188
+ const pangoMessage = buildPangoMessage(
189
+ toolName,
190
+ formattedArgs,
191
+ agent,
192
+ explainableLabel,
193
+ locked
194
+ );
162
195
  const argsList = [
163
196
  locked ? "--info" : "--question",
164
197
  "--modal",
165
- "--width=450",
198
+ "--width=480",
166
199
  "--title",
167
200
  title,
168
201
  "--text",
169
- message,
202
+ pangoMessage,
170
203
  "--ok-label",
171
- locked ? "Waiting..." : "Allow",
204
+ locked ? "Waiting..." : "Allow \u21B5",
172
205
  "--timeout",
173
206
  "300"
174
207
  ];
175
208
  if (!locked) {
176
- argsList.push("--cancel-label", "Block");
209
+ argsList.push("--cancel-label", "Block \u238B");
177
210
  argsList.push("--extra-button", "Always Allow");
178
211
  }
179
212
  childProcess = (0, import_child_process.spawn)("zenity", argsList);
@@ -201,6 +234,8 @@ end run`;
201
234
  // src/core.ts
202
235
  var PAUSED_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "PAUSED");
203
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");
204
239
  function checkPause() {
205
240
  try {
206
241
  if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
@@ -257,36 +292,39 @@ function writeTrustSession(toolName, durationMs) {
257
292
  }
258
293
  }
259
294
  }
260
- function appendAuditModeEntry(toolName, args) {
295
+ function appendToLog(logPath, entry) {
261
296
  try {
262
- const entry = JSON.stringify({
263
- ts: (/* @__PURE__ */ new Date()).toISOString(),
264
- tool: toolName,
265
- args,
266
- decision: "would-have-blocked",
267
- source: "audit-mode"
268
- });
269
- const logPath = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
270
297
  const dir = import_path.default.dirname(logPath);
271
298
  if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
272
- import_fs.default.appendFileSync(logPath, entry + "\n");
299
+ import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
273
300
  } catch {
274
301
  }
275
302
  }
276
- var DANGEROUS_WORDS = [
277
- "delete",
278
- "drop",
279
- "remove",
280
- "terminate",
281
- "refund",
282
- "write",
283
- "update",
284
- "destroy",
285
- "rm",
286
- "rmdir",
287
- "purge",
288
- "format"
289
- ];
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
+ }
290
328
  function tokenize(toolName) {
291
329
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
292
330
  }
@@ -380,16 +418,41 @@ async function analyzeShellCommand(command) {
380
418
  }
381
419
  return { actions, paths, allTokens };
382
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
+ ];
383
445
  var DEFAULT_CONFIG = {
384
446
  settings: {
385
447
  mode: "standard",
386
448
  autoStartDaemon: true,
387
- enableUndo: false,
449
+ enableUndo: true,
450
+ // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
388
451
  enableHookLogDebug: false,
389
452
  approvers: { native: true, browser: true, cloud: true, terminal: true }
390
453
  },
391
454
  policy: {
392
- sandboxPaths: [],
455
+ sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
393
456
  dangerousWords: DANGEROUS_WORDS,
394
457
  ignoredTools: [
395
458
  "list_*",
@@ -397,12 +460,44 @@ var DEFAULT_CONFIG = {
397
460
  "read_*",
398
461
  "describe_*",
399
462
  "read",
463
+ "glob",
400
464
  "grep",
401
465
  "ls",
402
- "askuserquestion"
466
+ "notebookread",
467
+ "notebookedit",
468
+ "webfetch",
469
+ "websearch",
470
+ "exitplanmode",
471
+ "askuserquestion",
472
+ "agent",
473
+ "task*",
474
+ "toolsearch",
475
+ "mcp__ide__*",
476
+ "getDiagnostics"
403
477
  ],
404
- toolInspection: { bash: "command", shell: "command" },
405
- 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
+ ]
406
501
  },
407
502
  environments: {}
408
503
  };
@@ -440,20 +535,15 @@ async function evaluatePolicy(toolName, args, agent) {
440
535
  }
441
536
  const isManual = agent === "Terminal";
442
537
  if (isManual) {
443
- const NUCLEAR_COMMANDS = [
444
- "drop",
445
- "destroy",
446
- "purge",
447
- "rmdir",
448
- "format",
449
- "truncate",
450
- "alter",
451
- "grant",
452
- "revoke",
453
- "docker"
454
- ];
455
- const hasNuclear = allTokens.some((t) => NUCLEAR_COMMANDS.includes(t.toLowerCase()));
456
- 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" };
457
547
  }
458
548
  if (pathTokens.length > 0 && config.policy.sandboxPaths.length > 0) {
459
549
  const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
@@ -467,27 +557,39 @@ async function evaluatePolicy(toolName, args, agent) {
467
557
  if (pathTokens.length > 0) {
468
558
  const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
469
559
  if (anyBlocked)
470
- 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
+ };
471
564
  const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
472
565
  if (allAllowed) return { decision: "allow" };
473
566
  }
474
- 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
+ };
475
571
  }
476
572
  }
573
+ let matchedDangerousWord;
477
574
  const isDangerous = allTokens.some(
478
575
  (token) => config.policy.dangerousWords.some((word) => {
479
576
  const w = word.toLowerCase();
480
- if (token === w) return true;
481
- try {
482
- return new RegExp(`\\b${w}\\b`, "i").test(token);
483
- } catch {
484
- return false;
485
- }
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;
486
586
  })
487
587
  );
488
588
  if (isDangerous) {
489
- const label = isManual ? "Manual Nuclear Protection" : "Project/Global Config (Dangerous Word)";
490
- return { decision: "review", blockedByLabel: label };
589
+ return {
590
+ decision: "review",
591
+ blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`
592
+ };
491
593
  }
492
594
  if (config.settings.mode === "strict") {
493
595
  const envConfig = getActiveEnvironment(config);
@@ -602,13 +704,16 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
602
704
  approvers.browser = false;
603
705
  approvers.terminal = false;
604
706
  }
707
+ if (config.settings.enableHookLogDebug && !isTestEnv2) {
708
+ appendHookDebug(toolName, args, meta);
709
+ }
605
710
  const isManual = meta?.agent === "Terminal";
606
711
  let explainableLabel = "Local Config";
607
712
  if (config.settings.mode === "audit") {
608
713
  if (!isIgnoredTool(toolName)) {
609
714
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
610
715
  if (policyResult.decision === "review") {
611
- appendAuditModeEntry(toolName, args);
716
+ appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
612
717
  sendDesktopNotification(
613
718
  "Node9 Audit Mode",
614
719
  `Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
@@ -620,20 +725,24 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
620
725
  if (!isIgnoredTool(toolName)) {
621
726
  if (getActiveTrustSession(toolName)) {
622
727
  if (creds?.apiKey) auditLocalAllow(toolName, args, "trust", creds, meta);
728
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
623
729
  return { approved: true, checkedBy: "trust" };
624
730
  }
625
731
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
626
732
  if (policyResult.decision === "allow") {
627
733
  if (creds?.apiKey) auditLocalAllow(toolName, args, "local-policy", creds, meta);
734
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
628
735
  return { approved: true, checkedBy: "local-policy" };
629
736
  }
630
737
  explainableLabel = policyResult.blockedByLabel || "Local Config";
631
738
  const persistent = getPersistentDecision(toolName);
632
739
  if (persistent === "allow") {
633
740
  if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
741
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
634
742
  return { approved: true, checkedBy: "persistent" };
635
743
  }
636
744
  if (persistent === "deny") {
745
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "persistent-deny", meta);
637
746
  return {
638
747
  approved: false,
639
748
  reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
@@ -643,6 +752,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
643
752
  }
644
753
  } else {
645
754
  if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
755
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
646
756
  return { approved: true };
647
757
  }
648
758
  let cloudRequestId = null;
@@ -867,6 +977,15 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
867
977
  if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
868
978
  await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
869
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
+ }
870
989
  return finalResult;
871
990
  }
872
991
  function getConfig() {
@@ -897,8 +1016,8 @@ function getConfig() {
897
1016
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
898
1017
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
899
1018
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
900
- if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
901
1019
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
1020
+ if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
902
1021
  if (p.toolInspection)
903
1022
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
904
1023
  if (p.rules) mergedPolicy.rules.push(...p.rules);