@node9/proxy 1.7.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -250,6 +250,70 @@ import fs2 from "fs";
250
250
  import path2 from "path";
251
251
  import os2 from "os";
252
252
  import crypto from "crypto";
253
+ function validateShieldDefinition(raw, filePath) {
254
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
255
+ process.stderr.write(`[node9] Shield file is not an object: ${filePath}
256
+ `);
257
+ return null;
258
+ }
259
+ const r = raw;
260
+ if (typeof r.name !== "string" || !r.name) {
261
+ process.stderr.write(`[node9] Shield file missing 'name': ${filePath}
262
+ `);
263
+ return null;
264
+ }
265
+ if (typeof r.description !== "string") {
266
+ process.stderr.write(`[node9] Shield file missing 'description': ${filePath}
267
+ `);
268
+ return null;
269
+ }
270
+ if (!Array.isArray(r.aliases)) {
271
+ process.stderr.write(`[node9] Shield file missing 'aliases' array: ${filePath}
272
+ `);
273
+ return null;
274
+ }
275
+ if (!Array.isArray(r.smartRules)) {
276
+ process.stderr.write(`[node9] Shield file missing 'smartRules' array: ${filePath}
277
+ `);
278
+ return null;
279
+ }
280
+ if (!Array.isArray(r.dangerousWords)) {
281
+ process.stderr.write(`[node9] Shield file missing 'dangerousWords' array: ${filePath}
282
+ `);
283
+ return null;
284
+ }
285
+ return r;
286
+ }
287
+ function loadShieldsFromDir(dir, label) {
288
+ const result = {};
289
+ let entries;
290
+ try {
291
+ entries = fs2.readdirSync(dir).filter((f) => f.endsWith(".json"));
292
+ } catch (err2) {
293
+ if (err2.code !== "ENOENT") {
294
+ process.stderr.write(`[node9] Could not read ${label} shields dir ${dir}: ${String(err2)}
295
+ `);
296
+ }
297
+ return result;
298
+ }
299
+ for (const file of entries) {
300
+ const filePath = path2.join(dir, file);
301
+ try {
302
+ const raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
303
+ const shield = validateShieldDefinition(raw, filePath);
304
+ if (shield) result[shield.name] = shield;
305
+ } catch (err2) {
306
+ process.stderr.write(`[node9] Failed to load ${label} shield ${file}: ${String(err2)}
307
+ `);
308
+ }
309
+ }
310
+ return result;
311
+ }
312
+ function buildSHIELDS() {
313
+ const builtins = loadShieldsFromDir(BUILTIN_DIR, "builtin");
314
+ const userShields = loadShieldsFromDir(USER_SHIELDS_DIR, "user");
315
+ return { ...builtins, ...userShields };
316
+ }
253
317
  function resolveShieldName(input) {
254
318
  const lower = input.toLowerCase();
255
319
  if (SHIELDS[lower]) return lower;
@@ -356,177 +420,30 @@ function resolveShieldRule(shieldName, identifier) {
356
420
  }
357
421
  return null;
358
422
  }
359
- var SHIELDS, SHIELDS_STATE_FILE;
423
+ function installShield(name, shieldJson) {
424
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
425
+ throw new Error(
426
+ `Invalid shield name '${name}': only alphanumeric characters, hyphens, and underscores are allowed`
427
+ );
428
+ }
429
+ const shield = validateShieldDefinition(shieldJson, `<downloaded:${name}>`);
430
+ if (!shield) throw new Error(`Downloaded shield '${name}' failed validation`);
431
+ if (shield.name !== name) {
432
+ throw new Error(`Shield name mismatch: file declares '${shield.name}' but expected '${name}'`);
433
+ }
434
+ fs2.mkdirSync(USER_SHIELDS_DIR, { recursive: true });
435
+ const filePath = path2.join(USER_SHIELDS_DIR, `${name}.json`);
436
+ const tmp = `${filePath}.${crypto.randomBytes(6).toString("hex")}.tmp`;
437
+ fs2.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
438
+ fs2.renameSync(tmp, filePath);
439
+ }
440
+ var BUILTIN_DIR, USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE;
360
441
  var init_shields = __esm({
361
442
  "src/shields.ts"() {
362
443
  "use strict";
363
- SHIELDS = {
364
- postgres: {
365
- name: "postgres",
366
- description: "Protects PostgreSQL databases from destructive AI operations",
367
- aliases: ["pg", "postgresql"],
368
- smartRules: [
369
- {
370
- name: "shield:postgres:block-drop-table",
371
- tool: "*",
372
- conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
373
- verdict: "block",
374
- reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
375
- },
376
- {
377
- name: "shield:postgres:block-truncate",
378
- tool: "*",
379
- conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
380
- verdict: "block",
381
- reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
382
- },
383
- {
384
- name: "shield:postgres:block-drop-column",
385
- tool: "*",
386
- conditions: [
387
- { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
388
- ],
389
- verdict: "block",
390
- reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
391
- },
392
- {
393
- name: "shield:postgres:review-grant-revoke",
394
- tool: "*",
395
- conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
396
- verdict: "review",
397
- reason: "Permission changes require human approval (Postgres shield)"
398
- }
399
- ],
400
- dangerousWords: ["dropdb", "pg_dropcluster"]
401
- },
402
- github: {
403
- name: "github",
404
- description: "Protects GitHub repositories from destructive AI operations",
405
- aliases: ["git"],
406
- smartRules: [
407
- {
408
- // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
409
- // This rule adds coverage for `git push --delete` which the built-in does not match.
410
- name: "shield:github:review-delete-branch-remote",
411
- tool: "bash",
412
- conditions: [
413
- {
414
- field: "command",
415
- op: "matches",
416
- value: "git\\s+push\\s+.*--delete",
417
- flags: "i"
418
- }
419
- ],
420
- verdict: "review",
421
- reason: "Remote branch deletion requires human approval (GitHub shield)"
422
- },
423
- {
424
- name: "shield:github:block-delete-repo",
425
- tool: "*",
426
- conditions: [
427
- { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
428
- ],
429
- verdict: "block",
430
- reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
431
- }
432
- ],
433
- dangerousWords: []
434
- },
435
- aws: {
436
- name: "aws",
437
- description: "Protects AWS infrastructure from destructive AI operations",
438
- aliases: ["amazon"],
439
- smartRules: [
440
- {
441
- name: "shield:aws:block-delete-s3-bucket",
442
- tool: "*",
443
- conditions: [
444
- {
445
- field: "command",
446
- op: "matches",
447
- value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
448
- flags: "i"
449
- }
450
- ],
451
- verdict: "block",
452
- reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
453
- },
454
- {
455
- name: "shield:aws:review-iam-changes",
456
- tool: "*",
457
- conditions: [
458
- {
459
- field: "command",
460
- op: "matches",
461
- value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
462
- flags: "i"
463
- }
464
- ],
465
- verdict: "review",
466
- reason: "IAM changes require human approval (AWS shield)"
467
- },
468
- {
469
- name: "shield:aws:block-ec2-terminate",
470
- tool: "*",
471
- conditions: [
472
- {
473
- field: "command",
474
- op: "matches",
475
- value: "aws\\s+ec2\\s+terminate-instances",
476
- flags: "i"
477
- }
478
- ],
479
- verdict: "block",
480
- reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
481
- },
482
- {
483
- name: "shield:aws:review-rds-delete",
484
- tool: "*",
485
- conditions: [
486
- { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
487
- ],
488
- verdict: "review",
489
- reason: "RDS deletion requires human approval (AWS shield)"
490
- }
491
- ],
492
- dangerousWords: []
493
- },
494
- filesystem: {
495
- name: "filesystem",
496
- description: "Protects the local filesystem from dangerous AI operations",
497
- aliases: ["fs"],
498
- smartRules: [
499
- {
500
- name: "shield:filesystem:review-chmod-777",
501
- tool: "bash",
502
- conditions: [
503
- { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
504
- ],
505
- verdict: "review",
506
- reason: "chmod 777 requires human approval (filesystem shield)"
507
- },
508
- {
509
- name: "shield:filesystem:review-write-etc",
510
- tool: "bash",
511
- conditions: [
512
- {
513
- field: "command",
514
- // Narrow to write-indicative operations to avoid approval fatigue on reads.
515
- // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
516
- op: "matches",
517
- value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
518
- }
519
- ],
520
- verdict: "review",
521
- reason: "Writing to /etc requires human approval (filesystem shield)"
522
- }
523
- ],
524
- // dd removed: too common as a legitimate tool (disk imaging, file ops).
525
- // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
526
- // wipefs retained: rarely legitimate in an agent context and not in built-ins.
527
- dangerousWords: ["wipefs"]
528
- }
529
- };
444
+ BUILTIN_DIR = path2.join(__dirname, "shields", "builtin");
445
+ USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
446
+ SHIELDS = buildSHIELDS();
530
447
  SHIELDS_STATE_FILE = path2.join(os2.homedir(), ".node9", "shields.json");
531
448
  }
532
449
  });
@@ -2503,7 +2420,7 @@ function isDaemonRunning() {
2503
2420
  return false;
2504
2421
  }
2505
2422
  }
2506
- async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
2423
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched) {
2507
2424
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2508
2425
  const ctrl = new AbortController();
2509
2426
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -2524,7 +2441,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2524
2441
  ...cwd && { cwd },
2525
2442
  ...recoveryCommand && { recoveryCommand },
2526
2443
  ...skipBackgroundAuth && { skipBackgroundAuth: true },
2527
- ...viewOnly && { viewOnly: true }
2444
+ ...viewOnly && { viewOnly: true },
2445
+ ...localSmartRuleMatched && { localSmartRuleMatched: true }
2528
2446
  }),
2529
2447
  signal: ctrl.signal
2530
2448
  });
@@ -3214,6 +3132,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3214
3132
  let policyMatchedWord;
3215
3133
  let riskMetadata;
3216
3134
  let statefulRecoveryCommand;
3135
+ let localSmartRuleMatched = false;
3217
3136
  let taintWarning = null;
3218
3137
  if (isNetworkTool(toolName, args)) {
3219
3138
  const filePaths = extractFilePaths(toolName, args);
@@ -3357,6 +3276,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3357
3276
  explainableLabel = policyResult.blockedByLabel || "Local Config";
3358
3277
  policyMatchedField = policyResult.matchedField;
3359
3278
  policyMatchedWord = policyResult.matchedWord;
3279
+ if (policyResult.ruleName) localSmartRuleMatched = true;
3360
3280
  riskMetadata = computeRiskMetadata(
3361
3281
  args,
3362
3282
  policyResult.tier ?? 6,
@@ -3399,22 +3319,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3399
3319
  }
3400
3320
  let cloudRequestId = null;
3401
3321
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
3402
- if (cloudEnforced) {
3322
+ if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3403
3323
  try {
3404
3324
  const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
3405
3325
  if (!initResult.pending) {
3406
3326
  if (initResult.shadowMode) {
3407
3327
  return { approved: true, checkedBy: "cloud" };
3408
3328
  }
3409
- return {
3410
- approved: !!initResult.approved,
3411
- reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
3412
- checkedBy: initResult.approved ? "cloud" : void 0,
3413
- blockedBy: initResult.approved ? void 0 : "team-policy",
3414
- blockedByLabel: "Organization Policy (SaaS)"
3415
- };
3329
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
3330
+ return {
3331
+ approved: !!initResult.approved,
3332
+ reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
3333
+ checkedBy: initResult.approved ? "cloud" : void 0,
3334
+ blockedBy: initResult.approved ? void 0 : "team-policy",
3335
+ blockedByLabel: "Organization Policy (SaaS)"
3336
+ };
3337
+ }
3338
+ }
3339
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
3340
+ cloudRequestId = initResult.requestId || null;
3416
3341
  }
3417
- cloudRequestId = initResult.requestId || null;
3418
3342
  if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
3419
3343
  } catch {
3420
3344
  }
@@ -3460,7 +3384,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3460
3384
  riskMetadata,
3461
3385
  options?.activityId,
3462
3386
  options?.cwd,
3463
- statefulRecoveryCommand
3387
+ statefulRecoveryCommand,
3388
+ void 0,
3389
+ void 0,
3390
+ localSmartRuleMatched || options?.localSmartRuleMatched
3464
3391
  );
3465
3392
  daemonEntryId = entry.id;
3466
3393
  daemonAllowCount = entry.allowCount;
@@ -3468,7 +3395,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3468
3395
  }
3469
3396
  }
3470
3397
  }
3471
- if (cloudEnforced && cloudRequestId) {
3398
+ if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3472
3399
  racePromises.push(
3473
3400
  (async () => {
3474
3401
  try {
@@ -6135,7 +6062,8 @@ data: ${JSON.stringify(item.data)}
6135
6062
  viewOnly = false,
6136
6063
  fromCLI = false,
6137
6064
  activityId,
6138
- cwd
6065
+ cwd,
6066
+ localSmartRuleMatched = false
6139
6067
  } = JSON.parse(body);
6140
6068
  const id = fromCLI && typeof activityId === "string" && activityId || randomUUID4();
6141
6069
  const entry = {
@@ -6215,7 +6143,7 @@ data: ${JSON.stringify(item.data)}
6215
6143
  agent: typeof agent === "string" ? agent : void 0,
6216
6144
  mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
6217
6145
  },
6218
- { calledFromDaemon: true }
6146
+ { calledFromDaemon: true, localSmartRuleMatched: !!localSmartRuleMatched }
6219
6147
  ).then((result) => {
6220
6148
  const e = pending.get(id);
6221
6149
  if (!e) return;
@@ -7052,6 +6980,7 @@ async function startTail(options = {}) {
7052
6980
  }
7053
6981
  const connectionTime = Date.now();
7054
6982
  const activityPending = /* @__PURE__ */ new Map();
6983
+ const orphanedResults = /* @__PURE__ */ new Map();
7055
6984
  let csrfToken = "";
7056
6985
  const approvalQueue = [];
7057
6986
  let cardActive = false;
@@ -7386,9 +7315,14 @@ async function startTail(options = {}) {
7386
7315
  renderResult(data, data);
7387
7316
  return;
7388
7317
  }
7318
+ const orphaned = orphanedResults.get(data.id);
7319
+ if (orphaned) {
7320
+ orphanedResults.delete(data.id);
7321
+ renderResult(data, orphaned);
7322
+ return;
7323
+ }
7389
7324
  activityPending.set(data.id, data);
7390
- const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
7391
- if (slowTool) renderPending(data);
7325
+ renderPending(data);
7392
7326
  }
7393
7327
  if (event === "snapshot") {
7394
7328
  const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
@@ -7407,6 +7341,8 @@ async function startTail(options = {}) {
7407
7341
  if (original) {
7408
7342
  renderResult(original, data);
7409
7343
  activityPending.delete(data.id);
7344
+ } else {
7345
+ orphanedResults.set(data.id, data);
7410
7346
  }
7411
7347
  }
7412
7348
  }
@@ -7646,6 +7582,29 @@ function renderOffline() {
7646
7582
  process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
7647
7583
  `);
7648
7584
  }
7585
+ function readActiveShieldsHud() {
7586
+ const now = Date.now();
7587
+ if (shieldsCache && now - shieldsCache.ts < SHIELDS_CACHE_TTL_MS) {
7588
+ return shieldsCache.value;
7589
+ }
7590
+ try {
7591
+ const shieldsPath = path29.join(os22.homedir(), ".node9", "shields.json");
7592
+ if (!fs26.existsSync(shieldsPath)) {
7593
+ shieldsCache = { value: [], ts: now };
7594
+ return [];
7595
+ }
7596
+ const parsed = JSON.parse(fs26.readFileSync(shieldsPath, "utf-8"));
7597
+ if (!Array.isArray(parsed.active)) {
7598
+ shieldsCache = { value: [], ts: now };
7599
+ return [];
7600
+ }
7601
+ const value = parsed.active.filter((s) => typeof s === "string").map((s) => s.slice(0, 64)).slice(0, 20);
7602
+ shieldsCache = { value, ts: now };
7603
+ return value;
7604
+ } catch {
7605
+ return [];
7606
+ }
7607
+ }
7649
7608
  function renderSecurityLine(status) {
7650
7609
  const parts = [];
7651
7610
  parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
@@ -7663,6 +7622,18 @@ function renderSecurityLine(status) {
7663
7622
  };
7664
7623
  const mc = modeColors[status.mode] ?? WHITE;
7665
7624
  parts.push(`${dim("|")} ${color(mc, modeIcon[status.mode] ?? "")}${color(mc, status.mode)}`);
7625
+ const activeShields = readActiveShieldsHud();
7626
+ if (activeShields.length > 0) {
7627
+ const shieldAbbrevs = {
7628
+ "bash-safe": "bash",
7629
+ filesystem: "fs",
7630
+ postgres: "pg",
7631
+ github: "gh",
7632
+ aws: "aws"
7633
+ };
7634
+ const labels = activeShields.map((s) => shieldAbbrevs[s] ?? s).join(" ");
7635
+ parts.push(color(DIM, `[${labels}]`));
7636
+ }
7666
7637
  if (status.mode === "observe") {
7667
7638
  parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
7668
7639
  if (status.session.wouldBlock > 0) {
@@ -7759,7 +7730,7 @@ async function main() {
7759
7730
  renderOffline();
7760
7731
  }
7761
7732
  }
7762
- var RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
7733
+ var RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
7763
7734
  var init_hud = __esm({
7764
7735
  "src/cli/hud.ts"() {
7765
7736
  "use strict";
@@ -7777,6 +7748,8 @@ var init_hud = __esm({
7777
7748
  BAR_FILLED = "\u2588";
7778
7749
  BAR_EMPTY = "\u2591";
7779
7750
  BAR_WIDTH = 10;
7751
+ shieldsCache = null;
7752
+ SHIELDS_CACHE_TTL_MS = 2e3;
7780
7753
  }
7781
7754
  });
7782
7755
 
@@ -7790,6 +7763,7 @@ import path14 from "path";
7790
7763
  import os10 from "os";
7791
7764
  import chalk from "chalk";
7792
7765
  import { confirm } from "@inquirer/prompts";
7766
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
7793
7767
  var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7794
7768
  function hasNode9McpServer(servers) {
7795
7769
  const entry = servers["node9"];
@@ -8153,7 +8127,8 @@ function detectAgents(homeDir2 = os10.homedir()) {
8153
8127
  return {
8154
8128
  claude: exists(path14.join(homeDir2, ".claude")) || exists(path14.join(homeDir2, ".claude.json")),
8155
8129
  gemini: exists(path14.join(homeDir2, ".gemini")),
8156
- cursor: exists(path14.join(homeDir2, ".cursor"))
8130
+ cursor: exists(path14.join(homeDir2, ".cursor")),
8131
+ codex: exists(path14.join(homeDir2, ".codex"))
8157
8132
  };
8158
8133
  }
8159
8134
  async function setupCursor() {
@@ -8218,6 +8193,82 @@ async function setupCursor() {
8218
8193
  printDaemonTip();
8219
8194
  }
8220
8195
  }
8196
+ function readToml(filePath) {
8197
+ try {
8198
+ if (fs11.existsSync(filePath)) {
8199
+ return parseToml(fs11.readFileSync(filePath, "utf-8"));
8200
+ }
8201
+ } catch {
8202
+ }
8203
+ return null;
8204
+ }
8205
+ function writeToml(filePath, data) {
8206
+ const dir = path14.dirname(filePath);
8207
+ if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
8208
+ fs11.writeFileSync(filePath, stringifyToml(data));
8209
+ }
8210
+ async function setupCodex() {
8211
+ const homeDir2 = os10.homedir();
8212
+ const configPath = path14.join(homeDir2, ".codex", "config.toml");
8213
+ const config = readToml(configPath) ?? {};
8214
+ const servers = config.mcp_servers ?? {};
8215
+ let anythingChanged = false;
8216
+ if (!hasNode9McpServer(servers)) {
8217
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
8218
+ config.mcp_servers = servers;
8219
+ writeToml(configPath, config);
8220
+ console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
8221
+ anythingChanged = true;
8222
+ }
8223
+ const serversToWrap = [];
8224
+ for (const [name, server] of Object.entries(servers)) {
8225
+ if (!server.command || server.command === "node9") continue;
8226
+ const parts = [server.command, ...server.args ?? []];
8227
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
8228
+ }
8229
+ if (serversToWrap.length > 0) {
8230
+ console.log(chalk.bold("The following existing entries will be modified:\n"));
8231
+ console.log(chalk.white(` ${configPath}`));
8232
+ for (const { name, originalCmd } of serversToWrap) {
8233
+ console.log(chalk.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
8234
+ }
8235
+ console.log("");
8236
+ const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
8237
+ if (proceed) {
8238
+ for (const { name, parts } of serversToWrap) {
8239
+ servers[name] = { ...servers[name], command: "node9", args: parts };
8240
+ }
8241
+ config.mcp_servers = servers;
8242
+ writeToml(configPath, config);
8243
+ console.log(chalk.green(`
8244
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
8245
+ anythingChanged = true;
8246
+ } else {
8247
+ console.log(chalk.yellow(" Skipped MCP server wrapping."));
8248
+ }
8249
+ console.log("");
8250
+ }
8251
+ console.log(
8252
+ chalk.yellow(
8253
+ " \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."
8254
+ )
8255
+ );
8256
+ console.log("");
8257
+ if (!anythingChanged && serversToWrap.length === 0) {
8258
+ console.log(
8259
+ chalk.blue(
8260
+ "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.codex/config.toml and re-run."
8261
+ )
8262
+ );
8263
+ printDaemonTip();
8264
+ return;
8265
+ }
8266
+ if (anythingChanged) {
8267
+ console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Codex via MCP proxy!"));
8268
+ console.log(chalk.gray(" Restart Codex for changes to take effect."));
8269
+ printDaemonTip();
8270
+ }
8271
+ }
8221
8272
  function setupHud() {
8222
8273
  const homeDir2 = os10.homedir();
8223
8274
  const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
@@ -8935,7 +8986,7 @@ RAW: ${raw}
8935
8986
  }
8936
8987
  }) + "\n"
8937
8988
  );
8938
- process.exit(0);
8989
+ process.exit(2);
8939
8990
  };
8940
8991
  if (!toolName) {
8941
8992
  sendBlock("Node9: unrecognised hook payload \u2014 tool name missing.");
@@ -9170,6 +9221,27 @@ init_shields();
9170
9221
  init_audit();
9171
9222
  init_config();
9172
9223
  import chalk6 from "chalk";
9224
+
9225
+ // src/utils/https-fetch.ts
9226
+ import https from "https";
9227
+ function httpsFetch(url) {
9228
+ return new Promise((resolve, reject) => {
9229
+ https.get(url, (res) => {
9230
+ if (res.statusCode !== 200) {
9231
+ reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
9232
+ res.resume();
9233
+ return;
9234
+ }
9235
+ const chunks = [];
9236
+ res.on("data", (chunk) => chunks.push(chunk));
9237
+ res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
9238
+ res.on("error", reject);
9239
+ }).on("error", reject);
9240
+ });
9241
+ }
9242
+
9243
+ // src/cli/commands/shield.ts
9244
+ var COMMUNITY_INDEX_URL = "https://raw.githubusercontent.com/node9ai/node9-proxy/main/shields/community/index.json";
9173
9245
  function registerShieldCommand(program2) {
9174
9246
  const shieldCmd = program2.command("shield").description("Manage pre-packaged security shield templates");
9175
9247
  shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
@@ -9229,7 +9301,32 @@ function registerShieldCommand(program2) {
9229
9301
  \u{1F6E1}\uFE0F Shield "${name}" disabled.
9230
9302
  `));
9231
9303
  });
9232
- shieldCmd.command("list").description("Show all available shields").action(() => {
9304
+ shieldCmd.command("list").description("Show available shields (add --community to browse the marketplace)").option("--community", "List shields available from the community marketplace").action((opts) => {
9305
+ if (opts.community) {
9306
+ console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Community Shield Marketplace\n"));
9307
+ console.log(chalk6.gray(" Fetching index\u2026\n"));
9308
+ httpsFetch(COMMUNITY_INDEX_URL).then((body) => {
9309
+ const entries = JSON.parse(body);
9310
+ const installed = new Set(listShields().map((s) => s.name));
9311
+ for (const e of entries) {
9312
+ const tag = installed.has(e.name) ? chalk6.green("installed") : chalk6.gray("available");
9313
+ console.log(
9314
+ ` ${tag} ${chalk6.cyan(e.name.padEnd(12))} ${e.description} ${chalk6.gray(`by ${e.author}`)}`
9315
+ );
9316
+ }
9317
+ console.log("");
9318
+ console.log(
9319
+ chalk6.gray(` Install a shield: ${chalk6.cyan("node9 shield install <name>")}
9320
+ `)
9321
+ );
9322
+ }).catch((err2) => {
9323
+ console.error(chalk6.red(`
9324
+ \u274C Could not fetch community index: ${String(err2)}
9325
+ `));
9326
+ process.exit(1);
9327
+ });
9328
+ return;
9329
+ }
9233
9330
  const active = new Set(readActiveShields());
9234
9331
  console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
9235
9332
  for (const shield of listShields()) {
@@ -9239,6 +9336,10 @@ function registerShieldCommand(program2) {
9239
9336
  console.log(chalk6.gray(` aliases: ${shield.aliases.join(", ")}`));
9240
9337
  }
9241
9338
  console.log("");
9339
+ console.log(
9340
+ chalk6.gray(` Browse community shields: ${chalk6.cyan("node9 shield list --community")}
9341
+ `)
9342
+ );
9242
9343
  });
9243
9344
  shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
9244
9345
  const active = readActiveShields();
@@ -9376,6 +9477,52 @@ function registerShieldCommand(program2) {
9376
9477
  `)
9377
9478
  );
9378
9479
  });
9480
+ shieldCmd.command("install <name>").description("Install a shield from the community marketplace into ~/.node9/shields/").action((name) => {
9481
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
9482
+ console.error(
9483
+ chalk6.red(
9484
+ `
9485
+ \u274C Invalid shield name: only alphanumeric characters, hyphens, and underscores are allowed
9486
+ `
9487
+ )
9488
+ );
9489
+ process.exit(1);
9490
+ }
9491
+ console.log(chalk6.bold(`
9492
+ \u{1F6E1}\uFE0F Installing shield "${name}"\u2026
9493
+ `));
9494
+ httpsFetch(COMMUNITY_INDEX_URL).then((indexBody) => {
9495
+ const entries = JSON.parse(indexBody);
9496
+ const entry = entries.find((e) => e.name === name);
9497
+ if (!entry) {
9498
+ const names = entries.map((e) => chalk6.cyan(e.name)).join(", ");
9499
+ console.error(
9500
+ chalk6.red(`\u274C Shield "${name}" not found in the community marketplace.
9501
+ `)
9502
+ );
9503
+ console.error(` Available: ${names}
9504
+ `);
9505
+ process.exit(1);
9506
+ }
9507
+ return httpsFetch(entry.url);
9508
+ }).then((shieldBody) => {
9509
+ const shieldJson = JSON.parse(shieldBody);
9510
+ installShield(name, shieldJson);
9511
+ console.log(
9512
+ chalk6.green(`\u2705 Shield "${name}" installed to ~/.node9/shields/${name}.json`)
9513
+ );
9514
+ console.log(
9515
+ chalk6.gray(` Activate it with: ${chalk6.cyan(`node9 shield enable ${name}`)}
9516
+ `)
9517
+ );
9518
+ appendConfigAudit({ event: "shield-install", shield: name });
9519
+ }).catch((err2) => {
9520
+ console.error(chalk6.red(`
9521
+ \u274C Install failed: ${String(err2)}
9522
+ `));
9523
+ process.exit(1);
9524
+ });
9525
+ });
9379
9526
  }
9380
9527
  function registerConfigShowCommand(program2) {
9381
9528
  program2.command("config show").description(
@@ -9910,7 +10057,9 @@ import chalk11 from "chalk";
9910
10057
  import fs23 from "fs";
9911
10058
  import path25 from "path";
9912
10059
  import os19 from "os";
9913
- import https from "https";
10060
+ import https2 from "https";
10061
+ init_shields();
10062
+ var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
9914
10063
  function fireTelemetryPing(agents) {
9915
10064
  try {
9916
10065
  const body = JSON.stringify({
@@ -9919,7 +10068,7 @@ function fireTelemetryPing(agents) {
9919
10068
  os: process.platform,
9920
10069
  node9_version: process.env.npm_package_version ?? "unknown"
9921
10070
  });
9922
- const req = https.request(
10071
+ const req = https2.request(
9923
10072
  {
9924
10073
  hostname: "api.node9.ai",
9925
10074
  path: "/api/v1/telemetry",
@@ -9953,7 +10102,17 @@ function registerInitCommand(program2) {
9953
10102
  message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
9954
10103
  default: true
9955
10104
  });
9956
- if (enableShields) chosenMode = "standard";
10105
+ if (enableShields) {
10106
+ chosenMode = "standard";
10107
+ try {
10108
+ const current = readActiveShields();
10109
+ const merged = Array.from(/* @__PURE__ */ new Set([...current, ...DEFAULT_SHIELDS]));
10110
+ const hasNewShields = DEFAULT_SHIELDS.some((s) => !current.includes(s));
10111
+ if (hasNewShields) writeActiveShields(merged);
10112
+ } catch (err2) {
10113
+ console.log(chalk11.yellow(` \u26A0\uFE0F Could not update shields: ${String(err2)}`));
10114
+ }
10115
+ }
9957
10116
  console.log("");
9958
10117
  }
9959
10118
  const configPath = path25.join(os19.homedir(), ".node9", "config.json");
@@ -9991,9 +10150,9 @@ function registerInitCommand(program2) {
9991
10150
  );
9992
10151
  if (found.length === 0) {
9993
10152
  console.log(
9994
- chalk11.gray("No AI agents detected. Install Claude Code, Gemini CLI, or Cursor")
10153
+ chalk11.gray("No AI agents detected. Install Claude Code, Gemini CLI, Cursor, or Codex")
9995
10154
  );
9996
- console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor>"));
10155
+ console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
9997
10156
  return;
9998
10157
  }
9999
10158
  console.log(chalk11.bold("Detected agents:"));
@@ -10006,6 +10165,7 @@ function registerInitCommand(program2) {
10006
10165
  if (agent === "claude") await setupClaude();
10007
10166
  else if (agent === "gemini") await setupGemini();
10008
10167
  else if (agent === "cursor") await setupCursor();
10168
+ else if (agent === "codex") await setupCodex();
10009
10169
  console.log("");
10010
10170
  }
10011
10171
  {
@@ -10627,6 +10787,20 @@ var TOOLS = [
10627
10787
  required: ["service"]
10628
10788
  }
10629
10789
  },
10790
+ {
10791
+ name: "node9_shield_disable",
10792
+ description: "Disable a node9 shield. Use node9_shield_list to see currently active shields.",
10793
+ inputSchema: {
10794
+ type: "object",
10795
+ properties: {
10796
+ service: {
10797
+ type: "string",
10798
+ description: 'Shield name to disable (e.g. "postgres", "aws", "github", "filesystem").'
10799
+ }
10800
+ },
10801
+ required: ["service"]
10802
+ }
10803
+ },
10630
10804
  {
10631
10805
  name: "node9_approver_list",
10632
10806
  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 +10930,24 @@ function handleShieldEnable(args) {
10756
10930
  const shield = getShield(name);
10757
10931
  return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
10758
10932
  }
10933
+ function handleShieldDisable(args) {
10934
+ const service = args.service;
10935
+ if (typeof service !== "string" || !service) {
10936
+ throw new Error("service is required");
10937
+ }
10938
+ const name = resolveShieldName(service);
10939
+ if (!name) {
10940
+ throw new Error(
10941
+ `Unknown shield: "${service}". Run node9_shield_list to see available shields.`
10942
+ );
10943
+ }
10944
+ const active = readActiveShields();
10945
+ if (!active.includes(name)) {
10946
+ return `Shield "${name}" is not active.`;
10947
+ }
10948
+ writeActiveShields(active.filter((s) => s !== name));
10949
+ return `Shield "${name}" disabled.`;
10950
+ }
10759
10951
  var GLOBAL_CONFIG_PATH2 = path27.join(os20.homedir(), ".node9", "config.json");
10760
10952
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
10761
10953
  function readGlobalConfigRaw() {
@@ -10884,6 +11076,8 @@ function runMcpServer() {
10884
11076
  text = handleShieldList();
10885
11077
  } else if (toolName === "node9_shield_enable") {
10886
11078
  text = handleShieldEnable(toolArgs);
11079
+ } else if (toolName === "node9_shield_disable") {
11080
+ text = handleShieldDisable(toolArgs);
10887
11081
  } else if (toolName === "node9_approver_list") {
10888
11082
  text = handleApproverList();
10889
11083
  } else if (toolName === "node9_approver_set") {