@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.js CHANGED
@@ -513,6 +513,84 @@ var init_shields = __esm({
513
513
  ],
514
514
  dangerousWords: []
515
515
  },
516
+ "bash-safe": {
517
+ name: "bash-safe",
518
+ description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
519
+ aliases: ["bash", "shell"],
520
+ smartRules: [
521
+ {
522
+ name: "shield:bash-safe:block-pipe-to-shell",
523
+ tool: "bash",
524
+ conditions: [
525
+ {
526
+ field: "command",
527
+ op: "matches",
528
+ value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
529
+ flags: "i"
530
+ }
531
+ ],
532
+ verdict: "block",
533
+ reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
534
+ },
535
+ {
536
+ name: "shield:bash-safe:block-obfuscated-exec",
537
+ tool: "bash",
538
+ conditions: [
539
+ {
540
+ field: "command",
541
+ op: "matches",
542
+ value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
543
+ flags: "i"
544
+ }
545
+ ],
546
+ verdict: "block",
547
+ reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
548
+ },
549
+ {
550
+ name: "shield:bash-safe:block-rm-root",
551
+ tool: "bash",
552
+ conditions: [
553
+ {
554
+ field: "command",
555
+ op: "matches",
556
+ 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*$",
557
+ flags: "i"
558
+ }
559
+ ],
560
+ verdict: "block",
561
+ reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
562
+ },
563
+ {
564
+ name: "shield:bash-safe:block-disk-overwrite",
565
+ tool: "bash",
566
+ conditions: [
567
+ {
568
+ field: "command",
569
+ op: "matches",
570
+ value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
571
+ flags: "i"
572
+ }
573
+ ],
574
+ verdict: "block",
575
+ reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
576
+ },
577
+ {
578
+ name: "shield:bash-safe:review-eval",
579
+ tool: "bash",
580
+ conditions: [
581
+ {
582
+ field: "command",
583
+ op: "matches",
584
+ value: '\\beval\\s+[\\$`("]',
585
+ flags: "i"
586
+ }
587
+ ],
588
+ verdict: "review",
589
+ reason: "eval of dynamic content requires human approval (bash-safe shield)"
590
+ }
591
+ ],
592
+ dangerousWords: []
593
+ },
516
594
  filesystem: {
517
595
  name: "filesystem",
518
596
  description: "Protects the local filesystem from dangerous AI operations",
@@ -799,7 +877,7 @@ var init_config = __esm({
799
877
  DEFAULT_CONFIG = {
800
878
  version: "1.0",
801
879
  settings: {
802
- mode: "audit",
880
+ mode: "standard",
803
881
  autoStartDaemon: true,
804
882
  enableUndo: true,
805
883
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
@@ -3198,6 +3276,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
3198
3276
  await notifyActivity({
3199
3277
  id: actId,
3200
3278
  tool: toolName,
3279
+ args,
3201
3280
  ts: actTs,
3202
3281
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
3203
3282
  label: result.blockedByLabel,
@@ -5476,6 +5555,7 @@ var init_session_counters = __esm({
5476
5555
  _blocked = 0;
5477
5556
  _dlpHits = 0;
5478
5557
  _wouldBlock = 0;
5558
+ _estimatedCost = 0;
5479
5559
  _lastRuleHit = null;
5480
5560
  _lastBlockedTool = null;
5481
5561
  incrementAllowed() {
@@ -5490,6 +5570,10 @@ var init_session_counters = __esm({
5490
5570
  incrementWouldBlock() {
5491
5571
  this._wouldBlock++;
5492
5572
  }
5573
+ addCost(amount) {
5574
+ if (!isFinite(amount) || amount < 0) return;
5575
+ this._estimatedCost += amount;
5576
+ }
5493
5577
  recordRuleHit(label) {
5494
5578
  this._lastRuleHit = label;
5495
5579
  }
@@ -5502,6 +5586,7 @@ var init_session_counters = __esm({
5502
5586
  blocked: this._blocked,
5503
5587
  dlpHits: this._dlpHits,
5504
5588
  wouldBlock: this._wouldBlock,
5589
+ estimatedCost: this._estimatedCost,
5505
5590
  lastRuleHit: this._lastRuleHit,
5506
5591
  lastBlockedTool: this._lastBlockedTool
5507
5592
  };
@@ -5511,6 +5596,7 @@ var init_session_counters = __esm({
5511
5596
  this._blocked = 0;
5512
5597
  this._dlpHits = 0;
5513
5598
  this._wouldBlock = 0;
5599
+ this._estimatedCost = 0;
5514
5600
  this._lastRuleHit = null;
5515
5601
  this._lastBlockedTool = null;
5516
5602
  }
@@ -5735,15 +5821,38 @@ function openBrowser(url) {
5735
5821
  } catch {
5736
5822
  }
5737
5823
  }
5824
+ function estimateToolCost(tool, args) {
5825
+ const a = args ?? {};
5826
+ const t = tool.toLowerCase().replace(/[^a-z_]/g, "_");
5827
+ if (t.includes("read") || t === "glob" || t === "grep") {
5828
+ const filePath = a.file_path ?? a.path;
5829
+ if (filePath) {
5830
+ try {
5831
+ const bytes = import_fs13.default.statSync(filePath).size;
5832
+ return bytes / BYTES_PER_TOKEN / 1e6 * INPUT_PRICE_PER_1M;
5833
+ } catch {
5834
+ }
5835
+ }
5836
+ }
5837
+ if (t.includes("write")) {
5838
+ const content = a.content ?? "";
5839
+ return String(content).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
5840
+ }
5841
+ if (t.includes("edit") || t === "str_replace_based_edit_tool") {
5842
+ const newStr = a.new_string ?? "";
5843
+ return String(newStr).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
5844
+ }
5845
+ return void 0;
5846
+ }
5738
5847
  function broadcast(event, data) {
5739
5848
  if (event === "activity") {
5740
5849
  activityRing.push({ event, data });
5741
5850
  if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
5742
5851
  } else if (event === "activity-result") {
5743
- const { id, status, label } = data;
5852
+ const { id, status, label, costEstimate } = data;
5744
5853
  for (let i = activityRing.length - 1; i >= 0; i--) {
5745
5854
  if (activityRing[i].data.id === id) {
5746
- Object.assign(activityRing[i].data, { status, label });
5855
+ Object.assign(activityRing[i].data, { status, label, costEstimate });
5747
5856
  break;
5748
5857
  }
5749
5858
  }
@@ -5845,10 +5954,13 @@ function startActivitySocket() {
5845
5954
  sessionCounters.incrementBlocked();
5846
5955
  sessionCounters.recordBlockedTool(data.tool);
5847
5956
  }
5957
+ const costEstimate = data.status === "allow" ? estimateToolCost(data.tool, data.args) : void 0;
5958
+ if (costEstimate != null && costEstimate > 0) sessionCounters.addCost(costEstimate);
5848
5959
  broadcast("activity-result", {
5849
5960
  id: data.id,
5850
5961
  status: data.status,
5851
- label: data.label
5962
+ label: data.label,
5963
+ costEstimate
5852
5964
  });
5853
5965
  }
5854
5966
  } catch {
@@ -5865,7 +5977,7 @@ function startActivitySocket() {
5865
5977
  }
5866
5978
  });
5867
5979
  }
5868
- var import_net2, import_fs13, import_path16, import_os11, import_child_process3, import_crypto5, 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;
5980
+ var import_net2, import_fs13, import_path16, import_os11, import_child_process3, import_crypto5, 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;
5869
5981
  var init_state2 = __esm({
5870
5982
  "src/daemon/state.ts"() {
5871
5983
  "use strict";
@@ -5909,6 +6021,9 @@ var init_state2 = __esm({
5909
6021
  ACTIVITY_RING_SIZE = 100;
5910
6022
  activityRing = [];
5911
6023
  SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
6024
+ INPUT_PRICE_PER_1M = 3;
6025
+ OUTPUT_PRICE_PER_1M = 15;
6026
+ BYTES_PER_TOKEN = 4;
5912
6027
  WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
5913
6028
  "write",
5914
6029
  "write_file",
@@ -6366,7 +6481,8 @@ data: ${JSON.stringify(item.data)}
6366
6481
  allowed: counters.allowed,
6367
6482
  blocked: counters.blocked,
6368
6483
  dlpHits: counters.dlpHits,
6369
- wouldBlock: counters.wouldBlock
6484
+ wouldBlock: counters.wouldBlock,
6485
+ estimatedCost: counters.estimatedCost
6370
6486
  },
6371
6487
  taintedCount: taintStore.list().length,
6372
6488
  lastRuleHit: counters.lastRuleHit,
@@ -6498,6 +6614,7 @@ data: ${JSON.stringify(item.data)}
6498
6614
  }
6499
6615
  if (req.method === "POST" && pathname === "/events/clear") {
6500
6616
  activityRing.length = 0;
6617
+ sessionCounters.reset();
6501
6618
  res.writeHead(200, { "Content-Type": "application/json" });
6502
6619
  return res.end(JSON.stringify({ ok: true }));
6503
6620
  }
@@ -6811,7 +6928,7 @@ function formatBase(activity) {
6811
6928
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
6812
6929
  const icon = getIcon(activity.tool);
6813
6930
  const toolName = activity.tool.slice(0, 16).padEnd(16);
6814
- const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
6931
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(import_os21.default.homedir(), "~");
6815
6932
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
6816
6933
  return `${import_chalk17.default.gray(time)} ${icon} ${import_chalk17.default.white.bold(toolName)} ${import_chalk17.default.dim(argsPreview)}`;
6817
6934
  }
@@ -6825,11 +6942,13 @@ function renderResult(activity, result) {
6825
6942
  } else {
6826
6943
  status = import_chalk17.default.red("\u2717 BLOCK");
6827
6944
  }
6945
+ const cost = result.costEstimate ?? activity.costEstimate;
6946
+ const costSuffix = cost == null ? "" : import_chalk17.default.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
6828
6947
  if (process.stdout.isTTY) {
6829
6948
  import_readline5.default.clearLine(process.stdout, 0);
6830
6949
  import_readline5.default.cursorTo(process.stdout, 0);
6831
6950
  }
6832
- console.log(`${base} ${status}`);
6951
+ console.log(`${base} ${status}${costSuffix}`);
6833
6952
  }
6834
6953
  function renderPending(activity) {
6835
6954
  if (!process.stdout.isTTY) return;
@@ -6958,6 +7077,39 @@ function buildRecoveryCardLines(req) {
6958
7077
  ``
6959
7078
  ];
6960
7079
  }
7080
+ function readApproversFromDisk() {
7081
+ const configPath = import_path28.default.join(import_os21.default.homedir(), ".node9", "config.json");
7082
+ try {
7083
+ const raw = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
7084
+ const settings = raw.settings ?? {};
7085
+ return settings.approvers ?? {};
7086
+ } catch {
7087
+ return {};
7088
+ }
7089
+ }
7090
+ function approverStatusLine() {
7091
+ const a = readApproversFromDisk();
7092
+ const fmt = (label, key) => {
7093
+ const on = a[key] !== false;
7094
+ return `[${key[0]}]${label.slice(1)} ${on ? import_chalk17.default.green("\u2713") : import_chalk17.default.dim("\u2717")}`;
7095
+ };
7096
+ return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
7097
+ }
7098
+ function toggleApprover(channel) {
7099
+ const configPath = import_path28.default.join(import_os21.default.homedir(), ".node9", "config.json");
7100
+ try {
7101
+ const raw = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
7102
+ const settings = raw.settings ?? {};
7103
+ const approvers = settings.approvers ?? {};
7104
+ approvers[channel] = approvers[channel] === false;
7105
+ settings.approvers = approvers;
7106
+ raw.settings = settings;
7107
+ import_fs25.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
7108
+ } catch (err2) {
7109
+ process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
7110
+ `);
7111
+ }
7112
+ }
6961
7113
  async function startTail(options = {}) {
6962
7114
  const port = await ensureDaemon();
6963
7115
  if (options.clear) {
@@ -6996,6 +7148,7 @@ async function startTail(options = {}) {
6996
7148
  }
6997
7149
  const connectionTime = Date.now();
6998
7150
  const activityPending = /* @__PURE__ */ new Map();
7151
+ const orphanedResults = /* @__PURE__ */ new Map();
6999
7152
  let csrfToken = "";
7000
7153
  const approvalQueue = [];
7001
7154
  let cardActive = false;
@@ -7004,6 +7157,44 @@ async function startTail(options = {}) {
7004
7157
  const localAllowCounts = /* @__PURE__ */ new Map();
7005
7158
  const canApprove = process.stdout.isTTY && process.stdin.isTTY;
7006
7159
  if (canApprove) import_readline5.default.emitKeypressEvents(process.stdin);
7160
+ let idleKeypressHandler = null;
7161
+ function enterIdleMode() {
7162
+ if (!canApprove || idleKeypressHandler !== null) return;
7163
+ try {
7164
+ process.stdin.setRawMode(true);
7165
+ } catch {
7166
+ return;
7167
+ }
7168
+ process.stdin.resume();
7169
+ idleKeypressHandler = (_str, key) => {
7170
+ const name = key?.name ?? "";
7171
+ if (key?.ctrl && name === "c") {
7172
+ process.kill(process.pid, "SIGINT");
7173
+ return;
7174
+ }
7175
+ if (name === "q") {
7176
+ process.kill(process.pid, "SIGINT");
7177
+ return;
7178
+ }
7179
+ const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
7180
+ if (channel) {
7181
+ toggleApprover(channel);
7182
+ console.log(import_chalk17.default.dim(` Approvers: ${approverStatusLine()}`));
7183
+ }
7184
+ };
7185
+ process.stdin.on("keypress", idleKeypressHandler);
7186
+ }
7187
+ function exitIdleMode() {
7188
+ if (idleKeypressHandler) {
7189
+ process.stdin.removeListener("keypress", idleKeypressHandler);
7190
+ idleKeypressHandler = null;
7191
+ }
7192
+ try {
7193
+ process.stdin.setRawMode(false);
7194
+ } catch {
7195
+ }
7196
+ process.stdin.pause();
7197
+ }
7007
7198
  function clearCard() {
7008
7199
  if (cardLineCount > 0) {
7009
7200
  import_readline5.default.moveCursor(process.stdout, 0, -cardLineCount);
@@ -7022,10 +7213,12 @@ async function startTail(options = {}) {
7022
7213
  }
7023
7214
  function showNextCard() {
7024
7215
  if (cardActive || approvalQueue.length === 0 || !canApprove) return;
7216
+ exitIdleMode();
7025
7217
  try {
7026
7218
  process.stdin.setRawMode(true);
7027
7219
  } catch {
7028
7220
  cardActive = false;
7221
+ enterIdleMode();
7029
7222
  return;
7030
7223
  }
7031
7224
  cardActive = true;
@@ -7037,12 +7230,8 @@ async function startTail(options = {}) {
7037
7230
  const handler = onKeypress;
7038
7231
  onKeypress = null;
7039
7232
  if (handler) process.stdin.removeListener("keypress", handler);
7040
- try {
7041
- process.stdin.setRawMode(false);
7042
- } catch {
7043
- }
7044
- process.stdin.pause();
7045
7233
  cancelActiveCard = null;
7234
+ enterIdleMode();
7046
7235
  };
7047
7236
  const settle = (action) => {
7048
7237
  if (settled) return;
@@ -7169,18 +7358,16 @@ async function startTail(options = {}) {
7169
7358
  console.log(import_chalk17.default.cyan.bold(`
7170
7359
  \u{1F6F0}\uFE0F Node9 tail `) + import_chalk17.default.dim(`\u2192 ${dashboardUrl}`));
7171
7360
  if (canApprove) {
7172
- console.log(
7173
- import_chalk17.default.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
7174
- );
7361
+ console.log(import_chalk17.default.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
7362
+ console.log(import_chalk17.default.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
7175
7363
  }
7176
7364
  if (options.history) {
7177
- console.log(import_chalk17.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
7365
+ console.log(import_chalk17.default.dim("Showing history + live events.\n"));
7178
7366
  } else {
7179
- console.log(
7180
- import_chalk17.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
7181
- );
7367
+ console.log(import_chalk17.default.dim("Showing live events only. Use --history to include past.\n"));
7182
7368
  }
7183
7369
  process.on("SIGINT", () => {
7370
+ exitIdleMode();
7184
7371
  clearCard();
7185
7372
  process.stdout.write(SHOW_CURSOR);
7186
7373
  if (process.stdout.isTTY) {
@@ -7196,6 +7383,7 @@ async function startTail(options = {}) {
7196
7383
  console.error(import_chalk17.default.red(`Failed to connect: HTTP ${res.statusCode}`));
7197
7384
  process.exit(1);
7198
7385
  }
7386
+ if (canApprove) enterIdleMode();
7199
7387
  let currentEvent = "";
7200
7388
  let currentData = "";
7201
7389
  res.on("error", () => {
@@ -7295,9 +7483,14 @@ async function startTail(options = {}) {
7295
7483
  renderResult(data, data);
7296
7484
  return;
7297
7485
  }
7486
+ const orphaned = orphanedResults.get(data.id);
7487
+ if (orphaned) {
7488
+ orphanedResults.delete(data.id);
7489
+ renderResult(data, orphaned);
7490
+ return;
7491
+ }
7298
7492
  activityPending.set(data.id, data);
7299
- const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
7300
- if (slowTool) renderPending(data);
7493
+ renderPending(data);
7301
7494
  }
7302
7495
  if (event === "snapshot") {
7303
7496
  const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
@@ -7316,6 +7509,8 @@ async function startTail(options = {}) {
7316
7509
  if (original) {
7317
7510
  renderResult(original, data);
7318
7511
  activityPending.delete(data.id);
7512
+ } else {
7513
+ orphanedResults.set(data.id, data);
7319
7514
  }
7320
7515
  }
7321
7516
  }
@@ -7558,6 +7753,29 @@ function renderOffline() {
7558
7753
  process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
7559
7754
  `);
7560
7755
  }
7756
+ function readActiveShieldsHud() {
7757
+ const now = Date.now();
7758
+ if (shieldsCache && now - shieldsCache.ts < SHIELDS_CACHE_TTL_MS) {
7759
+ return shieldsCache.value;
7760
+ }
7761
+ try {
7762
+ const shieldsPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "shields.json");
7763
+ if (!import_fs26.default.existsSync(shieldsPath)) {
7764
+ shieldsCache = { value: [], ts: now };
7765
+ return [];
7766
+ }
7767
+ const parsed = JSON.parse(import_fs26.default.readFileSync(shieldsPath, "utf-8"));
7768
+ if (!Array.isArray(parsed.active)) {
7769
+ shieldsCache = { value: [], ts: now };
7770
+ return [];
7771
+ }
7772
+ const value = parsed.active.filter((s) => typeof s === "string").map((s) => s.slice(0, 64)).slice(0, 20);
7773
+ shieldsCache = { value, ts: now };
7774
+ return value;
7775
+ } catch {
7776
+ return [];
7777
+ }
7778
+ }
7561
7779
  function renderSecurityLine(status) {
7562
7780
  const parts = [];
7563
7781
  parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
@@ -7575,6 +7793,18 @@ function renderSecurityLine(status) {
7575
7793
  };
7576
7794
  const mc = modeColors[status.mode] ?? WHITE;
7577
7795
  parts.push(`${dim("|")} ${color(mc, modeIcon[status.mode] ?? "")}${color(mc, status.mode)}`);
7796
+ const activeShields = readActiveShieldsHud();
7797
+ if (activeShields.length > 0) {
7798
+ const shieldAbbrevs = {
7799
+ "bash-safe": "bash",
7800
+ filesystem: "fs",
7801
+ postgres: "pg",
7802
+ github: "gh",
7803
+ aws: "aws"
7804
+ };
7805
+ const labels = activeShields.map((s) => shieldAbbrevs[s] ?? s).join(" ");
7806
+ parts.push(color(DIM, `[${labels}]`));
7807
+ }
7578
7808
  if (status.mode === "observe") {
7579
7809
  parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
7580
7810
  if (status.session.wouldBlock > 0) {
@@ -7589,6 +7819,11 @@ function renderSecurityLine(status) {
7589
7819
  parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
7590
7820
  }
7591
7821
  }
7822
+ if (status.session.estimatedCost > 0) {
7823
+ const cost = status.session.estimatedCost;
7824
+ const costStr = cost >= 0.01 ? `$${cost.toFixed(2)}` : cost >= 1e-3 ? `$${cost.toFixed(3)}` : "<$0.001";
7825
+ parts.push(color(DIM, `~${costStr}`));
7826
+ }
7592
7827
  if (status.taintedCount > 0) {
7593
7828
  parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
7594
7829
  }
@@ -7666,7 +7901,7 @@ async function main() {
7666
7901
  renderOffline();
7667
7902
  }
7668
7903
  }
7669
- var import_fs26, import_path29, import_os22, import_http3, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
7904
+ var import_fs26, import_path29, import_os22, import_http3, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
7670
7905
  var init_hud = __esm({
7671
7906
  "src/cli/hud.ts"() {
7672
7907
  "use strict";
@@ -7688,6 +7923,8 @@ var init_hud = __esm({
7688
7923
  BAR_FILLED = "\u2588";
7689
7924
  BAR_EMPTY = "\u2591";
7690
7925
  BAR_WIDTH = 10;
7926
+ shieldsCache = null;
7927
+ SHIELDS_CACHE_TTL_MS = 2e3;
7691
7928
  }
7692
7929
  });
7693
7930
 
@@ -7701,6 +7938,7 @@ var import_path14 = __toESM(require("path"));
7701
7938
  var import_os10 = __toESM(require("os"));
7702
7939
  var import_chalk = __toESM(require("chalk"));
7703
7940
  var import_prompts = require("@inquirer/prompts");
7941
+ var import_smol_toml = require("smol-toml");
7704
7942
  var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7705
7943
  function hasNode9McpServer(servers) {
7706
7944
  const entry = servers["node9"];
@@ -8064,7 +8302,8 @@ function detectAgents(homeDir2 = import_os10.default.homedir()) {
8064
8302
  return {
8065
8303
  claude: exists(import_path14.default.join(homeDir2, ".claude")) || exists(import_path14.default.join(homeDir2, ".claude.json")),
8066
8304
  gemini: exists(import_path14.default.join(homeDir2, ".gemini")),
8067
- cursor: exists(import_path14.default.join(homeDir2, ".cursor"))
8305
+ cursor: exists(import_path14.default.join(homeDir2, ".cursor")),
8306
+ codex: exists(import_path14.default.join(homeDir2, ".codex"))
8068
8307
  };
8069
8308
  }
8070
8309
  async function setupCursor() {
@@ -8129,6 +8368,82 @@ async function setupCursor() {
8129
8368
  printDaemonTip();
8130
8369
  }
8131
8370
  }
8371
+ function readToml(filePath) {
8372
+ try {
8373
+ if (import_fs11.default.existsSync(filePath)) {
8374
+ return (0, import_smol_toml.parse)(import_fs11.default.readFileSync(filePath, "utf-8"));
8375
+ }
8376
+ } catch {
8377
+ }
8378
+ return null;
8379
+ }
8380
+ function writeToml(filePath, data) {
8381
+ const dir = import_path14.default.dirname(filePath);
8382
+ if (!import_fs11.default.existsSync(dir)) import_fs11.default.mkdirSync(dir, { recursive: true });
8383
+ import_fs11.default.writeFileSync(filePath, (0, import_smol_toml.stringify)(data));
8384
+ }
8385
+ async function setupCodex() {
8386
+ const homeDir2 = import_os10.default.homedir();
8387
+ const configPath = import_path14.default.join(homeDir2, ".codex", "config.toml");
8388
+ const config = readToml(configPath) ?? {};
8389
+ const servers = config.mcp_servers ?? {};
8390
+ let anythingChanged = false;
8391
+ if (!hasNode9McpServer(servers)) {
8392
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
8393
+ config.mcp_servers = servers;
8394
+ writeToml(configPath, config);
8395
+ console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
8396
+ anythingChanged = true;
8397
+ }
8398
+ const serversToWrap = [];
8399
+ for (const [name, server] of Object.entries(servers)) {
8400
+ if (!server.command || server.command === "node9") continue;
8401
+ const parts = [server.command, ...server.args ?? []];
8402
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
8403
+ }
8404
+ if (serversToWrap.length > 0) {
8405
+ console.log(import_chalk.default.bold("The following existing entries will be modified:\n"));
8406
+ console.log(import_chalk.default.white(` ${configPath}`));
8407
+ for (const { name, originalCmd } of serversToWrap) {
8408
+ console.log(import_chalk.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
8409
+ }
8410
+ console.log("");
8411
+ const proceed = await (0, import_prompts.confirm)({ message: "Wrap these MCP servers?", default: true });
8412
+ if (proceed) {
8413
+ for (const { name, parts } of serversToWrap) {
8414
+ servers[name] = { ...servers[name], command: "node9", args: parts };
8415
+ }
8416
+ config.mcp_servers = servers;
8417
+ writeToml(configPath, config);
8418
+ console.log(import_chalk.default.green(`
8419
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
8420
+ anythingChanged = true;
8421
+ } else {
8422
+ console.log(import_chalk.default.yellow(" Skipped MCP server wrapping."));
8423
+ }
8424
+ console.log("");
8425
+ }
8426
+ console.log(
8427
+ import_chalk.default.yellow(
8428
+ " \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."
8429
+ )
8430
+ );
8431
+ console.log("");
8432
+ if (!anythingChanged && serversToWrap.length === 0) {
8433
+ console.log(
8434
+ import_chalk.default.blue(
8435
+ "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.codex/config.toml and re-run."
8436
+ )
8437
+ );
8438
+ printDaemonTip();
8439
+ return;
8440
+ }
8441
+ if (anythingChanged) {
8442
+ console.log(import_chalk.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Codex via MCP proxy!"));
8443
+ console.log(import_chalk.default.gray(" Restart Codex for changes to take effect."));
8444
+ printDaemonTip();
8445
+ }
8446
+ }
8132
8447
  function setupHud() {
8133
8448
  const homeDir2 = import_os10.default.homedir();
8134
8449
  const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
@@ -8621,6 +8936,9 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
8621
8936
  if (filesRes.status === 0) {
8622
8937
  capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
8623
8938
  }
8939
+ if (capturedFiles.length === 0) {
8940
+ return prevEntry.hash;
8941
+ }
8624
8942
  const diffRes = (0, import_child_process8.spawnSync)("git", ["diff", prevEntry.hash, commitHash], {
8625
8943
  env: shadowEnv,
8626
8944
  timeout: GIT_TIMEOUT
@@ -9817,25 +10135,91 @@ var import_chalk11 = __toESM(require("chalk"));
9817
10135
  var import_fs23 = __toESM(require("fs"));
9818
10136
  var import_path25 = __toESM(require("path"));
9819
10137
  var import_os19 = __toESM(require("os"));
10138
+ var import_https = __toESM(require("https"));
9820
10139
  init_core();
10140
+ init_shields();
10141
+ var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
10142
+ function fireTelemetryPing(agents) {
10143
+ try {
10144
+ const body = JSON.stringify({
10145
+ event: "init_completed",
10146
+ agents_detected: agents,
10147
+ os: process.platform,
10148
+ node9_version: process.env.npm_package_version ?? "unknown"
10149
+ });
10150
+ const req = import_https.default.request(
10151
+ {
10152
+ hostname: "api.node9.ai",
10153
+ path: "/api/v1/telemetry",
10154
+ method: "POST",
10155
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
10156
+ timeout: 3e3
10157
+ },
10158
+ (res) => {
10159
+ res.resume();
10160
+ }
10161
+ );
10162
+ req.on("error", () => {
10163
+ });
10164
+ req.on("timeout", () => {
10165
+ req.destroy();
10166
+ });
10167
+ req.end(body);
10168
+ } catch {
10169
+ }
10170
+ }
9821
10171
  function registerInitCommand(program2) {
9822
10172
  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) => {
9823
10173
  console.log(import_chalk11.default.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
10174
+ let chosenMode = options.mode.toLowerCase();
10175
+ if (!["standard", "strict", "audit"].includes(chosenMode)) {
10176
+ chosenMode = DEFAULT_CONFIG.settings.mode;
10177
+ }
10178
+ {
10179
+ const { confirm: confirm3 } = await import("@inquirer/prompts");
10180
+ const enableShields = await confirm3({
10181
+ message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
10182
+ default: true
10183
+ });
10184
+ if (enableShields) {
10185
+ chosenMode = "standard";
10186
+ try {
10187
+ const current = readActiveShields();
10188
+ const merged = Array.from(/* @__PURE__ */ new Set([...current, ...DEFAULT_SHIELDS]));
10189
+ const hasNewShields = DEFAULT_SHIELDS.some((s) => !current.includes(s));
10190
+ if (hasNewShields) writeActiveShields(merged);
10191
+ } catch (err2) {
10192
+ console.log(import_chalk11.default.yellow(` \u26A0\uFE0F Could not update shields: ${String(err2)}`));
10193
+ }
10194
+ }
10195
+ console.log("");
10196
+ }
9824
10197
  const configPath = import_path25.default.join(import_os19.default.homedir(), ".node9", "config.json");
9825
10198
  if (import_fs23.default.existsSync(configPath) && !options.force) {
9826
- console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
10199
+ try {
10200
+ const existing = JSON.parse(import_fs23.default.readFileSync(configPath, "utf-8"));
10201
+ const settings = existing.settings ?? {};
10202
+ if (settings.mode !== chosenMode) {
10203
+ settings.mode = chosenMode;
10204
+ existing.settings = settings;
10205
+ import_fs23.default.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
10206
+ console.log(import_chalk11.default.green(`\u2705 Mode updated: ${chosenMode}`));
10207
+ } else {
10208
+ console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
10209
+ }
10210
+ } catch {
10211
+ console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
10212
+ }
9827
10213
  } else {
9828
- const requestedMode = options.mode.toLowerCase();
9829
- const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
9830
10214
  const configToSave = {
9831
10215
  ...DEFAULT_CONFIG,
9832
- settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
10216
+ settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
9833
10217
  };
9834
10218
  const dir = import_path25.default.dirname(configPath);
9835
10219
  if (!import_fs23.default.existsSync(dir)) import_fs23.default.mkdirSync(dir, { recursive: true });
9836
- import_fs23.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
10220
+ import_fs23.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
9837
10221
  console.log(import_chalk11.default.green(`\u2705 Config created: ${configPath}`));
9838
- console.log(import_chalk11.default.gray(` Mode: ${safeMode}`));
10222
+ console.log(import_chalk11.default.gray(` Mode: ${chosenMode}`));
9839
10223
  }
9840
10224
  if (options.skipSetup) return;
9841
10225
  console.log("");
@@ -9845,9 +10229,9 @@ function registerInitCommand(program2) {
9845
10229
  );
9846
10230
  if (found.length === 0) {
9847
10231
  console.log(
9848
- import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, or Cursor")
10232
+ import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, Cursor, or Codex")
9849
10233
  );
9850
- console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor>"));
10234
+ console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
9851
10235
  return;
9852
10236
  }
9853
10237
  console.log(import_chalk11.default.bold("Detected agents:"));
@@ -9860,16 +10244,23 @@ function registerInitCommand(program2) {
9860
10244
  if (agent === "claude") await setupClaude();
9861
10245
  else if (agent === "gemini") await setupGemini();
9862
10246
  else if (agent === "cursor") await setupCursor();
10247
+ else if (agent === "codex") await setupCodex();
9863
10248
  console.log("");
9864
10249
  }
9865
- if (detected.claude) {
9866
- setupHud();
9867
- console.log(import_chalk11.default.green("\u2705 node9 HUD added to Claude Code statusline"));
9868
- console.log(import_chalk11.default.gray(" Restart Claude Code to activate the security statusline."));
10250
+ {
10251
+ const { confirm: confirm3 } = await import("@inquirer/prompts");
10252
+ const sendTelemetry = await confirm3({
10253
+ message: "Send anonymous usage stats to help improve node9? (no code, no args)",
10254
+ default: true
10255
+ });
10256
+ if (sendTelemetry) fireTelemetryPing(found);
9869
10257
  console.log("");
9870
10258
  }
9871
10259
  console.log(import_chalk11.default.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
9872
- console.log(import_chalk11.default.gray(" Run: node9 daemon start"));
10260
+ console.log("");
10261
+ console.log(import_chalk11.default.white(" Start watching: ") + import_chalk11.default.cyan("node9 tail"));
10262
+ console.log(import_chalk11.default.white(" Browser view: ") + import_chalk11.default.cyan("node9 daemon --openui"));
10263
+ console.log(import_chalk11.default.white(" Cloud dashboard: ") + import_chalk11.default.cyan("node9.ai"));
9873
10264
  });
9874
10265
  }
9875
10266
 
@@ -10475,6 +10866,44 @@ var TOOLS = [
10475
10866
  required: ["service"]
10476
10867
  }
10477
10868
  },
10869
+ {
10870
+ name: "node9_shield_disable",
10871
+ description: "Disable a node9 shield. Use node9_shield_list to see currently active shields.",
10872
+ inputSchema: {
10873
+ type: "object",
10874
+ properties: {
10875
+ service: {
10876
+ type: "string",
10877
+ description: 'Shield name to disable (e.g. "postgres", "aws", "github", "filesystem").'
10878
+ }
10879
+ },
10880
+ required: ["service"]
10881
+ }
10882
+ },
10883
+ {
10884
+ name: "node9_approver_list",
10885
+ 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).",
10886
+ inputSchema: { type: "object", properties: {}, required: [] }
10887
+ },
10888
+ {
10889
+ name: "node9_approver_set",
10890
+ 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.",
10891
+ inputSchema: {
10892
+ type: "object",
10893
+ properties: {
10894
+ channel: {
10895
+ type: "string",
10896
+ enum: ["native", "browser", "cloud", "terminal"],
10897
+ description: "Approver channel to configure."
10898
+ },
10899
+ enabled: {
10900
+ type: "boolean",
10901
+ description: "true to enable the channel, false to disable it."
10902
+ }
10903
+ },
10904
+ required: ["channel", "enabled"]
10905
+ }
10906
+ },
10478
10907
  {
10479
10908
  name: "node9_undo_list",
10480
10909
  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.",
@@ -10580,6 +11009,79 @@ function handleShieldEnable(args) {
10580
11009
  const shield = getShield(name);
10581
11010
  return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
10582
11011
  }
11012
+ function handleShieldDisable(args) {
11013
+ const service = args.service;
11014
+ if (typeof service !== "string" || !service) {
11015
+ throw new Error("service is required");
11016
+ }
11017
+ const name = resolveShieldName(service);
11018
+ if (!name) {
11019
+ throw new Error(
11020
+ `Unknown shield: "${service}". Run node9_shield_list to see available shields.`
11021
+ );
11022
+ }
11023
+ const active = readActiveShields();
11024
+ if (!active.includes(name)) {
11025
+ return `Shield "${name}" is not active.`;
11026
+ }
11027
+ writeActiveShields(active.filter((s) => s !== name));
11028
+ return `Shield "${name}" disabled.`;
11029
+ }
11030
+ var GLOBAL_CONFIG_PATH2 = import_path27.default.join(import_os20.default.homedir(), ".node9", "config.json");
11031
+ var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
11032
+ function readGlobalConfigRaw() {
11033
+ try {
11034
+ if (import_fs24.default.existsSync(GLOBAL_CONFIG_PATH2)) {
11035
+ return JSON.parse(import_fs24.default.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
11036
+ }
11037
+ } catch {
11038
+ }
11039
+ return {};
11040
+ }
11041
+ function writeGlobalConfigRaw(data) {
11042
+ const dir = import_path27.default.dirname(GLOBAL_CONFIG_PATH2);
11043
+ if (!import_fs24.default.existsSync(dir)) import_fs24.default.mkdirSync(dir, { recursive: true });
11044
+ import_fs24.default.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
11045
+ }
11046
+ function handleApproverList() {
11047
+ const config = getConfig();
11048
+ const approvers = config.settings.approvers;
11049
+ const lines = ["Approver channels:\n"];
11050
+ for (const ch of APPROVER_CHANNELS) {
11051
+ const on = approvers[ch];
11052
+ lines.push(` ${on ? "[enabled] " : "[disabled]"} ${ch}`);
11053
+ }
11054
+ const enabledCount = APPROVER_CHANNELS.filter((ch) => approvers[ch]).length;
11055
+ if (enabledCount === 0) {
11056
+ lines.push("\nWARNING: all approver channels are disabled \u2014 node9 cannot prompt for approval.");
11057
+ }
11058
+ return lines.join("\n");
11059
+ }
11060
+ function handleApproverSet(args) {
11061
+ const channel = args.channel;
11062
+ const enabled = args.enabled;
11063
+ if (!channel || !APPROVER_CHANNELS.includes(channel)) {
11064
+ throw new Error(
11065
+ `Invalid channel: "${channel}". Must be one of: ${APPROVER_CHANNELS.join(", ")}.`
11066
+ );
11067
+ }
11068
+ if (typeof enabled !== "boolean") {
11069
+ throw new Error("enabled must be a boolean (true or false).");
11070
+ }
11071
+ const raw = readGlobalConfigRaw();
11072
+ const settings = raw.settings ?? {};
11073
+ const approvers = settings.approvers ?? {};
11074
+ approvers[channel] = enabled;
11075
+ settings.approvers = approvers;
11076
+ raw.settings = settings;
11077
+ writeGlobalConfigRaw(raw);
11078
+ const currentApprovers = getConfig().settings.approvers;
11079
+ const anyEnabled = APPROVER_CHANNELS.some(
11080
+ (ch) => ch === channel ? enabled : currentApprovers[ch]
11081
+ );
11082
+ const suffix = anyEnabled ? "" : "\nWARNING: all approver channels are now disabled \u2014 node9 cannot prompt for approval.";
11083
+ return `Approver channel "${channel}" ${enabled ? "enabled" : "disabled"} in ~/.node9/config.json.${suffix}`;
11084
+ }
10583
11085
  function handleUndoList() {
10584
11086
  const history = getSnapshotHistory();
10585
11087
  if (history.length === 0) {
@@ -10653,6 +11155,12 @@ function runMcpServer() {
10653
11155
  text = handleShieldList();
10654
11156
  } else if (toolName === "node9_shield_enable") {
10655
11157
  text = handleShieldEnable(toolArgs);
11158
+ } else if (toolName === "node9_shield_disable") {
11159
+ text = handleShieldDisable(toolArgs);
11160
+ } else if (toolName === "node9_approver_list") {
11161
+ text = handleApproverList();
11162
+ } else if (toolName === "node9_approver_set") {
11163
+ text = handleApproverSet(toolArgs);
10656
11164
  } else if (toolName === "node9_undo_list") {
10657
11165
  text = handleUndoList();
10658
11166
  } else if (toolName === "node9_undo_revert") {