@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.mjs CHANGED
@@ -79,21 +79,47 @@ function sendDesktopNotification(title, body) {
79
79
  } catch {
80
80
  }
81
81
  }
82
+ function escapePango(text) {
83
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
84
+ }
85
+ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
86
+ const lines = [];
87
+ if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
88
+ lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
89
+ lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
90
+ lines.push("");
91
+ lines.push(formattedArgs);
92
+ if (!locked) {
93
+ lines.push("");
94
+ lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
95
+ }
96
+ return lines.join("\n");
97
+ }
98
+ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
99
+ const lines = [];
100
+ if (locked) {
101
+ lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
102
+ lines.push("");
103
+ }
104
+ lines.push(
105
+ `<b>\u{1F916} ${escapePango(agent || "AI Agent")}</b> | <b>\u{1F527} <tt>${escapePango(toolName)}</tt></b>`
106
+ );
107
+ lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
108
+ lines.push("");
109
+ lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
110
+ if (!locked) {
111
+ lines.push("");
112
+ lines.push(
113
+ '<small>\u21B5 Enter = <b>Allow \u21B5</b> | \u238B Esc = <b>Block \u238B</b> | "Always Allow" = never ask again</small>'
114
+ );
115
+ }
116
+ return lines.join("\n");
117
+ }
82
118
  async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
83
119
  if (isTestEnv()) return "deny";
84
120
  const formattedArgs = formatArgs(args);
85
121
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
86
- let message = "";
87
- if (locked) message += `\u26A0\uFE0F LOCKED BY ADMIN POLICY
88
- `;
89
- message += `Tool: ${toolName}
90
- `;
91
- message += `Agent: ${agent || "AI Agent"}
92
- `;
93
- message += `Rule: ${explainableLabel || "Security Policy"}
94
-
95
- `;
96
- message += `${formattedArgs}`;
122
+ const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
97
123
  process.stderr.write(chalk.yellow(`
98
124
  \u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
99
125
  `));
@@ -114,7 +140,7 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
114
140
  }
115
141
  try {
116
142
  if (process.platform === "darwin") {
117
- const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block", "Always Allow", "Allow"} default button "Allow" cancel button "Block"`;
143
+ 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"`;
118
144
  const script = `on run argv
119
145
  tell application "System Events"
120
146
  activate
@@ -123,21 +149,28 @@ end tell
123
149
  end run`;
124
150
  childProcess = spawn("osascript", ["-e", script, "--", message, title]);
125
151
  } else if (process.platform === "linux") {
152
+ const pangoMessage = buildPangoMessage(
153
+ toolName,
154
+ formattedArgs,
155
+ agent,
156
+ explainableLabel,
157
+ locked
158
+ );
126
159
  const argsList = [
127
160
  locked ? "--info" : "--question",
128
161
  "--modal",
129
- "--width=450",
162
+ "--width=480",
130
163
  "--title",
131
164
  title,
132
165
  "--text",
133
- message,
166
+ pangoMessage,
134
167
  "--ok-label",
135
- locked ? "Waiting..." : "Allow",
168
+ locked ? "Waiting..." : "Allow \u21B5",
136
169
  "--timeout",
137
170
  "300"
138
171
  ];
139
172
  if (!locked) {
140
- argsList.push("--cancel-label", "Block");
173
+ argsList.push("--cancel-label", "Block \u238B");
141
174
  argsList.push("--extra-button", "Always Allow");
142
175
  }
143
176
  childProcess = spawn("zenity", argsList);
@@ -165,6 +198,8 @@ end run`;
165
198
  // src/core.ts
166
199
  var PAUSED_FILE = path.join(os.homedir(), ".node9", "PAUSED");
167
200
  var TRUST_FILE = path.join(os.homedir(), ".node9", "trust.json");
201
+ var LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
202
+ var HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
168
203
  function checkPause() {
169
204
  try {
170
205
  if (!fs.existsSync(PAUSED_FILE)) return { paused: false };
@@ -221,36 +256,39 @@ function writeTrustSession(toolName, durationMs) {
221
256
  }
222
257
  }
223
258
  }
224
- function appendAuditModeEntry(toolName, args) {
259
+ function appendToLog(logPath, entry) {
225
260
  try {
226
- const entry = JSON.stringify({
227
- ts: (/* @__PURE__ */ new Date()).toISOString(),
228
- tool: toolName,
229
- args,
230
- decision: "would-have-blocked",
231
- source: "audit-mode"
232
- });
233
- const logPath = path.join(os.homedir(), ".node9", "audit.log");
234
261
  const dir = path.dirname(logPath);
235
262
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
236
- fs.appendFileSync(logPath, entry + "\n");
263
+ fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
237
264
  } catch {
238
265
  }
239
266
  }
240
- var DANGEROUS_WORDS = [
241
- "delete",
242
- "drop",
243
- "remove",
244
- "terminate",
245
- "refund",
246
- "write",
247
- "update",
248
- "destroy",
249
- "rm",
250
- "rmdir",
251
- "purge",
252
- "format"
253
- ];
267
+ function appendHookDebug(toolName, args, meta) {
268
+ const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
269
+ appendToLog(HOOK_DEBUG_LOG, {
270
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
271
+ tool: toolName,
272
+ args: safeArgs,
273
+ agent: meta?.agent,
274
+ mcpServer: meta?.mcpServer,
275
+ hostname: os.hostname(),
276
+ cwd: process.cwd()
277
+ });
278
+ }
279
+ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
280
+ const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
281
+ appendToLog(LOCAL_AUDIT_LOG, {
282
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
283
+ tool: toolName,
284
+ args: safeArgs,
285
+ decision,
286
+ checkedBy,
287
+ agent: meta?.agent,
288
+ mcpServer: meta?.mcpServer,
289
+ hostname: os.hostname()
290
+ });
291
+ }
254
292
  function tokenize(toolName) {
255
293
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
256
294
  }
@@ -344,16 +382,41 @@ async function analyzeShellCommand(command) {
344
382
  }
345
383
  return { actions, paths, allTokens };
346
384
  }
385
+ function redactSecrets(text) {
386
+ if (!text) return text;
387
+ let redacted = text;
388
+ redacted = redacted.replace(
389
+ /(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._\-\/\\=]+/gi,
390
+ "$1********"
391
+ );
392
+ redacted = redacted.replace(
393
+ /(api[_-]?key|secret|password|token)([:=]\s*['"]?)[a-zA-Z0-9._\-]{8,}/gi,
394
+ "$1$2********"
395
+ );
396
+ return redacted;
397
+ }
398
+ var DANGEROUS_WORDS = [
399
+ "drop",
400
+ "truncate",
401
+ "purge",
402
+ "format",
403
+ "destroy",
404
+ "terminate",
405
+ "revoke",
406
+ "docker",
407
+ "psql"
408
+ ];
347
409
  var DEFAULT_CONFIG = {
348
410
  settings: {
349
411
  mode: "standard",
350
412
  autoStartDaemon: true,
351
- enableUndo: false,
413
+ enableUndo: true,
414
+ // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
352
415
  enableHookLogDebug: false,
353
416
  approvers: { native: true, browser: true, cloud: true, terminal: true }
354
417
  },
355
418
  policy: {
356
- sandboxPaths: [],
419
+ sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
357
420
  dangerousWords: DANGEROUS_WORDS,
358
421
  ignoredTools: [
359
422
  "list_*",
@@ -361,12 +424,44 @@ var DEFAULT_CONFIG = {
361
424
  "read_*",
362
425
  "describe_*",
363
426
  "read",
427
+ "glob",
364
428
  "grep",
365
429
  "ls",
366
- "askuserquestion"
430
+ "notebookread",
431
+ "notebookedit",
432
+ "webfetch",
433
+ "websearch",
434
+ "exitplanmode",
435
+ "askuserquestion",
436
+ "agent",
437
+ "task*",
438
+ "toolsearch",
439
+ "mcp__ide__*",
440
+ "getDiagnostics"
367
441
  ],
368
- toolInspection: { bash: "command", shell: "command" },
369
- rules: [{ action: "rm", allowPaths: ["**/node_modules/**", "dist/**", ".DS_Store"] }]
442
+ toolInspection: {
443
+ bash: "command",
444
+ shell: "command",
445
+ run_shell_command: "command",
446
+ "terminal.execute": "command",
447
+ "postgres:query": "sql"
448
+ },
449
+ rules: [
450
+ {
451
+ action: "rm",
452
+ allowPaths: [
453
+ "**/node_modules/**",
454
+ "dist/**",
455
+ "build/**",
456
+ ".next/**",
457
+ "coverage/**",
458
+ ".cache/**",
459
+ "tmp/**",
460
+ "temp/**",
461
+ ".DS_Store"
462
+ ]
463
+ }
464
+ ]
370
465
  },
371
466
  environments: {}
372
467
  };
@@ -404,20 +499,15 @@ async function evaluatePolicy(toolName, args, agent) {
404
499
  }
405
500
  const isManual = agent === "Terminal";
406
501
  if (isManual) {
407
- const NUCLEAR_COMMANDS = [
408
- "drop",
409
- "destroy",
410
- "purge",
411
- "rmdir",
412
- "format",
413
- "truncate",
414
- "alter",
415
- "grant",
416
- "revoke",
417
- "docker"
418
- ];
419
- const hasNuclear = allTokens.some((t) => NUCLEAR_COMMANDS.includes(t.toLowerCase()));
420
- if (!hasNuclear) return { decision: "allow" };
502
+ const SYSTEM_DISASTER_COMMANDS = ["mkfs", "shred", "dd", "drop", "truncate", "purge"];
503
+ const hasSystemDisaster = allTokens.some(
504
+ (t) => SYSTEM_DISASTER_COMMANDS.includes(t.toLowerCase())
505
+ );
506
+ const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
507
+ if (hasSystemDisaster || isRootWipe) {
508
+ return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
509
+ }
510
+ return { decision: "allow" };
421
511
  }
422
512
  if (pathTokens.length > 0 && config.policy.sandboxPaths.length > 0) {
423
513
  const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
@@ -431,27 +521,39 @@ async function evaluatePolicy(toolName, args, agent) {
431
521
  if (pathTokens.length > 0) {
432
522
  const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
433
523
  if (anyBlocked)
434
- return { decision: "review", blockedByLabel: "Project/Global Config (Rule Block)" };
524
+ return {
525
+ decision: "review",
526
+ blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
527
+ };
435
528
  const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
436
529
  if (allAllowed) return { decision: "allow" };
437
530
  }
438
- return { decision: "review", blockedByLabel: "Project/Global Config (Rule Default Block)" };
531
+ return {
532
+ decision: "review",
533
+ blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
534
+ };
439
535
  }
440
536
  }
537
+ let matchedDangerousWord;
441
538
  const isDangerous = allTokens.some(
442
539
  (token) => config.policy.dangerousWords.some((word) => {
443
540
  const w = word.toLowerCase();
444
- if (token === w) return true;
445
- try {
446
- return new RegExp(`\\b${w}\\b`, "i").test(token);
447
- } catch {
448
- return false;
449
- }
541
+ const hit = token === w || (() => {
542
+ try {
543
+ return new RegExp(`\\b${w}\\b`, "i").test(token);
544
+ } catch {
545
+ return false;
546
+ }
547
+ })();
548
+ if (hit && !matchedDangerousWord) matchedDangerousWord = word;
549
+ return hit;
450
550
  })
451
551
  );
452
552
  if (isDangerous) {
453
- const label = isManual ? "Manual Nuclear Protection" : "Project/Global Config (Dangerous Word)";
454
- return { decision: "review", blockedByLabel: label };
553
+ return {
554
+ decision: "review",
555
+ blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`
556
+ };
455
557
  }
456
558
  if (config.settings.mode === "strict") {
457
559
  const envConfig = getActiveEnvironment(config);
@@ -566,13 +668,16 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
566
668
  approvers.browser = false;
567
669
  approvers.terminal = false;
568
670
  }
671
+ if (config.settings.enableHookLogDebug && !isTestEnv2) {
672
+ appendHookDebug(toolName, args, meta);
673
+ }
569
674
  const isManual = meta?.agent === "Terminal";
570
675
  let explainableLabel = "Local Config";
571
676
  if (config.settings.mode === "audit") {
572
677
  if (!isIgnoredTool(toolName)) {
573
678
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
574
679
  if (policyResult.decision === "review") {
575
- appendAuditModeEntry(toolName, args);
680
+ appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
576
681
  sendDesktopNotification(
577
682
  "Node9 Audit Mode",
578
683
  `Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
@@ -584,20 +689,24 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
584
689
  if (!isIgnoredTool(toolName)) {
585
690
  if (getActiveTrustSession(toolName)) {
586
691
  if (creds?.apiKey) auditLocalAllow(toolName, args, "trust", creds, meta);
692
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
587
693
  return { approved: true, checkedBy: "trust" };
588
694
  }
589
695
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
590
696
  if (policyResult.decision === "allow") {
591
697
  if (creds?.apiKey) auditLocalAllow(toolName, args, "local-policy", creds, meta);
698
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
592
699
  return { approved: true, checkedBy: "local-policy" };
593
700
  }
594
701
  explainableLabel = policyResult.blockedByLabel || "Local Config";
595
702
  const persistent = getPersistentDecision(toolName);
596
703
  if (persistent === "allow") {
597
704
  if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
705
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
598
706
  return { approved: true, checkedBy: "persistent" };
599
707
  }
600
708
  if (persistent === "deny") {
709
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "persistent-deny", meta);
601
710
  return {
602
711
  approved: false,
603
712
  reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
@@ -607,6 +716,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
607
716
  }
608
717
  } else {
609
718
  if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
719
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
610
720
  return { approved: true };
611
721
  }
612
722
  let cloudRequestId = null;
@@ -831,6 +941,15 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
831
941
  if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
832
942
  await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
833
943
  }
944
+ if (!isManual) {
945
+ appendLocalAudit(
946
+ toolName,
947
+ args,
948
+ finalResult.approved ? "allow" : "deny",
949
+ finalResult.checkedBy || finalResult.blockedBy || "unknown",
950
+ meta
951
+ );
952
+ }
834
953
  return finalResult;
835
954
  }
836
955
  function getConfig() {
@@ -861,8 +980,8 @@ function getConfig() {
861
980
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
862
981
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
863
982
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
864
- if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
865
983
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
984
+ if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
866
985
  if (p.toolInspection)
867
986
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
868
987
  if (p.rules) mergedPolicy.rules.push(...p.rules);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",