@node9/proxy 1.6.0 → 1.7.1

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
@@ -491,6 +491,84 @@ var init_shields = __esm({
491
491
  ],
492
492
  dangerousWords: []
493
493
  },
494
+ "bash-safe": {
495
+ name: "bash-safe",
496
+ description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
497
+ aliases: ["bash", "shell"],
498
+ smartRules: [
499
+ {
500
+ name: "shield:bash-safe:block-pipe-to-shell",
501
+ tool: "bash",
502
+ conditions: [
503
+ {
504
+ field: "command",
505
+ op: "matches",
506
+ value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
507
+ flags: "i"
508
+ }
509
+ ],
510
+ verdict: "block",
511
+ reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
512
+ },
513
+ {
514
+ name: "shield:bash-safe:block-obfuscated-exec",
515
+ tool: "bash",
516
+ conditions: [
517
+ {
518
+ field: "command",
519
+ op: "matches",
520
+ value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
521
+ flags: "i"
522
+ }
523
+ ],
524
+ verdict: "block",
525
+ reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
526
+ },
527
+ {
528
+ name: "shield:bash-safe:block-rm-root",
529
+ tool: "bash",
530
+ conditions: [
531
+ {
532
+ field: "command",
533
+ op: "matches",
534
+ value: "rm\\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)[a-zA-Z]*\\s+(\\/|~|\\$HOME|\\$\\{HOME\\})\\s*$",
535
+ flags: "i"
536
+ }
537
+ ],
538
+ verdict: "block",
539
+ reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
540
+ },
541
+ {
542
+ name: "shield:bash-safe:block-disk-overwrite",
543
+ tool: "bash",
544
+ conditions: [
545
+ {
546
+ field: "command",
547
+ op: "matches",
548
+ value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
549
+ flags: "i"
550
+ }
551
+ ],
552
+ verdict: "block",
553
+ reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
554
+ },
555
+ {
556
+ name: "shield:bash-safe:review-eval",
557
+ tool: "bash",
558
+ conditions: [
559
+ {
560
+ field: "command",
561
+ op: "matches",
562
+ value: '\\beval\\s+[\\$`("]',
563
+ flags: "i"
564
+ }
565
+ ],
566
+ verdict: "review",
567
+ reason: "eval of dynamic content requires human approval (bash-safe shield)"
568
+ }
569
+ ],
570
+ dangerousWords: []
571
+ },
494
572
  filesystem: {
495
573
  name: "filesystem",
496
574
  description: "Protects the local filesystem from dangerous AI operations",
@@ -777,7 +855,7 @@ var init_config = __esm({
777
855
  DEFAULT_CONFIG = {
778
856
  version: "1.0",
779
857
  settings: {
780
- mode: "audit",
858
+ mode: "standard",
781
859
  autoStartDaemon: true,
782
860
  enableUndo: true,
783
861
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
@@ -3176,6 +3254,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
3176
3254
  await notifyActivity({
3177
3255
  id: actId,
3178
3256
  tool: toolName,
3257
+ args,
3179
3258
  ts: actTs,
3180
3259
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
3181
3260
  label: result.blockedByLabel,
@@ -5453,6 +5532,7 @@ var init_session_counters = __esm({
5453
5532
  _blocked = 0;
5454
5533
  _dlpHits = 0;
5455
5534
  _wouldBlock = 0;
5535
+ _estimatedCost = 0;
5456
5536
  _lastRuleHit = null;
5457
5537
  _lastBlockedTool = null;
5458
5538
  incrementAllowed() {
@@ -5467,6 +5547,10 @@ var init_session_counters = __esm({
5467
5547
  incrementWouldBlock() {
5468
5548
  this._wouldBlock++;
5469
5549
  }
5550
+ addCost(amount) {
5551
+ if (!isFinite(amount) || amount < 0) return;
5552
+ this._estimatedCost += amount;
5553
+ }
5470
5554
  recordRuleHit(label) {
5471
5555
  this._lastRuleHit = label;
5472
5556
  }
@@ -5479,6 +5563,7 @@ var init_session_counters = __esm({
5479
5563
  blocked: this._blocked,
5480
5564
  dlpHits: this._dlpHits,
5481
5565
  wouldBlock: this._wouldBlock,
5566
+ estimatedCost: this._estimatedCost,
5482
5567
  lastRuleHit: this._lastRuleHit,
5483
5568
  lastBlockedTool: this._lastBlockedTool
5484
5569
  };
@@ -5488,6 +5573,7 @@ var init_session_counters = __esm({
5488
5573
  this._blocked = 0;
5489
5574
  this._dlpHits = 0;
5490
5575
  this._wouldBlock = 0;
5576
+ this._estimatedCost = 0;
5491
5577
  this._lastRuleHit = null;
5492
5578
  this._lastBlockedTool = null;
5493
5579
  }
@@ -5718,15 +5804,38 @@ function openBrowser(url) {
5718
5804
  } catch {
5719
5805
  }
5720
5806
  }
5807
+ function estimateToolCost(tool, args) {
5808
+ const a = args ?? {};
5809
+ const t = tool.toLowerCase().replace(/[^a-z_]/g, "_");
5810
+ if (t.includes("read") || t === "glob" || t === "grep") {
5811
+ const filePath = a.file_path ?? a.path;
5812
+ if (filePath) {
5813
+ try {
5814
+ const bytes = fs13.statSync(filePath).size;
5815
+ return bytes / BYTES_PER_TOKEN / 1e6 * INPUT_PRICE_PER_1M;
5816
+ } catch {
5817
+ }
5818
+ }
5819
+ }
5820
+ if (t.includes("write")) {
5821
+ const content = a.content ?? "";
5822
+ return String(content).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
5823
+ }
5824
+ if (t.includes("edit") || t === "str_replace_based_edit_tool") {
5825
+ const newStr = a.new_string ?? "";
5826
+ return String(newStr).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
5827
+ }
5828
+ return void 0;
5829
+ }
5721
5830
  function broadcast(event, data) {
5722
5831
  if (event === "activity") {
5723
5832
  activityRing.push({ event, data });
5724
5833
  if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
5725
5834
  } else if (event === "activity-result") {
5726
- const { id, status, label } = data;
5835
+ const { id, status, label, costEstimate } = data;
5727
5836
  for (let i = activityRing.length - 1; i >= 0; i--) {
5728
5837
  if (activityRing[i].data.id === id) {
5729
- Object.assign(activityRing[i].data, { status, label });
5838
+ Object.assign(activityRing[i].data, { status, label, costEstimate });
5730
5839
  break;
5731
5840
  }
5732
5841
  }
@@ -5828,10 +5937,13 @@ function startActivitySocket() {
5828
5937
  sessionCounters.incrementBlocked();
5829
5938
  sessionCounters.recordBlockedTool(data.tool);
5830
5939
  }
5940
+ const costEstimate = data.status === "allow" ? estimateToolCost(data.tool, data.args) : void 0;
5941
+ if (costEstimate != null && costEstimate > 0) sessionCounters.addCost(costEstimate);
5831
5942
  broadcast("activity-result", {
5832
5943
  id: data.id,
5833
5944
  status: data.status,
5834
- label: data.label
5945
+ label: data.label,
5946
+ costEstimate
5835
5947
  });
5836
5948
  }
5837
5949
  } catch {
@@ -5848,7 +5960,7 @@ function startActivitySocket() {
5848
5960
  }
5849
5961
  });
5850
5962
  }
5851
- var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE, WRITE_TOOL_NAMES;
5963
+ var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES;
5852
5964
  var init_state2 = __esm({
5853
5965
  "src/daemon/state.ts"() {
5854
5966
  "use strict";
@@ -5886,6 +5998,9 @@ var init_state2 = __esm({
5886
5998
  ACTIVITY_RING_SIZE = 100;
5887
5999
  activityRing = [];
5888
6000
  SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
6001
+ INPUT_PRICE_PER_1M = 3;
6002
+ OUTPUT_PRICE_PER_1M = 15;
6003
+ BYTES_PER_TOKEN = 4;
5889
6004
  WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
5890
6005
  "write",
5891
6006
  "write_file",
@@ -6349,7 +6464,8 @@ data: ${JSON.stringify(item.data)}
6349
6464
  allowed: counters.allowed,
6350
6465
  blocked: counters.blocked,
6351
6466
  dlpHits: counters.dlpHits,
6352
- wouldBlock: counters.wouldBlock
6467
+ wouldBlock: counters.wouldBlock,
6468
+ estimatedCost: counters.estimatedCost
6353
6469
  },
6354
6470
  taintedCount: taintStore.list().length,
6355
6471
  lastRuleHit: counters.lastRuleHit,
@@ -6481,6 +6597,7 @@ data: ${JSON.stringify(item.data)}
6481
6597
  }
6482
6598
  if (req.method === "POST" && pathname === "/events/clear") {
6483
6599
  activityRing.length = 0;
6600
+ sessionCounters.reset();
6484
6601
  res.writeHead(200, { "Content-Type": "application/json" });
6485
6602
  return res.end(JSON.stringify({ ok: true }));
6486
6603
  }
@@ -6793,7 +6910,7 @@ function formatBase(activity) {
6793
6910
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
6794
6911
  const icon = getIcon(activity.tool);
6795
6912
  const toolName = activity.tool.slice(0, 16).padEnd(16);
6796
- const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
6913
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os21.homedir(), "~");
6797
6914
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
6798
6915
  return `${chalk17.gray(time)} ${icon} ${chalk17.white.bold(toolName)} ${chalk17.dim(argsPreview)}`;
6799
6916
  }
@@ -6807,11 +6924,13 @@ function renderResult(activity, result) {
6807
6924
  } else {
6808
6925
  status = chalk17.red("\u2717 BLOCK");
6809
6926
  }
6927
+ const cost = result.costEstimate ?? activity.costEstimate;
6928
+ const costSuffix = cost == null ? "" : chalk17.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
6810
6929
  if (process.stdout.isTTY) {
6811
6930
  readline5.clearLine(process.stdout, 0);
6812
6931
  readline5.cursorTo(process.stdout, 0);
6813
6932
  }
6814
- console.log(`${base} ${status}`);
6933
+ console.log(`${base} ${status}${costSuffix}`);
6815
6934
  }
6816
6935
  function renderPending(activity) {
6817
6936
  if (!process.stdout.isTTY) return;
@@ -6940,6 +7059,39 @@ function buildRecoveryCardLines(req) {
6940
7059
  ``
6941
7060
  ];
6942
7061
  }
7062
+ function readApproversFromDisk() {
7063
+ const configPath = path28.join(os21.homedir(), ".node9", "config.json");
7064
+ try {
7065
+ const raw = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
7066
+ const settings = raw.settings ?? {};
7067
+ return settings.approvers ?? {};
7068
+ } catch {
7069
+ return {};
7070
+ }
7071
+ }
7072
+ function approverStatusLine() {
7073
+ const a = readApproversFromDisk();
7074
+ const fmt = (label, key) => {
7075
+ const on = a[key] !== false;
7076
+ return `[${key[0]}]${label.slice(1)} ${on ? chalk17.green("\u2713") : chalk17.dim("\u2717")}`;
7077
+ };
7078
+ return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
7079
+ }
7080
+ function toggleApprover(channel) {
7081
+ const configPath = path28.join(os21.homedir(), ".node9", "config.json");
7082
+ try {
7083
+ const raw = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
7084
+ const settings = raw.settings ?? {};
7085
+ const approvers = settings.approvers ?? {};
7086
+ approvers[channel] = approvers[channel] === false;
7087
+ settings.approvers = approvers;
7088
+ raw.settings = settings;
7089
+ fs25.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
7090
+ } catch (err2) {
7091
+ process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
7092
+ `);
7093
+ }
7094
+ }
6943
7095
  async function startTail(options = {}) {
6944
7096
  const port = await ensureDaemon();
6945
7097
  if (options.clear) {
@@ -6978,6 +7130,7 @@ async function startTail(options = {}) {
6978
7130
  }
6979
7131
  const connectionTime = Date.now();
6980
7132
  const activityPending = /* @__PURE__ */ new Map();
7133
+ const orphanedResults = /* @__PURE__ */ new Map();
6981
7134
  let csrfToken = "";
6982
7135
  const approvalQueue = [];
6983
7136
  let cardActive = false;
@@ -6986,6 +7139,44 @@ async function startTail(options = {}) {
6986
7139
  const localAllowCounts = /* @__PURE__ */ new Map();
6987
7140
  const canApprove = process.stdout.isTTY && process.stdin.isTTY;
6988
7141
  if (canApprove) readline5.emitKeypressEvents(process.stdin);
7142
+ let idleKeypressHandler = null;
7143
+ function enterIdleMode() {
7144
+ if (!canApprove || idleKeypressHandler !== null) return;
7145
+ try {
7146
+ process.stdin.setRawMode(true);
7147
+ } catch {
7148
+ return;
7149
+ }
7150
+ process.stdin.resume();
7151
+ idleKeypressHandler = (_str, key) => {
7152
+ const name = key?.name ?? "";
7153
+ if (key?.ctrl && name === "c") {
7154
+ process.kill(process.pid, "SIGINT");
7155
+ return;
7156
+ }
7157
+ if (name === "q") {
7158
+ process.kill(process.pid, "SIGINT");
7159
+ return;
7160
+ }
7161
+ const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
7162
+ if (channel) {
7163
+ toggleApprover(channel);
7164
+ console.log(chalk17.dim(` Approvers: ${approverStatusLine()}`));
7165
+ }
7166
+ };
7167
+ process.stdin.on("keypress", idleKeypressHandler);
7168
+ }
7169
+ function exitIdleMode() {
7170
+ if (idleKeypressHandler) {
7171
+ process.stdin.removeListener("keypress", idleKeypressHandler);
7172
+ idleKeypressHandler = null;
7173
+ }
7174
+ try {
7175
+ process.stdin.setRawMode(false);
7176
+ } catch {
7177
+ }
7178
+ process.stdin.pause();
7179
+ }
6989
7180
  function clearCard() {
6990
7181
  if (cardLineCount > 0) {
6991
7182
  readline5.moveCursor(process.stdout, 0, -cardLineCount);
@@ -7004,10 +7195,12 @@ async function startTail(options = {}) {
7004
7195
  }
7005
7196
  function showNextCard() {
7006
7197
  if (cardActive || approvalQueue.length === 0 || !canApprove) return;
7198
+ exitIdleMode();
7007
7199
  try {
7008
7200
  process.stdin.setRawMode(true);
7009
7201
  } catch {
7010
7202
  cardActive = false;
7203
+ enterIdleMode();
7011
7204
  return;
7012
7205
  }
7013
7206
  cardActive = true;
@@ -7019,12 +7212,8 @@ async function startTail(options = {}) {
7019
7212
  const handler = onKeypress;
7020
7213
  onKeypress = null;
7021
7214
  if (handler) process.stdin.removeListener("keypress", handler);
7022
- try {
7023
- process.stdin.setRawMode(false);
7024
- } catch {
7025
- }
7026
- process.stdin.pause();
7027
7215
  cancelActiveCard = null;
7216
+ enterIdleMode();
7028
7217
  };
7029
7218
  const settle = (action) => {
7030
7219
  if (settled) return;
@@ -7151,18 +7340,16 @@ async function startTail(options = {}) {
7151
7340
  console.log(chalk17.cyan.bold(`
7152
7341
  \u{1F6F0}\uFE0F Node9 tail `) + chalk17.dim(`\u2192 ${dashboardUrl}`));
7153
7342
  if (canApprove) {
7154
- console.log(
7155
- chalk17.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
7156
- );
7343
+ console.log(chalk17.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
7344
+ console.log(chalk17.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
7157
7345
  }
7158
7346
  if (options.history) {
7159
- console.log(chalk17.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
7347
+ console.log(chalk17.dim("Showing history + live events.\n"));
7160
7348
  } else {
7161
- console.log(
7162
- chalk17.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
7163
- );
7349
+ console.log(chalk17.dim("Showing live events only. Use --history to include past.\n"));
7164
7350
  }
7165
7351
  process.on("SIGINT", () => {
7352
+ exitIdleMode();
7166
7353
  clearCard();
7167
7354
  process.stdout.write(SHOW_CURSOR);
7168
7355
  if (process.stdout.isTTY) {
@@ -7178,6 +7365,7 @@ async function startTail(options = {}) {
7178
7365
  console.error(chalk17.red(`Failed to connect: HTTP ${res.statusCode}`));
7179
7366
  process.exit(1);
7180
7367
  }
7368
+ if (canApprove) enterIdleMode();
7181
7369
  let currentEvent = "";
7182
7370
  let currentData = "";
7183
7371
  res.on("error", () => {
@@ -7277,9 +7465,14 @@ async function startTail(options = {}) {
7277
7465
  renderResult(data, data);
7278
7466
  return;
7279
7467
  }
7468
+ const orphaned = orphanedResults.get(data.id);
7469
+ if (orphaned) {
7470
+ orphanedResults.delete(data.id);
7471
+ renderResult(data, orphaned);
7472
+ return;
7473
+ }
7280
7474
  activityPending.set(data.id, data);
7281
- const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
7282
- if (slowTool) renderPending(data);
7475
+ renderPending(data);
7283
7476
  }
7284
7477
  if (event === "snapshot") {
7285
7478
  const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
@@ -7298,6 +7491,8 @@ async function startTail(options = {}) {
7298
7491
  if (original) {
7299
7492
  renderResult(original, data);
7300
7493
  activityPending.delete(data.id);
7494
+ } else {
7495
+ orphanedResults.set(data.id, data);
7301
7496
  }
7302
7497
  }
7303
7498
  }
@@ -7537,6 +7732,29 @@ function renderOffline() {
7537
7732
  process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
7538
7733
  `);
7539
7734
  }
7735
+ function readActiveShieldsHud() {
7736
+ const now = Date.now();
7737
+ if (shieldsCache && now - shieldsCache.ts < SHIELDS_CACHE_TTL_MS) {
7738
+ return shieldsCache.value;
7739
+ }
7740
+ try {
7741
+ const shieldsPath = path29.join(os22.homedir(), ".node9", "shields.json");
7742
+ if (!fs26.existsSync(shieldsPath)) {
7743
+ shieldsCache = { value: [], ts: now };
7744
+ return [];
7745
+ }
7746
+ const parsed = JSON.parse(fs26.readFileSync(shieldsPath, "utf-8"));
7747
+ if (!Array.isArray(parsed.active)) {
7748
+ shieldsCache = { value: [], ts: now };
7749
+ return [];
7750
+ }
7751
+ const value = parsed.active.filter((s) => typeof s === "string").map((s) => s.slice(0, 64)).slice(0, 20);
7752
+ shieldsCache = { value, ts: now };
7753
+ return value;
7754
+ } catch {
7755
+ return [];
7756
+ }
7757
+ }
7540
7758
  function renderSecurityLine(status) {
7541
7759
  const parts = [];
7542
7760
  parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
@@ -7554,6 +7772,18 @@ function renderSecurityLine(status) {
7554
7772
  };
7555
7773
  const mc = modeColors[status.mode] ?? WHITE;
7556
7774
  parts.push(`${dim("|")} ${color(mc, modeIcon[status.mode] ?? "")}${color(mc, status.mode)}`);
7775
+ const activeShields = readActiveShieldsHud();
7776
+ if (activeShields.length > 0) {
7777
+ const shieldAbbrevs = {
7778
+ "bash-safe": "bash",
7779
+ filesystem: "fs",
7780
+ postgres: "pg",
7781
+ github: "gh",
7782
+ aws: "aws"
7783
+ };
7784
+ const labels = activeShields.map((s) => shieldAbbrevs[s] ?? s).join(" ");
7785
+ parts.push(color(DIM, `[${labels}]`));
7786
+ }
7557
7787
  if (status.mode === "observe") {
7558
7788
  parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
7559
7789
  if (status.session.wouldBlock > 0) {
@@ -7568,6 +7798,11 @@ function renderSecurityLine(status) {
7568
7798
  parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
7569
7799
  }
7570
7800
  }
7801
+ if (status.session.estimatedCost > 0) {
7802
+ const cost = status.session.estimatedCost;
7803
+ const costStr = cost >= 0.01 ? `$${cost.toFixed(2)}` : cost >= 1e-3 ? `$${cost.toFixed(3)}` : "<$0.001";
7804
+ parts.push(color(DIM, `~${costStr}`));
7805
+ }
7571
7806
  if (status.taintedCount > 0) {
7572
7807
  parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
7573
7808
  }
@@ -7645,7 +7880,7 @@ async function main() {
7645
7880
  renderOffline();
7646
7881
  }
7647
7882
  }
7648
- var RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
7883
+ var RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
7649
7884
  var init_hud = __esm({
7650
7885
  "src/cli/hud.ts"() {
7651
7886
  "use strict";
@@ -7663,6 +7898,8 @@ var init_hud = __esm({
7663
7898
  BAR_FILLED = "\u2588";
7664
7899
  BAR_EMPTY = "\u2591";
7665
7900
  BAR_WIDTH = 10;
7901
+ shieldsCache = null;
7902
+ SHIELDS_CACHE_TTL_MS = 2e3;
7666
7903
  }
7667
7904
  });
7668
7905
 
@@ -7676,6 +7913,7 @@ import path14 from "path";
7676
7913
  import os10 from "os";
7677
7914
  import chalk from "chalk";
7678
7915
  import { confirm } from "@inquirer/prompts";
7916
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
7679
7917
  var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7680
7918
  function hasNode9McpServer(servers) {
7681
7919
  const entry = servers["node9"];
@@ -8039,7 +8277,8 @@ function detectAgents(homeDir2 = os10.homedir()) {
8039
8277
  return {
8040
8278
  claude: exists(path14.join(homeDir2, ".claude")) || exists(path14.join(homeDir2, ".claude.json")),
8041
8279
  gemini: exists(path14.join(homeDir2, ".gemini")),
8042
- cursor: exists(path14.join(homeDir2, ".cursor"))
8280
+ cursor: exists(path14.join(homeDir2, ".cursor")),
8281
+ codex: exists(path14.join(homeDir2, ".codex"))
8043
8282
  };
8044
8283
  }
8045
8284
  async function setupCursor() {
@@ -8104,6 +8343,82 @@ async function setupCursor() {
8104
8343
  printDaemonTip();
8105
8344
  }
8106
8345
  }
8346
+ function readToml(filePath) {
8347
+ try {
8348
+ if (fs11.existsSync(filePath)) {
8349
+ return parseToml(fs11.readFileSync(filePath, "utf-8"));
8350
+ }
8351
+ } catch {
8352
+ }
8353
+ return null;
8354
+ }
8355
+ function writeToml(filePath, data) {
8356
+ const dir = path14.dirname(filePath);
8357
+ if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
8358
+ fs11.writeFileSync(filePath, stringifyToml(data));
8359
+ }
8360
+ async function setupCodex() {
8361
+ const homeDir2 = os10.homedir();
8362
+ const configPath = path14.join(homeDir2, ".codex", "config.toml");
8363
+ const config = readToml(configPath) ?? {};
8364
+ const servers = config.mcp_servers ?? {};
8365
+ let anythingChanged = false;
8366
+ if (!hasNode9McpServer(servers)) {
8367
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
8368
+ config.mcp_servers = servers;
8369
+ writeToml(configPath, config);
8370
+ console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
8371
+ anythingChanged = true;
8372
+ }
8373
+ const serversToWrap = [];
8374
+ for (const [name, server] of Object.entries(servers)) {
8375
+ if (!server.command || server.command === "node9") continue;
8376
+ const parts = [server.command, ...server.args ?? []];
8377
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
8378
+ }
8379
+ if (serversToWrap.length > 0) {
8380
+ console.log(chalk.bold("The following existing entries will be modified:\n"));
8381
+ console.log(chalk.white(` ${configPath}`));
8382
+ for (const { name, originalCmd } of serversToWrap) {
8383
+ console.log(chalk.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
8384
+ }
8385
+ console.log("");
8386
+ const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
8387
+ if (proceed) {
8388
+ for (const { name, parts } of serversToWrap) {
8389
+ servers[name] = { ...servers[name], command: "node9", args: parts };
8390
+ }
8391
+ config.mcp_servers = servers;
8392
+ writeToml(configPath, config);
8393
+ console.log(chalk.green(`
8394
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
8395
+ anythingChanged = true;
8396
+ } else {
8397
+ console.log(chalk.yellow(" Skipped MCP server wrapping."));
8398
+ }
8399
+ console.log("");
8400
+ }
8401
+ console.log(
8402
+ chalk.yellow(
8403
+ " \u26A0\uFE0F Note: Codex does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Codex.\n Native bash and file operations are not monitored."
8404
+ )
8405
+ );
8406
+ console.log("");
8407
+ if (!anythingChanged && serversToWrap.length === 0) {
8408
+ console.log(
8409
+ chalk.blue(
8410
+ "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.codex/config.toml and re-run."
8411
+ )
8412
+ );
8413
+ printDaemonTip();
8414
+ return;
8415
+ }
8416
+ if (anythingChanged) {
8417
+ console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Codex via MCP proxy!"));
8418
+ console.log(chalk.gray(" Restart Codex for changes to take effect."));
8419
+ printDaemonTip();
8420
+ }
8421
+ }
8107
8422
  function setupHud() {
8108
8423
  const homeDir2 = os10.homedir();
8109
8424
  const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
@@ -8596,6 +8911,9 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
8596
8911
  if (filesRes.status === 0) {
8597
8912
  capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
8598
8913
  }
8914
+ if (capturedFiles.length === 0) {
8915
+ return prevEntry.hash;
8916
+ }
8599
8917
  const diffRes = spawnSync4("git", ["diff", prevEntry.hash, commitHash], {
8600
8918
  env: shadowEnv,
8601
8919
  timeout: GIT_TIMEOUT
@@ -9793,24 +10111,90 @@ import chalk11 from "chalk";
9793
10111
  import fs23 from "fs";
9794
10112
  import path25 from "path";
9795
10113
  import os19 from "os";
10114
+ import https from "https";
10115
+ init_shields();
10116
+ var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
10117
+ function fireTelemetryPing(agents) {
10118
+ try {
10119
+ const body = JSON.stringify({
10120
+ event: "init_completed",
10121
+ agents_detected: agents,
10122
+ os: process.platform,
10123
+ node9_version: process.env.npm_package_version ?? "unknown"
10124
+ });
10125
+ const req = https.request(
10126
+ {
10127
+ hostname: "api.node9.ai",
10128
+ path: "/api/v1/telemetry",
10129
+ method: "POST",
10130
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
10131
+ timeout: 3e3
10132
+ },
10133
+ (res) => {
10134
+ res.resume();
10135
+ }
10136
+ );
10137
+ req.on("error", () => {
10138
+ });
10139
+ req.on("timeout", () => {
10140
+ req.destroy();
10141
+ });
10142
+ req.end(body);
10143
+ } catch {
10144
+ }
10145
+ }
9796
10146
  function registerInitCommand(program2) {
9797
10147
  program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").option("--skip-setup", "Only create config \u2014 do not wire AI agents").action(async (options) => {
9798
10148
  console.log(chalk11.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
10149
+ let chosenMode = options.mode.toLowerCase();
10150
+ if (!["standard", "strict", "audit"].includes(chosenMode)) {
10151
+ chosenMode = DEFAULT_CONFIG.settings.mode;
10152
+ }
10153
+ {
10154
+ const { confirm: confirm3 } = await import("@inquirer/prompts");
10155
+ const enableShields = await confirm3({
10156
+ message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
10157
+ default: true
10158
+ });
10159
+ if (enableShields) {
10160
+ chosenMode = "standard";
10161
+ try {
10162
+ const current = readActiveShields();
10163
+ const merged = Array.from(/* @__PURE__ */ new Set([...current, ...DEFAULT_SHIELDS]));
10164
+ const hasNewShields = DEFAULT_SHIELDS.some((s) => !current.includes(s));
10165
+ if (hasNewShields) writeActiveShields(merged);
10166
+ } catch (err2) {
10167
+ console.log(chalk11.yellow(` \u26A0\uFE0F Could not update shields: ${String(err2)}`));
10168
+ }
10169
+ }
10170
+ console.log("");
10171
+ }
9799
10172
  const configPath = path25.join(os19.homedir(), ".node9", "config.json");
9800
10173
  if (fs23.existsSync(configPath) && !options.force) {
9801
- console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
10174
+ try {
10175
+ const existing = JSON.parse(fs23.readFileSync(configPath, "utf-8"));
10176
+ const settings = existing.settings ?? {};
10177
+ if (settings.mode !== chosenMode) {
10178
+ settings.mode = chosenMode;
10179
+ existing.settings = settings;
10180
+ fs23.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
10181
+ console.log(chalk11.green(`\u2705 Mode updated: ${chosenMode}`));
10182
+ } else {
10183
+ console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
10184
+ }
10185
+ } catch {
10186
+ console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
10187
+ }
9802
10188
  } else {
9803
- const requestedMode = options.mode.toLowerCase();
9804
- const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
9805
10189
  const configToSave = {
9806
10190
  ...DEFAULT_CONFIG,
9807
- settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
10191
+ settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
9808
10192
  };
9809
10193
  const dir = path25.dirname(configPath);
9810
10194
  if (!fs23.existsSync(dir)) fs23.mkdirSync(dir, { recursive: true });
9811
- fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
10195
+ fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
9812
10196
  console.log(chalk11.green(`\u2705 Config created: ${configPath}`));
9813
- console.log(chalk11.gray(` Mode: ${safeMode}`));
10197
+ console.log(chalk11.gray(` Mode: ${chosenMode}`));
9814
10198
  }
9815
10199
  if (options.skipSetup) return;
9816
10200
  console.log("");
@@ -9820,9 +10204,9 @@ function registerInitCommand(program2) {
9820
10204
  );
9821
10205
  if (found.length === 0) {
9822
10206
  console.log(
9823
- chalk11.gray("No AI agents detected. Install Claude Code, Gemini CLI, or Cursor")
10207
+ chalk11.gray("No AI agents detected. Install Claude Code, Gemini CLI, Cursor, or Codex")
9824
10208
  );
9825
- console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor>"));
10209
+ console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
9826
10210
  return;
9827
10211
  }
9828
10212
  console.log(chalk11.bold("Detected agents:"));
@@ -9835,16 +10219,23 @@ function registerInitCommand(program2) {
9835
10219
  if (agent === "claude") await setupClaude();
9836
10220
  else if (agent === "gemini") await setupGemini();
9837
10221
  else if (agent === "cursor") await setupCursor();
10222
+ else if (agent === "codex") await setupCodex();
9838
10223
  console.log("");
9839
10224
  }
9840
- if (detected.claude) {
9841
- setupHud();
9842
- console.log(chalk11.green("\u2705 node9 HUD added to Claude Code statusline"));
9843
- console.log(chalk11.gray(" Restart Claude Code to activate the security statusline."));
10225
+ {
10226
+ const { confirm: confirm3 } = await import("@inquirer/prompts");
10227
+ const sendTelemetry = await confirm3({
10228
+ message: "Send anonymous usage stats to help improve node9? (no code, no args)",
10229
+ default: true
10230
+ });
10231
+ if (sendTelemetry) fireTelemetryPing(found);
9844
10232
  console.log("");
9845
10233
  }
9846
10234
  console.log(chalk11.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
9847
- console.log(chalk11.gray(" Run: node9 daemon start"));
10235
+ console.log("");
10236
+ console.log(chalk11.white(" Start watching: ") + chalk11.cyan("node9 tail"));
10237
+ console.log(chalk11.white(" Browser view: ") + chalk11.cyan("node9 daemon --openui"));
10238
+ console.log(chalk11.white(" Cloud dashboard: ") + chalk11.cyan("node9.ai"));
9848
10239
  });
9849
10240
  }
9850
10241
 
@@ -10450,6 +10841,44 @@ var TOOLS = [
10450
10841
  required: ["service"]
10451
10842
  }
10452
10843
  },
10844
+ {
10845
+ name: "node9_shield_disable",
10846
+ description: "Disable a node9 shield. Use node9_shield_list to see currently active shields.",
10847
+ inputSchema: {
10848
+ type: "object",
10849
+ properties: {
10850
+ service: {
10851
+ type: "string",
10852
+ description: 'Shield name to disable (e.g. "postgres", "aws", "github", "filesystem").'
10853
+ }
10854
+ },
10855
+ required: ["service"]
10856
+ }
10857
+ },
10858
+ {
10859
+ name: "node9_approver_list",
10860
+ description: "List all node9 approver channels and their current enabled/disabled state. Approvers are the channels through which node9 asks a human to approve risky tool calls. Channels: native (OS popup), browser (web UI), cloud (team policy server), terminal (stdin).",
10861
+ inputSchema: { type: "object", properties: {}, required: [] }
10862
+ },
10863
+ {
10864
+ name: "node9_approver_set",
10865
+ description: "Enable or disable a specific node9 approver channel in the global config (~/.node9/config.json). Use this to turn individual channels on or off without touching other settings. Channels: native, browser, cloud, terminal. WARNING: disabling all approvers means node9 cannot prompt for human approval \u2014 use with care.",
10866
+ inputSchema: {
10867
+ type: "object",
10868
+ properties: {
10869
+ channel: {
10870
+ type: "string",
10871
+ enum: ["native", "browser", "cloud", "terminal"],
10872
+ description: "Approver channel to configure."
10873
+ },
10874
+ enabled: {
10875
+ type: "boolean",
10876
+ description: "true to enable the channel, false to disable it."
10877
+ }
10878
+ },
10879
+ required: ["channel", "enabled"]
10880
+ }
10881
+ },
10453
10882
  {
10454
10883
  name: "node9_undo_list",
10455
10884
  description: "List the node9 snapshot history. Each entry shows the git hash, tool that triggered it, a short summary, affected files, working directory, and timestamp. Use this to find a hash before calling node9_undo_revert.",
@@ -10555,6 +10984,79 @@ function handleShieldEnable(args) {
10555
10984
  const shield = getShield(name);
10556
10985
  return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
10557
10986
  }
10987
+ function handleShieldDisable(args) {
10988
+ const service = args.service;
10989
+ if (typeof service !== "string" || !service) {
10990
+ throw new Error("service is required");
10991
+ }
10992
+ const name = resolveShieldName(service);
10993
+ if (!name) {
10994
+ throw new Error(
10995
+ `Unknown shield: "${service}". Run node9_shield_list to see available shields.`
10996
+ );
10997
+ }
10998
+ const active = readActiveShields();
10999
+ if (!active.includes(name)) {
11000
+ return `Shield "${name}" is not active.`;
11001
+ }
11002
+ writeActiveShields(active.filter((s) => s !== name));
11003
+ return `Shield "${name}" disabled.`;
11004
+ }
11005
+ var GLOBAL_CONFIG_PATH2 = path27.join(os20.homedir(), ".node9", "config.json");
11006
+ var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
11007
+ function readGlobalConfigRaw() {
11008
+ try {
11009
+ if (fs24.existsSync(GLOBAL_CONFIG_PATH2)) {
11010
+ return JSON.parse(fs24.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
11011
+ }
11012
+ } catch {
11013
+ }
11014
+ return {};
11015
+ }
11016
+ function writeGlobalConfigRaw(data) {
11017
+ const dir = path27.dirname(GLOBAL_CONFIG_PATH2);
11018
+ if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
11019
+ fs24.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
11020
+ }
11021
+ function handleApproverList() {
11022
+ const config = getConfig();
11023
+ const approvers = config.settings.approvers;
11024
+ const lines = ["Approver channels:\n"];
11025
+ for (const ch of APPROVER_CHANNELS) {
11026
+ const on = approvers[ch];
11027
+ lines.push(` ${on ? "[enabled] " : "[disabled]"} ${ch}`);
11028
+ }
11029
+ const enabledCount = APPROVER_CHANNELS.filter((ch) => approvers[ch]).length;
11030
+ if (enabledCount === 0) {
11031
+ lines.push("\nWARNING: all approver channels are disabled \u2014 node9 cannot prompt for approval.");
11032
+ }
11033
+ return lines.join("\n");
11034
+ }
11035
+ function handleApproverSet(args) {
11036
+ const channel = args.channel;
11037
+ const enabled = args.enabled;
11038
+ if (!channel || !APPROVER_CHANNELS.includes(channel)) {
11039
+ throw new Error(
11040
+ `Invalid channel: "${channel}". Must be one of: ${APPROVER_CHANNELS.join(", ")}.`
11041
+ );
11042
+ }
11043
+ if (typeof enabled !== "boolean") {
11044
+ throw new Error("enabled must be a boolean (true or false).");
11045
+ }
11046
+ const raw = readGlobalConfigRaw();
11047
+ const settings = raw.settings ?? {};
11048
+ const approvers = settings.approvers ?? {};
11049
+ approvers[channel] = enabled;
11050
+ settings.approvers = approvers;
11051
+ raw.settings = settings;
11052
+ writeGlobalConfigRaw(raw);
11053
+ const currentApprovers = getConfig().settings.approvers;
11054
+ const anyEnabled = APPROVER_CHANNELS.some(
11055
+ (ch) => ch === channel ? enabled : currentApprovers[ch]
11056
+ );
11057
+ const suffix = anyEnabled ? "" : "\nWARNING: all approver channels are now disabled \u2014 node9 cannot prompt for approval.";
11058
+ return `Approver channel "${channel}" ${enabled ? "enabled" : "disabled"} in ~/.node9/config.json.${suffix}`;
11059
+ }
10558
11060
  function handleUndoList() {
10559
11061
  const history = getSnapshotHistory();
10560
11062
  if (history.length === 0) {
@@ -10628,6 +11130,12 @@ function runMcpServer() {
10628
11130
  text = handleShieldList();
10629
11131
  } else if (toolName === "node9_shield_enable") {
10630
11132
  text = handleShieldEnable(toolArgs);
11133
+ } else if (toolName === "node9_shield_disable") {
11134
+ text = handleShieldDisable(toolArgs);
11135
+ } else if (toolName === "node9_approver_list") {
11136
+ text = handleApproverList();
11137
+ } else if (toolName === "node9_approver_set") {
11138
+ text = handleApproverSet(toolArgs);
10631
11139
  } else if (toolName === "node9_undo_list") {
10632
11140
  text = handleUndoList();
10633
11141
  } else if (toolName === "node9_undo_revert") {