@node9/proxy 1.7.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",
@@ -7070,6 +7148,7 @@ async function startTail(options = {}) {
7070
7148
  }
7071
7149
  const connectionTime = Date.now();
7072
7150
  const activityPending = /* @__PURE__ */ new Map();
7151
+ const orphanedResults = /* @__PURE__ */ new Map();
7073
7152
  let csrfToken = "";
7074
7153
  const approvalQueue = [];
7075
7154
  let cardActive = false;
@@ -7404,9 +7483,14 @@ async function startTail(options = {}) {
7404
7483
  renderResult(data, data);
7405
7484
  return;
7406
7485
  }
7486
+ const orphaned = orphanedResults.get(data.id);
7487
+ if (orphaned) {
7488
+ orphanedResults.delete(data.id);
7489
+ renderResult(data, orphaned);
7490
+ return;
7491
+ }
7407
7492
  activityPending.set(data.id, data);
7408
- const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
7409
- if (slowTool) renderPending(data);
7493
+ renderPending(data);
7410
7494
  }
7411
7495
  if (event === "snapshot") {
7412
7496
  const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
@@ -7425,6 +7509,8 @@ async function startTail(options = {}) {
7425
7509
  if (original) {
7426
7510
  renderResult(original, data);
7427
7511
  activityPending.delete(data.id);
7512
+ } else {
7513
+ orphanedResults.set(data.id, data);
7428
7514
  }
7429
7515
  }
7430
7516
  }
@@ -7667,6 +7753,29 @@ function renderOffline() {
7667
7753
  process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
7668
7754
  `);
7669
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
+ }
7670
7779
  function renderSecurityLine(status) {
7671
7780
  const parts = [];
7672
7781
  parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
@@ -7684,6 +7793,18 @@ function renderSecurityLine(status) {
7684
7793
  };
7685
7794
  const mc = modeColors[status.mode] ?? WHITE;
7686
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
+ }
7687
7808
  if (status.mode === "observe") {
7688
7809
  parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
7689
7810
  if (status.session.wouldBlock > 0) {
@@ -7780,7 +7901,7 @@ async function main() {
7780
7901
  renderOffline();
7781
7902
  }
7782
7903
  }
7783
- 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;
7784
7905
  var init_hud = __esm({
7785
7906
  "src/cli/hud.ts"() {
7786
7907
  "use strict";
@@ -7802,6 +7923,8 @@ var init_hud = __esm({
7802
7923
  BAR_FILLED = "\u2588";
7803
7924
  BAR_EMPTY = "\u2591";
7804
7925
  BAR_WIDTH = 10;
7926
+ shieldsCache = null;
7927
+ SHIELDS_CACHE_TTL_MS = 2e3;
7805
7928
  }
7806
7929
  });
7807
7930
 
@@ -7815,6 +7938,7 @@ var import_path14 = __toESM(require("path"));
7815
7938
  var import_os10 = __toESM(require("os"));
7816
7939
  var import_chalk = __toESM(require("chalk"));
7817
7940
  var import_prompts = require("@inquirer/prompts");
7941
+ var import_smol_toml = require("smol-toml");
7818
7942
  var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7819
7943
  function hasNode9McpServer(servers) {
7820
7944
  const entry = servers["node9"];
@@ -8178,7 +8302,8 @@ function detectAgents(homeDir2 = import_os10.default.homedir()) {
8178
8302
  return {
8179
8303
  claude: exists(import_path14.default.join(homeDir2, ".claude")) || exists(import_path14.default.join(homeDir2, ".claude.json")),
8180
8304
  gemini: exists(import_path14.default.join(homeDir2, ".gemini")),
8181
- 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"))
8182
8307
  };
8183
8308
  }
8184
8309
  async function setupCursor() {
@@ -8243,6 +8368,82 @@ async function setupCursor() {
8243
8368
  printDaemonTip();
8244
8369
  }
8245
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
+ }
8246
8447
  function setupHud() {
8247
8448
  const homeDir2 = import_os10.default.homedir();
8248
8449
  const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
@@ -9936,6 +10137,8 @@ var import_path25 = __toESM(require("path"));
9936
10137
  var import_os19 = __toESM(require("os"));
9937
10138
  var import_https = __toESM(require("https"));
9938
10139
  init_core();
10140
+ init_shields();
10141
+ var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
9939
10142
  function fireTelemetryPing(agents) {
9940
10143
  try {
9941
10144
  const body = JSON.stringify({
@@ -9978,7 +10181,17 @@ function registerInitCommand(program2) {
9978
10181
  message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
9979
10182
  default: true
9980
10183
  });
9981
- if (enableShields) chosenMode = "standard";
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
+ }
9982
10195
  console.log("");
9983
10196
  }
9984
10197
  const configPath = import_path25.default.join(import_os19.default.homedir(), ".node9", "config.json");
@@ -10016,9 +10229,9 @@ function registerInitCommand(program2) {
10016
10229
  );
10017
10230
  if (found.length === 0) {
10018
10231
  console.log(
10019
- 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")
10020
10233
  );
10021
- 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>"));
10022
10235
  return;
10023
10236
  }
10024
10237
  console.log(import_chalk11.default.bold("Detected agents:"));
@@ -10031,6 +10244,7 @@ function registerInitCommand(program2) {
10031
10244
  if (agent === "claude") await setupClaude();
10032
10245
  else if (agent === "gemini") await setupGemini();
10033
10246
  else if (agent === "cursor") await setupCursor();
10247
+ else if (agent === "codex") await setupCodex();
10034
10248
  console.log("");
10035
10249
  }
10036
10250
  {
@@ -10652,6 +10866,20 @@ var TOOLS = [
10652
10866
  required: ["service"]
10653
10867
  }
10654
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
+ },
10655
10883
  {
10656
10884
  name: "node9_approver_list",
10657
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).",
@@ -10781,6 +11009,24 @@ function handleShieldEnable(args) {
10781
11009
  const shield = getShield(name);
10782
11010
  return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
10783
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
+ }
10784
11030
  var GLOBAL_CONFIG_PATH2 = import_path27.default.join(import_os20.default.homedir(), ".node9", "config.json");
10785
11031
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
10786
11032
  function readGlobalConfigRaw() {
@@ -10909,6 +11155,8 @@ function runMcpServer() {
10909
11155
  text = handleShieldList();
10910
11156
  } else if (toolName === "node9_shield_enable") {
10911
11157
  text = handleShieldEnable(toolArgs);
11158
+ } else if (toolName === "node9_shield_disable") {
11159
+ text = handleShieldDisable(toolArgs);
10912
11160
  } else if (toolName === "node9_approver_list") {
10913
11161
  text = handleApproverList();
10914
11162
  } else if (toolName === "node9_approver_set") {
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",
@@ -7052,6 +7130,7 @@ async function startTail(options = {}) {
7052
7130
  }
7053
7131
  const connectionTime = Date.now();
7054
7132
  const activityPending = /* @__PURE__ */ new Map();
7133
+ const orphanedResults = /* @__PURE__ */ new Map();
7055
7134
  let csrfToken = "";
7056
7135
  const approvalQueue = [];
7057
7136
  let cardActive = false;
@@ -7386,9 +7465,14 @@ async function startTail(options = {}) {
7386
7465
  renderResult(data, data);
7387
7466
  return;
7388
7467
  }
7468
+ const orphaned = orphanedResults.get(data.id);
7469
+ if (orphaned) {
7470
+ orphanedResults.delete(data.id);
7471
+ renderResult(data, orphaned);
7472
+ return;
7473
+ }
7389
7474
  activityPending.set(data.id, data);
7390
- const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
7391
- if (slowTool) renderPending(data);
7475
+ renderPending(data);
7392
7476
  }
7393
7477
  if (event === "snapshot") {
7394
7478
  const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
@@ -7407,6 +7491,8 @@ async function startTail(options = {}) {
7407
7491
  if (original) {
7408
7492
  renderResult(original, data);
7409
7493
  activityPending.delete(data.id);
7494
+ } else {
7495
+ orphanedResults.set(data.id, data);
7410
7496
  }
7411
7497
  }
7412
7498
  }
@@ -7646,6 +7732,29 @@ function renderOffline() {
7646
7732
  process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
7647
7733
  `);
7648
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
+ }
7649
7758
  function renderSecurityLine(status) {
7650
7759
  const parts = [];
7651
7760
  parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
@@ -7663,6 +7772,18 @@ function renderSecurityLine(status) {
7663
7772
  };
7664
7773
  const mc = modeColors[status.mode] ?? WHITE;
7665
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
+ }
7666
7787
  if (status.mode === "observe") {
7667
7788
  parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
7668
7789
  if (status.session.wouldBlock > 0) {
@@ -7759,7 +7880,7 @@ async function main() {
7759
7880
  renderOffline();
7760
7881
  }
7761
7882
  }
7762
- 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;
7763
7884
  var init_hud = __esm({
7764
7885
  "src/cli/hud.ts"() {
7765
7886
  "use strict";
@@ -7777,6 +7898,8 @@ var init_hud = __esm({
7777
7898
  BAR_FILLED = "\u2588";
7778
7899
  BAR_EMPTY = "\u2591";
7779
7900
  BAR_WIDTH = 10;
7901
+ shieldsCache = null;
7902
+ SHIELDS_CACHE_TTL_MS = 2e3;
7780
7903
  }
7781
7904
  });
7782
7905
 
@@ -7790,6 +7913,7 @@ import path14 from "path";
7790
7913
  import os10 from "os";
7791
7914
  import chalk from "chalk";
7792
7915
  import { confirm } from "@inquirer/prompts";
7916
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
7793
7917
  var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7794
7918
  function hasNode9McpServer(servers) {
7795
7919
  const entry = servers["node9"];
@@ -8153,7 +8277,8 @@ function detectAgents(homeDir2 = os10.homedir()) {
8153
8277
  return {
8154
8278
  claude: exists(path14.join(homeDir2, ".claude")) || exists(path14.join(homeDir2, ".claude.json")),
8155
8279
  gemini: exists(path14.join(homeDir2, ".gemini")),
8156
- cursor: exists(path14.join(homeDir2, ".cursor"))
8280
+ cursor: exists(path14.join(homeDir2, ".cursor")),
8281
+ codex: exists(path14.join(homeDir2, ".codex"))
8157
8282
  };
8158
8283
  }
8159
8284
  async function setupCursor() {
@@ -8218,6 +8343,82 @@ async function setupCursor() {
8218
8343
  printDaemonTip();
8219
8344
  }
8220
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
+ }
8221
8422
  function setupHud() {
8222
8423
  const homeDir2 = os10.homedir();
8223
8424
  const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
@@ -9911,6 +10112,8 @@ import fs23 from "fs";
9911
10112
  import path25 from "path";
9912
10113
  import os19 from "os";
9913
10114
  import https from "https";
10115
+ init_shields();
10116
+ var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
9914
10117
  function fireTelemetryPing(agents) {
9915
10118
  try {
9916
10119
  const body = JSON.stringify({
@@ -9953,7 +10156,17 @@ function registerInitCommand(program2) {
9953
10156
  message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
9954
10157
  default: true
9955
10158
  });
9956
- if (enableShields) chosenMode = "standard";
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
+ }
9957
10170
  console.log("");
9958
10171
  }
9959
10172
  const configPath = path25.join(os19.homedir(), ".node9", "config.json");
@@ -9991,9 +10204,9 @@ function registerInitCommand(program2) {
9991
10204
  );
9992
10205
  if (found.length === 0) {
9993
10206
  console.log(
9994
- 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")
9995
10208
  );
9996
- console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor>"));
10209
+ console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
9997
10210
  return;
9998
10211
  }
9999
10212
  console.log(chalk11.bold("Detected agents:"));
@@ -10006,6 +10219,7 @@ function registerInitCommand(program2) {
10006
10219
  if (agent === "claude") await setupClaude();
10007
10220
  else if (agent === "gemini") await setupGemini();
10008
10221
  else if (agent === "cursor") await setupCursor();
10222
+ else if (agent === "codex") await setupCodex();
10009
10223
  console.log("");
10010
10224
  }
10011
10225
  {
@@ -10627,6 +10841,20 @@ var TOOLS = [
10627
10841
  required: ["service"]
10628
10842
  }
10629
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
+ },
10630
10858
  {
10631
10859
  name: "node9_approver_list",
10632
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).",
@@ -10756,6 +10984,24 @@ function handleShieldEnable(args) {
10756
10984
  const shield = getShield(name);
10757
10985
  return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
10758
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
+ }
10759
11005
  var GLOBAL_CONFIG_PATH2 = path27.join(os20.homedir(), ".node9", "config.json");
10760
11006
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
10761
11007
  function readGlobalConfigRaw() {
@@ -10884,6 +11130,8 @@ function runMcpServer() {
10884
11130
  text = handleShieldList();
10885
11131
  } else if (toolName === "node9_shield_enable") {
10886
11132
  text = handleShieldEnable(toolArgs);
11133
+ } else if (toolName === "node9_shield_disable") {
11134
+ text = handleShieldDisable(toolArgs);
10887
11135
  } else if (toolName === "node9_approver_list") {
10888
11136
  text = handleApproverList();
10889
11137
  } else if (toolName === "node9_approver_set") {
package/dist/index.js CHANGED
@@ -411,6 +411,84 @@ var SHIELDS = {
411
411
  ],
412
412
  dangerousWords: []
413
413
  },
414
+ "bash-safe": {
415
+ name: "bash-safe",
416
+ description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
417
+ aliases: ["bash", "shell"],
418
+ smartRules: [
419
+ {
420
+ name: "shield:bash-safe:block-pipe-to-shell",
421
+ tool: "bash",
422
+ conditions: [
423
+ {
424
+ field: "command",
425
+ op: "matches",
426
+ value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
427
+ flags: "i"
428
+ }
429
+ ],
430
+ verdict: "block",
431
+ reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
432
+ },
433
+ {
434
+ name: "shield:bash-safe:block-obfuscated-exec",
435
+ tool: "bash",
436
+ conditions: [
437
+ {
438
+ field: "command",
439
+ op: "matches",
440
+ value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
441
+ flags: "i"
442
+ }
443
+ ],
444
+ verdict: "block",
445
+ reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
446
+ },
447
+ {
448
+ name: "shield:bash-safe:block-rm-root",
449
+ tool: "bash",
450
+ conditions: [
451
+ {
452
+ field: "command",
453
+ op: "matches",
454
+ 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*$",
455
+ flags: "i"
456
+ }
457
+ ],
458
+ verdict: "block",
459
+ reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
460
+ },
461
+ {
462
+ name: "shield:bash-safe:block-disk-overwrite",
463
+ tool: "bash",
464
+ conditions: [
465
+ {
466
+ field: "command",
467
+ op: "matches",
468
+ value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
469
+ flags: "i"
470
+ }
471
+ ],
472
+ verdict: "block",
473
+ reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
474
+ },
475
+ {
476
+ name: "shield:bash-safe:review-eval",
477
+ tool: "bash",
478
+ conditions: [
479
+ {
480
+ field: "command",
481
+ op: "matches",
482
+ value: '\\beval\\s+[\\$`("]',
483
+ flags: "i"
484
+ }
485
+ ],
486
+ verdict: "review",
487
+ reason: "eval of dynamic content requires human approval (bash-safe shield)"
488
+ }
489
+ ],
490
+ dangerousWords: []
491
+ },
414
492
  filesystem: {
415
493
  name: "filesystem",
416
494
  description: "Protects the local filesystem from dangerous AI operations",
package/dist/index.mjs CHANGED
@@ -381,6 +381,84 @@ var SHIELDS = {
381
381
  ],
382
382
  dangerousWords: []
383
383
  },
384
+ "bash-safe": {
385
+ name: "bash-safe",
386
+ description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
387
+ aliases: ["bash", "shell"],
388
+ smartRules: [
389
+ {
390
+ name: "shield:bash-safe:block-pipe-to-shell",
391
+ tool: "bash",
392
+ conditions: [
393
+ {
394
+ field: "command",
395
+ op: "matches",
396
+ value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
397
+ flags: "i"
398
+ }
399
+ ],
400
+ verdict: "block",
401
+ reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
402
+ },
403
+ {
404
+ name: "shield:bash-safe:block-obfuscated-exec",
405
+ tool: "bash",
406
+ conditions: [
407
+ {
408
+ field: "command",
409
+ op: "matches",
410
+ value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
411
+ flags: "i"
412
+ }
413
+ ],
414
+ verdict: "block",
415
+ reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
416
+ },
417
+ {
418
+ name: "shield:bash-safe:block-rm-root",
419
+ tool: "bash",
420
+ conditions: [
421
+ {
422
+ field: "command",
423
+ op: "matches",
424
+ 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*$",
425
+ flags: "i"
426
+ }
427
+ ],
428
+ verdict: "block",
429
+ reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
430
+ },
431
+ {
432
+ name: "shield:bash-safe:block-disk-overwrite",
433
+ tool: "bash",
434
+ conditions: [
435
+ {
436
+ field: "command",
437
+ op: "matches",
438
+ value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
439
+ flags: "i"
440
+ }
441
+ ],
442
+ verdict: "block",
443
+ reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
444
+ },
445
+ {
446
+ name: "shield:bash-safe:review-eval",
447
+ tool: "bash",
448
+ conditions: [
449
+ {
450
+ field: "command",
451
+ op: "matches",
452
+ value: '\\beval\\s+[\\$`("]',
453
+ flags: "i"
454
+ }
455
+ ],
456
+ verdict: "review",
457
+ reason: "eval of dynamic content requires human approval (bash-safe shield)"
458
+ }
459
+ ],
460
+ dangerousWords: []
461
+ },
384
462
  filesystem: {
385
463
  name: "filesystem",
386
464
  description: "Protects the local filesystem from dangerous AI operations",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -75,6 +75,7 @@
75
75
  "picomatch": "^4.0.3",
76
76
  "safe-regex2": "^5.1.0",
77
77
  "sh-syntax": "^0.5.8",
78
+ "smol-toml": "^1.6.1",
78
79
  "zod": "^3.25.76"
79
80
  },
80
81
  "devDependencies": {