@node9/proxy 1.0.1 → 1.0.3

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;
@@ -614,8 +724,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
614
724
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
615
725
  if (cloudEnforced) {
616
726
  try {
617
- const envConfig = getActiveEnvironment(getConfig());
618
- const initResult = await initNode9SaaS(toolName, args, creds, envConfig?.slackChannel, meta);
727
+ const initResult = await initNode9SaaS(toolName, args, creds, meta);
619
728
  if (!initResult.pending) {
620
729
  return {
621
730
  approved: !!initResult.approved,
@@ -831,6 +940,15 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
831
940
  if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
832
941
  await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
833
942
  }
943
+ if (!isManual) {
944
+ appendLocalAudit(
945
+ toolName,
946
+ args,
947
+ finalResult.approved ? "allow" : "deny",
948
+ finalResult.checkedBy || finalResult.blockedBy || "unknown",
949
+ meta
950
+ );
951
+ }
834
952
  return finalResult;
835
953
  }
836
954
  function getConfig() {
@@ -860,9 +978,10 @@ function getConfig() {
860
978
  if (s.enableHookLogDebug !== void 0)
861
979
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
862
980
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
981
+ if (s.environment !== void 0) mergedSettings.environment = s.environment;
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);
@@ -889,7 +1008,7 @@ function tryLoadConfig(filePath) {
889
1008
  }
890
1009
  }
891
1010
  function getActiveEnvironment(config) {
892
- const env = process.env.NODE_ENV || "development";
1011
+ const env = config.settings.environment || process.env.NODE_ENV || "development";
893
1012
  return config.environments[env] ?? null;
894
1013
  }
895
1014
  function getCredentials() {
@@ -949,7 +1068,7 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
949
1068
  }).catch(() => {
950
1069
  });
951
1070
  }
952
- async function initNode9SaaS(toolName, args, creds, slackChannel, meta) {
1071
+ async function initNode9SaaS(toolName, args, creds, meta) {
953
1072
  const controller = new AbortController();
954
1073
  const timeout = setTimeout(() => controller.abort(), 1e4);
955
1074
  try {
@@ -959,7 +1078,6 @@ async function initNode9SaaS(toolName, args, creds, slackChannel, meta) {
959
1078
  body: JSON.stringify({
960
1079
  toolName,
961
1080
  args,
962
- slackChannel,
963
1081
  context: {
964
1082
  agent: meta?.agent,
965
1083
  mcpServer: meta?.mcpServer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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",