@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/index.js CHANGED
@@ -37,9 +37,9 @@ module.exports = __toCommonJS(src_exports);
37
37
  // src/core.ts
38
38
  var import_chalk2 = __toESM(require("chalk"));
39
39
  var import_prompts = require("@inquirer/prompts");
40
- var import_fs = __toESM(require("fs"));
41
- var import_path3 = __toESM(require("path"));
42
- var import_os = __toESM(require("os"));
40
+ var import_fs2 = __toESM(require("fs"));
41
+ var import_path4 = __toESM(require("path"));
42
+ var import_os2 = __toESM(require("os"));
43
43
  var import_picomatch = __toESM(require("picomatch"));
44
44
  var import_sh_syntax = require("sh-syntax");
45
45
 
@@ -369,25 +369,26 @@ var import_zod = require("zod");
369
369
  var noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
370
370
  message: "Value must not contain literal newline characters (use \\n instead)"
371
371
  });
372
- var validRegex = noNewlines.refine(
373
- (s) => {
374
- try {
375
- new RegExp(s);
376
- return true;
377
- } catch {
378
- return false;
379
- }
380
- },
381
- { message: "Value must be a valid regular expression" }
382
- );
383
372
  var SmartConditionSchema = import_zod.z.object({
384
373
  field: import_zod.z.string().min(1, "Condition field must not be empty"),
385
- op: import_zod.z.enum(["matches", "notMatches", "contains", "notContains", "exists", "notExists"], {
386
- errorMap: () => ({
387
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists"
388
- })
389
- }),
390
- value: validRegex.optional(),
374
+ op: import_zod.z.enum(
375
+ [
376
+ "matches",
377
+ "notMatches",
378
+ "contains",
379
+ "notContains",
380
+ "exists",
381
+ "notExists",
382
+ "matchesGlob",
383
+ "notMatchesGlob"
384
+ ],
385
+ {
386
+ errorMap: () => ({
387
+ message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
388
+ })
389
+ }
390
+ ),
391
+ value: import_zod.z.string().optional(),
391
392
  flags: import_zod.z.string().optional()
392
393
  });
393
394
  var SmartRuleSchema = import_zod.z.object({
@@ -400,11 +401,6 @@ var SmartRuleSchema = import_zod.z.object({
400
401
  }),
401
402
  reason: import_zod.z.string().optional()
402
403
  });
403
- var PolicyRuleSchema = import_zod.z.object({
404
- action: import_zod.z.string().min(1),
405
- allowPaths: import_zod.z.array(import_zod.z.string()).optional(),
406
- blockPaths: import_zod.z.array(import_zod.z.string()).optional()
407
- });
408
404
  var ConfigFileSchema = import_zod.z.object({
409
405
  version: import_zod.z.string().optional(),
410
406
  settings: import_zod.z.object({
@@ -429,12 +425,15 @@ var ConfigFileSchema = import_zod.z.object({
429
425
  dangerousWords: import_zod.z.array(noNewlines).optional(),
430
426
  ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
431
427
  toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
432
- rules: import_zod.z.array(PolicyRuleSchema).optional(),
433
428
  smartRules: import_zod.z.array(SmartRuleSchema).optional(),
434
429
  snapshot: import_zod.z.object({
435
430
  tools: import_zod.z.array(import_zod.z.string()).optional(),
436
431
  onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
437
432
  ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
433
+ }).optional(),
434
+ dlp: import_zod.z.object({
435
+ enabled: import_zod.z.boolean().optional(),
436
+ scanIgnoredTools: import_zod.z.boolean().optional()
438
437
  }).optional()
439
438
  }).optional(),
440
439
  environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
@@ -456,8 +455,8 @@ function sanitizeConfig(raw) {
456
455
  }
457
456
  }
458
457
  const lines = result.error.issues.map((issue) => {
459
- const path4 = issue.path.length > 0 ? issue.path.join(".") : "root";
460
- return ` \u2022 ${path4}: ${issue.message}`;
458
+ const path5 = issue.path.length > 0 ? issue.path.join(".") : "root";
459
+ return ` \u2022 ${path5}: ${issue.message}`;
461
460
  });
462
461
  return {
463
462
  sanitized,
@@ -466,18 +465,291 @@ ${lines.join("\n")}`
466
465
  };
467
466
  }
468
467
 
468
+ // src/shields.ts
469
+ var import_fs = __toESM(require("fs"));
470
+ var import_path3 = __toESM(require("path"));
471
+ var import_os = __toESM(require("os"));
472
+ var SHIELDS = {
473
+ postgres: {
474
+ name: "postgres",
475
+ description: "Protects PostgreSQL databases from destructive AI operations",
476
+ aliases: ["pg", "postgresql"],
477
+ smartRules: [
478
+ {
479
+ name: "shield:postgres:block-drop-table",
480
+ tool: "*",
481
+ conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
482
+ verdict: "block",
483
+ reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
484
+ },
485
+ {
486
+ name: "shield:postgres:block-truncate",
487
+ tool: "*",
488
+ conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
489
+ verdict: "block",
490
+ reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
491
+ },
492
+ {
493
+ name: "shield:postgres:block-drop-column",
494
+ tool: "*",
495
+ conditions: [
496
+ { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
497
+ ],
498
+ verdict: "block",
499
+ reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
500
+ },
501
+ {
502
+ name: "shield:postgres:review-grant-revoke",
503
+ tool: "*",
504
+ conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
505
+ verdict: "review",
506
+ reason: "Permission changes require human approval (Postgres shield)"
507
+ }
508
+ ],
509
+ dangerousWords: ["dropdb", "pg_dropcluster"]
510
+ },
511
+ github: {
512
+ name: "github",
513
+ description: "Protects GitHub repositories from destructive AI operations",
514
+ aliases: ["git"],
515
+ smartRules: [
516
+ {
517
+ // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
518
+ // This rule adds coverage for `git push --delete` which the built-in does not match.
519
+ name: "shield:github:review-delete-branch-remote",
520
+ tool: "bash",
521
+ conditions: [
522
+ {
523
+ field: "command",
524
+ op: "matches",
525
+ value: "git\\s+push\\s+.*--delete",
526
+ flags: "i"
527
+ }
528
+ ],
529
+ verdict: "review",
530
+ reason: "Remote branch deletion requires human approval (GitHub shield)"
531
+ },
532
+ {
533
+ name: "shield:github:block-delete-repo",
534
+ tool: "*",
535
+ conditions: [
536
+ { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
537
+ ],
538
+ verdict: "block",
539
+ reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
540
+ }
541
+ ],
542
+ dangerousWords: []
543
+ },
544
+ aws: {
545
+ name: "aws",
546
+ description: "Protects AWS infrastructure from destructive AI operations",
547
+ aliases: ["amazon"],
548
+ smartRules: [
549
+ {
550
+ name: "shield:aws:block-delete-s3-bucket",
551
+ tool: "*",
552
+ conditions: [
553
+ {
554
+ field: "command",
555
+ op: "matches",
556
+ value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
557
+ flags: "i"
558
+ }
559
+ ],
560
+ verdict: "block",
561
+ reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
562
+ },
563
+ {
564
+ name: "shield:aws:review-iam-changes",
565
+ tool: "*",
566
+ conditions: [
567
+ {
568
+ field: "command",
569
+ op: "matches",
570
+ value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
571
+ flags: "i"
572
+ }
573
+ ],
574
+ verdict: "review",
575
+ reason: "IAM changes require human approval (AWS shield)"
576
+ },
577
+ {
578
+ name: "shield:aws:block-ec2-terminate",
579
+ tool: "*",
580
+ conditions: [
581
+ {
582
+ field: "command",
583
+ op: "matches",
584
+ value: "aws\\s+ec2\\s+terminate-instances",
585
+ flags: "i"
586
+ }
587
+ ],
588
+ verdict: "block",
589
+ reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
590
+ },
591
+ {
592
+ name: "shield:aws:review-rds-delete",
593
+ tool: "*",
594
+ conditions: [
595
+ { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
596
+ ],
597
+ verdict: "review",
598
+ reason: "RDS deletion requires human approval (AWS shield)"
599
+ }
600
+ ],
601
+ dangerousWords: []
602
+ },
603
+ filesystem: {
604
+ name: "filesystem",
605
+ description: "Protects the local filesystem from dangerous AI operations",
606
+ aliases: ["fs"],
607
+ smartRules: [
608
+ {
609
+ name: "shield:filesystem:review-chmod-777",
610
+ tool: "bash",
611
+ conditions: [
612
+ { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
613
+ ],
614
+ verdict: "review",
615
+ reason: "chmod 777 requires human approval (filesystem shield)"
616
+ },
617
+ {
618
+ name: "shield:filesystem:review-write-etc",
619
+ tool: "bash",
620
+ conditions: [
621
+ {
622
+ field: "command",
623
+ // Narrow to write-indicative operations to avoid approval fatigue on reads.
624
+ // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
625
+ op: "matches",
626
+ value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
627
+ }
628
+ ],
629
+ verdict: "review",
630
+ reason: "Writing to /etc requires human approval (filesystem shield)"
631
+ }
632
+ ],
633
+ // dd removed: too common as a legitimate tool (disk imaging, file ops).
634
+ // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
635
+ // wipefs retained: rarely legitimate in an agent context and not in built-ins.
636
+ dangerousWords: ["wipefs"]
637
+ }
638
+ };
639
+ function resolveShieldName(input) {
640
+ const lower = input.toLowerCase();
641
+ if (SHIELDS[lower]) return lower;
642
+ for (const [name, def] of Object.entries(SHIELDS)) {
643
+ if (def.aliases.includes(lower)) return name;
644
+ }
645
+ return null;
646
+ }
647
+ function getShield(name) {
648
+ const resolved = resolveShieldName(name);
649
+ return resolved ? SHIELDS[resolved] : null;
650
+ }
651
+ var SHIELDS_STATE_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "shields.json");
652
+ function readActiveShields() {
653
+ try {
654
+ const raw = import_fs.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
655
+ if (!raw.trim()) return [];
656
+ const parsed = JSON.parse(raw);
657
+ if (Array.isArray(parsed.active)) {
658
+ return parsed.active.filter(
659
+ (e) => typeof e === "string" && e.length > 0 && e in SHIELDS
660
+ );
661
+ }
662
+ } catch (err) {
663
+ if (err.code !== "ENOENT") {
664
+ process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
665
+ `);
666
+ }
667
+ }
668
+ return [];
669
+ }
670
+
671
+ // src/dlp.ts
672
+ var DLP_PATTERNS = [
673
+ { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
674
+ { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
675
+ { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
676
+ { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
677
+ { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
678
+ {
679
+ name: "Private Key (PEM)",
680
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
681
+ severity: "block"
682
+ },
683
+ { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
684
+ ];
685
+ function maskSecret(raw, pattern) {
686
+ const match = raw.match(pattern);
687
+ if (!match) return "****";
688
+ const secret = match[0];
689
+ if (secret.length < 8) return "****";
690
+ const prefix = secret.slice(0, 4);
691
+ const suffix = secret.slice(-4);
692
+ const stars = "*".repeat(Math.min(secret.length - 8, 12));
693
+ return `${prefix}${stars}${suffix}`;
694
+ }
695
+ var MAX_DEPTH = 5;
696
+ var MAX_STRING_BYTES = 1e5;
697
+ var MAX_JSON_PARSE_BYTES = 1e4;
698
+ function scanArgs(args, depth = 0, fieldPath = "args") {
699
+ if (depth > MAX_DEPTH || args === null || args === void 0) return null;
700
+ if (Array.isArray(args)) {
701
+ for (let i = 0; i < args.length; i++) {
702
+ const match = scanArgs(args[i], depth + 1, `${fieldPath}[${i}]`);
703
+ if (match) return match;
704
+ }
705
+ return null;
706
+ }
707
+ if (typeof args === "object") {
708
+ for (const [key, value] of Object.entries(args)) {
709
+ const match = scanArgs(value, depth + 1, `${fieldPath}.${key}`);
710
+ if (match) return match;
711
+ }
712
+ return null;
713
+ }
714
+ if (typeof args === "string") {
715
+ const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
716
+ for (const pattern of DLP_PATTERNS) {
717
+ if (pattern.regex.test(text)) {
718
+ return {
719
+ patternName: pattern.name,
720
+ fieldPath,
721
+ redactedSample: maskSecret(text, pattern.regex),
722
+ severity: pattern.severity
723
+ };
724
+ }
725
+ }
726
+ if (text.length < MAX_JSON_PARSE_BYTES) {
727
+ const trimmed = text.trim();
728
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
729
+ try {
730
+ const parsed = JSON.parse(text);
731
+ const inner = scanArgs(parsed, depth + 1, fieldPath);
732
+ if (inner) return inner;
733
+ } catch {
734
+ }
735
+ }
736
+ }
737
+ }
738
+ return null;
739
+ }
740
+
469
741
  // src/core.ts
470
- var PAUSED_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "PAUSED");
471
- var TRUST_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "trust.json");
472
- var LOCAL_AUDIT_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "audit.log");
473
- var HOOK_DEBUG_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
742
+ var PAUSED_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
743
+ var TRUST_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "trust.json");
744
+ var LOCAL_AUDIT_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "audit.log");
745
+ var HOOK_DEBUG_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
474
746
  function checkPause() {
475
747
  try {
476
- if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
477
- const state = JSON.parse(import_fs.default.readFileSync(PAUSED_FILE, "utf-8"));
748
+ if (!import_fs2.default.existsSync(PAUSED_FILE)) return { paused: false };
749
+ const state = JSON.parse(import_fs2.default.readFileSync(PAUSED_FILE, "utf-8"));
478
750
  if (state.expiry > 0 && Date.now() >= state.expiry) {
479
751
  try {
480
- import_fs.default.unlinkSync(PAUSED_FILE);
752
+ import_fs2.default.unlinkSync(PAUSED_FILE);
481
753
  } catch {
482
754
  }
483
755
  return { paused: false };
@@ -488,20 +760,20 @@ function checkPause() {
488
760
  }
489
761
  }
490
762
  function atomicWriteSync(filePath, data, options) {
491
- const dir = import_path3.default.dirname(filePath);
492
- if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
493
- const tmpPath = `${filePath}.${import_os.default.hostname()}.${process.pid}.tmp`;
494
- import_fs.default.writeFileSync(tmpPath, data, options);
495
- import_fs.default.renameSync(tmpPath, filePath);
763
+ const dir = import_path4.default.dirname(filePath);
764
+ if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
765
+ const tmpPath = `${filePath}.${import_os2.default.hostname()}.${process.pid}.tmp`;
766
+ import_fs2.default.writeFileSync(tmpPath, data, options);
767
+ import_fs2.default.renameSync(tmpPath, filePath);
496
768
  }
497
769
  function getActiveTrustSession(toolName) {
498
770
  try {
499
- if (!import_fs.default.existsSync(TRUST_FILE)) return false;
500
- const trust = JSON.parse(import_fs.default.readFileSync(TRUST_FILE, "utf-8"));
771
+ if (!import_fs2.default.existsSync(TRUST_FILE)) return false;
772
+ const trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
501
773
  const now = Date.now();
502
774
  const active = trust.entries.filter((e) => e.expiry > now);
503
775
  if (active.length !== trust.entries.length) {
504
- import_fs.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
776
+ import_fs2.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
505
777
  }
506
778
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
507
779
  } catch {
@@ -512,8 +784,8 @@ function writeTrustSession(toolName, durationMs) {
512
784
  try {
513
785
  let trust = { entries: [] };
514
786
  try {
515
- if (import_fs.default.existsSync(TRUST_FILE)) {
516
- trust = JSON.parse(import_fs.default.readFileSync(TRUST_FILE, "utf-8"));
787
+ if (import_fs2.default.existsSync(TRUST_FILE)) {
788
+ trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
517
789
  }
518
790
  } catch {
519
791
  }
@@ -529,9 +801,9 @@ function writeTrustSession(toolName, durationMs) {
529
801
  }
530
802
  function appendToLog(logPath, entry) {
531
803
  try {
532
- const dir = import_path3.default.dirname(logPath);
533
- if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
534
- import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
804
+ const dir = import_path4.default.dirname(logPath);
805
+ if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
806
+ import_fs2.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
535
807
  } catch {
536
808
  }
537
809
  }
@@ -543,7 +815,7 @@ function appendHookDebug(toolName, args, meta) {
543
815
  args: safeArgs,
544
816
  agent: meta?.agent,
545
817
  mcpServer: meta?.mcpServer,
546
- hostname: import_os.default.hostname(),
818
+ hostname: import_os2.default.hostname(),
547
819
  cwd: process.cwd()
548
820
  });
549
821
  }
@@ -557,7 +829,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
557
829
  checkedBy,
558
830
  agent: meta?.agent,
559
831
  mcpServer: meta?.mcpServer,
560
- hostname: import_os.default.hostname()
832
+ hostname: import_os2.default.hostname()
561
833
  });
562
834
  }
563
835
  function tokenize(toolName) {
@@ -573,9 +845,9 @@ function matchesPattern(text, patterns) {
573
845
  const withoutDotSlash = text.replace(/^\.\//, "");
574
846
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
575
847
  }
576
- function getNestedValue(obj, path4) {
848
+ function getNestedValue(obj, path5) {
577
849
  if (!obj || typeof obj !== "object") return null;
578
- return path4.split(".").reduce((prev, curr) => prev?.[curr], obj);
850
+ return path5.split(".").reduce((prev, curr) => prev?.[curr], obj);
579
851
  }
580
852
  function evaluateSmartConditions(args, rule) {
581
853
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -608,6 +880,10 @@ function evaluateSmartConditions(args, rule) {
608
880
  return true;
609
881
  }
610
882
  }
883
+ case "matchesGlob":
884
+ return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
885
+ case "notMatchesGlob":
886
+ return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : true;
611
887
  default:
612
888
  return false;
613
889
  }
@@ -771,25 +1047,27 @@ var DEFAULT_CONFIG = {
771
1047
  onlyPaths: [],
772
1048
  ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
773
1049
  },
774
- rules: [
775
- // Only use the legacy rules format for simple path-based rm control.
776
- // All other command-level enforcement lives in smartRules below.
777
- {
778
- action: "rm",
779
- allowPaths: [
780
- "**/node_modules/**",
781
- "dist/**",
782
- "build/**",
783
- ".next/**",
784
- "coverage/**",
785
- ".cache/**",
786
- "tmp/**",
787
- "temp/**",
788
- ".DS_Store"
789
- ]
790
- }
791
- ],
792
1050
  smartRules: [
1051
+ // ── rm safety (critical — always evaluated first) ──────────────────────
1052
+ {
1053
+ name: "block-rm-rf-home",
1054
+ tool: "bash",
1055
+ conditionMode: "all",
1056
+ conditions: [
1057
+ {
1058
+ field: "command",
1059
+ op: "matches",
1060
+ value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
1061
+ },
1062
+ {
1063
+ field: "command",
1064
+ op: "matches",
1065
+ value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
1066
+ }
1067
+ ],
1068
+ verdict: "block",
1069
+ reason: "Recursive delete of home directory is irreversible"
1070
+ },
793
1071
  // ── SQL safety ────────────────────────────────────────────────────────
794
1072
  {
795
1073
  name: "no-delete-without-where",
@@ -880,16 +1158,42 @@ var DEFAULT_CONFIG = {
880
1158
  verdict: "block",
881
1159
  reason: "Piping remote script into a shell is a supply-chain attack vector"
882
1160
  }
883
- ]
1161
+ ],
1162
+ dlp: { enabled: true, scanIgnoredTools: true }
884
1163
  },
885
1164
  environments: {}
886
1165
  };
1166
+ var ADVISORY_SMART_RULES = [
1167
+ {
1168
+ name: "allow-rm-safe-paths",
1169
+ tool: "*",
1170
+ conditionMode: "all",
1171
+ conditions: [
1172
+ { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
1173
+ {
1174
+ field: "command",
1175
+ op: "matches",
1176
+ // Matches known-safe build artifact paths in the command.
1177
+ value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
1178
+ }
1179
+ ],
1180
+ verdict: "allow",
1181
+ reason: "Deleting a known-safe build artifact path"
1182
+ },
1183
+ {
1184
+ name: "review-rm",
1185
+ tool: "*",
1186
+ conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
1187
+ verdict: "review",
1188
+ reason: "rm can permanently delete files \u2014 confirm the target path"
1189
+ }
1190
+ ];
887
1191
  var cachedConfig = null;
888
1192
  function getInternalToken() {
889
1193
  try {
890
- const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
891
- if (!import_fs.default.existsSync(pidFile)) return null;
892
- const data = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
1194
+ const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1195
+ if (!import_fs2.default.existsSync(pidFile)) return null;
1196
+ const data = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
893
1197
  process.kill(data.pid, 0);
894
1198
  return data.internalToken ?? null;
895
1199
  } catch {
@@ -904,7 +1208,8 @@ async function evaluatePolicy(toolName, args, agent) {
904
1208
  (rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
905
1209
  );
906
1210
  if (matchedRule) {
907
- if (matchedRule.verdict === "allow") return { decision: "allow" };
1211
+ if (matchedRule.verdict === "allow")
1212
+ return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
908
1213
  return {
909
1214
  decision: matchedRule.verdict,
910
1215
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
@@ -915,13 +1220,11 @@ async function evaluatePolicy(toolName, args, agent) {
915
1220
  }
916
1221
  }
917
1222
  let allTokens = [];
918
- let actionTokens = [];
919
1223
  let pathTokens = [];
920
1224
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
921
1225
  if (shellCommand) {
922
1226
  const analyzed = await analyzeShellCommand(shellCommand);
923
1227
  allTokens = analyzed.allTokens;
924
- actionTokens = analyzed.actions;
925
1228
  pathTokens = analyzed.paths;
926
1229
  const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
927
1230
  if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
@@ -929,11 +1232,9 @@ async function evaluatePolicy(toolName, args, agent) {
929
1232
  }
930
1233
  if (isSqlTool(toolName, config.policy.toolInspection)) {
931
1234
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
932
- actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
933
1235
  }
934
1236
  } else {
935
1237
  allTokens = tokenize(toolName);
936
- actionTokens = [toolName];
937
1238
  if (args && typeof args === "object") {
938
1239
  const flattenedArgs = JSON.stringify(args).toLowerCase();
939
1240
  const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
@@ -956,29 +1257,6 @@ async function evaluatePolicy(toolName, args, agent) {
956
1257
  const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
957
1258
  if (allInSandbox) return { decision: "allow" };
958
1259
  }
959
- for (const action of actionTokens) {
960
- const rule = config.policy.rules.find(
961
- (r) => r.action === action || matchesPattern(action, r.action)
962
- );
963
- if (rule) {
964
- if (pathTokens.length > 0) {
965
- const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
966
- if (anyBlocked)
967
- return {
968
- decision: "review",
969
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
970
- tier: 5
971
- };
972
- const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
973
- if (allAllowed) return { decision: "allow" };
974
- }
975
- return {
976
- decision: "review",
977
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
978
- tier: 5
979
- };
980
- }
981
- }
982
1260
  let matchedDangerousWord;
983
1261
  const isDangerous = allTokens.some(
984
1262
  (token) => config.policy.dangerousWords.some((word) => {
@@ -1036,9 +1314,9 @@ var DAEMON_PORT = 7391;
1036
1314
  var DAEMON_HOST = "127.0.0.1";
1037
1315
  function isDaemonRunning() {
1038
1316
  try {
1039
- const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
1040
- if (!import_fs.default.existsSync(pidFile)) return false;
1041
- const { pid, port } = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
1317
+ const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1318
+ if (!import_fs2.default.existsSync(pidFile)) return false;
1319
+ const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1042
1320
  if (port !== DAEMON_PORT) return false;
1043
1321
  process.kill(pid, 0);
1044
1322
  return true;
@@ -1048,9 +1326,9 @@ function isDaemonRunning() {
1048
1326
  }
1049
1327
  function getPersistentDecision(toolName) {
1050
1328
  try {
1051
- const file = import_path3.default.join(import_os.default.homedir(), ".node9", "decisions.json");
1052
- if (!import_fs.default.existsSync(file)) return null;
1053
- const decisions = JSON.parse(import_fs.default.readFileSync(file, "utf-8"));
1329
+ const file = import_path4.default.join(import_os2.default.homedir(), ".node9", "decisions.json");
1330
+ if (!import_fs2.default.existsSync(file)) return null;
1331
+ const decisions = JSON.parse(import_fs2.default.readFileSync(file, "utf-8"));
1054
1332
  const d = decisions[toolName];
1055
1333
  if (d === "allow" || d === "deny") return d;
1056
1334
  } catch {
@@ -1149,6 +1427,22 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1149
1427
  let policyMatchedField;
1150
1428
  let policyMatchedWord;
1151
1429
  let riskMetadata;
1430
+ if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
1431
+ const dlpMatch = scanArgs(args);
1432
+ if (dlpMatch) {
1433
+ const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
1434
+ if (dlpMatch.severity === "block") {
1435
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
1436
+ return {
1437
+ approved: false,
1438
+ reason: dlpReason,
1439
+ blockedBy: "local-config",
1440
+ blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
1441
+ };
1442
+ }
1443
+ explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
1444
+ }
1445
+ }
1152
1446
  if (config.settings.mode === "audit") {
1153
1447
  if (!isIgnoredTool(toolName)) {
1154
1448
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
@@ -1388,7 +1682,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1388
1682
  racePromises.push(
1389
1683
  (async () => {
1390
1684
  try {
1391
- console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
1685
+ if (explainableLabel.includes("DLP")) {
1686
+ console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
1687
+ console.log(
1688
+ import_chalk2.default.red.bold(` A sensitive secret was detected in the tool arguments!`)
1689
+ );
1690
+ } else {
1691
+ console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
1692
+ }
1392
1693
  console.log(`${import_chalk2.default.bold("Action:")} ${import_chalk2.default.red(toolName)}`);
1393
1694
  console.log(`${import_chalk2.default.bold("Flagged By:")} ${import_chalk2.default.yellow(explainableLabel)}`);
1394
1695
  if (isRemoteLocked) {
@@ -1493,8 +1794,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1493
1794
  }
1494
1795
  function getConfig() {
1495
1796
  if (cachedConfig) return cachedConfig;
1496
- const globalPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
1497
- const projectPath = import_path3.default.join(process.cwd(), "node9.config.json");
1797
+ const globalPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
1798
+ const projectPath = import_path4.default.join(process.cwd(), "node9.config.json");
1498
1799
  const globalConfig = tryLoadConfig(globalPath);
1499
1800
  const projectConfig = tryLoadConfig(projectPath);
1500
1801
  const mergedSettings = {
@@ -1506,13 +1807,13 @@ function getConfig() {
1506
1807
  dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
1507
1808
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
1508
1809
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
1509
- rules: [...DEFAULT_CONFIG.policy.rules],
1510
1810
  smartRules: [...DEFAULT_CONFIG.policy.smartRules],
1511
1811
  snapshot: {
1512
1812
  tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
1513
1813
  onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
1514
1814
  ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
1515
- }
1815
+ },
1816
+ dlp: { ...DEFAULT_CONFIG.policy.dlp }
1516
1817
  };
1517
1818
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
1518
1819
  const applyLayer = (source) => {
@@ -1532,7 +1833,6 @@ function getConfig() {
1532
1833
  if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
1533
1834
  if (p.toolInspection)
1534
1835
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
1535
- if (p.rules) mergedPolicy.rules.push(...p.rules);
1536
1836
  if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
1537
1837
  if (p.snapshot) {
1538
1838
  const s2 = p.snapshot;
@@ -1540,6 +1840,11 @@ function getConfig() {
1540
1840
  if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
1541
1841
  if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
1542
1842
  }
1843
+ if (p.dlp) {
1844
+ const d = p.dlp;
1845
+ if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
1846
+ if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
1847
+ }
1543
1848
  const envs = source.environments || {};
1544
1849
  for (const [envName, envConfig] of Object.entries(envs)) {
1545
1850
  if (envConfig && typeof envConfig === "object") {
@@ -1554,6 +1859,19 @@ function getConfig() {
1554
1859
  };
1555
1860
  applyLayer(globalConfig);
1556
1861
  applyLayer(projectConfig);
1862
+ for (const shieldName of readActiveShields()) {
1863
+ const shield = getShield(shieldName);
1864
+ if (!shield) continue;
1865
+ const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
1866
+ for (const rule of shield.smartRules) {
1867
+ if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
1868
+ }
1869
+ for (const word of shield.dangerousWords) mergedPolicy.dangerousWords.push(word);
1870
+ }
1871
+ const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
1872
+ for (const rule of ADVISORY_SMART_RULES) {
1873
+ if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
1874
+ }
1557
1875
  if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
1558
1876
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
1559
1877
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
@@ -1569,10 +1887,10 @@ function getConfig() {
1569
1887
  return cachedConfig;
1570
1888
  }
1571
1889
  function tryLoadConfig(filePath) {
1572
- if (!import_fs.default.existsSync(filePath)) return null;
1890
+ if (!import_fs2.default.existsSync(filePath)) return null;
1573
1891
  let raw;
1574
1892
  try {
1575
- raw = JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
1893
+ raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
1576
1894
  } catch (err) {
1577
1895
  const msg = err instanceof Error ? err.message : String(err);
1578
1896
  process.stderr.write(
@@ -1634,9 +1952,9 @@ function getCredentials() {
1634
1952
  };
1635
1953
  }
1636
1954
  try {
1637
- const credPath = import_path3.default.join(import_os.default.homedir(), ".node9", "credentials.json");
1638
- if (import_fs.default.existsSync(credPath)) {
1639
- const creds = JSON.parse(import_fs.default.readFileSync(credPath, "utf-8"));
1955
+ const credPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
1956
+ if (import_fs2.default.existsSync(credPath)) {
1957
+ const creds = JSON.parse(import_fs2.default.readFileSync(credPath, "utf-8"));
1640
1958
  const profileName = process.env.NODE9_PROFILE || "default";
1641
1959
  const profile = creds[profileName];
1642
1960
  if (profile?.apiKey) {
@@ -1671,9 +1989,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1671
1989
  context: {
1672
1990
  agent: meta?.agent,
1673
1991
  mcpServer: meta?.mcpServer,
1674
- hostname: import_os.default.hostname(),
1992
+ hostname: import_os2.default.hostname(),
1675
1993
  cwd: process.cwd(),
1676
- platform: import_os.default.platform()
1994
+ platform: import_os2.default.platform()
1677
1995
  }
1678
1996
  }),
1679
1997
  signal: AbortSignal.timeout(5e3)
@@ -1694,9 +2012,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
1694
2012
  context: {
1695
2013
  agent: meta?.agent,
1696
2014
  mcpServer: meta?.mcpServer,
1697
- hostname: import_os.default.hostname(),
2015
+ hostname: import_os2.default.hostname(),
1698
2016
  cwd: process.cwd(),
1699
- platform: import_os.default.platform()
2017
+ platform: import_os2.default.platform()
1700
2018
  },
1701
2019
  ...riskMetadata && { riskMetadata }
1702
2020
  }),