@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/cli.mjs CHANGED
@@ -84,21 +84,47 @@ function sendDesktopNotification(title, body) {
84
84
  } catch {
85
85
  }
86
86
  }
87
+ function escapePango(text) {
88
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
89
+ }
90
+ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
91
+ const lines = [];
92
+ if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
93
+ lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
94
+ lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
95
+ lines.push("");
96
+ lines.push(formattedArgs);
97
+ if (!locked) {
98
+ lines.push("");
99
+ lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
100
+ }
101
+ return lines.join("\n");
102
+ }
103
+ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
104
+ const lines = [];
105
+ if (locked) {
106
+ lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
107
+ lines.push("");
108
+ }
109
+ lines.push(
110
+ `<b>\u{1F916} ${escapePango(agent || "AI Agent")}</b> | <b>\u{1F527} <tt>${escapePango(toolName)}</tt></b>`
111
+ );
112
+ lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
113
+ lines.push("");
114
+ lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
115
+ if (!locked) {
116
+ lines.push("");
117
+ lines.push(
118
+ '<small>\u21B5 Enter = <b>Allow \u21B5</b> | \u238B Esc = <b>Block \u238B</b> | "Always Allow" = never ask again</small>'
119
+ );
120
+ }
121
+ return lines.join("\n");
122
+ }
87
123
  async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
88
124
  if (isTestEnv()) return "deny";
89
125
  const formattedArgs = formatArgs(args);
90
126
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
91
- let message = "";
92
- if (locked) message += `\u26A0\uFE0F LOCKED BY ADMIN POLICY
93
- `;
94
- message += `Tool: ${toolName}
95
- `;
96
- message += `Agent: ${agent || "AI Agent"}
97
- `;
98
- message += `Rule: ${explainableLabel || "Security Policy"}
99
-
100
- `;
101
- message += `${formattedArgs}`;
127
+ const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
102
128
  process.stderr.write(chalk.yellow(`
103
129
  \u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
104
130
  `));
@@ -119,7 +145,7 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
119
145
  }
120
146
  try {
121
147
  if (process.platform === "darwin") {
122
- const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block", "Always Allow", "Allow"} default button "Allow" cancel button "Block"`;
148
+ 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"`;
123
149
  const script = `on run argv
124
150
  tell application "System Events"
125
151
  activate
@@ -128,21 +154,28 @@ end tell
128
154
  end run`;
129
155
  childProcess = spawn("osascript", ["-e", script, "--", message, title]);
130
156
  } else if (process.platform === "linux") {
157
+ const pangoMessage = buildPangoMessage(
158
+ toolName,
159
+ formattedArgs,
160
+ agent,
161
+ explainableLabel,
162
+ locked
163
+ );
131
164
  const argsList = [
132
165
  locked ? "--info" : "--question",
133
166
  "--modal",
134
- "--width=450",
167
+ "--width=480",
135
168
  "--title",
136
169
  title,
137
170
  "--text",
138
- message,
171
+ pangoMessage,
139
172
  "--ok-label",
140
- locked ? "Waiting..." : "Allow",
173
+ locked ? "Waiting..." : "Allow \u21B5",
141
174
  "--timeout",
142
175
  "300"
143
176
  ];
144
177
  if (!locked) {
145
- argsList.push("--cancel-label", "Block");
178
+ argsList.push("--cancel-label", "Block \u238B");
146
179
  argsList.push("--extra-button", "Always Allow");
147
180
  }
148
181
  childProcess = spawn("zenity", argsList);
@@ -170,6 +203,8 @@ end run`;
170
203
  // src/core.ts
171
204
  var PAUSED_FILE = path.join(os.homedir(), ".node9", "PAUSED");
172
205
  var TRUST_FILE = path.join(os.homedir(), ".node9", "trust.json");
206
+ var LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
207
+ var HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
173
208
  function checkPause() {
174
209
  try {
175
210
  if (!fs.existsSync(PAUSED_FILE)) return { paused: false };
@@ -236,36 +271,39 @@ function writeTrustSession(toolName, durationMs) {
236
271
  }
237
272
  }
238
273
  }
239
- function appendAuditModeEntry(toolName, args) {
274
+ function appendToLog(logPath, entry) {
240
275
  try {
241
- const entry = JSON.stringify({
242
- ts: (/* @__PURE__ */ new Date()).toISOString(),
243
- tool: toolName,
244
- args,
245
- decision: "would-have-blocked",
246
- source: "audit-mode"
247
- });
248
- const logPath = path.join(os.homedir(), ".node9", "audit.log");
249
276
  const dir = path.dirname(logPath);
250
277
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
251
- fs.appendFileSync(logPath, entry + "\n");
278
+ fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
252
279
  } catch {
253
280
  }
254
281
  }
255
- var DANGEROUS_WORDS = [
256
- "delete",
257
- "drop",
258
- "remove",
259
- "terminate",
260
- "refund",
261
- "write",
262
- "update",
263
- "destroy",
264
- "rm",
265
- "rmdir",
266
- "purge",
267
- "format"
268
- ];
282
+ function appendHookDebug(toolName, args, meta) {
283
+ const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
284
+ appendToLog(HOOK_DEBUG_LOG, {
285
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
286
+ tool: toolName,
287
+ args: safeArgs,
288
+ agent: meta?.agent,
289
+ mcpServer: meta?.mcpServer,
290
+ hostname: os.hostname(),
291
+ cwd: process.cwd()
292
+ });
293
+ }
294
+ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
295
+ const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
296
+ appendToLog(LOCAL_AUDIT_LOG, {
297
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
298
+ tool: toolName,
299
+ args: safeArgs,
300
+ decision,
301
+ checkedBy,
302
+ agent: meta?.agent,
303
+ mcpServer: meta?.mcpServer,
304
+ hostname: os.hostname()
305
+ });
306
+ }
269
307
  function tokenize(toolName) {
270
308
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
271
309
  }
@@ -372,16 +410,28 @@ function redactSecrets(text) {
372
410
  );
373
411
  return redacted;
374
412
  }
413
+ var DANGEROUS_WORDS = [
414
+ "drop",
415
+ "truncate",
416
+ "purge",
417
+ "format",
418
+ "destroy",
419
+ "terminate",
420
+ "revoke",
421
+ "docker",
422
+ "psql"
423
+ ];
375
424
  var DEFAULT_CONFIG = {
376
425
  settings: {
377
426
  mode: "standard",
378
427
  autoStartDaemon: true,
379
- enableUndo: false,
428
+ enableUndo: true,
429
+ // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
380
430
  enableHookLogDebug: false,
381
431
  approvers: { native: true, browser: true, cloud: true, terminal: true }
382
432
  },
383
433
  policy: {
384
- sandboxPaths: [],
434
+ sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
385
435
  dangerousWords: DANGEROUS_WORDS,
386
436
  ignoredTools: [
387
437
  "list_*",
@@ -389,12 +439,44 @@ var DEFAULT_CONFIG = {
389
439
  "read_*",
390
440
  "describe_*",
391
441
  "read",
442
+ "glob",
392
443
  "grep",
393
444
  "ls",
394
- "askuserquestion"
445
+ "notebookread",
446
+ "notebookedit",
447
+ "webfetch",
448
+ "websearch",
449
+ "exitplanmode",
450
+ "askuserquestion",
451
+ "agent",
452
+ "task*",
453
+ "toolsearch",
454
+ "mcp__ide__*",
455
+ "getDiagnostics"
395
456
  ],
396
- toolInspection: { bash: "command", shell: "command" },
397
- rules: [{ action: "rm", allowPaths: ["**/node_modules/**", "dist/**", ".DS_Store"] }]
457
+ toolInspection: {
458
+ bash: "command",
459
+ shell: "command",
460
+ run_shell_command: "command",
461
+ "terminal.execute": "command",
462
+ "postgres:query": "sql"
463
+ },
464
+ rules: [
465
+ {
466
+ action: "rm",
467
+ allowPaths: [
468
+ "**/node_modules/**",
469
+ "dist/**",
470
+ "build/**",
471
+ ".next/**",
472
+ "coverage/**",
473
+ ".cache/**",
474
+ "tmp/**",
475
+ "temp/**",
476
+ ".DS_Store"
477
+ ]
478
+ }
479
+ ]
398
480
  },
399
481
  environments: {}
400
482
  };
@@ -459,20 +541,15 @@ async function evaluatePolicy(toolName, args, agent) {
459
541
  }
460
542
  const isManual = agent === "Terminal";
461
543
  if (isManual) {
462
- const NUCLEAR_COMMANDS = [
463
- "drop",
464
- "destroy",
465
- "purge",
466
- "rmdir",
467
- "format",
468
- "truncate",
469
- "alter",
470
- "grant",
471
- "revoke",
472
- "docker"
473
- ];
474
- const hasNuclear = allTokens.some((t) => NUCLEAR_COMMANDS.includes(t.toLowerCase()));
475
- if (!hasNuclear) return { decision: "allow" };
544
+ const SYSTEM_DISASTER_COMMANDS = ["mkfs", "shred", "dd", "drop", "truncate", "purge"];
545
+ const hasSystemDisaster = allTokens.some(
546
+ (t) => SYSTEM_DISASTER_COMMANDS.includes(t.toLowerCase())
547
+ );
548
+ const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
549
+ if (hasSystemDisaster || isRootWipe) {
550
+ return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
551
+ }
552
+ return { decision: "allow" };
476
553
  }
477
554
  if (pathTokens.length > 0 && config.policy.sandboxPaths.length > 0) {
478
555
  const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
@@ -486,27 +563,39 @@ async function evaluatePolicy(toolName, args, agent) {
486
563
  if (pathTokens.length > 0) {
487
564
  const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
488
565
  if (anyBlocked)
489
- return { decision: "review", blockedByLabel: "Project/Global Config (Rule Block)" };
566
+ return {
567
+ decision: "review",
568
+ blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
569
+ };
490
570
  const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
491
571
  if (allAllowed) return { decision: "allow" };
492
572
  }
493
- return { decision: "review", blockedByLabel: "Project/Global Config (Rule Default Block)" };
573
+ return {
574
+ decision: "review",
575
+ blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
576
+ };
494
577
  }
495
578
  }
579
+ let matchedDangerousWord;
496
580
  const isDangerous = allTokens.some(
497
581
  (token) => config.policy.dangerousWords.some((word) => {
498
582
  const w = word.toLowerCase();
499
- if (token === w) return true;
500
- try {
501
- return new RegExp(`\\b${w}\\b`, "i").test(token);
502
- } catch {
503
- return false;
504
- }
583
+ const hit = token === w || (() => {
584
+ try {
585
+ return new RegExp(`\\b${w}\\b`, "i").test(token);
586
+ } catch {
587
+ return false;
588
+ }
589
+ })();
590
+ if (hit && !matchedDangerousWord) matchedDangerousWord = word;
591
+ return hit;
505
592
  })
506
593
  );
507
594
  if (isDangerous) {
508
- const label = isManual ? "Manual Nuclear Protection" : "Project/Global Config (Dangerous Word)";
509
- return { decision: "review", blockedByLabel: label };
595
+ return {
596
+ decision: "review",
597
+ blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`
598
+ };
510
599
  }
511
600
  if (config.settings.mode === "strict") {
512
601
  const envConfig = getActiveEnvironment(config);
@@ -621,13 +710,16 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
621
710
  approvers.browser = false;
622
711
  approvers.terminal = false;
623
712
  }
713
+ if (config.settings.enableHookLogDebug && !isTestEnv2) {
714
+ appendHookDebug(toolName, args, meta);
715
+ }
624
716
  const isManual = meta?.agent === "Terminal";
625
717
  let explainableLabel = "Local Config";
626
718
  if (config.settings.mode === "audit") {
627
719
  if (!isIgnoredTool(toolName)) {
628
720
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
629
721
  if (policyResult.decision === "review") {
630
- appendAuditModeEntry(toolName, args);
722
+ appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
631
723
  sendDesktopNotification(
632
724
  "Node9 Audit Mode",
633
725
  `Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
@@ -639,20 +731,24 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
639
731
  if (!isIgnoredTool(toolName)) {
640
732
  if (getActiveTrustSession(toolName)) {
641
733
  if (creds?.apiKey) auditLocalAllow(toolName, args, "trust", creds, meta);
734
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
642
735
  return { approved: true, checkedBy: "trust" };
643
736
  }
644
737
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
645
738
  if (policyResult.decision === "allow") {
646
739
  if (creds?.apiKey) auditLocalAllow(toolName, args, "local-policy", creds, meta);
740
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
647
741
  return { approved: true, checkedBy: "local-policy" };
648
742
  }
649
743
  explainableLabel = policyResult.blockedByLabel || "Local Config";
650
744
  const persistent = getPersistentDecision(toolName);
651
745
  if (persistent === "allow") {
652
746
  if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
747
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
653
748
  return { approved: true, checkedBy: "persistent" };
654
749
  }
655
750
  if (persistent === "deny") {
751
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "persistent-deny", meta);
656
752
  return {
657
753
  approved: false,
658
754
  reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
@@ -662,6 +758,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
662
758
  }
663
759
  } else {
664
760
  if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
761
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
665
762
  return { approved: true };
666
763
  }
667
764
  let cloudRequestId = null;
@@ -886,6 +983,15 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
886
983
  if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
887
984
  await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
888
985
  }
986
+ if (!isManual) {
987
+ appendLocalAudit(
988
+ toolName,
989
+ args,
990
+ finalResult.approved ? "allow" : "deny",
991
+ finalResult.checkedBy || finalResult.blockedBy || "unknown",
992
+ meta
993
+ );
994
+ }
889
995
  return finalResult;
890
996
  }
891
997
  function getConfig() {
@@ -916,8 +1022,8 @@ function getConfig() {
916
1022
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
917
1023
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
918
1024
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
919
- if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
920
1025
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
1026
+ if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
921
1027
  if (p.toolInspection)
922
1028
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
923
1029
  if (p.rules) mergedPolicy.rules.push(...p.rules);
@@ -3054,75 +3160,30 @@ program.command("addto").description("Integrate Node9 with an AI agent").addHelp
3054
3160
  console.error(chalk5.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
3055
3161
  process.exit(1);
3056
3162
  });
3057
- program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").action((options) => {
3163
+ 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) => {
3058
3164
  const configPath = path5.join(os5.homedir(), ".node9", "config.json");
3059
3165
  if (fs5.existsSync(configPath) && !options.force) {
3060
3166
  console.log(chalk5.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
3061
3167
  console.log(chalk5.gray(` Run with --force to overwrite.`));
3062
3168
  return;
3063
3169
  }
3064
- const defaultConfig = {
3065
- version: "1.0",
3170
+ const requestedMode = options.mode.toLowerCase();
3171
+ const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
3172
+ const configToSave = {
3173
+ ...DEFAULT_CONFIG,
3066
3174
  settings: {
3067
- mode: "standard",
3068
- autoStartDaemon: true,
3069
- enableUndo: true,
3070
- enableHookLogDebug: false,
3071
- approvers: { native: true, browser: true, cloud: true, terminal: true }
3072
- },
3073
- policy: {
3074
- sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
3075
- dangerousWords: DANGEROUS_WORDS,
3076
- ignoredTools: [
3077
- "list_*",
3078
- "get_*",
3079
- "read_*",
3080
- "describe_*",
3081
- "read",
3082
- "write",
3083
- "edit",
3084
- "glob",
3085
- "grep",
3086
- "ls",
3087
- "notebookread",
3088
- "notebookedit",
3089
- "webfetch",
3090
- "websearch",
3091
- "exitplanmode",
3092
- "askuserquestion",
3093
- "agent",
3094
- "task*"
3095
- ],
3096
- toolInspection: {
3097
- bash: "command",
3098
- shell: "command",
3099
- run_shell_command: "command",
3100
- "terminal.execute": "command",
3101
- "postgres:query": "sql"
3102
- },
3103
- rules: [
3104
- {
3105
- action: "rm",
3106
- allowPaths: [
3107
- "**/node_modules/**",
3108
- "dist/**",
3109
- "build/**",
3110
- ".next/**",
3111
- "coverage/**",
3112
- ".cache/**",
3113
- "tmp/**",
3114
- "temp/**",
3115
- ".DS_Store"
3116
- ]
3117
- }
3118
- ]
3175
+ ...DEFAULT_CONFIG.settings,
3176
+ mode: safeMode
3119
3177
  }
3120
3178
  };
3121
- if (!fs5.existsSync(path5.dirname(configPath)))
3122
- fs5.mkdirSync(path5.dirname(configPath), { recursive: true });
3123
- fs5.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
3179
+ const dir = path5.dirname(configPath);
3180
+ if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
3181
+ fs5.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
3124
3182
  console.log(chalk5.green(`\u2705 Global config created: ${configPath}`));
3125
- console.log(chalk5.gray(` Edit this file to add custom tool inspection or security rules.`));
3183
+ console.log(chalk5.cyan(` Mode set to: ${safeMode}`));
3184
+ console.log(
3185
+ chalk5.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
3186
+ );
3126
3187
  });
3127
3188
  program.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
3128
3189
  const creds = getCredentials();
@@ -3269,23 +3330,24 @@ RAW: ${raw}
3269
3330
  let aiFeedbackMessage = "";
3270
3331
  if (isHumanDecision) {
3271
3332
  aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: The human user specifically REJECTED this action.
3272
- REASON: ${msg || "No specific reason provided by user."}
3333
+ REASON: ${msg || "No specific reason provided by user."}
3273
3334
 
3274
- INSTRUCTIONS FOR AI AGENT:
3275
- - Do NOT retry this exact command immediately.
3276
- - Explain to the user that you understand they blocked the action.
3277
- - Ask the user if there is an alternative approach they would prefer, or if they intended to block this action entirely.
3278
- - 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.`;
3335
+ INSTRUCTIONS FOR AI AGENT:
3336
+ - Do NOT retry this exact command immediately.
3337
+ - Explain to the user that you understand they blocked the action.
3338
+ - Ask the user if there is an alternative approach they would prefer, or if they intended to block this action entirely.
3339
+ - 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.`;
3279
3340
  } else {
3280
3341
  aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: Action blocked by automated policy [${blockedByContext}].
3281
- REASON: ${msg}
3342
+ REASON: ${msg}
3282
3343
 
3283
- INSTRUCTIONS FOR AI AGENT:
3284
- - This command violates the current security configuration.
3285
- - Do NOT attempt to bypass this rule with bash syntax tricks; it will be blocked again.
3286
- - Pivot to a non-destructive or read-only alternative.
3287
- - Inform the user which security rule was triggered.`;
3344
+ INSTRUCTIONS FOR AI AGENT:
3345
+ - This command violates the current security configuration.
3346
+ - Do NOT attempt to bypass this rule with bash syntax tricks; it will be blocked again.
3347
+ - Pivot to a non-destructive or read-only alternative.
3348
+ - Inform the user which security rule was triggered.`;
3288
3349
  }
3350
+ console.error(chalk5.dim(` (Detailed instructions sent to AI agent)`));
3289
3351
  process.stdout.write(
3290
3352
  JSON.stringify({
3291
3353
  decision: "block",
@@ -3480,7 +3542,9 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
3480
3542
  process.exit(1);
3481
3543
  }
3482
3544
  const fullCommand = commandArgs.join(" ");
3483
- let result = await authorizeHeadless("shell", { command: fullCommand });
3545
+ let result = await authorizeHeadless("shell", { command: fullCommand }, true, {
3546
+ agent: "Terminal"
3547
+ });
3484
3548
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
3485
3549
  console.error(chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
3486
3550
  const daemonReady = await autoStartDaemonAndWait();