@node9/proxy 1.0.13 → 1.0.14

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
@@ -29,9 +29,9 @@ var import_commander = require("commander");
29
29
  // src/core.ts
30
30
  var import_chalk2 = __toESM(require("chalk"));
31
31
  var import_prompts = require("@inquirer/prompts");
32
- var import_fs = __toESM(require("fs"));
33
- var import_path3 = __toESM(require("path"));
34
- var import_os = __toESM(require("os"));
32
+ var import_fs2 = __toESM(require("fs"));
33
+ var import_path4 = __toESM(require("path"));
34
+ var import_os2 = __toESM(require("os"));
35
35
  var import_picomatch = __toESM(require("picomatch"));
36
36
  var import_sh_syntax = require("sh-syntax");
37
37
 
@@ -361,25 +361,26 @@ var import_zod = require("zod");
361
361
  var noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
362
362
  message: "Value must not contain literal newline characters (use \\n instead)"
363
363
  });
364
- var validRegex = noNewlines.refine(
365
- (s) => {
366
- try {
367
- new RegExp(s);
368
- return true;
369
- } catch {
370
- return false;
371
- }
372
- },
373
- { message: "Value must be a valid regular expression" }
374
- );
375
364
  var SmartConditionSchema = import_zod.z.object({
376
365
  field: import_zod.z.string().min(1, "Condition field must not be empty"),
377
- op: import_zod.z.enum(["matches", "notMatches", "contains", "notContains", "exists", "notExists"], {
378
- errorMap: () => ({
379
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists"
380
- })
381
- }),
382
- value: validRegex.optional(),
366
+ op: import_zod.z.enum(
367
+ [
368
+ "matches",
369
+ "notMatches",
370
+ "contains",
371
+ "notContains",
372
+ "exists",
373
+ "notExists",
374
+ "matchesGlob",
375
+ "notMatchesGlob"
376
+ ],
377
+ {
378
+ errorMap: () => ({
379
+ message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
380
+ })
381
+ }
382
+ ),
383
+ value: import_zod.z.string().optional(),
383
384
  flags: import_zod.z.string().optional()
384
385
  });
385
386
  var SmartRuleSchema = import_zod.z.object({
@@ -392,11 +393,6 @@ var SmartRuleSchema = import_zod.z.object({
392
393
  }),
393
394
  reason: import_zod.z.string().optional()
394
395
  });
395
- var PolicyRuleSchema = import_zod.z.object({
396
- action: import_zod.z.string().min(1),
397
- allowPaths: import_zod.z.array(import_zod.z.string()).optional(),
398
- blockPaths: import_zod.z.array(import_zod.z.string()).optional()
399
- });
400
396
  var ConfigFileSchema = import_zod.z.object({
401
397
  version: import_zod.z.string().optional(),
402
398
  settings: import_zod.z.object({
@@ -421,12 +417,15 @@ var ConfigFileSchema = import_zod.z.object({
421
417
  dangerousWords: import_zod.z.array(noNewlines).optional(),
422
418
  ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
423
419
  toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
424
- rules: import_zod.z.array(PolicyRuleSchema).optional(),
425
420
  smartRules: import_zod.z.array(SmartRuleSchema).optional(),
426
421
  snapshot: import_zod.z.object({
427
422
  tools: import_zod.z.array(import_zod.z.string()).optional(),
428
423
  onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
429
424
  ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
425
+ }).optional(),
426
+ dlp: import_zod.z.object({
427
+ enabled: import_zod.z.boolean().optional(),
428
+ scanIgnoredTools: import_zod.z.boolean().optional()
430
429
  }).optional()
431
430
  }).optional(),
432
431
  environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
@@ -448,8 +447,8 @@ function sanitizeConfig(raw) {
448
447
  }
449
448
  }
450
449
  const lines = result.error.issues.map((issue) => {
451
- const path8 = issue.path.length > 0 ? issue.path.join(".") : "root";
452
- return ` \u2022 ${path8}: ${issue.message}`;
450
+ const path9 = issue.path.length > 0 ? issue.path.join(".") : "root";
451
+ return ` \u2022 ${path9}: ${issue.message}`;
453
452
  });
454
453
  return {
455
454
  sanitized,
@@ -458,18 +457,301 @@ ${lines.join("\n")}`
458
457
  };
459
458
  }
460
459
 
460
+ // src/shields.ts
461
+ var import_fs = __toESM(require("fs"));
462
+ var import_path3 = __toESM(require("path"));
463
+ var import_os = __toESM(require("os"));
464
+ var import_crypto = __toESM(require("crypto"));
465
+ var SHIELDS = {
466
+ postgres: {
467
+ name: "postgres",
468
+ description: "Protects PostgreSQL databases from destructive AI operations",
469
+ aliases: ["pg", "postgresql"],
470
+ smartRules: [
471
+ {
472
+ name: "shield:postgres:block-drop-table",
473
+ tool: "*",
474
+ conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
475
+ verdict: "block",
476
+ reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
477
+ },
478
+ {
479
+ name: "shield:postgres:block-truncate",
480
+ tool: "*",
481
+ conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
482
+ verdict: "block",
483
+ reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
484
+ },
485
+ {
486
+ name: "shield:postgres:block-drop-column",
487
+ tool: "*",
488
+ conditions: [
489
+ { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
490
+ ],
491
+ verdict: "block",
492
+ reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
493
+ },
494
+ {
495
+ name: "shield:postgres:review-grant-revoke",
496
+ tool: "*",
497
+ conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
498
+ verdict: "review",
499
+ reason: "Permission changes require human approval (Postgres shield)"
500
+ }
501
+ ],
502
+ dangerousWords: ["dropdb", "pg_dropcluster"]
503
+ },
504
+ github: {
505
+ name: "github",
506
+ description: "Protects GitHub repositories from destructive AI operations",
507
+ aliases: ["git"],
508
+ smartRules: [
509
+ {
510
+ // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
511
+ // This rule adds coverage for `git push --delete` which the built-in does not match.
512
+ name: "shield:github:review-delete-branch-remote",
513
+ tool: "bash",
514
+ conditions: [
515
+ {
516
+ field: "command",
517
+ op: "matches",
518
+ value: "git\\s+push\\s+.*--delete",
519
+ flags: "i"
520
+ }
521
+ ],
522
+ verdict: "review",
523
+ reason: "Remote branch deletion requires human approval (GitHub shield)"
524
+ },
525
+ {
526
+ name: "shield:github:block-delete-repo",
527
+ tool: "*",
528
+ conditions: [
529
+ { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
530
+ ],
531
+ verdict: "block",
532
+ reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
533
+ }
534
+ ],
535
+ dangerousWords: []
536
+ },
537
+ aws: {
538
+ name: "aws",
539
+ description: "Protects AWS infrastructure from destructive AI operations",
540
+ aliases: ["amazon"],
541
+ smartRules: [
542
+ {
543
+ name: "shield:aws:block-delete-s3-bucket",
544
+ tool: "*",
545
+ conditions: [
546
+ {
547
+ field: "command",
548
+ op: "matches",
549
+ value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
550
+ flags: "i"
551
+ }
552
+ ],
553
+ verdict: "block",
554
+ reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
555
+ },
556
+ {
557
+ name: "shield:aws:review-iam-changes",
558
+ tool: "*",
559
+ conditions: [
560
+ {
561
+ field: "command",
562
+ op: "matches",
563
+ value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
564
+ flags: "i"
565
+ }
566
+ ],
567
+ verdict: "review",
568
+ reason: "IAM changes require human approval (AWS shield)"
569
+ },
570
+ {
571
+ name: "shield:aws:block-ec2-terminate",
572
+ tool: "*",
573
+ conditions: [
574
+ {
575
+ field: "command",
576
+ op: "matches",
577
+ value: "aws\\s+ec2\\s+terminate-instances",
578
+ flags: "i"
579
+ }
580
+ ],
581
+ verdict: "block",
582
+ reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
583
+ },
584
+ {
585
+ name: "shield:aws:review-rds-delete",
586
+ tool: "*",
587
+ conditions: [
588
+ { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
589
+ ],
590
+ verdict: "review",
591
+ reason: "RDS deletion requires human approval (AWS shield)"
592
+ }
593
+ ],
594
+ dangerousWords: []
595
+ },
596
+ filesystem: {
597
+ name: "filesystem",
598
+ description: "Protects the local filesystem from dangerous AI operations",
599
+ aliases: ["fs"],
600
+ smartRules: [
601
+ {
602
+ name: "shield:filesystem:review-chmod-777",
603
+ tool: "bash",
604
+ conditions: [
605
+ { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
606
+ ],
607
+ verdict: "review",
608
+ reason: "chmod 777 requires human approval (filesystem shield)"
609
+ },
610
+ {
611
+ name: "shield:filesystem:review-write-etc",
612
+ tool: "bash",
613
+ conditions: [
614
+ {
615
+ field: "command",
616
+ // Narrow to write-indicative operations to avoid approval fatigue on reads.
617
+ // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
618
+ op: "matches",
619
+ value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
620
+ }
621
+ ],
622
+ verdict: "review",
623
+ reason: "Writing to /etc requires human approval (filesystem shield)"
624
+ }
625
+ ],
626
+ // dd removed: too common as a legitimate tool (disk imaging, file ops).
627
+ // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
628
+ // wipefs retained: rarely legitimate in an agent context and not in built-ins.
629
+ dangerousWords: ["wipefs"]
630
+ }
631
+ };
632
+ function resolveShieldName(input) {
633
+ const lower = input.toLowerCase();
634
+ if (SHIELDS[lower]) return lower;
635
+ for (const [name, def] of Object.entries(SHIELDS)) {
636
+ if (def.aliases.includes(lower)) return name;
637
+ }
638
+ return null;
639
+ }
640
+ function getShield(name) {
641
+ const resolved = resolveShieldName(name);
642
+ return resolved ? SHIELDS[resolved] : null;
643
+ }
644
+ function listShields() {
645
+ return Object.values(SHIELDS);
646
+ }
647
+ var SHIELDS_STATE_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "shields.json");
648
+ function readActiveShields() {
649
+ try {
650
+ const raw = import_fs.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
651
+ if (!raw.trim()) return [];
652
+ const parsed = JSON.parse(raw);
653
+ if (Array.isArray(parsed.active)) {
654
+ return parsed.active.filter(
655
+ (e) => typeof e === "string" && e.length > 0 && e in SHIELDS
656
+ );
657
+ }
658
+ } catch (err) {
659
+ if (err.code !== "ENOENT") {
660
+ process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
661
+ `);
662
+ }
663
+ }
664
+ return [];
665
+ }
666
+ function writeActiveShields(active) {
667
+ import_fs.default.mkdirSync(import_path3.default.dirname(SHIELDS_STATE_FILE), { recursive: true });
668
+ const tmp = `${SHIELDS_STATE_FILE}.${import_crypto.default.randomBytes(6).toString("hex")}.tmp`;
669
+ import_fs.default.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
670
+ import_fs.default.renameSync(tmp, SHIELDS_STATE_FILE);
671
+ }
672
+
673
+ // src/dlp.ts
674
+ var DLP_PATTERNS = [
675
+ { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
676
+ { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
677
+ { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
678
+ { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
679
+ { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
680
+ {
681
+ name: "Private Key (PEM)",
682
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
683
+ severity: "block"
684
+ },
685
+ { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
686
+ ];
687
+ function maskSecret(raw, pattern) {
688
+ const match = raw.match(pattern);
689
+ if (!match) return "****";
690
+ const secret = match[0];
691
+ if (secret.length < 8) return "****";
692
+ const prefix = secret.slice(0, 4);
693
+ const suffix = secret.slice(-4);
694
+ const stars = "*".repeat(Math.min(secret.length - 8, 12));
695
+ return `${prefix}${stars}${suffix}`;
696
+ }
697
+ var MAX_DEPTH = 5;
698
+ var MAX_STRING_BYTES = 1e5;
699
+ var MAX_JSON_PARSE_BYTES = 1e4;
700
+ function scanArgs(args, depth = 0, fieldPath = "args") {
701
+ if (depth > MAX_DEPTH || args === null || args === void 0) return null;
702
+ if (Array.isArray(args)) {
703
+ for (let i = 0; i < args.length; i++) {
704
+ const match = scanArgs(args[i], depth + 1, `${fieldPath}[${i}]`);
705
+ if (match) return match;
706
+ }
707
+ return null;
708
+ }
709
+ if (typeof args === "object") {
710
+ for (const [key, value] of Object.entries(args)) {
711
+ const match = scanArgs(value, depth + 1, `${fieldPath}.${key}`);
712
+ if (match) return match;
713
+ }
714
+ return null;
715
+ }
716
+ if (typeof args === "string") {
717
+ const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
718
+ for (const pattern of DLP_PATTERNS) {
719
+ if (pattern.regex.test(text)) {
720
+ return {
721
+ patternName: pattern.name,
722
+ fieldPath,
723
+ redactedSample: maskSecret(text, pattern.regex),
724
+ severity: pattern.severity
725
+ };
726
+ }
727
+ }
728
+ if (text.length < MAX_JSON_PARSE_BYTES) {
729
+ const trimmed = text.trim();
730
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
731
+ try {
732
+ const parsed = JSON.parse(text);
733
+ const inner = scanArgs(parsed, depth + 1, fieldPath);
734
+ if (inner) return inner;
735
+ } catch {
736
+ }
737
+ }
738
+ }
739
+ }
740
+ return null;
741
+ }
742
+
461
743
  // src/core.ts
462
- var PAUSED_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "PAUSED");
463
- var TRUST_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "trust.json");
464
- var LOCAL_AUDIT_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "audit.log");
465
- var HOOK_DEBUG_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
744
+ var PAUSED_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
745
+ var TRUST_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "trust.json");
746
+ var LOCAL_AUDIT_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "audit.log");
747
+ var HOOK_DEBUG_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
466
748
  function checkPause() {
467
749
  try {
468
- if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
469
- const state = JSON.parse(import_fs.default.readFileSync(PAUSED_FILE, "utf-8"));
750
+ if (!import_fs2.default.existsSync(PAUSED_FILE)) return { paused: false };
751
+ const state = JSON.parse(import_fs2.default.readFileSync(PAUSED_FILE, "utf-8"));
470
752
  if (state.expiry > 0 && Date.now() >= state.expiry) {
471
753
  try {
472
- import_fs.default.unlinkSync(PAUSED_FILE);
754
+ import_fs2.default.unlinkSync(PAUSED_FILE);
473
755
  } catch {
474
756
  }
475
757
  return { paused: false };
@@ -480,11 +762,11 @@ function checkPause() {
480
762
  }
481
763
  }
482
764
  function atomicWriteSync(filePath, data, options) {
483
- const dir = import_path3.default.dirname(filePath);
484
- if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
485
- const tmpPath = `${filePath}.${import_os.default.hostname()}.${process.pid}.tmp`;
486
- import_fs.default.writeFileSync(tmpPath, data, options);
487
- import_fs.default.renameSync(tmpPath, filePath);
765
+ const dir = import_path4.default.dirname(filePath);
766
+ if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
767
+ const tmpPath = `${filePath}.${import_os2.default.hostname()}.${process.pid}.tmp`;
768
+ import_fs2.default.writeFileSync(tmpPath, data, options);
769
+ import_fs2.default.renameSync(tmpPath, filePath);
488
770
  }
489
771
  function pauseNode9(durationMs, durationStr) {
490
772
  const state = { expiry: Date.now() + durationMs, duration: durationStr };
@@ -492,18 +774,18 @@ function pauseNode9(durationMs, durationStr) {
492
774
  }
493
775
  function resumeNode9() {
494
776
  try {
495
- if (import_fs.default.existsSync(PAUSED_FILE)) import_fs.default.unlinkSync(PAUSED_FILE);
777
+ if (import_fs2.default.existsSync(PAUSED_FILE)) import_fs2.default.unlinkSync(PAUSED_FILE);
496
778
  } catch {
497
779
  }
498
780
  }
499
781
  function getActiveTrustSession(toolName) {
500
782
  try {
501
- if (!import_fs.default.existsSync(TRUST_FILE)) return false;
502
- const trust = JSON.parse(import_fs.default.readFileSync(TRUST_FILE, "utf-8"));
783
+ if (!import_fs2.default.existsSync(TRUST_FILE)) return false;
784
+ const trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
503
785
  const now = Date.now();
504
786
  const active = trust.entries.filter((e) => e.expiry > now);
505
787
  if (active.length !== trust.entries.length) {
506
- import_fs.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
788
+ import_fs2.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
507
789
  }
508
790
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
509
791
  } catch {
@@ -514,8 +796,8 @@ function writeTrustSession(toolName, durationMs) {
514
796
  try {
515
797
  let trust = { entries: [] };
516
798
  try {
517
- if (import_fs.default.existsSync(TRUST_FILE)) {
518
- trust = JSON.parse(import_fs.default.readFileSync(TRUST_FILE, "utf-8"));
799
+ if (import_fs2.default.existsSync(TRUST_FILE)) {
800
+ trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
519
801
  }
520
802
  } catch {
521
803
  }
@@ -531,9 +813,9 @@ function writeTrustSession(toolName, durationMs) {
531
813
  }
532
814
  function appendToLog(logPath, entry) {
533
815
  try {
534
- const dir = import_path3.default.dirname(logPath);
535
- if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
536
- import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
816
+ const dir = import_path4.default.dirname(logPath);
817
+ if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
818
+ import_fs2.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
537
819
  } catch {
538
820
  }
539
821
  }
@@ -545,7 +827,7 @@ function appendHookDebug(toolName, args, meta) {
545
827
  args: safeArgs,
546
828
  agent: meta?.agent,
547
829
  mcpServer: meta?.mcpServer,
548
- hostname: import_os.default.hostname(),
830
+ hostname: import_os2.default.hostname(),
549
831
  cwd: process.cwd()
550
832
  });
551
833
  }
@@ -559,7 +841,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
559
841
  checkedBy,
560
842
  agent: meta?.agent,
561
843
  mcpServer: meta?.mcpServer,
562
- hostname: import_os.default.hostname()
844
+ hostname: import_os2.default.hostname()
563
845
  });
564
846
  }
565
847
  function tokenize(toolName) {
@@ -575,9 +857,9 @@ function matchesPattern(text, patterns) {
575
857
  const withoutDotSlash = text.replace(/^\.\//, "");
576
858
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
577
859
  }
578
- function getNestedValue(obj, path8) {
860
+ function getNestedValue(obj, path9) {
579
861
  if (!obj || typeof obj !== "object") return null;
580
- return path8.split(".").reduce((prev, curr) => prev?.[curr], obj);
862
+ return path9.split(".").reduce((prev, curr) => prev?.[curr], obj);
581
863
  }
582
864
  function shouldSnapshot(toolName, args, config) {
583
865
  if (!config.settings.enableUndo) return false;
@@ -622,6 +904,10 @@ function evaluateSmartConditions(args, rule) {
622
904
  return true;
623
905
  }
624
906
  }
907
+ case "matchesGlob":
908
+ return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
909
+ case "notMatchesGlob":
910
+ return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : true;
625
911
  default:
626
912
  return false;
627
913
  }
@@ -785,25 +1071,27 @@ var DEFAULT_CONFIG = {
785
1071
  onlyPaths: [],
786
1072
  ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
787
1073
  },
788
- rules: [
789
- // Only use the legacy rules format for simple path-based rm control.
790
- // All other command-level enforcement lives in smartRules below.
791
- {
792
- action: "rm",
793
- allowPaths: [
794
- "**/node_modules/**",
795
- "dist/**",
796
- "build/**",
797
- ".next/**",
798
- "coverage/**",
799
- ".cache/**",
800
- "tmp/**",
801
- "temp/**",
802
- ".DS_Store"
803
- ]
804
- }
805
- ],
806
1074
  smartRules: [
1075
+ // ── rm safety (critical — always evaluated first) ──────────────────────
1076
+ {
1077
+ name: "block-rm-rf-home",
1078
+ tool: "bash",
1079
+ conditionMode: "all",
1080
+ conditions: [
1081
+ {
1082
+ field: "command",
1083
+ op: "matches",
1084
+ value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
1085
+ },
1086
+ {
1087
+ field: "command",
1088
+ op: "matches",
1089
+ value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
1090
+ }
1091
+ ],
1092
+ verdict: "block",
1093
+ reason: "Recursive delete of home directory is irreversible"
1094
+ },
807
1095
  // ── SQL safety ────────────────────────────────────────────────────────
808
1096
  {
809
1097
  name: "no-delete-without-where",
@@ -894,19 +1182,45 @@ var DEFAULT_CONFIG = {
894
1182
  verdict: "block",
895
1183
  reason: "Piping remote script into a shell is a supply-chain attack vector"
896
1184
  }
897
- ]
1185
+ ],
1186
+ dlp: { enabled: true, scanIgnoredTools: true }
898
1187
  },
899
1188
  environments: {}
900
1189
  };
1190
+ var ADVISORY_SMART_RULES = [
1191
+ {
1192
+ name: "allow-rm-safe-paths",
1193
+ tool: "*",
1194
+ conditionMode: "all",
1195
+ conditions: [
1196
+ { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
1197
+ {
1198
+ field: "command",
1199
+ op: "matches",
1200
+ // Matches known-safe build artifact paths in the command.
1201
+ value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
1202
+ }
1203
+ ],
1204
+ verdict: "allow",
1205
+ reason: "Deleting a known-safe build artifact path"
1206
+ },
1207
+ {
1208
+ name: "review-rm",
1209
+ tool: "*",
1210
+ conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
1211
+ verdict: "review",
1212
+ reason: "rm can permanently delete files \u2014 confirm the target path"
1213
+ }
1214
+ ];
901
1215
  var cachedConfig = null;
902
1216
  function _resetConfigCache() {
903
1217
  cachedConfig = null;
904
1218
  }
905
1219
  function getGlobalSettings() {
906
1220
  try {
907
- const globalConfigPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
908
- if (import_fs.default.existsSync(globalConfigPath)) {
909
- const parsed = JSON.parse(import_fs.default.readFileSync(globalConfigPath, "utf-8"));
1221
+ const globalConfigPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
1222
+ if (import_fs2.default.existsSync(globalConfigPath)) {
1223
+ const parsed = JSON.parse(import_fs2.default.readFileSync(globalConfigPath, "utf-8"));
910
1224
  const settings = parsed.settings || {};
911
1225
  return {
912
1226
  mode: settings.mode || "standard",
@@ -928,9 +1242,9 @@ function getGlobalSettings() {
928
1242
  }
929
1243
  function getInternalToken() {
930
1244
  try {
931
- const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
932
- if (!import_fs.default.existsSync(pidFile)) return null;
933
- const data = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
1245
+ const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1246
+ if (!import_fs2.default.existsSync(pidFile)) return null;
1247
+ const data = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
934
1248
  process.kill(data.pid, 0);
935
1249
  return data.internalToken ?? null;
936
1250
  } catch {
@@ -945,7 +1259,8 @@ async function evaluatePolicy(toolName, args, agent) {
945
1259
  (rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
946
1260
  );
947
1261
  if (matchedRule) {
948
- if (matchedRule.verdict === "allow") return { decision: "allow" };
1262
+ if (matchedRule.verdict === "allow")
1263
+ return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
949
1264
  return {
950
1265
  decision: matchedRule.verdict,
951
1266
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
@@ -956,13 +1271,11 @@ async function evaluatePolicy(toolName, args, agent) {
956
1271
  }
957
1272
  }
958
1273
  let allTokens = [];
959
- let actionTokens = [];
960
1274
  let pathTokens = [];
961
1275
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
962
1276
  if (shellCommand) {
963
1277
  const analyzed = await analyzeShellCommand(shellCommand);
964
1278
  allTokens = analyzed.allTokens;
965
- actionTokens = analyzed.actions;
966
1279
  pathTokens = analyzed.paths;
967
1280
  const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
968
1281
  if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
@@ -970,11 +1283,9 @@ async function evaluatePolicy(toolName, args, agent) {
970
1283
  }
971
1284
  if (isSqlTool(toolName, config.policy.toolInspection)) {
972
1285
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
973
- actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
974
1286
  }
975
1287
  } else {
976
1288
  allTokens = tokenize(toolName);
977
- actionTokens = [toolName];
978
1289
  if (args && typeof args === "object") {
979
1290
  const flattenedArgs = JSON.stringify(args).toLowerCase();
980
1291
  const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
@@ -997,29 +1308,6 @@ async function evaluatePolicy(toolName, args, agent) {
997
1308
  const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
998
1309
  if (allInSandbox) return { decision: "allow" };
999
1310
  }
1000
- for (const action of actionTokens) {
1001
- const rule = config.policy.rules.find(
1002
- (r) => r.action === action || matchesPattern(action, r.action)
1003
- );
1004
- if (rule) {
1005
- if (pathTokens.length > 0) {
1006
- const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
1007
- if (anyBlocked)
1008
- return {
1009
- decision: "review",
1010
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
1011
- tier: 5
1012
- };
1013
- const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
1014
- if (allAllowed) return { decision: "allow" };
1015
- }
1016
- return {
1017
- decision: "review",
1018
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
1019
- tier: 5
1020
- };
1021
- }
1022
- }
1023
1311
  let matchedDangerousWord;
1024
1312
  const isDangerous = allTokens.some(
1025
1313
  (token) => config.policy.dangerousWords.some((word) => {
@@ -1071,9 +1359,9 @@ async function evaluatePolicy(toolName, args, agent) {
1071
1359
  }
1072
1360
  async function explainPolicy(toolName, args) {
1073
1361
  const steps = [];
1074
- const globalPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
1075
- const projectPath = import_path3.default.join(process.cwd(), "node9.config.json");
1076
- const credsPath = import_path3.default.join(import_os.default.homedir(), ".node9", "credentials.json");
1362
+ const globalPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
1363
+ const projectPath = import_path4.default.join(process.cwd(), "node9.config.json");
1364
+ const credsPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
1077
1365
  const waterfall = [
1078
1366
  {
1079
1367
  tier: 1,
@@ -1084,19 +1372,19 @@ async function explainPolicy(toolName, args) {
1084
1372
  {
1085
1373
  tier: 2,
1086
1374
  label: "Cloud policy",
1087
- status: import_fs.default.existsSync(credsPath) ? "active" : "missing",
1088
- note: import_fs.default.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
1375
+ status: import_fs2.default.existsSync(credsPath) ? "active" : "missing",
1376
+ note: import_fs2.default.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
1089
1377
  },
1090
1378
  {
1091
1379
  tier: 3,
1092
1380
  label: "Project config",
1093
- status: import_fs.default.existsSync(projectPath) ? "active" : "missing",
1381
+ status: import_fs2.default.existsSync(projectPath) ? "active" : "missing",
1094
1382
  path: projectPath
1095
1383
  },
1096
1384
  {
1097
1385
  tier: 4,
1098
1386
  label: "Global config",
1099
- status: import_fs.default.existsSync(globalPath) ? "active" : "missing",
1387
+ status: import_fs2.default.existsSync(globalPath) ? "active" : "missing",
1100
1388
  path: globalPath
1101
1389
  },
1102
1390
  {
@@ -1107,7 +1395,28 @@ async function explainPolicy(toolName, args) {
1107
1395
  }
1108
1396
  ];
1109
1397
  const config = getConfig();
1110
- if (matchesPattern(toolName, config.policy.ignoredTools)) {
1398
+ const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
1399
+ if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
1400
+ const dlpMatch = args !== void 0 ? scanArgs(args) : null;
1401
+ if (dlpMatch) {
1402
+ steps.push({
1403
+ name: "DLP Content Scanner",
1404
+ outcome: dlpMatch.severity === "block" ? "block" : "review",
1405
+ detail: `\u{1F6A8} ${dlpMatch.patternName} detected in ${dlpMatch.fieldPath} \u2014 sample: ${dlpMatch.redactedSample}`,
1406
+ isFinal: dlpMatch.severity === "block"
1407
+ });
1408
+ if (dlpMatch.severity === "block") {
1409
+ return { tool: toolName, args, waterfall, steps, decision: "block" };
1410
+ }
1411
+ } else {
1412
+ steps.push({
1413
+ name: "DLP Content Scanner",
1414
+ outcome: "checked",
1415
+ detail: "No sensitive credentials detected in args"
1416
+ });
1417
+ }
1418
+ }
1419
+ if (wouldBeIgnored) {
1111
1420
  steps.push({
1112
1421
  name: "Ignored tools",
1113
1422
  outcome: "allow",
@@ -1160,13 +1469,11 @@ async function explainPolicy(toolName, args) {
1160
1469
  steps.push({ name: "Smart rules", outcome: "skip", detail: "No smart rules configured" });
1161
1470
  }
1162
1471
  let allTokens = [];
1163
- let actionTokens = [];
1164
1472
  let pathTokens = [];
1165
1473
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
1166
1474
  if (shellCommand) {
1167
1475
  const analyzed = await analyzeShellCommand(shellCommand);
1168
1476
  allTokens = analyzed.allTokens;
1169
- actionTokens = analyzed.actions;
1170
1477
  pathTokens = analyzed.paths;
1171
1478
  const patterns = Object.keys(config.policy.toolInspection);
1172
1479
  const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
@@ -1200,7 +1507,6 @@ async function explainPolicy(toolName, args) {
1200
1507
  });
1201
1508
  if (isSqlTool(toolName, config.policy.toolInspection)) {
1202
1509
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
1203
- actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
1204
1510
  steps.push({
1205
1511
  name: "SQL token stripping",
1206
1512
  outcome: "checked",
@@ -1209,7 +1515,6 @@ async function explainPolicy(toolName, args) {
1209
1515
  }
1210
1516
  } else {
1211
1517
  allTokens = tokenize(toolName);
1212
- actionTokens = [toolName];
1213
1518
  let detail = `No toolInspection match for "${toolName}" \u2014 tokens: [${allTokens.join(", ")}]`;
1214
1519
  if (args && typeof args === "object") {
1215
1520
  const flattenedArgs = JSON.stringify(args).toLowerCase();
@@ -1250,65 +1555,6 @@ async function explainPolicy(toolName, args) {
1250
1555
  detail: pathTokens.length === 0 ? "No path tokens found in input" : "No sandbox paths configured"
1251
1556
  });
1252
1557
  }
1253
- let ruleMatched = false;
1254
- for (const action of actionTokens) {
1255
- const rule = config.policy.rules.find(
1256
- (r) => r.action === action || matchesPattern(action, r.action)
1257
- );
1258
- if (rule) {
1259
- ruleMatched = true;
1260
- if (pathTokens.length > 0) {
1261
- const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
1262
- if (anyBlocked) {
1263
- steps.push({
1264
- name: "Policy rules",
1265
- outcome: "review",
1266
- detail: `Rule "${rule.action}" matched + path is in blockPaths`,
1267
- isFinal: true
1268
- });
1269
- return {
1270
- tool: toolName,
1271
- args,
1272
- waterfall,
1273
- steps,
1274
- decision: "review",
1275
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
1276
- };
1277
- }
1278
- const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
1279
- if (allAllowed) {
1280
- steps.push({
1281
- name: "Policy rules",
1282
- outcome: "allow",
1283
- detail: `Rule "${rule.action}" matched + all paths are in allowPaths`,
1284
- isFinal: true
1285
- });
1286
- return { tool: toolName, args, waterfall, steps, decision: "allow" };
1287
- }
1288
- }
1289
- steps.push({
1290
- name: "Policy rules",
1291
- outcome: "review",
1292
- detail: `Rule "${rule.action}" matched \u2014 default block (no path exception)`,
1293
- isFinal: true
1294
- });
1295
- return {
1296
- tool: toolName,
1297
- args,
1298
- waterfall,
1299
- steps,
1300
- decision: "review",
1301
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
1302
- };
1303
- }
1304
- }
1305
- if (!ruleMatched) {
1306
- steps.push({
1307
- name: "Policy rules",
1308
- outcome: "skip",
1309
- detail: config.policy.rules.length === 0 ? "No rules configured" : `No rule matched [${actionTokens.join(", ")}]`
1310
- });
1311
- }
1312
1558
  let matchedDangerousWord;
1313
1559
  const isDangerous = uniqueTokens.some(
1314
1560
  (token) => config.policy.dangerousWords.some((word) => {
@@ -1377,9 +1623,9 @@ var DAEMON_PORT = 7391;
1377
1623
  var DAEMON_HOST = "127.0.0.1";
1378
1624
  function isDaemonRunning() {
1379
1625
  try {
1380
- const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
1381
- if (!import_fs.default.existsSync(pidFile)) return false;
1382
- const { pid, port } = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
1626
+ const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1627
+ if (!import_fs2.default.existsSync(pidFile)) return false;
1628
+ const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1383
1629
  if (port !== DAEMON_PORT) return false;
1384
1630
  process.kill(pid, 0);
1385
1631
  return true;
@@ -1389,9 +1635,9 @@ function isDaemonRunning() {
1389
1635
  }
1390
1636
  function getPersistentDecision(toolName) {
1391
1637
  try {
1392
- const file = import_path3.default.join(import_os.default.homedir(), ".node9", "decisions.json");
1393
- if (!import_fs.default.existsSync(file)) return null;
1394
- const decisions = JSON.parse(import_fs.default.readFileSync(file, "utf-8"));
1638
+ const file = import_path4.default.join(import_os2.default.homedir(), ".node9", "decisions.json");
1639
+ if (!import_fs2.default.existsSync(file)) return null;
1640
+ const decisions = JSON.parse(import_fs2.default.readFileSync(file, "utf-8"));
1395
1641
  const d = decisions[toolName];
1396
1642
  if (d === "allow" || d === "deny") return d;
1397
1643
  } catch {
@@ -1490,6 +1736,22 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1490
1736
  let policyMatchedField;
1491
1737
  let policyMatchedWord;
1492
1738
  let riskMetadata;
1739
+ if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
1740
+ const dlpMatch = scanArgs(args);
1741
+ if (dlpMatch) {
1742
+ const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
1743
+ if (dlpMatch.severity === "block") {
1744
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
1745
+ return {
1746
+ approved: false,
1747
+ reason: dlpReason,
1748
+ blockedBy: "local-config",
1749
+ blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
1750
+ };
1751
+ }
1752
+ explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
1753
+ }
1754
+ }
1493
1755
  if (config.settings.mode === "audit") {
1494
1756
  if (!isIgnoredTool(toolName)) {
1495
1757
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
@@ -1729,7 +1991,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1729
1991
  racePromises.push(
1730
1992
  (async () => {
1731
1993
  try {
1732
- console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
1994
+ if (explainableLabel.includes("DLP")) {
1995
+ console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
1996
+ console.log(
1997
+ import_chalk2.default.red.bold(` A sensitive secret was detected in the tool arguments!`)
1998
+ );
1999
+ } else {
2000
+ console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
2001
+ }
1733
2002
  console.log(`${import_chalk2.default.bold("Action:")} ${import_chalk2.default.red(toolName)}`);
1734
2003
  console.log(`${import_chalk2.default.bold("Flagged By:")} ${import_chalk2.default.yellow(explainableLabel)}`);
1735
2004
  if (isRemoteLocked) {
@@ -1834,8 +2103,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1834
2103
  }
1835
2104
  function getConfig() {
1836
2105
  if (cachedConfig) return cachedConfig;
1837
- const globalPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
1838
- const projectPath = import_path3.default.join(process.cwd(), "node9.config.json");
2106
+ const globalPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
2107
+ const projectPath = import_path4.default.join(process.cwd(), "node9.config.json");
1839
2108
  const globalConfig = tryLoadConfig(globalPath);
1840
2109
  const projectConfig = tryLoadConfig(projectPath);
1841
2110
  const mergedSettings = {
@@ -1847,13 +2116,13 @@ function getConfig() {
1847
2116
  dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
1848
2117
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
1849
2118
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
1850
- rules: [...DEFAULT_CONFIG.policy.rules],
1851
2119
  smartRules: [...DEFAULT_CONFIG.policy.smartRules],
1852
2120
  snapshot: {
1853
2121
  tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
1854
2122
  onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
1855
2123
  ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
1856
- }
2124
+ },
2125
+ dlp: { ...DEFAULT_CONFIG.policy.dlp }
1857
2126
  };
1858
2127
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
1859
2128
  const applyLayer = (source) => {
@@ -1873,7 +2142,6 @@ function getConfig() {
1873
2142
  if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
1874
2143
  if (p.toolInspection)
1875
2144
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
1876
- if (p.rules) mergedPolicy.rules.push(...p.rules);
1877
2145
  if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
1878
2146
  if (p.snapshot) {
1879
2147
  const s2 = p.snapshot;
@@ -1881,6 +2149,11 @@ function getConfig() {
1881
2149
  if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
1882
2150
  if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
1883
2151
  }
2152
+ if (p.dlp) {
2153
+ const d = p.dlp;
2154
+ if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
2155
+ if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
2156
+ }
1884
2157
  const envs = source.environments || {};
1885
2158
  for (const [envName, envConfig] of Object.entries(envs)) {
1886
2159
  if (envConfig && typeof envConfig === "object") {
@@ -1895,6 +2168,19 @@ function getConfig() {
1895
2168
  };
1896
2169
  applyLayer(globalConfig);
1897
2170
  applyLayer(projectConfig);
2171
+ for (const shieldName of readActiveShields()) {
2172
+ const shield = getShield(shieldName);
2173
+ if (!shield) continue;
2174
+ const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
2175
+ for (const rule of shield.smartRules) {
2176
+ if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
2177
+ }
2178
+ for (const word of shield.dangerousWords) mergedPolicy.dangerousWords.push(word);
2179
+ }
2180
+ const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
2181
+ for (const rule of ADVISORY_SMART_RULES) {
2182
+ if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
2183
+ }
1898
2184
  if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
1899
2185
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
1900
2186
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
@@ -1910,10 +2196,10 @@ function getConfig() {
1910
2196
  return cachedConfig;
1911
2197
  }
1912
2198
  function tryLoadConfig(filePath) {
1913
- if (!import_fs.default.existsSync(filePath)) return null;
2199
+ if (!import_fs2.default.existsSync(filePath)) return null;
1914
2200
  let raw;
1915
2201
  try {
1916
- raw = JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
2202
+ raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
1917
2203
  } catch (err) {
1918
2204
  const msg = err instanceof Error ? err.message : String(err);
1919
2205
  process.stderr.write(
@@ -1975,9 +2261,9 @@ function getCredentials() {
1975
2261
  };
1976
2262
  }
1977
2263
  try {
1978
- const credPath = import_path3.default.join(import_os.default.homedir(), ".node9", "credentials.json");
1979
- if (import_fs.default.existsSync(credPath)) {
1980
- const creds = JSON.parse(import_fs.default.readFileSync(credPath, "utf-8"));
2264
+ const credPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
2265
+ if (import_fs2.default.existsSync(credPath)) {
2266
+ const creds = JSON.parse(import_fs2.default.readFileSync(credPath, "utf-8"));
1981
2267
  const profileName = process.env.NODE9_PROFILE || "default";
1982
2268
  const profile = creds[profileName];
1983
2269
  if (profile?.apiKey) {
@@ -2008,9 +2294,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
2008
2294
  context: {
2009
2295
  agent: meta?.agent,
2010
2296
  mcpServer: meta?.mcpServer,
2011
- hostname: import_os.default.hostname(),
2297
+ hostname: import_os2.default.hostname(),
2012
2298
  cwd: process.cwd(),
2013
- platform: import_os.default.platform()
2299
+ platform: import_os2.default.platform()
2014
2300
  }
2015
2301
  }),
2016
2302
  signal: AbortSignal.timeout(5e3)
@@ -2031,9 +2317,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
2031
2317
  context: {
2032
2318
  agent: meta?.agent,
2033
2319
  mcpServer: meta?.mcpServer,
2034
- hostname: import_os.default.hostname(),
2320
+ hostname: import_os2.default.hostname(),
2035
2321
  cwd: process.cwd(),
2036
- platform: import_os.default.platform()
2322
+ platform: import_os2.default.platform()
2037
2323
  },
2038
2324
  ...riskMetadata && { riskMetadata }
2039
2325
  }),
@@ -2092,9 +2378,9 @@ async function resolveNode9SaaS(requestId, creds, approved) {
2092
2378
  }
2093
2379
 
2094
2380
  // src/setup.ts
2095
- var import_fs2 = __toESM(require("fs"));
2096
- var import_path4 = __toESM(require("path"));
2097
- var import_os2 = __toESM(require("os"));
2381
+ var import_fs3 = __toESM(require("fs"));
2382
+ var import_path5 = __toESM(require("path"));
2383
+ var import_os3 = __toESM(require("os"));
2098
2384
  var import_chalk3 = __toESM(require("chalk"));
2099
2385
  var import_prompts2 = require("@inquirer/prompts");
2100
2386
  function printDaemonTip() {
@@ -2110,22 +2396,22 @@ function fullPathCommand(subcommand) {
2110
2396
  }
2111
2397
  function readJson(filePath) {
2112
2398
  try {
2113
- if (import_fs2.default.existsSync(filePath)) {
2114
- return JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
2399
+ if (import_fs3.default.existsSync(filePath)) {
2400
+ return JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
2115
2401
  }
2116
2402
  } catch {
2117
2403
  }
2118
2404
  return null;
2119
2405
  }
2120
2406
  function writeJson(filePath, data) {
2121
- const dir = import_path4.default.dirname(filePath);
2122
- if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
2123
- import_fs2.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
2407
+ const dir = import_path5.default.dirname(filePath);
2408
+ if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
2409
+ import_fs3.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
2124
2410
  }
2125
2411
  async function setupClaude() {
2126
- const homeDir2 = import_os2.default.homedir();
2127
- const mcpPath = import_path4.default.join(homeDir2, ".claude.json");
2128
- const hooksPath = import_path4.default.join(homeDir2, ".claude", "settings.json");
2412
+ const homeDir2 = import_os3.default.homedir();
2413
+ const mcpPath = import_path5.default.join(homeDir2, ".claude.json");
2414
+ const hooksPath = import_path5.default.join(homeDir2, ".claude", "settings.json");
2129
2415
  const claudeConfig = readJson(mcpPath) ?? {};
2130
2416
  const settings = readJson(hooksPath) ?? {};
2131
2417
  const servers = claudeConfig.mcpServers ?? {};
@@ -2199,8 +2485,8 @@ async function setupClaude() {
2199
2485
  }
2200
2486
  }
2201
2487
  async function setupGemini() {
2202
- const homeDir2 = import_os2.default.homedir();
2203
- const settingsPath = import_path4.default.join(homeDir2, ".gemini", "settings.json");
2488
+ const homeDir2 = import_os3.default.homedir();
2489
+ const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
2204
2490
  const settings = readJson(settingsPath) ?? {};
2205
2491
  const servers = settings.mcpServers ?? {};
2206
2492
  let anythingChanged = false;
@@ -2282,36 +2568,11 @@ async function setupGemini() {
2282
2568
  }
2283
2569
  }
2284
2570
  async function setupCursor() {
2285
- const homeDir2 = import_os2.default.homedir();
2286
- const mcpPath = import_path4.default.join(homeDir2, ".cursor", "mcp.json");
2287
- const hooksPath = import_path4.default.join(homeDir2, ".cursor", "hooks.json");
2571
+ const homeDir2 = import_os3.default.homedir();
2572
+ const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
2288
2573
  const mcpConfig = readJson(mcpPath) ?? {};
2289
- const hooksFile = readJson(hooksPath) ?? { version: 1 };
2290
2574
  const servers = mcpConfig.mcpServers ?? {};
2291
2575
  let anythingChanged = false;
2292
- if (!hooksFile.hooks) hooksFile.hooks = {};
2293
- const hasPreHook = hooksFile.hooks.preToolUse?.some(
2294
- (h) => h.command === "node9" && h.args?.includes("check") || h.command?.includes("cli.js")
2295
- );
2296
- if (!hasPreHook) {
2297
- if (!hooksFile.hooks.preToolUse) hooksFile.hooks.preToolUse = [];
2298
- hooksFile.hooks.preToolUse.push({ command: fullPathCommand("check") });
2299
- console.log(import_chalk3.default.green(" \u2705 preToolUse hook added \u2192 node9 check"));
2300
- anythingChanged = true;
2301
- }
2302
- const hasPostHook = hooksFile.hooks.postToolUse?.some(
2303
- (h) => h.command === "node9" && h.args?.includes("log") || h.command?.includes("cli.js")
2304
- );
2305
- if (!hasPostHook) {
2306
- if (!hooksFile.hooks.postToolUse) hooksFile.hooks.postToolUse = [];
2307
- hooksFile.hooks.postToolUse.push({ command: fullPathCommand("log") });
2308
- console.log(import_chalk3.default.green(" \u2705 postToolUse hook added \u2192 node9 log"));
2309
- anythingChanged = true;
2310
- }
2311
- if (anythingChanged) {
2312
- writeJson(hooksPath, hooksFile);
2313
- console.log("");
2314
- }
2315
2576
  const serversToWrap = [];
2316
2577
  for (const [name, server] of Object.entries(servers)) {
2317
2578
  if (!server.command || server.command === "node9") continue;
@@ -2340,13 +2601,23 @@ async function setupCursor() {
2340
2601
  }
2341
2602
  console.log("");
2342
2603
  }
2604
+ console.log(
2605
+ import_chalk3.default.yellow(
2606
+ " \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
2607
+ )
2608
+ );
2609
+ console.log("");
2343
2610
  if (!anythingChanged && serversToWrap.length === 0) {
2344
- console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Cursor."));
2611
+ console.log(
2612
+ import_chalk3.default.blue(
2613
+ "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
2614
+ )
2615
+ );
2345
2616
  printDaemonTip();
2346
2617
  return;
2347
2618
  }
2348
2619
  if (anythingChanged) {
2349
- console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor!"));
2620
+ console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
2350
2621
  console.log(import_chalk3.default.gray(" Restart Cursor for changes to take effect."));
2351
2622
  printDaemonTip();
2352
2623
  }
@@ -3396,34 +3667,34 @@ var UI_HTML_TEMPLATE = ui_default;
3396
3667
 
3397
3668
  // src/daemon/index.ts
3398
3669
  var import_http = __toESM(require("http"));
3399
- var import_fs3 = __toESM(require("fs"));
3400
- var import_path5 = __toESM(require("path"));
3401
- var import_os3 = __toESM(require("os"));
3670
+ var import_fs4 = __toESM(require("fs"));
3671
+ var import_path6 = __toESM(require("path"));
3672
+ var import_os4 = __toESM(require("os"));
3402
3673
  var import_child_process2 = require("child_process");
3403
- var import_crypto = require("crypto");
3674
+ var import_crypto2 = require("crypto");
3404
3675
  var import_chalk4 = __toESM(require("chalk"));
3405
3676
  var DAEMON_PORT2 = 7391;
3406
3677
  var DAEMON_HOST2 = "127.0.0.1";
3407
- var homeDir = import_os3.default.homedir();
3408
- var DAEMON_PID_FILE = import_path5.default.join(homeDir, ".node9", "daemon.pid");
3409
- var DECISIONS_FILE = import_path5.default.join(homeDir, ".node9", "decisions.json");
3410
- var GLOBAL_CONFIG_FILE = import_path5.default.join(homeDir, ".node9", "config.json");
3411
- var CREDENTIALS_FILE = import_path5.default.join(homeDir, ".node9", "credentials.json");
3412
- var AUDIT_LOG_FILE = import_path5.default.join(homeDir, ".node9", "audit.log");
3413
- var TRUST_FILE2 = import_path5.default.join(homeDir, ".node9", "trust.json");
3678
+ var homeDir = import_os4.default.homedir();
3679
+ var DAEMON_PID_FILE = import_path6.default.join(homeDir, ".node9", "daemon.pid");
3680
+ var DECISIONS_FILE = import_path6.default.join(homeDir, ".node9", "decisions.json");
3681
+ var GLOBAL_CONFIG_FILE = import_path6.default.join(homeDir, ".node9", "config.json");
3682
+ var CREDENTIALS_FILE = import_path6.default.join(homeDir, ".node9", "credentials.json");
3683
+ var AUDIT_LOG_FILE = import_path6.default.join(homeDir, ".node9", "audit.log");
3684
+ var TRUST_FILE2 = import_path6.default.join(homeDir, ".node9", "trust.json");
3414
3685
  function atomicWriteSync2(filePath, data, options) {
3415
- const dir = import_path5.default.dirname(filePath);
3416
- if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
3417
- const tmpPath = `${filePath}.${(0, import_crypto.randomUUID)()}.tmp`;
3418
- import_fs3.default.writeFileSync(tmpPath, data, options);
3419
- import_fs3.default.renameSync(tmpPath, filePath);
3686
+ const dir = import_path6.default.dirname(filePath);
3687
+ if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
3688
+ const tmpPath = `${filePath}.${(0, import_crypto2.randomUUID)()}.tmp`;
3689
+ import_fs4.default.writeFileSync(tmpPath, data, options);
3690
+ import_fs4.default.renameSync(tmpPath, filePath);
3420
3691
  }
3421
3692
  function writeTrustEntry(toolName, durationMs) {
3422
3693
  try {
3423
3694
  let trust = { entries: [] };
3424
3695
  try {
3425
- if (import_fs3.default.existsSync(TRUST_FILE2))
3426
- trust = JSON.parse(import_fs3.default.readFileSync(TRUST_FILE2, "utf-8"));
3696
+ if (import_fs4.default.existsSync(TRUST_FILE2))
3697
+ trust = JSON.parse(import_fs4.default.readFileSync(TRUST_FILE2, "utf-8"));
3427
3698
  } catch {
3428
3699
  }
3429
3700
  trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
@@ -3456,16 +3727,16 @@ function appendAuditLog(data) {
3456
3727
  decision: data.decision,
3457
3728
  source: "daemon"
3458
3729
  };
3459
- const dir = import_path5.default.dirname(AUDIT_LOG_FILE);
3460
- if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
3461
- import_fs3.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
3730
+ const dir = import_path6.default.dirname(AUDIT_LOG_FILE);
3731
+ if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
3732
+ import_fs4.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
3462
3733
  } catch {
3463
3734
  }
3464
3735
  }
3465
3736
  function getAuditHistory(limit = 20) {
3466
3737
  try {
3467
- if (!import_fs3.default.existsSync(AUDIT_LOG_FILE)) return [];
3468
- const lines = import_fs3.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
3738
+ if (!import_fs4.default.existsSync(AUDIT_LOG_FILE)) return [];
3739
+ const lines = import_fs4.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
3469
3740
  if (lines.length === 1 && lines[0] === "") return [];
3470
3741
  return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
3471
3742
  } catch {
@@ -3475,7 +3746,7 @@ function getAuditHistory(limit = 20) {
3475
3746
  var AUTO_DENY_MS = 12e4;
3476
3747
  function getOrgName() {
3477
3748
  try {
3478
- if (import_fs3.default.existsSync(CREDENTIALS_FILE)) {
3749
+ if (import_fs4.default.existsSync(CREDENTIALS_FILE)) {
3479
3750
  return "Node9 Cloud";
3480
3751
  }
3481
3752
  } catch {
@@ -3484,13 +3755,13 @@ function getOrgName() {
3484
3755
  }
3485
3756
  var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
3486
3757
  function hasStoredSlackKey() {
3487
- return import_fs3.default.existsSync(CREDENTIALS_FILE);
3758
+ return import_fs4.default.existsSync(CREDENTIALS_FILE);
3488
3759
  }
3489
3760
  function writeGlobalSetting(key, value) {
3490
3761
  let config = {};
3491
3762
  try {
3492
- if (import_fs3.default.existsSync(GLOBAL_CONFIG_FILE)) {
3493
- config = JSON.parse(import_fs3.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
3763
+ if (import_fs4.default.existsSync(GLOBAL_CONFIG_FILE)) {
3764
+ config = JSON.parse(import_fs4.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
3494
3765
  }
3495
3766
  } catch {
3496
3767
  }
@@ -3514,7 +3785,7 @@ function abandonPending() {
3514
3785
  });
3515
3786
  if (autoStarted) {
3516
3787
  try {
3517
- import_fs3.default.unlinkSync(DAEMON_PID_FILE);
3788
+ import_fs4.default.unlinkSync(DAEMON_PID_FILE);
3518
3789
  } catch {
3519
3790
  }
3520
3791
  setTimeout(() => {
@@ -3552,8 +3823,8 @@ function readBody(req) {
3552
3823
  }
3553
3824
  function readPersistentDecisions() {
3554
3825
  try {
3555
- if (import_fs3.default.existsSync(DECISIONS_FILE)) {
3556
- return JSON.parse(import_fs3.default.readFileSync(DECISIONS_FILE, "utf-8"));
3826
+ if (import_fs4.default.existsSync(DECISIONS_FILE)) {
3827
+ return JSON.parse(import_fs4.default.readFileSync(DECISIONS_FILE, "utf-8"));
3557
3828
  }
3558
3829
  } catch {
3559
3830
  }
@@ -3569,8 +3840,8 @@ function writePersistentDecision(toolName, decision) {
3569
3840
  }
3570
3841
  }
3571
3842
  function startDaemon() {
3572
- const csrfToken = (0, import_crypto.randomUUID)();
3573
- const internalToken = (0, import_crypto.randomUUID)();
3843
+ const csrfToken = (0, import_crypto2.randomUUID)();
3844
+ const internalToken = (0, import_crypto2.randomUUID)();
3574
3845
  const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
3575
3846
  const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
3576
3847
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
@@ -3580,7 +3851,7 @@ function startDaemon() {
3580
3851
  idleTimer = setTimeout(() => {
3581
3852
  if (autoStarted) {
3582
3853
  try {
3583
- import_fs3.default.unlinkSync(DAEMON_PID_FILE);
3854
+ import_fs4.default.unlinkSync(DAEMON_PID_FILE);
3584
3855
  } catch {
3585
3856
  }
3586
3857
  }
@@ -3651,7 +3922,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
3651
3922
  mcpServer,
3652
3923
  riskMetadata
3653
3924
  } = JSON.parse(body);
3654
- const id = (0, import_crypto.randomUUID)();
3925
+ const id = (0, import_crypto2.randomUUID)();
3655
3926
  const entry = {
3656
3927
  id,
3657
3928
  toolName,
@@ -3898,14 +4169,14 @@ data: ${JSON.stringify(readPersistentDecisions())}
3898
4169
  server.on("error", (e) => {
3899
4170
  if (e.code === "EADDRINUSE") {
3900
4171
  try {
3901
- if (import_fs3.default.existsSync(DAEMON_PID_FILE)) {
3902
- const { pid } = JSON.parse(import_fs3.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4172
+ if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
4173
+ const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
3903
4174
  process.kill(pid, 0);
3904
4175
  return process.exit(0);
3905
4176
  }
3906
4177
  } catch {
3907
4178
  try {
3908
- import_fs3.default.unlinkSync(DAEMON_PID_FILE);
4179
+ import_fs4.default.unlinkSync(DAEMON_PID_FILE);
3909
4180
  } catch {
3910
4181
  }
3911
4182
  server.listen(DAEMON_PORT2, DAEMON_HOST2);
@@ -3925,25 +4196,25 @@ data: ${JSON.stringify(readPersistentDecisions())}
3925
4196
  });
3926
4197
  }
3927
4198
  function stopDaemon() {
3928
- if (!import_fs3.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
4199
+ if (!import_fs4.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
3929
4200
  try {
3930
- const { pid } = JSON.parse(import_fs3.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4201
+ const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
3931
4202
  process.kill(pid, "SIGTERM");
3932
4203
  console.log(import_chalk4.default.green("\u2705 Stopped."));
3933
4204
  } catch {
3934
4205
  console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
3935
4206
  } finally {
3936
4207
  try {
3937
- import_fs3.default.unlinkSync(DAEMON_PID_FILE);
4208
+ import_fs4.default.unlinkSync(DAEMON_PID_FILE);
3938
4209
  } catch {
3939
4210
  }
3940
4211
  }
3941
4212
  }
3942
4213
  function daemonStatus() {
3943
- if (!import_fs3.default.existsSync(DAEMON_PID_FILE))
4214
+ if (!import_fs4.default.existsSync(DAEMON_PID_FILE))
3944
4215
  return console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
3945
4216
  try {
3946
- const { pid } = JSON.parse(import_fs3.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4217
+ const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
3947
4218
  process.kill(pid, 0);
3948
4219
  console.log(import_chalk4.default.green("Node9 daemon: running"));
3949
4220
  } catch {
@@ -3957,30 +4228,30 @@ var import_execa = require("execa");
3957
4228
  var import_execa2 = require("execa");
3958
4229
  var import_chalk5 = __toESM(require("chalk"));
3959
4230
  var import_readline = __toESM(require("readline"));
3960
- var import_fs5 = __toESM(require("fs"));
3961
- var import_path7 = __toESM(require("path"));
3962
- var import_os5 = __toESM(require("os"));
4231
+ var import_fs6 = __toESM(require("fs"));
4232
+ var import_path8 = __toESM(require("path"));
4233
+ var import_os6 = __toESM(require("os"));
3963
4234
 
3964
4235
  // src/undo.ts
3965
4236
  var import_child_process3 = require("child_process");
3966
- var import_fs4 = __toESM(require("fs"));
3967
- var import_path6 = __toESM(require("path"));
3968
- var import_os4 = __toESM(require("os"));
3969
- var SNAPSHOT_STACK_PATH = import_path6.default.join(import_os4.default.homedir(), ".node9", "snapshots.json");
3970
- var UNDO_LATEST_PATH = import_path6.default.join(import_os4.default.homedir(), ".node9", "undo_latest.txt");
4237
+ var import_fs5 = __toESM(require("fs"));
4238
+ var import_path7 = __toESM(require("path"));
4239
+ var import_os5 = __toESM(require("os"));
4240
+ var SNAPSHOT_STACK_PATH = import_path7.default.join(import_os5.default.homedir(), ".node9", "snapshots.json");
4241
+ var UNDO_LATEST_PATH = import_path7.default.join(import_os5.default.homedir(), ".node9", "undo_latest.txt");
3971
4242
  var MAX_SNAPSHOTS = 10;
3972
4243
  function readStack() {
3973
4244
  try {
3974
- if (import_fs4.default.existsSync(SNAPSHOT_STACK_PATH))
3975
- return JSON.parse(import_fs4.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
4245
+ if (import_fs5.default.existsSync(SNAPSHOT_STACK_PATH))
4246
+ return JSON.parse(import_fs5.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
3976
4247
  } catch {
3977
4248
  }
3978
4249
  return [];
3979
4250
  }
3980
4251
  function writeStack(stack) {
3981
- const dir = import_path6.default.dirname(SNAPSHOT_STACK_PATH);
3982
- if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
3983
- import_fs4.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
4252
+ const dir = import_path7.default.dirname(SNAPSHOT_STACK_PATH);
4253
+ if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
4254
+ import_fs5.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
3984
4255
  }
3985
4256
  function buildArgsSummary(tool, args) {
3986
4257
  if (!args || typeof args !== "object") return "";
@@ -3996,13 +4267,13 @@ function buildArgsSummary(tool, args) {
3996
4267
  async function createShadowSnapshot(tool = "unknown", args = {}) {
3997
4268
  try {
3998
4269
  const cwd = process.cwd();
3999
- if (!import_fs4.default.existsSync(import_path6.default.join(cwd, ".git"))) return null;
4000
- const tempIndex = import_path6.default.join(cwd, ".git", `node9_index_${Date.now()}`);
4270
+ if (!import_fs5.default.existsSync(import_path7.default.join(cwd, ".git"))) return null;
4271
+ const tempIndex = import_path7.default.join(cwd, ".git", `node9_index_${Date.now()}`);
4001
4272
  const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
4002
4273
  (0, import_child_process3.spawnSync)("git", ["add", "-A"], { env });
4003
4274
  const treeRes = (0, import_child_process3.spawnSync)("git", ["write-tree"], { env });
4004
4275
  const treeHash = treeRes.stdout.toString().trim();
4005
- if (import_fs4.default.existsSync(tempIndex)) import_fs4.default.unlinkSync(tempIndex);
4276
+ if (import_fs5.default.existsSync(tempIndex)) import_fs5.default.unlinkSync(tempIndex);
4006
4277
  if (!treeHash || treeRes.status !== 0) return null;
4007
4278
  const commitRes = (0, import_child_process3.spawnSync)("git", [
4008
4279
  "commit-tree",
@@ -4023,7 +4294,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}) {
4023
4294
  stack.push(entry);
4024
4295
  if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
4025
4296
  writeStack(stack);
4026
- import_fs4.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
4297
+ import_fs5.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
4027
4298
  return commitHash;
4028
4299
  } catch (err) {
4029
4300
  if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
@@ -4061,9 +4332,9 @@ function applyUndo(hash, cwd) {
4061
4332
  const tracked = (0, import_child_process3.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
4062
4333
  const untracked = (0, import_child_process3.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
4063
4334
  for (const file of [...tracked, ...untracked]) {
4064
- const fullPath = import_path6.default.join(dir, file);
4065
- if (!snapshotFiles.has(file) && import_fs4.default.existsSync(fullPath)) {
4066
- import_fs4.default.unlinkSync(fullPath);
4335
+ const fullPath = import_path7.default.join(dir, file);
4336
+ if (!snapshotFiles.has(file) && import_fs5.default.existsSync(fullPath)) {
4337
+ import_fs5.default.unlinkSync(fullPath);
4067
4338
  }
4068
4339
  }
4069
4340
  return true;
@@ -4075,7 +4346,7 @@ function applyUndo(hash, cwd) {
4075
4346
  // src/cli.ts
4076
4347
  var import_prompts3 = require("@inquirer/prompts");
4077
4348
  var { version } = JSON.parse(
4078
- import_fs5.default.readFileSync(import_path7.default.join(__dirname, "../package.json"), "utf-8")
4349
+ import_fs6.default.readFileSync(import_path8.default.join(__dirname, "../package.json"), "utf-8")
4079
4350
  );
4080
4351
  function parseDuration(str) {
4081
4352
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -4107,6 +4378,15 @@ INSTRUCTIONS:
4107
4378
  - If you believe this action is critical, explain your reasoning and ask them to run "node9 pause 15m" to proceed.`;
4108
4379
  }
4109
4380
  const label = blockedByLabel.toLowerCase();
4381
+ if (label.includes("dlp") || label.includes("secret detected") || label.includes("credential review")) {
4382
+ return `NODE9 SECURITY ALERT: A sensitive credential (API key, token, or private key) was found in your tool call arguments.
4383
+ CRITICAL INSTRUCTION: Do NOT retry this action.
4384
+ REQUIRED ACTIONS:
4385
+ 1. Remove the hardcoded credential from your command or code.
4386
+ 2. Use an environment variable or a dedicated secrets manager instead.
4387
+ 3. Treat the leaked credential as compromised and rotate it immediately.
4388
+ Do NOT attempt to bypass this check or pass the credential through another tool.`;
4389
+ }
4110
4390
  if (label.includes("sql safety") && label.includes("delete without where")) {
4111
4391
  return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
4112
4392
  INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
@@ -4268,14 +4548,14 @@ async function runProxy(targetCommand) {
4268
4548
  }
4269
4549
  program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
4270
4550
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
4271
- const credPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "credentials.json");
4272
- if (!import_fs5.default.existsSync(import_path7.default.dirname(credPath)))
4273
- import_fs5.default.mkdirSync(import_path7.default.dirname(credPath), { recursive: true });
4551
+ const credPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "credentials.json");
4552
+ if (!import_fs6.default.existsSync(import_path8.default.dirname(credPath)))
4553
+ import_fs6.default.mkdirSync(import_path8.default.dirname(credPath), { recursive: true });
4274
4554
  const profileName = options.profile || "default";
4275
4555
  let existingCreds = {};
4276
4556
  try {
4277
- if (import_fs5.default.existsSync(credPath)) {
4278
- const raw = JSON.parse(import_fs5.default.readFileSync(credPath, "utf-8"));
4557
+ if (import_fs6.default.existsSync(credPath)) {
4558
+ const raw = JSON.parse(import_fs6.default.readFileSync(credPath, "utf-8"));
4279
4559
  if (raw.apiKey) {
4280
4560
  existingCreds = {
4281
4561
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -4287,13 +4567,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4287
4567
  } catch {
4288
4568
  }
4289
4569
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
4290
- import_fs5.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
4570
+ import_fs6.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
4291
4571
  if (profileName === "default") {
4292
- const configPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
4572
+ const configPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "config.json");
4293
4573
  let config = {};
4294
4574
  try {
4295
- if (import_fs5.default.existsSync(configPath))
4296
- config = JSON.parse(import_fs5.default.readFileSync(configPath, "utf-8"));
4575
+ if (import_fs6.default.existsSync(configPath))
4576
+ config = JSON.parse(import_fs6.default.readFileSync(configPath, "utf-8"));
4297
4577
  } catch {
4298
4578
  }
4299
4579
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -4308,9 +4588,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4308
4588
  approvers.cloud = false;
4309
4589
  }
4310
4590
  s.approvers = approvers;
4311
- if (!import_fs5.default.existsSync(import_path7.default.dirname(configPath)))
4312
- import_fs5.default.mkdirSync(import_path7.default.dirname(configPath), { recursive: true });
4313
- import_fs5.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
4591
+ if (!import_fs6.default.existsSync(import_path8.default.dirname(configPath)))
4592
+ import_fs6.default.mkdirSync(import_path8.default.dirname(configPath), { recursive: true });
4593
+ import_fs6.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
4314
4594
  }
4315
4595
  if (options.profile && profileName !== "default") {
4316
4596
  console.log(import_chalk5.default.green(`\u2705 Profile "${profileName}" saved`));
@@ -4349,7 +4629,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
4349
4629
  process.exit(1);
4350
4630
  });
4351
4631
  program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
4352
- const homeDir2 = import_os5.default.homedir();
4632
+ const homeDir2 = import_os6.default.homedir();
4353
4633
  let failures = 0;
4354
4634
  function pass(msg) {
4355
4635
  console.log(import_chalk5.default.green(" \u2705 ") + msg);
@@ -4395,10 +4675,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4395
4675
  );
4396
4676
  }
4397
4677
  section("Configuration");
4398
- const globalConfigPath = import_path7.default.join(homeDir2, ".node9", "config.json");
4399
- if (import_fs5.default.existsSync(globalConfigPath)) {
4678
+ const globalConfigPath = import_path8.default.join(homeDir2, ".node9", "config.json");
4679
+ if (import_fs6.default.existsSync(globalConfigPath)) {
4400
4680
  try {
4401
- JSON.parse(import_fs5.default.readFileSync(globalConfigPath, "utf-8"));
4681
+ JSON.parse(import_fs6.default.readFileSync(globalConfigPath, "utf-8"));
4402
4682
  pass("~/.node9/config.json found and valid");
4403
4683
  } catch {
4404
4684
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -4406,17 +4686,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
4406
4686
  } else {
4407
4687
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
4408
4688
  }
4409
- const projectConfigPath = import_path7.default.join(process.cwd(), "node9.config.json");
4410
- if (import_fs5.default.existsSync(projectConfigPath)) {
4689
+ const projectConfigPath = import_path8.default.join(process.cwd(), "node9.config.json");
4690
+ if (import_fs6.default.existsSync(projectConfigPath)) {
4411
4691
  try {
4412
- JSON.parse(import_fs5.default.readFileSync(projectConfigPath, "utf-8"));
4692
+ JSON.parse(import_fs6.default.readFileSync(projectConfigPath, "utf-8"));
4413
4693
  pass("node9.config.json found and valid (project)");
4414
4694
  } catch {
4415
4695
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
4416
4696
  }
4417
4697
  }
4418
- const credsPath = import_path7.default.join(homeDir2, ".node9", "credentials.json");
4419
- if (import_fs5.default.existsSync(credsPath)) {
4698
+ const credsPath = import_path8.default.join(homeDir2, ".node9", "credentials.json");
4699
+ if (import_fs6.default.existsSync(credsPath)) {
4420
4700
  pass("Cloud credentials found (~/.node9/credentials.json)");
4421
4701
  } else {
4422
4702
  warn(
@@ -4425,10 +4705,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4425
4705
  );
4426
4706
  }
4427
4707
  section("Agent Hooks");
4428
- const claudeSettingsPath = import_path7.default.join(homeDir2, ".claude", "settings.json");
4429
- if (import_fs5.default.existsSync(claudeSettingsPath)) {
4708
+ const claudeSettingsPath = import_path8.default.join(homeDir2, ".claude", "settings.json");
4709
+ if (import_fs6.default.existsSync(claudeSettingsPath)) {
4430
4710
  try {
4431
- const cs = JSON.parse(import_fs5.default.readFileSync(claudeSettingsPath, "utf-8"));
4711
+ const cs = JSON.parse(import_fs6.default.readFileSync(claudeSettingsPath, "utf-8"));
4432
4712
  const hasHook = cs.hooks?.PreToolUse?.some(
4433
4713
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4434
4714
  );
@@ -4441,10 +4721,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4441
4721
  } else {
4442
4722
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
4443
4723
  }
4444
- const geminiSettingsPath = import_path7.default.join(homeDir2, ".gemini", "settings.json");
4445
- if (import_fs5.default.existsSync(geminiSettingsPath)) {
4724
+ const geminiSettingsPath = import_path8.default.join(homeDir2, ".gemini", "settings.json");
4725
+ if (import_fs6.default.existsSync(geminiSettingsPath)) {
4446
4726
  try {
4447
- const gs = JSON.parse(import_fs5.default.readFileSync(geminiSettingsPath, "utf-8"));
4727
+ const gs = JSON.parse(import_fs6.default.readFileSync(geminiSettingsPath, "utf-8"));
4448
4728
  const hasHook = gs.hooks?.BeforeTool?.some(
4449
4729
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4450
4730
  );
@@ -4457,10 +4737,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4457
4737
  } else {
4458
4738
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
4459
4739
  }
4460
- const cursorHooksPath = import_path7.default.join(homeDir2, ".cursor", "hooks.json");
4461
- if (import_fs5.default.existsSync(cursorHooksPath)) {
4740
+ const cursorHooksPath = import_path8.default.join(homeDir2, ".cursor", "hooks.json");
4741
+ if (import_fs6.default.existsSync(cursorHooksPath)) {
4462
4742
  try {
4463
- const cur = JSON.parse(import_fs5.default.readFileSync(cursorHooksPath, "utf-8"));
4743
+ const cur = JSON.parse(import_fs6.default.readFileSync(cursorHooksPath, "utf-8"));
4464
4744
  const hasHook = cur.hooks?.preToolUse?.some(
4465
4745
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
4466
4746
  );
@@ -4562,8 +4842,8 @@ program.command("explain").description(
4562
4842
  console.log("");
4563
4843
  });
4564
4844
  program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
4565
- const configPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
4566
- if (import_fs5.default.existsSync(configPath) && !options.force) {
4845
+ const configPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "config.json");
4846
+ if (import_fs6.default.existsSync(configPath) && !options.force) {
4567
4847
  console.log(import_chalk5.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
4568
4848
  console.log(import_chalk5.default.gray(` Run with --force to overwrite.`));
4569
4849
  return;
@@ -4577,9 +4857,9 @@ program.command("init").description("Create ~/.node9/config.json with default po
4577
4857
  mode: safeMode
4578
4858
  }
4579
4859
  };
4580
- const dir = import_path7.default.dirname(configPath);
4581
- if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
4582
- import_fs5.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
4860
+ const dir = import_path8.default.dirname(configPath);
4861
+ if (!import_fs6.default.existsSync(dir)) import_fs6.default.mkdirSync(dir, { recursive: true });
4862
+ import_fs6.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
4583
4863
  console.log(import_chalk5.default.green(`\u2705 Global config created: ${configPath}`));
4584
4864
  console.log(import_chalk5.default.cyan(` Mode set to: ${safeMode}`));
4585
4865
  console.log(
@@ -4597,14 +4877,14 @@ function formatRelativeTime(timestamp) {
4597
4877
  return new Date(timestamp).toLocaleDateString();
4598
4878
  }
4599
4879
  program.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
4600
- const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "audit.log");
4601
- if (!import_fs5.default.existsSync(logPath)) {
4880
+ const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "audit.log");
4881
+ if (!import_fs6.default.existsSync(logPath)) {
4602
4882
  console.log(
4603
4883
  import_chalk5.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
4604
4884
  );
4605
4885
  return;
4606
4886
  }
4607
- const raw = import_fs5.default.readFileSync(logPath, "utf-8");
4887
+ const raw = import_fs6.default.readFileSync(logPath, "utf-8");
4608
4888
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
4609
4889
  let entries = lines.flatMap((line) => {
4610
4890
  try {
@@ -4687,13 +4967,13 @@ program.command("status").description("Show current Node9 mode, policy source, a
4687
4967
  console.log("");
4688
4968
  const modeLabel = settings.mode === "audit" ? import_chalk5.default.blue("audit") : settings.mode === "strict" ? import_chalk5.default.red("strict") : import_chalk5.default.white("standard");
4689
4969
  console.log(` Mode: ${modeLabel}`);
4690
- const projectConfig = import_path7.default.join(process.cwd(), "node9.config.json");
4691
- const globalConfig = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
4970
+ const projectConfig = import_path8.default.join(process.cwd(), "node9.config.json");
4971
+ const globalConfig = import_path8.default.join(import_os6.default.homedir(), ".node9", "config.json");
4692
4972
  console.log(
4693
- ` Local: ${import_fs5.default.existsSync(projectConfig) ? import_chalk5.default.green("Active (node9.config.json)") : import_chalk5.default.gray("Not present")}`
4973
+ ` Local: ${import_fs6.default.existsSync(projectConfig) ? import_chalk5.default.green("Active (node9.config.json)") : import_chalk5.default.gray("Not present")}`
4694
4974
  );
4695
4975
  console.log(
4696
- ` Global: ${import_fs5.default.existsSync(globalConfig) ? import_chalk5.default.green("Active (~/.node9/config.json)") : import_chalk5.default.gray("Not present")}`
4976
+ ` Global: ${import_fs6.default.existsSync(globalConfig) ? import_chalk5.default.green("Active (~/.node9/config.json)") : import_chalk5.default.gray("Not present")}`
4697
4977
  );
4698
4978
  if (mergedConfig.policy.sandboxPaths.length > 0) {
4699
4979
  console.log(
@@ -4756,9 +5036,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
4756
5036
  } catch (err) {
4757
5037
  const tempConfig = getConfig();
4758
5038
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
4759
- const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
5039
+ const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log");
4760
5040
  const errMsg = err instanceof Error ? err.message : String(err);
4761
- import_fs5.default.appendFileSync(
5041
+ import_fs6.default.appendFileSync(
4762
5042
  logPath,
4763
5043
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
4764
5044
  RAW: ${raw}
@@ -4776,10 +5056,10 @@ RAW: ${raw}
4776
5056
  }
4777
5057
  const config = getConfig();
4778
5058
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
4779
- const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4780
- if (!import_fs5.default.existsSync(import_path7.default.dirname(logPath)))
4781
- import_fs5.default.mkdirSync(import_path7.default.dirname(logPath), { recursive: true });
4782
- import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
5059
+ const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log");
5060
+ if (!import_fs6.default.existsSync(import_path8.default.dirname(logPath)))
5061
+ import_fs6.default.mkdirSync(import_path8.default.dirname(logPath), { recursive: true });
5062
+ import_fs6.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
4783
5063
  `);
4784
5064
  }
4785
5065
  const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
@@ -4790,8 +5070,14 @@ RAW: ${raw}
4790
5070
  const sendBlock = (msg, result2) => {
4791
5071
  const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
4792
5072
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
4793
- console.error(import_chalk5.default.red(`
5073
+ if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
5074
+ console.error(import_chalk5.default.bgRed.white.bold(`
5075
+ \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
5076
+ console.error(import_chalk5.default.red.bold(` A sensitive secret was found in the tool arguments!`));
5077
+ } else {
5078
+ console.error(import_chalk5.default.red(`
4794
5079
  \u{1F6D1} Node9 blocked "${toolName}"`));
5080
+ }
4795
5081
  console.error(import_chalk5.default.gray(` Triggered by: ${blockedByContext}`));
4796
5082
  if (result2?.changeHint) console.error(import_chalk5.default.cyan(` To change: ${result2.changeHint}`));
4797
5083
  console.error("");
@@ -4850,9 +5136,9 @@ RAW: ${raw}
4850
5136
  });
4851
5137
  } catch (err) {
4852
5138
  if (process.env.NODE9_DEBUG === "1") {
4853
- const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
5139
+ const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log");
4854
5140
  const errMsg = err instanceof Error ? err.message : String(err);
4855
- import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
5141
+ import_fs6.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
4856
5142
  `);
4857
5143
  }
4858
5144
  process.exit(0);
@@ -4897,10 +5183,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
4897
5183
  decision: "allowed",
4898
5184
  source: "post-hook"
4899
5185
  };
4900
- const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "audit.log");
4901
- if (!import_fs5.default.existsSync(import_path7.default.dirname(logPath)))
4902
- import_fs5.default.mkdirSync(import_path7.default.dirname(logPath), { recursive: true });
4903
- import_fs5.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
5186
+ const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "audit.log");
5187
+ if (!import_fs6.default.existsSync(import_path8.default.dirname(logPath)))
5188
+ import_fs6.default.mkdirSync(import_path8.default.dirname(logPath), { recursive: true });
5189
+ import_fs6.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
4904
5190
  const config = getConfig();
4905
5191
  if (shouldSnapshot(tool, {}, config)) {
4906
5192
  await createShadowSnapshot();
@@ -5082,13 +5368,103 @@ program.command("undo").description(
5082
5368
  console.log(import_chalk5.default.gray("\nCancelled.\n"));
5083
5369
  }
5084
5370
  });
5371
+ var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
5372
+ shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
5373
+ const name = resolveShieldName(service);
5374
+ if (!name) {
5375
+ console.error(import_chalk5.default.red(`
5376
+ \u274C Unknown shield: "${service}"
5377
+ `));
5378
+ console.log(`Run ${import_chalk5.default.cyan("node9 shield list")} to see available shields.
5379
+ `);
5380
+ process.exit(1);
5381
+ }
5382
+ const shield = getShield(name);
5383
+ const active = readActiveShields();
5384
+ if (active.includes(name)) {
5385
+ console.log(import_chalk5.default.yellow(`
5386
+ \u2139\uFE0F Shield "${name}" is already active.
5387
+ `));
5388
+ return;
5389
+ }
5390
+ writeActiveShields([...active, name]);
5391
+ console.log(import_chalk5.default.green(`
5392
+ \u{1F6E1}\uFE0F Shield "${name}" enabled.`));
5393
+ console.log(import_chalk5.default.gray(` ${shield.smartRules.length} smart rules now active.`));
5394
+ if (shield.dangerousWords.length > 0)
5395
+ console.log(import_chalk5.default.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
5396
+ if (name === "filesystem") {
5397
+ console.log(
5398
+ import_chalk5.default.yellow(
5399
+ `
5400
+ \u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
5401
+ Tools like unlink, find -delete, or language-level file ops are not intercepted.`
5402
+ )
5403
+ );
5404
+ }
5405
+ console.log("");
5406
+ });
5407
+ shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
5408
+ const name = resolveShieldName(service);
5409
+ if (!name) {
5410
+ console.error(import_chalk5.default.red(`
5411
+ \u274C Unknown shield: "${service}"
5412
+ `));
5413
+ console.log(`Run ${import_chalk5.default.cyan("node9 shield list")} to see available shields.
5414
+ `);
5415
+ process.exit(1);
5416
+ }
5417
+ const active = readActiveShields();
5418
+ if (!active.includes(name)) {
5419
+ console.log(import_chalk5.default.yellow(`
5420
+ \u2139\uFE0F Shield "${name}" is not active.
5421
+ `));
5422
+ return;
5423
+ }
5424
+ writeActiveShields(active.filter((s) => s !== name));
5425
+ console.log(import_chalk5.default.green(`
5426
+ \u{1F6E1}\uFE0F Shield "${name}" disabled.
5427
+ `));
5428
+ });
5429
+ shieldCmd.command("list").description("Show all available shields").action(() => {
5430
+ const active = new Set(readActiveShields());
5431
+ console.log(import_chalk5.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
5432
+ for (const shield of listShields()) {
5433
+ const status = active.has(shield.name) ? import_chalk5.default.green("\u25CF enabled") : import_chalk5.default.gray("\u25CB disabled");
5434
+ console.log(` ${status} ${import_chalk5.default.cyan(shield.name.padEnd(12))} ${shield.description}`);
5435
+ if (shield.aliases.length > 0)
5436
+ console.log(import_chalk5.default.gray(` aliases: ${shield.aliases.join(", ")}`));
5437
+ }
5438
+ console.log("");
5439
+ });
5440
+ shieldCmd.command("status").description("Show which shields are currently active").action(() => {
5441
+ const active = readActiveShields();
5442
+ if (active.length === 0) {
5443
+ console.log(import_chalk5.default.yellow("\n\u2139\uFE0F No shields are active.\n"));
5444
+ console.log(`Run ${import_chalk5.default.cyan("node9 shield list")} to see available shields.
5445
+ `);
5446
+ return;
5447
+ }
5448
+ console.log(import_chalk5.default.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
5449
+ for (const name of active) {
5450
+ const shield = getShield(name);
5451
+ if (!shield) continue;
5452
+ console.log(` ${import_chalk5.default.green("\u25CF")} ${import_chalk5.default.cyan(name)}`);
5453
+ console.log(
5454
+ import_chalk5.default.gray(
5455
+ ` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
5456
+ )
5457
+ );
5458
+ }
5459
+ console.log("");
5460
+ });
5085
5461
  process.on("unhandledRejection", (reason) => {
5086
5462
  const isCheckHook = process.argv[2] === "check";
5087
5463
  if (isCheckHook) {
5088
5464
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
5089
- const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
5465
+ const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log");
5090
5466
  const msg = reason instanceof Error ? reason.message : String(reason);
5091
- import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
5467
+ import_fs6.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
5092
5468
  `);
5093
5469
  }
5094
5470
  process.exit(0);