@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.js CHANGED
@@ -268,6 +268,70 @@ var init_config_schema = __esm({
268
268
  });
269
269
 
270
270
  // src/shields.ts
271
+ function validateShieldDefinition(raw, filePath) {
272
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
273
+ process.stderr.write(`[node9] Shield file is not an object: ${filePath}
274
+ `);
275
+ return null;
276
+ }
277
+ const r = raw;
278
+ if (typeof r.name !== "string" || !r.name) {
279
+ process.stderr.write(`[node9] Shield file missing 'name': ${filePath}
280
+ `);
281
+ return null;
282
+ }
283
+ if (typeof r.description !== "string") {
284
+ process.stderr.write(`[node9] Shield file missing 'description': ${filePath}
285
+ `);
286
+ return null;
287
+ }
288
+ if (!Array.isArray(r.aliases)) {
289
+ process.stderr.write(`[node9] Shield file missing 'aliases' array: ${filePath}
290
+ `);
291
+ return null;
292
+ }
293
+ if (!Array.isArray(r.smartRules)) {
294
+ process.stderr.write(`[node9] Shield file missing 'smartRules' array: ${filePath}
295
+ `);
296
+ return null;
297
+ }
298
+ if (!Array.isArray(r.dangerousWords)) {
299
+ process.stderr.write(`[node9] Shield file missing 'dangerousWords' array: ${filePath}
300
+ `);
301
+ return null;
302
+ }
303
+ return r;
304
+ }
305
+ function loadShieldsFromDir(dir, label) {
306
+ const result = {};
307
+ let entries;
308
+ try {
309
+ entries = import_fs2.default.readdirSync(dir).filter((f) => f.endsWith(".json"));
310
+ } catch (err2) {
311
+ if (err2.code !== "ENOENT") {
312
+ process.stderr.write(`[node9] Could not read ${label} shields dir ${dir}: ${String(err2)}
313
+ `);
314
+ }
315
+ return result;
316
+ }
317
+ for (const file of entries) {
318
+ const filePath = import_path2.default.join(dir, file);
319
+ try {
320
+ const raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
321
+ const shield = validateShieldDefinition(raw, filePath);
322
+ if (shield) result[shield.name] = shield;
323
+ } catch (err2) {
324
+ process.stderr.write(`[node9] Failed to load ${label} shield ${file}: ${String(err2)}
325
+ `);
326
+ }
327
+ }
328
+ return result;
329
+ }
330
+ function buildSHIELDS() {
331
+ const builtins = loadShieldsFromDir(BUILTIN_DIR, "builtin");
332
+ const userShields = loadShieldsFromDir(USER_SHIELDS_DIR, "user");
333
+ return { ...builtins, ...userShields };
334
+ }
271
335
  function resolveShieldName(input) {
272
336
  const lower = input.toLowerCase();
273
337
  if (SHIELDS[lower]) return lower;
@@ -374,7 +438,24 @@ function resolveShieldRule(shieldName, identifier) {
374
438
  }
375
439
  return null;
376
440
  }
377
- var import_fs2, import_path2, import_os2, import_crypto2, SHIELDS, SHIELDS_STATE_FILE;
441
+ function installShield(name, shieldJson) {
442
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
443
+ throw new Error(
444
+ `Invalid shield name '${name}': only alphanumeric characters, hyphens, and underscores are allowed`
445
+ );
446
+ }
447
+ const shield = validateShieldDefinition(shieldJson, `<downloaded:${name}>`);
448
+ if (!shield) throw new Error(`Downloaded shield '${name}' failed validation`);
449
+ if (shield.name !== name) {
450
+ throw new Error(`Shield name mismatch: file declares '${shield.name}' but expected '${name}'`);
451
+ }
452
+ import_fs2.default.mkdirSync(USER_SHIELDS_DIR, { recursive: true });
453
+ const filePath = import_path2.default.join(USER_SHIELDS_DIR, `${name}.json`);
454
+ const tmp = `${filePath}.${import_crypto2.default.randomBytes(6).toString("hex")}.tmp`;
455
+ import_fs2.default.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
456
+ import_fs2.default.renameSync(tmp, filePath);
457
+ }
458
+ var import_fs2, import_path2, import_os2, import_crypto2, BUILTIN_DIR, USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE;
378
459
  var init_shields = __esm({
379
460
  "src/shields.ts"() {
380
461
  "use strict";
@@ -382,173 +463,9 @@ var init_shields = __esm({
382
463
  import_path2 = __toESM(require("path"));
383
464
  import_os2 = __toESM(require("os"));
384
465
  import_crypto2 = __toESM(require("crypto"));
385
- SHIELDS = {
386
- postgres: {
387
- name: "postgres",
388
- description: "Protects PostgreSQL databases from destructive AI operations",
389
- aliases: ["pg", "postgresql"],
390
- smartRules: [
391
- {
392
- name: "shield:postgres:block-drop-table",
393
- tool: "*",
394
- conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
395
- verdict: "block",
396
- reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
397
- },
398
- {
399
- name: "shield:postgres:block-truncate",
400
- tool: "*",
401
- conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
402
- verdict: "block",
403
- reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
404
- },
405
- {
406
- name: "shield:postgres:block-drop-column",
407
- tool: "*",
408
- conditions: [
409
- { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
410
- ],
411
- verdict: "block",
412
- reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
413
- },
414
- {
415
- name: "shield:postgres:review-grant-revoke",
416
- tool: "*",
417
- conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
418
- verdict: "review",
419
- reason: "Permission changes require human approval (Postgres shield)"
420
- }
421
- ],
422
- dangerousWords: ["dropdb", "pg_dropcluster"]
423
- },
424
- github: {
425
- name: "github",
426
- description: "Protects GitHub repositories from destructive AI operations",
427
- aliases: ["git"],
428
- smartRules: [
429
- {
430
- // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
431
- // This rule adds coverage for `git push --delete` which the built-in does not match.
432
- name: "shield:github:review-delete-branch-remote",
433
- tool: "bash",
434
- conditions: [
435
- {
436
- field: "command",
437
- op: "matches",
438
- value: "git\\s+push\\s+.*--delete",
439
- flags: "i"
440
- }
441
- ],
442
- verdict: "review",
443
- reason: "Remote branch deletion requires human approval (GitHub shield)"
444
- },
445
- {
446
- name: "shield:github:block-delete-repo",
447
- tool: "*",
448
- conditions: [
449
- { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
450
- ],
451
- verdict: "block",
452
- reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
453
- }
454
- ],
455
- dangerousWords: []
456
- },
457
- aws: {
458
- name: "aws",
459
- description: "Protects AWS infrastructure from destructive AI operations",
460
- aliases: ["amazon"],
461
- smartRules: [
462
- {
463
- name: "shield:aws:block-delete-s3-bucket",
464
- tool: "*",
465
- conditions: [
466
- {
467
- field: "command",
468
- op: "matches",
469
- value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
470
- flags: "i"
471
- }
472
- ],
473
- verdict: "block",
474
- reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
475
- },
476
- {
477
- name: "shield:aws:review-iam-changes",
478
- tool: "*",
479
- conditions: [
480
- {
481
- field: "command",
482
- op: "matches",
483
- value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
484
- flags: "i"
485
- }
486
- ],
487
- verdict: "review",
488
- reason: "IAM changes require human approval (AWS shield)"
489
- },
490
- {
491
- name: "shield:aws:block-ec2-terminate",
492
- tool: "*",
493
- conditions: [
494
- {
495
- field: "command",
496
- op: "matches",
497
- value: "aws\\s+ec2\\s+terminate-instances",
498
- flags: "i"
499
- }
500
- ],
501
- verdict: "block",
502
- reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
503
- },
504
- {
505
- name: "shield:aws:review-rds-delete",
506
- tool: "*",
507
- conditions: [
508
- { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
509
- ],
510
- verdict: "review",
511
- reason: "RDS deletion requires human approval (AWS shield)"
512
- }
513
- ],
514
- dangerousWords: []
515
- },
516
- filesystem: {
517
- name: "filesystem",
518
- description: "Protects the local filesystem from dangerous AI operations",
519
- aliases: ["fs"],
520
- smartRules: [
521
- {
522
- name: "shield:filesystem:review-chmod-777",
523
- tool: "bash",
524
- conditions: [
525
- { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
526
- ],
527
- verdict: "review",
528
- reason: "chmod 777 requires human approval (filesystem shield)"
529
- },
530
- {
531
- name: "shield:filesystem:review-write-etc",
532
- tool: "bash",
533
- conditions: [
534
- {
535
- field: "command",
536
- // Narrow to write-indicative operations to avoid approval fatigue on reads.
537
- // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
538
- op: "matches",
539
- value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
540
- }
541
- ],
542
- verdict: "review",
543
- reason: "Writing to /etc requires human approval (filesystem shield)"
544
- }
545
- ],
546
- // dd removed: too common as a legitimate tool (disk imaging, file ops).
547
- // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
548
- // wipefs retained: rarely legitimate in an agent context and not in built-ins.
549
- dangerousWords: ["wipefs"]
550
- }
551
- };
466
+ BUILTIN_DIR = import_path2.default.join(__dirname, "shields", "builtin");
467
+ USER_SHIELDS_DIR = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields");
468
+ SHIELDS = buildSHIELDS();
552
469
  SHIELDS_STATE_FILE = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields.json");
553
470
  }
554
471
  });
@@ -2520,7 +2437,7 @@ function isDaemonRunning() {
2520
2437
  return false;
2521
2438
  }
2522
2439
  }
2523
- async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
2440
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched) {
2524
2441
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2525
2442
  const ctrl = new AbortController();
2526
2443
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -2541,7 +2458,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2541
2458
  ...cwd && { cwd },
2542
2459
  ...recoveryCommand && { recoveryCommand },
2543
2460
  ...skipBackgroundAuth && { skipBackgroundAuth: true },
2544
- ...viewOnly && { viewOnly: true }
2461
+ ...viewOnly && { viewOnly: true },
2462
+ ...localSmartRuleMatched && { localSmartRuleMatched: true }
2545
2463
  }),
2546
2464
  signal: ctrl.signal
2547
2465
  });
@@ -3236,6 +3154,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3236
3154
  let policyMatchedWord;
3237
3155
  let riskMetadata;
3238
3156
  let statefulRecoveryCommand;
3157
+ let localSmartRuleMatched = false;
3239
3158
  let taintWarning = null;
3240
3159
  if (isNetworkTool(toolName, args)) {
3241
3160
  const filePaths = extractFilePaths(toolName, args);
@@ -3379,6 +3298,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3379
3298
  explainableLabel = policyResult.blockedByLabel || "Local Config";
3380
3299
  policyMatchedField = policyResult.matchedField;
3381
3300
  policyMatchedWord = policyResult.matchedWord;
3301
+ if (policyResult.ruleName) localSmartRuleMatched = true;
3382
3302
  riskMetadata = computeRiskMetadata(
3383
3303
  args,
3384
3304
  policyResult.tier ?? 6,
@@ -3421,22 +3341,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3421
3341
  }
3422
3342
  let cloudRequestId = null;
3423
3343
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
3424
- if (cloudEnforced) {
3344
+ if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3425
3345
  try {
3426
3346
  const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
3427
3347
  if (!initResult.pending) {
3428
3348
  if (initResult.shadowMode) {
3429
3349
  return { approved: true, checkedBy: "cloud" };
3430
3350
  }
3431
- return {
3432
- approved: !!initResult.approved,
3433
- reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
3434
- checkedBy: initResult.approved ? "cloud" : void 0,
3435
- blockedBy: initResult.approved ? void 0 : "team-policy",
3436
- blockedByLabel: "Organization Policy (SaaS)"
3437
- };
3351
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
3352
+ return {
3353
+ approved: !!initResult.approved,
3354
+ reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
3355
+ checkedBy: initResult.approved ? "cloud" : void 0,
3356
+ blockedBy: initResult.approved ? void 0 : "team-policy",
3357
+ blockedByLabel: "Organization Policy (SaaS)"
3358
+ };
3359
+ }
3360
+ }
3361
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
3362
+ cloudRequestId = initResult.requestId || null;
3438
3363
  }
3439
- cloudRequestId = initResult.requestId || null;
3440
3364
  if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
3441
3365
  } catch {
3442
3366
  }
@@ -3482,7 +3406,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3482
3406
  riskMetadata,
3483
3407
  options?.activityId,
3484
3408
  options?.cwd,
3485
- statefulRecoveryCommand
3409
+ statefulRecoveryCommand,
3410
+ void 0,
3411
+ void 0,
3412
+ localSmartRuleMatched || options?.localSmartRuleMatched
3486
3413
  );
3487
3414
  daemonEntryId = entry.id;
3488
3415
  daemonAllowCount = entry.allowCount;
@@ -3490,7 +3417,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3490
3417
  }
3491
3418
  }
3492
3419
  }
3493
- if (cloudEnforced && cloudRequestId) {
3420
+ if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3494
3421
  racePromises.push(
3495
3422
  (async () => {
3496
3423
  try {
@@ -6152,7 +6079,8 @@ data: ${JSON.stringify(item.data)}
6152
6079
  viewOnly = false,
6153
6080
  fromCLI = false,
6154
6081
  activityId,
6155
- cwd
6082
+ cwd,
6083
+ localSmartRuleMatched = false
6156
6084
  } = JSON.parse(body);
6157
6085
  const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto6.randomUUID)();
6158
6086
  const entry = {
@@ -6232,7 +6160,7 @@ data: ${JSON.stringify(item.data)}
6232
6160
  agent: typeof agent === "string" ? agent : void 0,
6233
6161
  mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
6234
6162
  },
6235
- { calledFromDaemon: true }
6163
+ { calledFromDaemon: true, localSmartRuleMatched: !!localSmartRuleMatched }
6236
6164
  ).then((result) => {
6237
6165
  const e = pending.get(id);
6238
6166
  if (!e) return;
@@ -7070,6 +6998,7 @@ async function startTail(options = {}) {
7070
6998
  }
7071
6999
  const connectionTime = Date.now();
7072
7000
  const activityPending = /* @__PURE__ */ new Map();
7001
+ const orphanedResults = /* @__PURE__ */ new Map();
7073
7002
  let csrfToken = "";
7074
7003
  const approvalQueue = [];
7075
7004
  let cardActive = false;
@@ -7404,9 +7333,14 @@ async function startTail(options = {}) {
7404
7333
  renderResult(data, data);
7405
7334
  return;
7406
7335
  }
7336
+ const orphaned = orphanedResults.get(data.id);
7337
+ if (orphaned) {
7338
+ orphanedResults.delete(data.id);
7339
+ renderResult(data, orphaned);
7340
+ return;
7341
+ }
7407
7342
  activityPending.set(data.id, data);
7408
- const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
7409
- if (slowTool) renderPending(data);
7343
+ renderPending(data);
7410
7344
  }
7411
7345
  if (event === "snapshot") {
7412
7346
  const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
@@ -7425,6 +7359,8 @@ async function startTail(options = {}) {
7425
7359
  if (original) {
7426
7360
  renderResult(original, data);
7427
7361
  activityPending.delete(data.id);
7362
+ } else {
7363
+ orphanedResults.set(data.id, data);
7428
7364
  }
7429
7365
  }
7430
7366
  }
@@ -7667,6 +7603,29 @@ function renderOffline() {
7667
7603
  process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
7668
7604
  `);
7669
7605
  }
7606
+ function readActiveShieldsHud() {
7607
+ const now = Date.now();
7608
+ if (shieldsCache && now - shieldsCache.ts < SHIELDS_CACHE_TTL_MS) {
7609
+ return shieldsCache.value;
7610
+ }
7611
+ try {
7612
+ const shieldsPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "shields.json");
7613
+ if (!import_fs26.default.existsSync(shieldsPath)) {
7614
+ shieldsCache = { value: [], ts: now };
7615
+ return [];
7616
+ }
7617
+ const parsed = JSON.parse(import_fs26.default.readFileSync(shieldsPath, "utf-8"));
7618
+ if (!Array.isArray(parsed.active)) {
7619
+ shieldsCache = { value: [], ts: now };
7620
+ return [];
7621
+ }
7622
+ const value = parsed.active.filter((s) => typeof s === "string").map((s) => s.slice(0, 64)).slice(0, 20);
7623
+ shieldsCache = { value, ts: now };
7624
+ return value;
7625
+ } catch {
7626
+ return [];
7627
+ }
7628
+ }
7670
7629
  function renderSecurityLine(status) {
7671
7630
  const parts = [];
7672
7631
  parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
@@ -7684,6 +7643,18 @@ function renderSecurityLine(status) {
7684
7643
  };
7685
7644
  const mc = modeColors[status.mode] ?? WHITE;
7686
7645
  parts.push(`${dim("|")} ${color(mc, modeIcon[status.mode] ?? "")}${color(mc, status.mode)}`);
7646
+ const activeShields = readActiveShieldsHud();
7647
+ if (activeShields.length > 0) {
7648
+ const shieldAbbrevs = {
7649
+ "bash-safe": "bash",
7650
+ filesystem: "fs",
7651
+ postgres: "pg",
7652
+ github: "gh",
7653
+ aws: "aws"
7654
+ };
7655
+ const labels = activeShields.map((s) => shieldAbbrevs[s] ?? s).join(" ");
7656
+ parts.push(color(DIM, `[${labels}]`));
7657
+ }
7687
7658
  if (status.mode === "observe") {
7688
7659
  parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
7689
7660
  if (status.session.wouldBlock > 0) {
@@ -7780,7 +7751,7 @@ async function main() {
7780
7751
  renderOffline();
7781
7752
  }
7782
7753
  }
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;
7754
+ 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
7755
  var init_hud = __esm({
7785
7756
  "src/cli/hud.ts"() {
7786
7757
  "use strict";
@@ -7802,6 +7773,8 @@ var init_hud = __esm({
7802
7773
  BAR_FILLED = "\u2588";
7803
7774
  BAR_EMPTY = "\u2591";
7804
7775
  BAR_WIDTH = 10;
7776
+ shieldsCache = null;
7777
+ SHIELDS_CACHE_TTL_MS = 2e3;
7805
7778
  }
7806
7779
  });
7807
7780
 
@@ -7815,6 +7788,7 @@ var import_path14 = __toESM(require("path"));
7815
7788
  var import_os10 = __toESM(require("os"));
7816
7789
  var import_chalk = __toESM(require("chalk"));
7817
7790
  var import_prompts = require("@inquirer/prompts");
7791
+ var import_smol_toml = require("smol-toml");
7818
7792
  var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7819
7793
  function hasNode9McpServer(servers) {
7820
7794
  const entry = servers["node9"];
@@ -8178,7 +8152,8 @@ function detectAgents(homeDir2 = import_os10.default.homedir()) {
8178
8152
  return {
8179
8153
  claude: exists(import_path14.default.join(homeDir2, ".claude")) || exists(import_path14.default.join(homeDir2, ".claude.json")),
8180
8154
  gemini: exists(import_path14.default.join(homeDir2, ".gemini")),
8181
- cursor: exists(import_path14.default.join(homeDir2, ".cursor"))
8155
+ cursor: exists(import_path14.default.join(homeDir2, ".cursor")),
8156
+ codex: exists(import_path14.default.join(homeDir2, ".codex"))
8182
8157
  };
8183
8158
  }
8184
8159
  async function setupCursor() {
@@ -8243,6 +8218,82 @@ async function setupCursor() {
8243
8218
  printDaemonTip();
8244
8219
  }
8245
8220
  }
8221
+ function readToml(filePath) {
8222
+ try {
8223
+ if (import_fs11.default.existsSync(filePath)) {
8224
+ return (0, import_smol_toml.parse)(import_fs11.default.readFileSync(filePath, "utf-8"));
8225
+ }
8226
+ } catch {
8227
+ }
8228
+ return null;
8229
+ }
8230
+ function writeToml(filePath, data) {
8231
+ const dir = import_path14.default.dirname(filePath);
8232
+ if (!import_fs11.default.existsSync(dir)) import_fs11.default.mkdirSync(dir, { recursive: true });
8233
+ import_fs11.default.writeFileSync(filePath, (0, import_smol_toml.stringify)(data));
8234
+ }
8235
+ async function setupCodex() {
8236
+ const homeDir2 = import_os10.default.homedir();
8237
+ const configPath = import_path14.default.join(homeDir2, ".codex", "config.toml");
8238
+ const config = readToml(configPath) ?? {};
8239
+ const servers = config.mcp_servers ?? {};
8240
+ let anythingChanged = false;
8241
+ if (!hasNode9McpServer(servers)) {
8242
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
8243
+ config.mcp_servers = servers;
8244
+ writeToml(configPath, config);
8245
+ console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
8246
+ anythingChanged = true;
8247
+ }
8248
+ const serversToWrap = [];
8249
+ for (const [name, server] of Object.entries(servers)) {
8250
+ if (!server.command || server.command === "node9") continue;
8251
+ const parts = [server.command, ...server.args ?? []];
8252
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
8253
+ }
8254
+ if (serversToWrap.length > 0) {
8255
+ console.log(import_chalk.default.bold("The following existing entries will be modified:\n"));
8256
+ console.log(import_chalk.default.white(` ${configPath}`));
8257
+ for (const { name, originalCmd } of serversToWrap) {
8258
+ console.log(import_chalk.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
8259
+ }
8260
+ console.log("");
8261
+ const proceed = await (0, import_prompts.confirm)({ message: "Wrap these MCP servers?", default: true });
8262
+ if (proceed) {
8263
+ for (const { name, parts } of serversToWrap) {
8264
+ servers[name] = { ...servers[name], command: "node9", args: parts };
8265
+ }
8266
+ config.mcp_servers = servers;
8267
+ writeToml(configPath, config);
8268
+ console.log(import_chalk.default.green(`
8269
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
8270
+ anythingChanged = true;
8271
+ } else {
8272
+ console.log(import_chalk.default.yellow(" Skipped MCP server wrapping."));
8273
+ }
8274
+ console.log("");
8275
+ }
8276
+ console.log(
8277
+ import_chalk.default.yellow(
8278
+ " \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."
8279
+ )
8280
+ );
8281
+ console.log("");
8282
+ if (!anythingChanged && serversToWrap.length === 0) {
8283
+ console.log(
8284
+ import_chalk.default.blue(
8285
+ "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.codex/config.toml and re-run."
8286
+ )
8287
+ );
8288
+ printDaemonTip();
8289
+ return;
8290
+ }
8291
+ if (anythingChanged) {
8292
+ console.log(import_chalk.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Codex via MCP proxy!"));
8293
+ console.log(import_chalk.default.gray(" Restart Codex for changes to take effect."));
8294
+ printDaemonTip();
8295
+ }
8296
+ }
8246
8297
  function setupHud() {
8247
8298
  const homeDir2 = import_os10.default.homedir();
8248
8299
  const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
@@ -8960,7 +9011,7 @@ RAW: ${raw}
8960
9011
  }
8961
9012
  }) + "\n"
8962
9013
  );
8963
- process.exit(0);
9014
+ process.exit(2);
8964
9015
  };
8965
9016
  if (!toolName) {
8966
9017
  sendBlock("Node9: unrecognised hook payload \u2014 tool name missing.");
@@ -9195,6 +9246,27 @@ var import_chalk6 = __toESM(require("chalk"));
9195
9246
  init_shields();
9196
9247
  init_audit();
9197
9248
  init_config();
9249
+
9250
+ // src/utils/https-fetch.ts
9251
+ var import_https = __toESM(require("https"));
9252
+ function httpsFetch(url) {
9253
+ return new Promise((resolve, reject) => {
9254
+ import_https.default.get(url, (res) => {
9255
+ if (res.statusCode !== 200) {
9256
+ reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
9257
+ res.resume();
9258
+ return;
9259
+ }
9260
+ const chunks = [];
9261
+ res.on("data", (chunk) => chunks.push(chunk));
9262
+ res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
9263
+ res.on("error", reject);
9264
+ }).on("error", reject);
9265
+ });
9266
+ }
9267
+
9268
+ // src/cli/commands/shield.ts
9269
+ var COMMUNITY_INDEX_URL = "https://raw.githubusercontent.com/node9ai/node9-proxy/main/shields/community/index.json";
9198
9270
  function registerShieldCommand(program2) {
9199
9271
  const shieldCmd = program2.command("shield").description("Manage pre-packaged security shield templates");
9200
9272
  shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
@@ -9254,7 +9326,32 @@ function registerShieldCommand(program2) {
9254
9326
  \u{1F6E1}\uFE0F Shield "${name}" disabled.
9255
9327
  `));
9256
9328
  });
9257
- shieldCmd.command("list").description("Show all available shields").action(() => {
9329
+ shieldCmd.command("list").description("Show available shields (add --community to browse the marketplace)").option("--community", "List shields available from the community marketplace").action((opts) => {
9330
+ if (opts.community) {
9331
+ console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Community Shield Marketplace\n"));
9332
+ console.log(import_chalk6.default.gray(" Fetching index\u2026\n"));
9333
+ httpsFetch(COMMUNITY_INDEX_URL).then((body) => {
9334
+ const entries = JSON.parse(body);
9335
+ const installed = new Set(listShields().map((s) => s.name));
9336
+ for (const e of entries) {
9337
+ const tag = installed.has(e.name) ? import_chalk6.default.green("installed") : import_chalk6.default.gray("available");
9338
+ console.log(
9339
+ ` ${tag} ${import_chalk6.default.cyan(e.name.padEnd(12))} ${e.description} ${import_chalk6.default.gray(`by ${e.author}`)}`
9340
+ );
9341
+ }
9342
+ console.log("");
9343
+ console.log(
9344
+ import_chalk6.default.gray(` Install a shield: ${import_chalk6.default.cyan("node9 shield install <name>")}
9345
+ `)
9346
+ );
9347
+ }).catch((err2) => {
9348
+ console.error(import_chalk6.default.red(`
9349
+ \u274C Could not fetch community index: ${String(err2)}
9350
+ `));
9351
+ process.exit(1);
9352
+ });
9353
+ return;
9354
+ }
9258
9355
  const active = new Set(readActiveShields());
9259
9356
  console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
9260
9357
  for (const shield of listShields()) {
@@ -9264,6 +9361,10 @@ function registerShieldCommand(program2) {
9264
9361
  console.log(import_chalk6.default.gray(` aliases: ${shield.aliases.join(", ")}`));
9265
9362
  }
9266
9363
  console.log("");
9364
+ console.log(
9365
+ import_chalk6.default.gray(` Browse community shields: ${import_chalk6.default.cyan("node9 shield list --community")}
9366
+ `)
9367
+ );
9267
9368
  });
9268
9369
  shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
9269
9370
  const active = readActiveShields();
@@ -9401,6 +9502,52 @@ function registerShieldCommand(program2) {
9401
9502
  `)
9402
9503
  );
9403
9504
  });
9505
+ shieldCmd.command("install <name>").description("Install a shield from the community marketplace into ~/.node9/shields/").action((name) => {
9506
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
9507
+ console.error(
9508
+ import_chalk6.default.red(
9509
+ `
9510
+ \u274C Invalid shield name: only alphanumeric characters, hyphens, and underscores are allowed
9511
+ `
9512
+ )
9513
+ );
9514
+ process.exit(1);
9515
+ }
9516
+ console.log(import_chalk6.default.bold(`
9517
+ \u{1F6E1}\uFE0F Installing shield "${name}"\u2026
9518
+ `));
9519
+ httpsFetch(COMMUNITY_INDEX_URL).then((indexBody) => {
9520
+ const entries = JSON.parse(indexBody);
9521
+ const entry = entries.find((e) => e.name === name);
9522
+ if (!entry) {
9523
+ const names = entries.map((e) => import_chalk6.default.cyan(e.name)).join(", ");
9524
+ console.error(
9525
+ import_chalk6.default.red(`\u274C Shield "${name}" not found in the community marketplace.
9526
+ `)
9527
+ );
9528
+ console.error(` Available: ${names}
9529
+ `);
9530
+ process.exit(1);
9531
+ }
9532
+ return httpsFetch(entry.url);
9533
+ }).then((shieldBody) => {
9534
+ const shieldJson = JSON.parse(shieldBody);
9535
+ installShield(name, shieldJson);
9536
+ console.log(
9537
+ import_chalk6.default.green(`\u2705 Shield "${name}" installed to ~/.node9/shields/${name}.json`)
9538
+ );
9539
+ console.log(
9540
+ import_chalk6.default.gray(` Activate it with: ${import_chalk6.default.cyan(`node9 shield enable ${name}`)}
9541
+ `)
9542
+ );
9543
+ appendConfigAudit({ event: "shield-install", shield: name });
9544
+ }).catch((err2) => {
9545
+ console.error(import_chalk6.default.red(`
9546
+ \u274C Install failed: ${String(err2)}
9547
+ `));
9548
+ process.exit(1);
9549
+ });
9550
+ });
9404
9551
  }
9405
9552
  function registerConfigShowCommand(program2) {
9406
9553
  program2.command("config show").description(
@@ -9934,8 +10081,10 @@ var import_chalk11 = __toESM(require("chalk"));
9934
10081
  var import_fs23 = __toESM(require("fs"));
9935
10082
  var import_path25 = __toESM(require("path"));
9936
10083
  var import_os19 = __toESM(require("os"));
9937
- var import_https = __toESM(require("https"));
10084
+ var import_https2 = __toESM(require("https"));
9938
10085
  init_core();
10086
+ init_shields();
10087
+ var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
9939
10088
  function fireTelemetryPing(agents) {
9940
10089
  try {
9941
10090
  const body = JSON.stringify({
@@ -9944,7 +10093,7 @@ function fireTelemetryPing(agents) {
9944
10093
  os: process.platform,
9945
10094
  node9_version: process.env.npm_package_version ?? "unknown"
9946
10095
  });
9947
- const req = import_https.default.request(
10096
+ const req = import_https2.default.request(
9948
10097
  {
9949
10098
  hostname: "api.node9.ai",
9950
10099
  path: "/api/v1/telemetry",
@@ -9978,7 +10127,17 @@ function registerInitCommand(program2) {
9978
10127
  message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
9979
10128
  default: true
9980
10129
  });
9981
- if (enableShields) chosenMode = "standard";
10130
+ if (enableShields) {
10131
+ chosenMode = "standard";
10132
+ try {
10133
+ const current = readActiveShields();
10134
+ const merged = Array.from(/* @__PURE__ */ new Set([...current, ...DEFAULT_SHIELDS]));
10135
+ const hasNewShields = DEFAULT_SHIELDS.some((s) => !current.includes(s));
10136
+ if (hasNewShields) writeActiveShields(merged);
10137
+ } catch (err2) {
10138
+ console.log(import_chalk11.default.yellow(` \u26A0\uFE0F Could not update shields: ${String(err2)}`));
10139
+ }
10140
+ }
9982
10141
  console.log("");
9983
10142
  }
9984
10143
  const configPath = import_path25.default.join(import_os19.default.homedir(), ".node9", "config.json");
@@ -10016,9 +10175,9 @@ function registerInitCommand(program2) {
10016
10175
  );
10017
10176
  if (found.length === 0) {
10018
10177
  console.log(
10019
- import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, or Cursor")
10178
+ import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, Cursor, or Codex")
10020
10179
  );
10021
- console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor>"));
10180
+ console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
10022
10181
  return;
10023
10182
  }
10024
10183
  console.log(import_chalk11.default.bold("Detected agents:"));
@@ -10031,6 +10190,7 @@ function registerInitCommand(program2) {
10031
10190
  if (agent === "claude") await setupClaude();
10032
10191
  else if (agent === "gemini") await setupGemini();
10033
10192
  else if (agent === "cursor") await setupCursor();
10193
+ else if (agent === "codex") await setupCodex();
10034
10194
  console.log("");
10035
10195
  }
10036
10196
  {
@@ -10652,6 +10812,20 @@ var TOOLS = [
10652
10812
  required: ["service"]
10653
10813
  }
10654
10814
  },
10815
+ {
10816
+ name: "node9_shield_disable",
10817
+ description: "Disable a node9 shield. Use node9_shield_list to see currently active shields.",
10818
+ inputSchema: {
10819
+ type: "object",
10820
+ properties: {
10821
+ service: {
10822
+ type: "string",
10823
+ description: 'Shield name to disable (e.g. "postgres", "aws", "github", "filesystem").'
10824
+ }
10825
+ },
10826
+ required: ["service"]
10827
+ }
10828
+ },
10655
10829
  {
10656
10830
  name: "node9_approver_list",
10657
10831
  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 +10955,24 @@ function handleShieldEnable(args) {
10781
10955
  const shield = getShield(name);
10782
10956
  return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
10783
10957
  }
10958
+ function handleShieldDisable(args) {
10959
+ const service = args.service;
10960
+ if (typeof service !== "string" || !service) {
10961
+ throw new Error("service is required");
10962
+ }
10963
+ const name = resolveShieldName(service);
10964
+ if (!name) {
10965
+ throw new Error(
10966
+ `Unknown shield: "${service}". Run node9_shield_list to see available shields.`
10967
+ );
10968
+ }
10969
+ const active = readActiveShields();
10970
+ if (!active.includes(name)) {
10971
+ return `Shield "${name}" is not active.`;
10972
+ }
10973
+ writeActiveShields(active.filter((s) => s !== name));
10974
+ return `Shield "${name}" disabled.`;
10975
+ }
10784
10976
  var GLOBAL_CONFIG_PATH2 = import_path27.default.join(import_os20.default.homedir(), ".node9", "config.json");
10785
10977
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
10786
10978
  function readGlobalConfigRaw() {
@@ -10909,6 +11101,8 @@ function runMcpServer() {
10909
11101
  text = handleShieldList();
10910
11102
  } else if (toolName === "node9_shield_enable") {
10911
11103
  text = handleShieldEnable(toolArgs);
11104
+ } else if (toolName === "node9_shield_disable") {
11105
+ text = handleShieldDisable(toolArgs);
10912
11106
  } else if (toolName === "node9_approver_list") {
10913
11107
  text = handleApproverList();
10914
11108
  } else if (toolName === "node9_approver_set") {