@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.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  // src/core.ts
2
2
  import chalk2 from "chalk";
3
3
  import { confirm } from "@inquirer/prompts";
4
- import fs from "fs";
5
- import path3 from "path";
6
- import os from "os";
4
+ import fs2 from "fs";
5
+ import path4 from "path";
6
+ import os2 from "os";
7
7
  import pm from "picomatch";
8
8
  import { parse } from "sh-syntax";
9
9
 
@@ -333,25 +333,26 @@ import { z } from "zod";
333
333
  var noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
334
334
  message: "Value must not contain literal newline characters (use \\n instead)"
335
335
  });
336
- var validRegex = noNewlines.refine(
337
- (s) => {
338
- try {
339
- new RegExp(s);
340
- return true;
341
- } catch {
342
- return false;
343
- }
344
- },
345
- { message: "Value must be a valid regular expression" }
346
- );
347
336
  var SmartConditionSchema = z.object({
348
337
  field: z.string().min(1, "Condition field must not be empty"),
349
- op: z.enum(["matches", "notMatches", "contains", "notContains", "exists", "notExists"], {
350
- errorMap: () => ({
351
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists"
352
- })
353
- }),
354
- value: validRegex.optional(),
338
+ op: z.enum(
339
+ [
340
+ "matches",
341
+ "notMatches",
342
+ "contains",
343
+ "notContains",
344
+ "exists",
345
+ "notExists",
346
+ "matchesGlob",
347
+ "notMatchesGlob"
348
+ ],
349
+ {
350
+ errorMap: () => ({
351
+ message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
352
+ })
353
+ }
354
+ ),
355
+ value: z.string().optional(),
355
356
  flags: z.string().optional()
356
357
  });
357
358
  var SmartRuleSchema = z.object({
@@ -364,11 +365,6 @@ var SmartRuleSchema = z.object({
364
365
  }),
365
366
  reason: z.string().optional()
366
367
  });
367
- var PolicyRuleSchema = z.object({
368
- action: z.string().min(1),
369
- allowPaths: z.array(z.string()).optional(),
370
- blockPaths: z.array(z.string()).optional()
371
- });
372
368
  var ConfigFileSchema = z.object({
373
369
  version: z.string().optional(),
374
370
  settings: z.object({
@@ -393,12 +389,15 @@ var ConfigFileSchema = z.object({
393
389
  dangerousWords: z.array(noNewlines).optional(),
394
390
  ignoredTools: z.array(z.string()).optional(),
395
391
  toolInspection: z.record(z.string()).optional(),
396
- rules: z.array(PolicyRuleSchema).optional(),
397
392
  smartRules: z.array(SmartRuleSchema).optional(),
398
393
  snapshot: z.object({
399
394
  tools: z.array(z.string()).optional(),
400
395
  onlyPaths: z.array(z.string()).optional(),
401
396
  ignorePaths: z.array(z.string()).optional()
397
+ }).optional(),
398
+ dlp: z.object({
399
+ enabled: z.boolean().optional(),
400
+ scanIgnoredTools: z.boolean().optional()
402
401
  }).optional()
403
402
  }).optional(),
404
403
  environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
@@ -420,8 +419,8 @@ function sanitizeConfig(raw) {
420
419
  }
421
420
  }
422
421
  const lines = result.error.issues.map((issue) => {
423
- const path4 = issue.path.length > 0 ? issue.path.join(".") : "root";
424
- return ` \u2022 ${path4}: ${issue.message}`;
422
+ const path5 = issue.path.length > 0 ? issue.path.join(".") : "root";
423
+ return ` \u2022 ${path5}: ${issue.message}`;
425
424
  });
426
425
  return {
427
426
  sanitized,
@@ -430,18 +429,291 @@ ${lines.join("\n")}`
430
429
  };
431
430
  }
432
431
 
432
+ // src/shields.ts
433
+ import fs from "fs";
434
+ import path3 from "path";
435
+ import os from "os";
436
+ var SHIELDS = {
437
+ postgres: {
438
+ name: "postgres",
439
+ description: "Protects PostgreSQL databases from destructive AI operations",
440
+ aliases: ["pg", "postgresql"],
441
+ smartRules: [
442
+ {
443
+ name: "shield:postgres:block-drop-table",
444
+ tool: "*",
445
+ conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
446
+ verdict: "block",
447
+ reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
448
+ },
449
+ {
450
+ name: "shield:postgres:block-truncate",
451
+ tool: "*",
452
+ conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
453
+ verdict: "block",
454
+ reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
455
+ },
456
+ {
457
+ name: "shield:postgres:block-drop-column",
458
+ tool: "*",
459
+ conditions: [
460
+ { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
461
+ ],
462
+ verdict: "block",
463
+ reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
464
+ },
465
+ {
466
+ name: "shield:postgres:review-grant-revoke",
467
+ tool: "*",
468
+ conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
469
+ verdict: "review",
470
+ reason: "Permission changes require human approval (Postgres shield)"
471
+ }
472
+ ],
473
+ dangerousWords: ["dropdb", "pg_dropcluster"]
474
+ },
475
+ github: {
476
+ name: "github",
477
+ description: "Protects GitHub repositories from destructive AI operations",
478
+ aliases: ["git"],
479
+ smartRules: [
480
+ {
481
+ // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
482
+ // This rule adds coverage for `git push --delete` which the built-in does not match.
483
+ name: "shield:github:review-delete-branch-remote",
484
+ tool: "bash",
485
+ conditions: [
486
+ {
487
+ field: "command",
488
+ op: "matches",
489
+ value: "git\\s+push\\s+.*--delete",
490
+ flags: "i"
491
+ }
492
+ ],
493
+ verdict: "review",
494
+ reason: "Remote branch deletion requires human approval (GitHub shield)"
495
+ },
496
+ {
497
+ name: "shield:github:block-delete-repo",
498
+ tool: "*",
499
+ conditions: [
500
+ { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
501
+ ],
502
+ verdict: "block",
503
+ reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
504
+ }
505
+ ],
506
+ dangerousWords: []
507
+ },
508
+ aws: {
509
+ name: "aws",
510
+ description: "Protects AWS infrastructure from destructive AI operations",
511
+ aliases: ["amazon"],
512
+ smartRules: [
513
+ {
514
+ name: "shield:aws:block-delete-s3-bucket",
515
+ tool: "*",
516
+ conditions: [
517
+ {
518
+ field: "command",
519
+ op: "matches",
520
+ value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
521
+ flags: "i"
522
+ }
523
+ ],
524
+ verdict: "block",
525
+ reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
526
+ },
527
+ {
528
+ name: "shield:aws:review-iam-changes",
529
+ tool: "*",
530
+ conditions: [
531
+ {
532
+ field: "command",
533
+ op: "matches",
534
+ value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
535
+ flags: "i"
536
+ }
537
+ ],
538
+ verdict: "review",
539
+ reason: "IAM changes require human approval (AWS shield)"
540
+ },
541
+ {
542
+ name: "shield:aws:block-ec2-terminate",
543
+ tool: "*",
544
+ conditions: [
545
+ {
546
+ field: "command",
547
+ op: "matches",
548
+ value: "aws\\s+ec2\\s+terminate-instances",
549
+ flags: "i"
550
+ }
551
+ ],
552
+ verdict: "block",
553
+ reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
554
+ },
555
+ {
556
+ name: "shield:aws:review-rds-delete",
557
+ tool: "*",
558
+ conditions: [
559
+ { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
560
+ ],
561
+ verdict: "review",
562
+ reason: "RDS deletion requires human approval (AWS shield)"
563
+ }
564
+ ],
565
+ dangerousWords: []
566
+ },
567
+ filesystem: {
568
+ name: "filesystem",
569
+ description: "Protects the local filesystem from dangerous AI operations",
570
+ aliases: ["fs"],
571
+ smartRules: [
572
+ {
573
+ name: "shield:filesystem:review-chmod-777",
574
+ tool: "bash",
575
+ conditions: [
576
+ { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
577
+ ],
578
+ verdict: "review",
579
+ reason: "chmod 777 requires human approval (filesystem shield)"
580
+ },
581
+ {
582
+ name: "shield:filesystem:review-write-etc",
583
+ tool: "bash",
584
+ conditions: [
585
+ {
586
+ field: "command",
587
+ // Narrow to write-indicative operations to avoid approval fatigue on reads.
588
+ // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
589
+ op: "matches",
590
+ value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
591
+ }
592
+ ],
593
+ verdict: "review",
594
+ reason: "Writing to /etc requires human approval (filesystem shield)"
595
+ }
596
+ ],
597
+ // dd removed: too common as a legitimate tool (disk imaging, file ops).
598
+ // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
599
+ // wipefs retained: rarely legitimate in an agent context and not in built-ins.
600
+ dangerousWords: ["wipefs"]
601
+ }
602
+ };
603
+ function resolveShieldName(input) {
604
+ const lower = input.toLowerCase();
605
+ if (SHIELDS[lower]) return lower;
606
+ for (const [name, def] of Object.entries(SHIELDS)) {
607
+ if (def.aliases.includes(lower)) return name;
608
+ }
609
+ return null;
610
+ }
611
+ function getShield(name) {
612
+ const resolved = resolveShieldName(name);
613
+ return resolved ? SHIELDS[resolved] : null;
614
+ }
615
+ var SHIELDS_STATE_FILE = path3.join(os.homedir(), ".node9", "shields.json");
616
+ function readActiveShields() {
617
+ try {
618
+ const raw = fs.readFileSync(SHIELDS_STATE_FILE, "utf-8");
619
+ if (!raw.trim()) return [];
620
+ const parsed = JSON.parse(raw);
621
+ if (Array.isArray(parsed.active)) {
622
+ return parsed.active.filter(
623
+ (e) => typeof e === "string" && e.length > 0 && e in SHIELDS
624
+ );
625
+ }
626
+ } catch (err) {
627
+ if (err.code !== "ENOENT") {
628
+ process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
629
+ `);
630
+ }
631
+ }
632
+ return [];
633
+ }
634
+
635
+ // src/dlp.ts
636
+ var DLP_PATTERNS = [
637
+ { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
638
+ { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
639
+ { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
640
+ { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
641
+ { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
642
+ {
643
+ name: "Private Key (PEM)",
644
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
645
+ severity: "block"
646
+ },
647
+ { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
648
+ ];
649
+ function maskSecret(raw, pattern) {
650
+ const match = raw.match(pattern);
651
+ if (!match) return "****";
652
+ const secret = match[0];
653
+ if (secret.length < 8) return "****";
654
+ const prefix = secret.slice(0, 4);
655
+ const suffix = secret.slice(-4);
656
+ const stars = "*".repeat(Math.min(secret.length - 8, 12));
657
+ return `${prefix}${stars}${suffix}`;
658
+ }
659
+ var MAX_DEPTH = 5;
660
+ var MAX_STRING_BYTES = 1e5;
661
+ var MAX_JSON_PARSE_BYTES = 1e4;
662
+ function scanArgs(args, depth = 0, fieldPath = "args") {
663
+ if (depth > MAX_DEPTH || args === null || args === void 0) return null;
664
+ if (Array.isArray(args)) {
665
+ for (let i = 0; i < args.length; i++) {
666
+ const match = scanArgs(args[i], depth + 1, `${fieldPath}[${i}]`);
667
+ if (match) return match;
668
+ }
669
+ return null;
670
+ }
671
+ if (typeof args === "object") {
672
+ for (const [key, value] of Object.entries(args)) {
673
+ const match = scanArgs(value, depth + 1, `${fieldPath}.${key}`);
674
+ if (match) return match;
675
+ }
676
+ return null;
677
+ }
678
+ if (typeof args === "string") {
679
+ const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
680
+ for (const pattern of DLP_PATTERNS) {
681
+ if (pattern.regex.test(text)) {
682
+ return {
683
+ patternName: pattern.name,
684
+ fieldPath,
685
+ redactedSample: maskSecret(text, pattern.regex),
686
+ severity: pattern.severity
687
+ };
688
+ }
689
+ }
690
+ if (text.length < MAX_JSON_PARSE_BYTES) {
691
+ const trimmed = text.trim();
692
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
693
+ try {
694
+ const parsed = JSON.parse(text);
695
+ const inner = scanArgs(parsed, depth + 1, fieldPath);
696
+ if (inner) return inner;
697
+ } catch {
698
+ }
699
+ }
700
+ }
701
+ }
702
+ return null;
703
+ }
704
+
433
705
  // src/core.ts
434
- var PAUSED_FILE = path3.join(os.homedir(), ".node9", "PAUSED");
435
- var TRUST_FILE = path3.join(os.homedir(), ".node9", "trust.json");
436
- var LOCAL_AUDIT_LOG = path3.join(os.homedir(), ".node9", "audit.log");
437
- var HOOK_DEBUG_LOG = path3.join(os.homedir(), ".node9", "hook-debug.log");
706
+ var PAUSED_FILE = path4.join(os2.homedir(), ".node9", "PAUSED");
707
+ var TRUST_FILE = path4.join(os2.homedir(), ".node9", "trust.json");
708
+ var LOCAL_AUDIT_LOG = path4.join(os2.homedir(), ".node9", "audit.log");
709
+ var HOOK_DEBUG_LOG = path4.join(os2.homedir(), ".node9", "hook-debug.log");
438
710
  function checkPause() {
439
711
  try {
440
- if (!fs.existsSync(PAUSED_FILE)) return { paused: false };
441
- const state = JSON.parse(fs.readFileSync(PAUSED_FILE, "utf-8"));
712
+ if (!fs2.existsSync(PAUSED_FILE)) return { paused: false };
713
+ const state = JSON.parse(fs2.readFileSync(PAUSED_FILE, "utf-8"));
442
714
  if (state.expiry > 0 && Date.now() >= state.expiry) {
443
715
  try {
444
- fs.unlinkSync(PAUSED_FILE);
716
+ fs2.unlinkSync(PAUSED_FILE);
445
717
  } catch {
446
718
  }
447
719
  return { paused: false };
@@ -452,20 +724,20 @@ function checkPause() {
452
724
  }
453
725
  }
454
726
  function atomicWriteSync(filePath, data, options) {
455
- const dir = path3.dirname(filePath);
456
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
457
- const tmpPath = `${filePath}.${os.hostname()}.${process.pid}.tmp`;
458
- fs.writeFileSync(tmpPath, data, options);
459
- fs.renameSync(tmpPath, filePath);
727
+ const dir = path4.dirname(filePath);
728
+ if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
729
+ const tmpPath = `${filePath}.${os2.hostname()}.${process.pid}.tmp`;
730
+ fs2.writeFileSync(tmpPath, data, options);
731
+ fs2.renameSync(tmpPath, filePath);
460
732
  }
461
733
  function getActiveTrustSession(toolName) {
462
734
  try {
463
- if (!fs.existsSync(TRUST_FILE)) return false;
464
- const trust = JSON.parse(fs.readFileSync(TRUST_FILE, "utf-8"));
735
+ if (!fs2.existsSync(TRUST_FILE)) return false;
736
+ const trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
465
737
  const now = Date.now();
466
738
  const active = trust.entries.filter((e) => e.expiry > now);
467
739
  if (active.length !== trust.entries.length) {
468
- fs.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
740
+ fs2.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
469
741
  }
470
742
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
471
743
  } catch {
@@ -476,8 +748,8 @@ function writeTrustSession(toolName, durationMs) {
476
748
  try {
477
749
  let trust = { entries: [] };
478
750
  try {
479
- if (fs.existsSync(TRUST_FILE)) {
480
- trust = JSON.parse(fs.readFileSync(TRUST_FILE, "utf-8"));
751
+ if (fs2.existsSync(TRUST_FILE)) {
752
+ trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
481
753
  }
482
754
  } catch {
483
755
  }
@@ -493,9 +765,9 @@ function writeTrustSession(toolName, durationMs) {
493
765
  }
494
766
  function appendToLog(logPath, entry) {
495
767
  try {
496
- const dir = path3.dirname(logPath);
497
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
498
- fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
768
+ const dir = path4.dirname(logPath);
769
+ if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
770
+ fs2.appendFileSync(logPath, JSON.stringify(entry) + "\n");
499
771
  } catch {
500
772
  }
501
773
  }
@@ -507,7 +779,7 @@ function appendHookDebug(toolName, args, meta) {
507
779
  args: safeArgs,
508
780
  agent: meta?.agent,
509
781
  mcpServer: meta?.mcpServer,
510
- hostname: os.hostname(),
782
+ hostname: os2.hostname(),
511
783
  cwd: process.cwd()
512
784
  });
513
785
  }
@@ -521,7 +793,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
521
793
  checkedBy,
522
794
  agent: meta?.agent,
523
795
  mcpServer: meta?.mcpServer,
524
- hostname: os.hostname()
796
+ hostname: os2.hostname()
525
797
  });
526
798
  }
527
799
  function tokenize(toolName) {
@@ -537,9 +809,9 @@ function matchesPattern(text, patterns) {
537
809
  const withoutDotSlash = text.replace(/^\.\//, "");
538
810
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
539
811
  }
540
- function getNestedValue(obj, path4) {
812
+ function getNestedValue(obj, path5) {
541
813
  if (!obj || typeof obj !== "object") return null;
542
- return path4.split(".").reduce((prev, curr) => prev?.[curr], obj);
814
+ return path5.split(".").reduce((prev, curr) => prev?.[curr], obj);
543
815
  }
544
816
  function evaluateSmartConditions(args, rule) {
545
817
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -572,6 +844,10 @@ function evaluateSmartConditions(args, rule) {
572
844
  return true;
573
845
  }
574
846
  }
847
+ case "matchesGlob":
848
+ return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
849
+ case "notMatchesGlob":
850
+ return val !== null && cond.value ? !pm.isMatch(val, cond.value) : true;
575
851
  default:
576
852
  return false;
577
853
  }
@@ -735,25 +1011,27 @@ var DEFAULT_CONFIG = {
735
1011
  onlyPaths: [],
736
1012
  ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
737
1013
  },
738
- rules: [
739
- // Only use the legacy rules format for simple path-based rm control.
740
- // All other command-level enforcement lives in smartRules below.
741
- {
742
- action: "rm",
743
- allowPaths: [
744
- "**/node_modules/**",
745
- "dist/**",
746
- "build/**",
747
- ".next/**",
748
- "coverage/**",
749
- ".cache/**",
750
- "tmp/**",
751
- "temp/**",
752
- ".DS_Store"
753
- ]
754
- }
755
- ],
756
1014
  smartRules: [
1015
+ // ── rm safety (critical — always evaluated first) ──────────────────────
1016
+ {
1017
+ name: "block-rm-rf-home",
1018
+ tool: "bash",
1019
+ conditionMode: "all",
1020
+ conditions: [
1021
+ {
1022
+ field: "command",
1023
+ op: "matches",
1024
+ value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
1025
+ },
1026
+ {
1027
+ field: "command",
1028
+ op: "matches",
1029
+ value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
1030
+ }
1031
+ ],
1032
+ verdict: "block",
1033
+ reason: "Recursive delete of home directory is irreversible"
1034
+ },
757
1035
  // ── SQL safety ────────────────────────────────────────────────────────
758
1036
  {
759
1037
  name: "no-delete-without-where",
@@ -844,16 +1122,42 @@ var DEFAULT_CONFIG = {
844
1122
  verdict: "block",
845
1123
  reason: "Piping remote script into a shell is a supply-chain attack vector"
846
1124
  }
847
- ]
1125
+ ],
1126
+ dlp: { enabled: true, scanIgnoredTools: true }
848
1127
  },
849
1128
  environments: {}
850
1129
  };
1130
+ var ADVISORY_SMART_RULES = [
1131
+ {
1132
+ name: "allow-rm-safe-paths",
1133
+ tool: "*",
1134
+ conditionMode: "all",
1135
+ conditions: [
1136
+ { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
1137
+ {
1138
+ field: "command",
1139
+ op: "matches",
1140
+ // Matches known-safe build artifact paths in the command.
1141
+ value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
1142
+ }
1143
+ ],
1144
+ verdict: "allow",
1145
+ reason: "Deleting a known-safe build artifact path"
1146
+ },
1147
+ {
1148
+ name: "review-rm",
1149
+ tool: "*",
1150
+ conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
1151
+ verdict: "review",
1152
+ reason: "rm can permanently delete files \u2014 confirm the target path"
1153
+ }
1154
+ ];
851
1155
  var cachedConfig = null;
852
1156
  function getInternalToken() {
853
1157
  try {
854
- const pidFile = path3.join(os.homedir(), ".node9", "daemon.pid");
855
- if (!fs.existsSync(pidFile)) return null;
856
- const data = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
1158
+ const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1159
+ if (!fs2.existsSync(pidFile)) return null;
1160
+ const data = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
857
1161
  process.kill(data.pid, 0);
858
1162
  return data.internalToken ?? null;
859
1163
  } catch {
@@ -868,7 +1172,8 @@ async function evaluatePolicy(toolName, args, agent) {
868
1172
  (rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
869
1173
  );
870
1174
  if (matchedRule) {
871
- if (matchedRule.verdict === "allow") return { decision: "allow" };
1175
+ if (matchedRule.verdict === "allow")
1176
+ return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
872
1177
  return {
873
1178
  decision: matchedRule.verdict,
874
1179
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
@@ -879,13 +1184,11 @@ async function evaluatePolicy(toolName, args, agent) {
879
1184
  }
880
1185
  }
881
1186
  let allTokens = [];
882
- let actionTokens = [];
883
1187
  let pathTokens = [];
884
1188
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
885
1189
  if (shellCommand) {
886
1190
  const analyzed = await analyzeShellCommand(shellCommand);
887
1191
  allTokens = analyzed.allTokens;
888
- actionTokens = analyzed.actions;
889
1192
  pathTokens = analyzed.paths;
890
1193
  const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
891
1194
  if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
@@ -893,11 +1196,9 @@ async function evaluatePolicy(toolName, args, agent) {
893
1196
  }
894
1197
  if (isSqlTool(toolName, config.policy.toolInspection)) {
895
1198
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
896
- actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
897
1199
  }
898
1200
  } else {
899
1201
  allTokens = tokenize(toolName);
900
- actionTokens = [toolName];
901
1202
  if (args && typeof args === "object") {
902
1203
  const flattenedArgs = JSON.stringify(args).toLowerCase();
903
1204
  const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
@@ -920,29 +1221,6 @@ async function evaluatePolicy(toolName, args, agent) {
920
1221
  const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
921
1222
  if (allInSandbox) return { decision: "allow" };
922
1223
  }
923
- for (const action of actionTokens) {
924
- const rule = config.policy.rules.find(
925
- (r) => r.action === action || matchesPattern(action, r.action)
926
- );
927
- if (rule) {
928
- if (pathTokens.length > 0) {
929
- const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
930
- if (anyBlocked)
931
- return {
932
- decision: "review",
933
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
934
- tier: 5
935
- };
936
- const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
937
- if (allAllowed) return { decision: "allow" };
938
- }
939
- return {
940
- decision: "review",
941
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
942
- tier: 5
943
- };
944
- }
945
- }
946
1224
  let matchedDangerousWord;
947
1225
  const isDangerous = allTokens.some(
948
1226
  (token) => config.policy.dangerousWords.some((word) => {
@@ -1000,9 +1278,9 @@ var DAEMON_PORT = 7391;
1000
1278
  var DAEMON_HOST = "127.0.0.1";
1001
1279
  function isDaemonRunning() {
1002
1280
  try {
1003
- const pidFile = path3.join(os.homedir(), ".node9", "daemon.pid");
1004
- if (!fs.existsSync(pidFile)) return false;
1005
- const { pid, port } = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
1281
+ const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1282
+ if (!fs2.existsSync(pidFile)) return false;
1283
+ const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1006
1284
  if (port !== DAEMON_PORT) return false;
1007
1285
  process.kill(pid, 0);
1008
1286
  return true;
@@ -1012,9 +1290,9 @@ function isDaemonRunning() {
1012
1290
  }
1013
1291
  function getPersistentDecision(toolName) {
1014
1292
  try {
1015
- const file = path3.join(os.homedir(), ".node9", "decisions.json");
1016
- if (!fs.existsSync(file)) return null;
1017
- const decisions = JSON.parse(fs.readFileSync(file, "utf-8"));
1293
+ const file = path4.join(os2.homedir(), ".node9", "decisions.json");
1294
+ if (!fs2.existsSync(file)) return null;
1295
+ const decisions = JSON.parse(fs2.readFileSync(file, "utf-8"));
1018
1296
  const d = decisions[toolName];
1019
1297
  if (d === "allow" || d === "deny") return d;
1020
1298
  } catch {
@@ -1113,6 +1391,22 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1113
1391
  let policyMatchedField;
1114
1392
  let policyMatchedWord;
1115
1393
  let riskMetadata;
1394
+ if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
1395
+ const dlpMatch = scanArgs(args);
1396
+ if (dlpMatch) {
1397
+ const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
1398
+ if (dlpMatch.severity === "block") {
1399
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
1400
+ return {
1401
+ approved: false,
1402
+ reason: dlpReason,
1403
+ blockedBy: "local-config",
1404
+ blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
1405
+ };
1406
+ }
1407
+ explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
1408
+ }
1409
+ }
1116
1410
  if (config.settings.mode === "audit") {
1117
1411
  if (!isIgnoredTool(toolName)) {
1118
1412
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
@@ -1352,7 +1646,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1352
1646
  racePromises.push(
1353
1647
  (async () => {
1354
1648
  try {
1355
- console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
1649
+ if (explainableLabel.includes("DLP")) {
1650
+ console.log(chalk2.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
1651
+ console.log(
1652
+ chalk2.red.bold(` A sensitive secret was detected in the tool arguments!`)
1653
+ );
1654
+ } else {
1655
+ console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
1656
+ }
1356
1657
  console.log(`${chalk2.bold("Action:")} ${chalk2.red(toolName)}`);
1357
1658
  console.log(`${chalk2.bold("Flagged By:")} ${chalk2.yellow(explainableLabel)}`);
1358
1659
  if (isRemoteLocked) {
@@ -1457,8 +1758,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1457
1758
  }
1458
1759
  function getConfig() {
1459
1760
  if (cachedConfig) return cachedConfig;
1460
- const globalPath = path3.join(os.homedir(), ".node9", "config.json");
1461
- const projectPath = path3.join(process.cwd(), "node9.config.json");
1761
+ const globalPath = path4.join(os2.homedir(), ".node9", "config.json");
1762
+ const projectPath = path4.join(process.cwd(), "node9.config.json");
1462
1763
  const globalConfig = tryLoadConfig(globalPath);
1463
1764
  const projectConfig = tryLoadConfig(projectPath);
1464
1765
  const mergedSettings = {
@@ -1470,13 +1771,13 @@ function getConfig() {
1470
1771
  dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
1471
1772
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
1472
1773
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
1473
- rules: [...DEFAULT_CONFIG.policy.rules],
1474
1774
  smartRules: [...DEFAULT_CONFIG.policy.smartRules],
1475
1775
  snapshot: {
1476
1776
  tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
1477
1777
  onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
1478
1778
  ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
1479
- }
1779
+ },
1780
+ dlp: { ...DEFAULT_CONFIG.policy.dlp }
1480
1781
  };
1481
1782
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
1482
1783
  const applyLayer = (source) => {
@@ -1496,7 +1797,6 @@ function getConfig() {
1496
1797
  if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
1497
1798
  if (p.toolInspection)
1498
1799
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
1499
- if (p.rules) mergedPolicy.rules.push(...p.rules);
1500
1800
  if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
1501
1801
  if (p.snapshot) {
1502
1802
  const s2 = p.snapshot;
@@ -1504,6 +1804,11 @@ function getConfig() {
1504
1804
  if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
1505
1805
  if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
1506
1806
  }
1807
+ if (p.dlp) {
1808
+ const d = p.dlp;
1809
+ if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
1810
+ if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
1811
+ }
1507
1812
  const envs = source.environments || {};
1508
1813
  for (const [envName, envConfig] of Object.entries(envs)) {
1509
1814
  if (envConfig && typeof envConfig === "object") {
@@ -1518,6 +1823,19 @@ function getConfig() {
1518
1823
  };
1519
1824
  applyLayer(globalConfig);
1520
1825
  applyLayer(projectConfig);
1826
+ for (const shieldName of readActiveShields()) {
1827
+ const shield = getShield(shieldName);
1828
+ if (!shield) continue;
1829
+ const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
1830
+ for (const rule of shield.smartRules) {
1831
+ if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
1832
+ }
1833
+ for (const word of shield.dangerousWords) mergedPolicy.dangerousWords.push(word);
1834
+ }
1835
+ const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
1836
+ for (const rule of ADVISORY_SMART_RULES) {
1837
+ if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
1838
+ }
1521
1839
  if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
1522
1840
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
1523
1841
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
@@ -1533,10 +1851,10 @@ function getConfig() {
1533
1851
  return cachedConfig;
1534
1852
  }
1535
1853
  function tryLoadConfig(filePath) {
1536
- if (!fs.existsSync(filePath)) return null;
1854
+ if (!fs2.existsSync(filePath)) return null;
1537
1855
  let raw;
1538
1856
  try {
1539
- raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
1857
+ raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
1540
1858
  } catch (err) {
1541
1859
  const msg = err instanceof Error ? err.message : String(err);
1542
1860
  process.stderr.write(
@@ -1598,9 +1916,9 @@ function getCredentials() {
1598
1916
  };
1599
1917
  }
1600
1918
  try {
1601
- const credPath = path3.join(os.homedir(), ".node9", "credentials.json");
1602
- if (fs.existsSync(credPath)) {
1603
- const creds = JSON.parse(fs.readFileSync(credPath, "utf-8"));
1919
+ const credPath = path4.join(os2.homedir(), ".node9", "credentials.json");
1920
+ if (fs2.existsSync(credPath)) {
1921
+ const creds = JSON.parse(fs2.readFileSync(credPath, "utf-8"));
1604
1922
  const profileName = process.env.NODE9_PROFILE || "default";
1605
1923
  const profile = creds[profileName];
1606
1924
  if (profile?.apiKey) {
@@ -1635,9 +1953,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1635
1953
  context: {
1636
1954
  agent: meta?.agent,
1637
1955
  mcpServer: meta?.mcpServer,
1638
- hostname: os.hostname(),
1956
+ hostname: os2.hostname(),
1639
1957
  cwd: process.cwd(),
1640
- platform: os.platform()
1958
+ platform: os2.platform()
1641
1959
  }
1642
1960
  }),
1643
1961
  signal: AbortSignal.timeout(5e3)
@@ -1658,9 +1976,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
1658
1976
  context: {
1659
1977
  agent: meta?.agent,
1660
1978
  mcpServer: meta?.mcpServer,
1661
- hostname: os.hostname(),
1979
+ hostname: os2.hostname(),
1662
1980
  cwd: process.cwd(),
1663
- platform: os.platform()
1981
+ platform: os2.platform()
1664
1982
  },
1665
1983
  ...riskMetadata && { riskMetadata }
1666
1984
  }),