@node9/proxy 1.7.1 → 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,251 +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
- "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
- },
594
- filesystem: {
595
- name: "filesystem",
596
- description: "Protects the local filesystem from dangerous AI operations",
597
- aliases: ["fs"],
598
- smartRules: [
599
- {
600
- name: "shield:filesystem:review-chmod-777",
601
- tool: "bash",
602
- conditions: [
603
- { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
604
- ],
605
- verdict: "review",
606
- reason: "chmod 777 requires human approval (filesystem shield)"
607
- },
608
- {
609
- name: "shield:filesystem:review-write-etc",
610
- tool: "bash",
611
- conditions: [
612
- {
613
- field: "command",
614
- // Narrow to write-indicative operations to avoid approval fatigue on reads.
615
- // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
616
- op: "matches",
617
- value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
618
- }
619
- ],
620
- verdict: "review",
621
- reason: "Writing to /etc requires human approval (filesystem shield)"
622
- }
623
- ],
624
- // dd removed: too common as a legitimate tool (disk imaging, file ops).
625
- // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
626
- // wipefs retained: rarely legitimate in an agent context and not in built-ins.
627
- dangerousWords: ["wipefs"]
628
- }
629
- };
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();
630
469
  SHIELDS_STATE_FILE = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields.json");
631
470
  }
632
471
  });
@@ -2598,7 +2437,7 @@ function isDaemonRunning() {
2598
2437
  return false;
2599
2438
  }
2600
2439
  }
2601
- 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) {
2602
2441
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2603
2442
  const ctrl = new AbortController();
2604
2443
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -2619,7 +2458,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2619
2458
  ...cwd && { cwd },
2620
2459
  ...recoveryCommand && { recoveryCommand },
2621
2460
  ...skipBackgroundAuth && { skipBackgroundAuth: true },
2622
- ...viewOnly && { viewOnly: true }
2461
+ ...viewOnly && { viewOnly: true },
2462
+ ...localSmartRuleMatched && { localSmartRuleMatched: true }
2623
2463
  }),
2624
2464
  signal: ctrl.signal
2625
2465
  });
@@ -3314,6 +3154,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3314
3154
  let policyMatchedWord;
3315
3155
  let riskMetadata;
3316
3156
  let statefulRecoveryCommand;
3157
+ let localSmartRuleMatched = false;
3317
3158
  let taintWarning = null;
3318
3159
  if (isNetworkTool(toolName, args)) {
3319
3160
  const filePaths = extractFilePaths(toolName, args);
@@ -3457,6 +3298,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3457
3298
  explainableLabel = policyResult.blockedByLabel || "Local Config";
3458
3299
  policyMatchedField = policyResult.matchedField;
3459
3300
  policyMatchedWord = policyResult.matchedWord;
3301
+ if (policyResult.ruleName) localSmartRuleMatched = true;
3460
3302
  riskMetadata = computeRiskMetadata(
3461
3303
  args,
3462
3304
  policyResult.tier ?? 6,
@@ -3499,22 +3341,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3499
3341
  }
3500
3342
  let cloudRequestId = null;
3501
3343
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
3502
- if (cloudEnforced) {
3344
+ if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3503
3345
  try {
3504
3346
  const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
3505
3347
  if (!initResult.pending) {
3506
3348
  if (initResult.shadowMode) {
3507
3349
  return { approved: true, checkedBy: "cloud" };
3508
3350
  }
3509
- return {
3510
- approved: !!initResult.approved,
3511
- reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
3512
- checkedBy: initResult.approved ? "cloud" : void 0,
3513
- blockedBy: initResult.approved ? void 0 : "team-policy",
3514
- blockedByLabel: "Organization Policy (SaaS)"
3515
- };
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;
3516
3363
  }
3517
- cloudRequestId = initResult.requestId || null;
3518
3364
  if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
3519
3365
  } catch {
3520
3366
  }
@@ -3560,7 +3406,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3560
3406
  riskMetadata,
3561
3407
  options?.activityId,
3562
3408
  options?.cwd,
3563
- statefulRecoveryCommand
3409
+ statefulRecoveryCommand,
3410
+ void 0,
3411
+ void 0,
3412
+ localSmartRuleMatched || options?.localSmartRuleMatched
3564
3413
  );
3565
3414
  daemonEntryId = entry.id;
3566
3415
  daemonAllowCount = entry.allowCount;
@@ -3568,7 +3417,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3568
3417
  }
3569
3418
  }
3570
3419
  }
3571
- if (cloudEnforced && cloudRequestId) {
3420
+ if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3572
3421
  racePromises.push(
3573
3422
  (async () => {
3574
3423
  try {
@@ -6230,7 +6079,8 @@ data: ${JSON.stringify(item.data)}
6230
6079
  viewOnly = false,
6231
6080
  fromCLI = false,
6232
6081
  activityId,
6233
- cwd
6082
+ cwd,
6083
+ localSmartRuleMatched = false
6234
6084
  } = JSON.parse(body);
6235
6085
  const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto6.randomUUID)();
6236
6086
  const entry = {
@@ -6310,7 +6160,7 @@ data: ${JSON.stringify(item.data)}
6310
6160
  agent: typeof agent === "string" ? agent : void 0,
6311
6161
  mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
6312
6162
  },
6313
- { calledFromDaemon: true }
6163
+ { calledFromDaemon: true, localSmartRuleMatched: !!localSmartRuleMatched }
6314
6164
  ).then((result) => {
6315
6165
  const e = pending.get(id);
6316
6166
  if (!e) return;
@@ -9161,7 +9011,7 @@ RAW: ${raw}
9161
9011
  }
9162
9012
  }) + "\n"
9163
9013
  );
9164
- process.exit(0);
9014
+ process.exit(2);
9165
9015
  };
9166
9016
  if (!toolName) {
9167
9017
  sendBlock("Node9: unrecognised hook payload \u2014 tool name missing.");
@@ -9396,6 +9246,27 @@ var import_chalk6 = __toESM(require("chalk"));
9396
9246
  init_shields();
9397
9247
  init_audit();
9398
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";
9399
9270
  function registerShieldCommand(program2) {
9400
9271
  const shieldCmd = program2.command("shield").description("Manage pre-packaged security shield templates");
9401
9272
  shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
@@ -9455,7 +9326,32 @@ function registerShieldCommand(program2) {
9455
9326
  \u{1F6E1}\uFE0F Shield "${name}" disabled.
9456
9327
  `));
9457
9328
  });
9458
- 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
+ }
9459
9355
  const active = new Set(readActiveShields());
9460
9356
  console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
9461
9357
  for (const shield of listShields()) {
@@ -9465,6 +9361,10 @@ function registerShieldCommand(program2) {
9465
9361
  console.log(import_chalk6.default.gray(` aliases: ${shield.aliases.join(", ")}`));
9466
9362
  }
9467
9363
  console.log("");
9364
+ console.log(
9365
+ import_chalk6.default.gray(` Browse community shields: ${import_chalk6.default.cyan("node9 shield list --community")}
9366
+ `)
9367
+ );
9468
9368
  });
9469
9369
  shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
9470
9370
  const active = readActiveShields();
@@ -9602,6 +9502,52 @@ function registerShieldCommand(program2) {
9602
9502
  `)
9603
9503
  );
9604
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
+ });
9605
9551
  }
9606
9552
  function registerConfigShowCommand(program2) {
9607
9553
  program2.command("config show").description(
@@ -10135,7 +10081,7 @@ var import_chalk11 = __toESM(require("chalk"));
10135
10081
  var import_fs23 = __toESM(require("fs"));
10136
10082
  var import_path25 = __toESM(require("path"));
10137
10083
  var import_os19 = __toESM(require("os"));
10138
- var import_https = __toESM(require("https"));
10084
+ var import_https2 = __toESM(require("https"));
10139
10085
  init_core();
10140
10086
  init_shields();
10141
10087
  var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
@@ -10147,7 +10093,7 @@ function fireTelemetryPing(agents) {
10147
10093
  os: process.platform,
10148
10094
  node9_version: process.env.npm_package_version ?? "unknown"
10149
10095
  });
10150
- const req = import_https.default.request(
10096
+ const req = import_https2.default.request(
10151
10097
  {
10152
10098
  hostname: "api.node9.ai",
10153
10099
  path: "/api/v1/telemetry",