@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/README.md CHANGED
@@ -11,6 +11,23 @@ While others try to _guess_ if a prompt is malicious (Semantic Security), Node9
11
11
 
12
12
  ---
13
13
 
14
+ ## 💎 The "Aha!" Moment
15
+
16
+ **AIs are literal.** When you ask an agent to "Fix my disk space," it might decide to run `docker system prune -af`.
17
+
18
+ <p align="center">
19
+ <img src="https://github.com/user-attachments/assets/0e45e843-4cf7-408e-95ce-23fb09525ee4" width="100%">
20
+ </p>
21
+
22
+ **With Node9, the interaction looks like this:**
23
+
24
+ 1. **🤖 AI attempts a "Nuke":** `Bash("docker system prune -af --volumes")`
25
+ 2. **🛡️ Node9 Intercepts:** An OS-native popup appears immediately.
26
+ 3. **🛑 User Blocks:** You click "Block" in the popup.
27
+ 4. **🧠 AI Negotiates:** Node9 explains the block to the AI. The AI responds: _"I understand. I will pivot to a safer cleanup, like removing only large log files instead."_
28
+
29
+ ---
30
+
14
31
  ## ⚡ Key Architectural Upgrades
15
32
 
16
33
  ### 🏁 The Multi-Channel Race Engine
@@ -26,6 +43,14 @@ Node9 initiates a **Concurrent Race** across all enabled channels. The first cha
26
43
 
27
44
  Node9 doesn't just "cut the wire." When a command is blocked, it injects a **Structured Negotiation Prompt** back into the AI’s context window. This teaches the AI why it was stopped and instructs it to pivot to a safer alternative or apologize to the human.
28
45
 
46
+ ### ⏪ Shadow Git Snapshots (Auto-Undo)
47
+
48
+ Node9 takes silent, lightweight Git snapshots right before an AI agent is allowed to edit or delete files. If the AI hallucinates and ruins your code, don't waste time manualy fixing it. Just run:
49
+
50
+ ```bash
51
+ node9 undo
52
+ ```
53
+
29
54
  ### 🌊 The Resolution Waterfall
30
55
 
31
56
  Security posture is resolved using a strict 5-tier waterfall:
@@ -47,8 +72,8 @@ npm install -g @node9/proxy
47
72
  node9 addto claude
48
73
  node9 addto gemini
49
74
 
50
- # 2. (Optional) Connect to Slack for remote approvals
51
- node9 login <your_api_key>
75
+ # 2. Initialize your local safety net
76
+ node9 init
52
77
 
53
78
  # 3. Check your status
54
79
  node9 status
@@ -121,9 +146,8 @@ A corporate policy has locked this action. You must click the "Approve" button i
121
146
  - [x] **AI Negotiation Loop** (Instructional feedback loop to guide LLM behavior)
122
147
  - [x] **Resolution Waterfall** (Cascading configuration: Env > Cloud > Project > Global)
123
148
  - [x] **Native OS Dialogs** (Sub-second approval via Mac/Win/Linux system windows)
124
- - [x] **One-command Agent Setup** (`node9 addto claude | gemini | cursor`)
149
+ - [x] **Shadow Git Snapshots** (1-click Undo for AI hallucinations)
125
150
  - [x] **Identity-Aware Execution** (Differentiates between Human vs. AI risk levels)
126
- - [ ] **Shadow Git Snapshots** (1-click Undo for AI hallucinations)
127
151
  - [ ] **Execution Sandboxing** (Simulate dangerous commands in a virtual FS before applying)
128
152
  - [ ] **Multi-Admin Quorum** (Require 2+ human signatures for high-stakes production actions)
129
153
  - [ ] **SOC2 Tamper-proof Audit Trail** (Cryptographically signed, cloud-managed logs)
package/dist/cli.js CHANGED
@@ -107,21 +107,47 @@ function sendDesktopNotification(title, body) {
107
107
  } catch {
108
108
  }
109
109
  }
110
+ function escapePango(text) {
111
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
112
+ }
113
+ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
114
+ const lines = [];
115
+ if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
116
+ lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
117
+ lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
118
+ lines.push("");
119
+ lines.push(formattedArgs);
120
+ if (!locked) {
121
+ lines.push("");
122
+ lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
123
+ }
124
+ return lines.join("\n");
125
+ }
126
+ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
127
+ const lines = [];
128
+ if (locked) {
129
+ lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
130
+ lines.push("");
131
+ }
132
+ lines.push(
133
+ `<b>\u{1F916} ${escapePango(agent || "AI Agent")}</b> | <b>\u{1F527} <tt>${escapePango(toolName)}</tt></b>`
134
+ );
135
+ lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
136
+ lines.push("");
137
+ lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
138
+ if (!locked) {
139
+ lines.push("");
140
+ lines.push(
141
+ '<small>\u21B5 Enter = <b>Allow \u21B5</b> | \u238B Esc = <b>Block \u238B</b> | "Always Allow" = never ask again</small>'
142
+ );
143
+ }
144
+ return lines.join("\n");
145
+ }
110
146
  async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
111
147
  if (isTestEnv()) return "deny";
112
148
  const formattedArgs = formatArgs(args);
113
149
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
114
- let message = "";
115
- if (locked) message += `\u26A0\uFE0F LOCKED BY ADMIN POLICY
116
- `;
117
- message += `Tool: ${toolName}
118
- `;
119
- message += `Agent: ${agent || "AI Agent"}
120
- `;
121
- message += `Rule: ${explainableLabel || "Security Policy"}
122
-
123
- `;
124
- message += `${formattedArgs}`;
150
+ const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
125
151
  process.stderr.write(import_chalk.default.yellow(`
126
152
  \u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
127
153
  `));
@@ -142,7 +168,7 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
142
168
  }
143
169
  try {
144
170
  if (process.platform === "darwin") {
145
- const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block", "Always Allow", "Allow"} default button "Allow" cancel button "Block"`;
171
+ 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"`;
146
172
  const script = `on run argv
147
173
  tell application "System Events"
148
174
  activate
@@ -151,21 +177,28 @@ end tell
151
177
  end run`;
152
178
  childProcess = (0, import_child_process.spawn)("osascript", ["-e", script, "--", message, title]);
153
179
  } else if (process.platform === "linux") {
180
+ const pangoMessage = buildPangoMessage(
181
+ toolName,
182
+ formattedArgs,
183
+ agent,
184
+ explainableLabel,
185
+ locked
186
+ );
154
187
  const argsList = [
155
188
  locked ? "--info" : "--question",
156
189
  "--modal",
157
- "--width=450",
190
+ "--width=480",
158
191
  "--title",
159
192
  title,
160
193
  "--text",
161
- message,
194
+ pangoMessage,
162
195
  "--ok-label",
163
- locked ? "Waiting..." : "Allow",
196
+ locked ? "Waiting..." : "Allow \u21B5",
164
197
  "--timeout",
165
198
  "300"
166
199
  ];
167
200
  if (!locked) {
168
- argsList.push("--cancel-label", "Block");
201
+ argsList.push("--cancel-label", "Block \u238B");
169
202
  argsList.push("--extra-button", "Always Allow");
170
203
  }
171
204
  childProcess = (0, import_child_process.spawn)("zenity", argsList);
@@ -193,6 +226,8 @@ end run`;
193
226
  // src/core.ts
194
227
  var PAUSED_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "PAUSED");
195
228
  var TRUST_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "trust.json");
229
+ var LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
230
+ var HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
196
231
  function checkPause() {
197
232
  try {
198
233
  if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
@@ -259,36 +294,39 @@ function writeTrustSession(toolName, durationMs) {
259
294
  }
260
295
  }
261
296
  }
262
- function appendAuditModeEntry(toolName, args) {
297
+ function appendToLog(logPath, entry) {
263
298
  try {
264
- const entry = JSON.stringify({
265
- ts: (/* @__PURE__ */ new Date()).toISOString(),
266
- tool: toolName,
267
- args,
268
- decision: "would-have-blocked",
269
- source: "audit-mode"
270
- });
271
- const logPath = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
272
299
  const dir = import_path.default.dirname(logPath);
273
300
  if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
274
- import_fs.default.appendFileSync(logPath, entry + "\n");
301
+ import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
275
302
  } catch {
276
303
  }
277
304
  }
278
- var DANGEROUS_WORDS = [
279
- "delete",
280
- "drop",
281
- "remove",
282
- "terminate",
283
- "refund",
284
- "write",
285
- "update",
286
- "destroy",
287
- "rm",
288
- "rmdir",
289
- "purge",
290
- "format"
291
- ];
305
+ function appendHookDebug(toolName, args, meta) {
306
+ const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
307
+ appendToLog(HOOK_DEBUG_LOG, {
308
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
309
+ tool: toolName,
310
+ args: safeArgs,
311
+ agent: meta?.agent,
312
+ mcpServer: meta?.mcpServer,
313
+ hostname: import_os.default.hostname(),
314
+ cwd: process.cwd()
315
+ });
316
+ }
317
+ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
318
+ const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
319
+ appendToLog(LOCAL_AUDIT_LOG, {
320
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
321
+ tool: toolName,
322
+ args: safeArgs,
323
+ decision,
324
+ checkedBy,
325
+ agent: meta?.agent,
326
+ mcpServer: meta?.mcpServer,
327
+ hostname: import_os.default.hostname()
328
+ });
329
+ }
292
330
  function tokenize(toolName) {
293
331
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
294
332
  }
@@ -395,16 +433,28 @@ function redactSecrets(text) {
395
433
  );
396
434
  return redacted;
397
435
  }
436
+ var DANGEROUS_WORDS = [
437
+ "drop",
438
+ "truncate",
439
+ "purge",
440
+ "format",
441
+ "destroy",
442
+ "terminate",
443
+ "revoke",
444
+ "docker",
445
+ "psql"
446
+ ];
398
447
  var DEFAULT_CONFIG = {
399
448
  settings: {
400
449
  mode: "standard",
401
450
  autoStartDaemon: true,
402
- enableUndo: false,
451
+ enableUndo: true,
452
+ // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
403
453
  enableHookLogDebug: false,
404
454
  approvers: { native: true, browser: true, cloud: true, terminal: true }
405
455
  },
406
456
  policy: {
407
- sandboxPaths: [],
457
+ sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
408
458
  dangerousWords: DANGEROUS_WORDS,
409
459
  ignoredTools: [
410
460
  "list_*",
@@ -412,12 +462,44 @@ var DEFAULT_CONFIG = {
412
462
  "read_*",
413
463
  "describe_*",
414
464
  "read",
465
+ "glob",
415
466
  "grep",
416
467
  "ls",
417
- "askuserquestion"
468
+ "notebookread",
469
+ "notebookedit",
470
+ "webfetch",
471
+ "websearch",
472
+ "exitplanmode",
473
+ "askuserquestion",
474
+ "agent",
475
+ "task*",
476
+ "toolsearch",
477
+ "mcp__ide__*",
478
+ "getDiagnostics"
418
479
  ],
419
- toolInspection: { bash: "command", shell: "command" },
420
- rules: [{ action: "rm", allowPaths: ["**/node_modules/**", "dist/**", ".DS_Store"] }]
480
+ toolInspection: {
481
+ bash: "command",
482
+ shell: "command",
483
+ run_shell_command: "command",
484
+ "terminal.execute": "command",
485
+ "postgres:query": "sql"
486
+ },
487
+ rules: [
488
+ {
489
+ action: "rm",
490
+ allowPaths: [
491
+ "**/node_modules/**",
492
+ "dist/**",
493
+ "build/**",
494
+ ".next/**",
495
+ "coverage/**",
496
+ ".cache/**",
497
+ "tmp/**",
498
+ "temp/**",
499
+ ".DS_Store"
500
+ ]
501
+ }
502
+ ]
421
503
  },
422
504
  environments: {}
423
505
  };
@@ -482,20 +564,15 @@ async function evaluatePolicy(toolName, args, agent) {
482
564
  }
483
565
  const isManual = agent === "Terminal";
484
566
  if (isManual) {
485
- const NUCLEAR_COMMANDS = [
486
- "drop",
487
- "destroy",
488
- "purge",
489
- "rmdir",
490
- "format",
491
- "truncate",
492
- "alter",
493
- "grant",
494
- "revoke",
495
- "docker"
496
- ];
497
- const hasNuclear = allTokens.some((t) => NUCLEAR_COMMANDS.includes(t.toLowerCase()));
498
- if (!hasNuclear) return { decision: "allow" };
567
+ const SYSTEM_DISASTER_COMMANDS = ["mkfs", "shred", "dd", "drop", "truncate", "purge"];
568
+ const hasSystemDisaster = allTokens.some(
569
+ (t) => SYSTEM_DISASTER_COMMANDS.includes(t.toLowerCase())
570
+ );
571
+ const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
572
+ if (hasSystemDisaster || isRootWipe) {
573
+ return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
574
+ }
575
+ return { decision: "allow" };
499
576
  }
500
577
  if (pathTokens.length > 0 && config.policy.sandboxPaths.length > 0) {
501
578
  const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
@@ -509,27 +586,39 @@ async function evaluatePolicy(toolName, args, agent) {
509
586
  if (pathTokens.length > 0) {
510
587
  const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
511
588
  if (anyBlocked)
512
- return { decision: "review", blockedByLabel: "Project/Global Config (Rule Block)" };
589
+ return {
590
+ decision: "review",
591
+ blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
592
+ };
513
593
  const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
514
594
  if (allAllowed) return { decision: "allow" };
515
595
  }
516
- return { decision: "review", blockedByLabel: "Project/Global Config (Rule Default Block)" };
596
+ return {
597
+ decision: "review",
598
+ blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
599
+ };
517
600
  }
518
601
  }
602
+ let matchedDangerousWord;
519
603
  const isDangerous = allTokens.some(
520
604
  (token) => config.policy.dangerousWords.some((word) => {
521
605
  const w = word.toLowerCase();
522
- if (token === w) return true;
523
- try {
524
- return new RegExp(`\\b${w}\\b`, "i").test(token);
525
- } catch {
526
- return false;
527
- }
606
+ const hit = token === w || (() => {
607
+ try {
608
+ return new RegExp(`\\b${w}\\b`, "i").test(token);
609
+ } catch {
610
+ return false;
611
+ }
612
+ })();
613
+ if (hit && !matchedDangerousWord) matchedDangerousWord = word;
614
+ return hit;
528
615
  })
529
616
  );
530
617
  if (isDangerous) {
531
- const label = isManual ? "Manual Nuclear Protection" : "Project/Global Config (Dangerous Word)";
532
- return { decision: "review", blockedByLabel: label };
618
+ return {
619
+ decision: "review",
620
+ blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`
621
+ };
533
622
  }
534
623
  if (config.settings.mode === "strict") {
535
624
  const envConfig = getActiveEnvironment(config);
@@ -644,13 +733,16 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
644
733
  approvers.browser = false;
645
734
  approvers.terminal = false;
646
735
  }
736
+ if (config.settings.enableHookLogDebug && !isTestEnv2) {
737
+ appendHookDebug(toolName, args, meta);
738
+ }
647
739
  const isManual = meta?.agent === "Terminal";
648
740
  let explainableLabel = "Local Config";
649
741
  if (config.settings.mode === "audit") {
650
742
  if (!isIgnoredTool(toolName)) {
651
743
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
652
744
  if (policyResult.decision === "review") {
653
- appendAuditModeEntry(toolName, args);
745
+ appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
654
746
  sendDesktopNotification(
655
747
  "Node9 Audit Mode",
656
748
  `Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
@@ -662,20 +754,24 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
662
754
  if (!isIgnoredTool(toolName)) {
663
755
  if (getActiveTrustSession(toolName)) {
664
756
  if (creds?.apiKey) auditLocalAllow(toolName, args, "trust", creds, meta);
757
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
665
758
  return { approved: true, checkedBy: "trust" };
666
759
  }
667
760
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
668
761
  if (policyResult.decision === "allow") {
669
762
  if (creds?.apiKey) auditLocalAllow(toolName, args, "local-policy", creds, meta);
763
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
670
764
  return { approved: true, checkedBy: "local-policy" };
671
765
  }
672
766
  explainableLabel = policyResult.blockedByLabel || "Local Config";
673
767
  const persistent = getPersistentDecision(toolName);
674
768
  if (persistent === "allow") {
675
769
  if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
770
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
676
771
  return { approved: true, checkedBy: "persistent" };
677
772
  }
678
773
  if (persistent === "deny") {
774
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "persistent-deny", meta);
679
775
  return {
680
776
  approved: false,
681
777
  reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
@@ -685,6 +781,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
685
781
  }
686
782
  } else {
687
783
  if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
784
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
688
785
  return { approved: true };
689
786
  }
690
787
  let cloudRequestId = null;
@@ -692,8 +789,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
692
789
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
693
790
  if (cloudEnforced) {
694
791
  try {
695
- const envConfig = getActiveEnvironment(getConfig());
696
- const initResult = await initNode9SaaS(toolName, args, creds, envConfig?.slackChannel, meta);
792
+ const initResult = await initNode9SaaS(toolName, args, creds, meta);
697
793
  if (!initResult.pending) {
698
794
  return {
699
795
  approved: !!initResult.approved,
@@ -909,6 +1005,15 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
909
1005
  if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
910
1006
  await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
911
1007
  }
1008
+ if (!isManual) {
1009
+ appendLocalAudit(
1010
+ toolName,
1011
+ args,
1012
+ finalResult.approved ? "allow" : "deny",
1013
+ finalResult.checkedBy || finalResult.blockedBy || "unknown",
1014
+ meta
1015
+ );
1016
+ }
912
1017
  return finalResult;
913
1018
  }
914
1019
  function getConfig() {
@@ -938,9 +1043,10 @@ function getConfig() {
938
1043
  if (s.enableHookLogDebug !== void 0)
939
1044
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
940
1045
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
1046
+ if (s.environment !== void 0) mergedSettings.environment = s.environment;
941
1047
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
942
- if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
943
1048
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
1049
+ if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
944
1050
  if (p.toolInspection)
945
1051
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
946
1052
  if (p.rules) mergedPolicy.rules.push(...p.rules);
@@ -967,7 +1073,7 @@ function tryLoadConfig(filePath) {
967
1073
  }
968
1074
  }
969
1075
  function getActiveEnvironment(config) {
970
- const env = process.env.NODE_ENV || "development";
1076
+ const env = config.settings.environment || process.env.NODE_ENV || "development";
971
1077
  return config.environments[env] ?? null;
972
1078
  }
973
1079
  function getCredentials() {
@@ -1023,7 +1129,7 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1023
1129
  }).catch(() => {
1024
1130
  });
1025
1131
  }
1026
- async function initNode9SaaS(toolName, args, creds, slackChannel, meta) {
1132
+ async function initNode9SaaS(toolName, args, creds, meta) {
1027
1133
  const controller = new AbortController();
1028
1134
  const timeout = setTimeout(() => controller.abort(), 1e4);
1029
1135
  try {
@@ -1033,7 +1139,6 @@ async function initNode9SaaS(toolName, args, creds, slackChannel, meta) {
1033
1139
  body: JSON.stringify({
1034
1140
  toolName,
1035
1141
  args,
1036
- slackChannel,
1037
1142
  context: {
1038
1143
  agent: meta?.agent,
1039
1144
  mcpServer: meta?.mcpServer,
@@ -3077,75 +3182,30 @@ program.command("addto").description("Integrate Node9 with an AI agent").addHelp
3077
3182
  console.error(import_chalk5.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
3078
3183
  process.exit(1);
3079
3184
  });
3080
- program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").action((options) => {
3185
+ program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
3081
3186
  const configPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "config.json");
3082
3187
  if (import_fs5.default.existsSync(configPath) && !options.force) {
3083
3188
  console.log(import_chalk5.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
3084
3189
  console.log(import_chalk5.default.gray(` Run with --force to overwrite.`));
3085
3190
  return;
3086
3191
  }
3087
- const defaultConfig = {
3088
- version: "1.0",
3192
+ const requestedMode = options.mode.toLowerCase();
3193
+ const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
3194
+ const configToSave = {
3195
+ ...DEFAULT_CONFIG,
3089
3196
  settings: {
3090
- mode: "standard",
3091
- autoStartDaemon: true,
3092
- enableUndo: true,
3093
- enableHookLogDebug: false,
3094
- approvers: { native: true, browser: true, cloud: true, terminal: true }
3095
- },
3096
- policy: {
3097
- sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
3098
- dangerousWords: DANGEROUS_WORDS,
3099
- ignoredTools: [
3100
- "list_*",
3101
- "get_*",
3102
- "read_*",
3103
- "describe_*",
3104
- "read",
3105
- "write",
3106
- "edit",
3107
- "glob",
3108
- "grep",
3109
- "ls",
3110
- "notebookread",
3111
- "notebookedit",
3112
- "webfetch",
3113
- "websearch",
3114
- "exitplanmode",
3115
- "askuserquestion",
3116
- "agent",
3117
- "task*"
3118
- ],
3119
- toolInspection: {
3120
- bash: "command",
3121
- shell: "command",
3122
- run_shell_command: "command",
3123
- "terminal.execute": "command",
3124
- "postgres:query": "sql"
3125
- },
3126
- rules: [
3127
- {
3128
- action: "rm",
3129
- allowPaths: [
3130
- "**/node_modules/**",
3131
- "dist/**",
3132
- "build/**",
3133
- ".next/**",
3134
- "coverage/**",
3135
- ".cache/**",
3136
- "tmp/**",
3137
- "temp/**",
3138
- ".DS_Store"
3139
- ]
3140
- }
3141
- ]
3197
+ ...DEFAULT_CONFIG.settings,
3198
+ mode: safeMode
3142
3199
  }
3143
3200
  };
3144
- if (!import_fs5.default.existsSync(import_path5.default.dirname(configPath)))
3145
- import_fs5.default.mkdirSync(import_path5.default.dirname(configPath), { recursive: true });
3146
- import_fs5.default.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
3201
+ const dir = import_path5.default.dirname(configPath);
3202
+ if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
3203
+ import_fs5.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
3147
3204
  console.log(import_chalk5.default.green(`\u2705 Global config created: ${configPath}`));
3148
- console.log(import_chalk5.default.gray(` Edit this file to add custom tool inspection or security rules.`));
3205
+ console.log(import_chalk5.default.cyan(` Mode set to: ${safeMode}`));
3206
+ console.log(
3207
+ import_chalk5.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
3208
+ );
3149
3209
  });
3150
3210
  program.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
3151
3211
  const creds = getCredentials();
@@ -3292,23 +3352,24 @@ RAW: ${raw}
3292
3352
  let aiFeedbackMessage = "";
3293
3353
  if (isHumanDecision) {
3294
3354
  aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: The human user specifically REJECTED this action.
3295
- REASON: ${msg || "No specific reason provided by user."}
3355
+ REASON: ${msg || "No specific reason provided by user."}
3296
3356
 
3297
- INSTRUCTIONS FOR AI AGENT:
3298
- - Do NOT retry this exact command immediately.
3299
- - Explain to the user that you understand they blocked the action.
3300
- - Ask the user if there is an alternative approach they would prefer, or if they intended to block this action entirely.
3301
- - If you believe this action is critical, explain your reasoning to the user and ask them to run 'node9 pause 15m' to allow you to proceed.`;
3357
+ INSTRUCTIONS FOR AI AGENT:
3358
+ - Do NOT retry this exact command immediately.
3359
+ - Explain to the user that you understand they blocked the action.
3360
+ - Ask the user if there is an alternative approach they would prefer, or if they intended to block this action entirely.
3361
+ - If you believe this action is critical, explain your reasoning to the user and ask them to run 'node9 pause 15m' to allow you to proceed.`;
3302
3362
  } else {
3303
3363
  aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: Action blocked by automated policy [${blockedByContext}].
3304
- REASON: ${msg}
3364
+ REASON: ${msg}
3305
3365
 
3306
- INSTRUCTIONS FOR AI AGENT:
3307
- - This command violates the current security configuration.
3308
- - Do NOT attempt to bypass this rule with bash syntax tricks; it will be blocked again.
3309
- - Pivot to a non-destructive or read-only alternative.
3310
- - Inform the user which security rule was triggered.`;
3366
+ INSTRUCTIONS FOR AI AGENT:
3367
+ - This command violates the current security configuration.
3368
+ - Do NOT attempt to bypass this rule with bash syntax tricks; it will be blocked again.
3369
+ - Pivot to a non-destructive or read-only alternative.
3370
+ - Inform the user which security rule was triggered.`;
3311
3371
  }
3372
+ console.error(import_chalk5.default.dim(` (Detailed instructions sent to AI agent)`));
3312
3373
  process.stdout.write(
3313
3374
  JSON.stringify({
3314
3375
  decision: "block",
@@ -3503,7 +3564,9 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
3503
3564
  process.exit(1);
3504
3565
  }
3505
3566
  const fullCommand = commandArgs.join(" ");
3506
- let result = await authorizeHeadless("shell", { command: fullCommand });
3567
+ let result = await authorizeHeadless("shell", { command: fullCommand }, true, {
3568
+ agent: "Terminal"
3569
+ });
3507
3570
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
3508
3571
  console.error(import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
3509
3572
  const daemonReady = await autoStartDaemonAndWait();