@node9/proxy 1.0.13 → 1.0.15
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/README.md +188 -119
- package/dist/cli.js +2335 -1097
- package/dist/cli.mjs +2315 -1075
- package/dist/index.js +500 -125
- package/dist/index.mjs +500 -125
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -37,9 +37,11 @@ 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
|
|
41
|
-
var
|
|
42
|
-
var
|
|
40
|
+
var import_fs2 = __toESM(require("fs"));
|
|
41
|
+
var import_path4 = __toESM(require("path"));
|
|
42
|
+
var import_os2 = __toESM(require("os"));
|
|
43
|
+
var import_net = __toESM(require("net"));
|
|
44
|
+
var import_crypto = require("crypto");
|
|
43
45
|
var import_picomatch = __toESM(require("picomatch"));
|
|
44
46
|
var import_sh_syntax = require("sh-syntax");
|
|
45
47
|
|
|
@@ -369,25 +371,26 @@ var import_zod = require("zod");
|
|
|
369
371
|
var noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
370
372
|
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
371
373
|
});
|
|
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
374
|
var SmartConditionSchema = import_zod.z.object({
|
|
384
375
|
field: import_zod.z.string().min(1, "Condition field must not be empty"),
|
|
385
|
-
op: import_zod.z.enum(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
376
|
+
op: import_zod.z.enum(
|
|
377
|
+
[
|
|
378
|
+
"matches",
|
|
379
|
+
"notMatches",
|
|
380
|
+
"contains",
|
|
381
|
+
"notContains",
|
|
382
|
+
"exists",
|
|
383
|
+
"notExists",
|
|
384
|
+
"matchesGlob",
|
|
385
|
+
"notMatchesGlob"
|
|
386
|
+
],
|
|
387
|
+
{
|
|
388
|
+
errorMap: () => ({
|
|
389
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
),
|
|
393
|
+
value: import_zod.z.string().optional(),
|
|
391
394
|
flags: import_zod.z.string().optional()
|
|
392
395
|
});
|
|
393
396
|
var SmartRuleSchema = import_zod.z.object({
|
|
@@ -400,11 +403,6 @@ var SmartRuleSchema = import_zod.z.object({
|
|
|
400
403
|
}),
|
|
401
404
|
reason: import_zod.z.string().optional()
|
|
402
405
|
});
|
|
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
406
|
var ConfigFileSchema = import_zod.z.object({
|
|
409
407
|
version: import_zod.z.string().optional(),
|
|
410
408
|
settings: import_zod.z.object({
|
|
@@ -429,12 +427,15 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
429
427
|
dangerousWords: import_zod.z.array(noNewlines).optional(),
|
|
430
428
|
ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
431
429
|
toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
|
|
432
|
-
rules: import_zod.z.array(PolicyRuleSchema).optional(),
|
|
433
430
|
smartRules: import_zod.z.array(SmartRuleSchema).optional(),
|
|
434
431
|
snapshot: import_zod.z.object({
|
|
435
432
|
tools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
436
433
|
onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
437
434
|
ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
435
|
+
}).optional(),
|
|
436
|
+
dlp: import_zod.z.object({
|
|
437
|
+
enabled: import_zod.z.boolean().optional(),
|
|
438
|
+
scanIgnoredTools: import_zod.z.boolean().optional()
|
|
438
439
|
}).optional()
|
|
439
440
|
}).optional(),
|
|
440
441
|
environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
|
|
@@ -456,8 +457,8 @@ function sanitizeConfig(raw) {
|
|
|
456
457
|
}
|
|
457
458
|
}
|
|
458
459
|
const lines = result.error.issues.map((issue) => {
|
|
459
|
-
const
|
|
460
|
-
return ` \u2022 ${
|
|
460
|
+
const path5 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
461
|
+
return ` \u2022 ${path5}: ${issue.message}`;
|
|
461
462
|
});
|
|
462
463
|
return {
|
|
463
464
|
sanitized,
|
|
@@ -466,18 +467,291 @@ ${lines.join("\n")}`
|
|
|
466
467
|
};
|
|
467
468
|
}
|
|
468
469
|
|
|
470
|
+
// src/shields.ts
|
|
471
|
+
var import_fs = __toESM(require("fs"));
|
|
472
|
+
var import_path3 = __toESM(require("path"));
|
|
473
|
+
var import_os = __toESM(require("os"));
|
|
474
|
+
var SHIELDS = {
|
|
475
|
+
postgres: {
|
|
476
|
+
name: "postgres",
|
|
477
|
+
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
478
|
+
aliases: ["pg", "postgresql"],
|
|
479
|
+
smartRules: [
|
|
480
|
+
{
|
|
481
|
+
name: "shield:postgres:block-drop-table",
|
|
482
|
+
tool: "*",
|
|
483
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
484
|
+
verdict: "block",
|
|
485
|
+
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: "shield:postgres:block-truncate",
|
|
489
|
+
tool: "*",
|
|
490
|
+
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
491
|
+
verdict: "block",
|
|
492
|
+
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: "shield:postgres:block-drop-column",
|
|
496
|
+
tool: "*",
|
|
497
|
+
conditions: [
|
|
498
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
499
|
+
],
|
|
500
|
+
verdict: "block",
|
|
501
|
+
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
name: "shield:postgres:review-grant-revoke",
|
|
505
|
+
tool: "*",
|
|
506
|
+
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
507
|
+
verdict: "review",
|
|
508
|
+
reason: "Permission changes require human approval (Postgres shield)"
|
|
509
|
+
}
|
|
510
|
+
],
|
|
511
|
+
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
512
|
+
},
|
|
513
|
+
github: {
|
|
514
|
+
name: "github",
|
|
515
|
+
description: "Protects GitHub repositories from destructive AI operations",
|
|
516
|
+
aliases: ["git"],
|
|
517
|
+
smartRules: [
|
|
518
|
+
{
|
|
519
|
+
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
520
|
+
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
521
|
+
name: "shield:github:review-delete-branch-remote",
|
|
522
|
+
tool: "bash",
|
|
523
|
+
conditions: [
|
|
524
|
+
{
|
|
525
|
+
field: "command",
|
|
526
|
+
op: "matches",
|
|
527
|
+
value: "git\\s+push\\s+.*--delete",
|
|
528
|
+
flags: "i"
|
|
529
|
+
}
|
|
530
|
+
],
|
|
531
|
+
verdict: "review",
|
|
532
|
+
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
name: "shield:github:block-delete-repo",
|
|
536
|
+
tool: "*",
|
|
537
|
+
conditions: [
|
|
538
|
+
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
539
|
+
],
|
|
540
|
+
verdict: "block",
|
|
541
|
+
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
542
|
+
}
|
|
543
|
+
],
|
|
544
|
+
dangerousWords: []
|
|
545
|
+
},
|
|
546
|
+
aws: {
|
|
547
|
+
name: "aws",
|
|
548
|
+
description: "Protects AWS infrastructure from destructive AI operations",
|
|
549
|
+
aliases: ["amazon"],
|
|
550
|
+
smartRules: [
|
|
551
|
+
{
|
|
552
|
+
name: "shield:aws:block-delete-s3-bucket",
|
|
553
|
+
tool: "*",
|
|
554
|
+
conditions: [
|
|
555
|
+
{
|
|
556
|
+
field: "command",
|
|
557
|
+
op: "matches",
|
|
558
|
+
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
559
|
+
flags: "i"
|
|
560
|
+
}
|
|
561
|
+
],
|
|
562
|
+
verdict: "block",
|
|
563
|
+
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
name: "shield:aws:review-iam-changes",
|
|
567
|
+
tool: "*",
|
|
568
|
+
conditions: [
|
|
569
|
+
{
|
|
570
|
+
field: "command",
|
|
571
|
+
op: "matches",
|
|
572
|
+
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
573
|
+
flags: "i"
|
|
574
|
+
}
|
|
575
|
+
],
|
|
576
|
+
verdict: "review",
|
|
577
|
+
reason: "IAM changes require human approval (AWS shield)"
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
name: "shield:aws:block-ec2-terminate",
|
|
581
|
+
tool: "*",
|
|
582
|
+
conditions: [
|
|
583
|
+
{
|
|
584
|
+
field: "command",
|
|
585
|
+
op: "matches",
|
|
586
|
+
value: "aws\\s+ec2\\s+terminate-instances",
|
|
587
|
+
flags: "i"
|
|
588
|
+
}
|
|
589
|
+
],
|
|
590
|
+
verdict: "block",
|
|
591
|
+
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
name: "shield:aws:review-rds-delete",
|
|
595
|
+
tool: "*",
|
|
596
|
+
conditions: [
|
|
597
|
+
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
598
|
+
],
|
|
599
|
+
verdict: "review",
|
|
600
|
+
reason: "RDS deletion requires human approval (AWS shield)"
|
|
601
|
+
}
|
|
602
|
+
],
|
|
603
|
+
dangerousWords: []
|
|
604
|
+
},
|
|
605
|
+
filesystem: {
|
|
606
|
+
name: "filesystem",
|
|
607
|
+
description: "Protects the local filesystem from dangerous AI operations",
|
|
608
|
+
aliases: ["fs"],
|
|
609
|
+
smartRules: [
|
|
610
|
+
{
|
|
611
|
+
name: "shield:filesystem:review-chmod-777",
|
|
612
|
+
tool: "bash",
|
|
613
|
+
conditions: [
|
|
614
|
+
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
615
|
+
],
|
|
616
|
+
verdict: "review",
|
|
617
|
+
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
name: "shield:filesystem:review-write-etc",
|
|
621
|
+
tool: "bash",
|
|
622
|
+
conditions: [
|
|
623
|
+
{
|
|
624
|
+
field: "command",
|
|
625
|
+
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
626
|
+
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
627
|
+
op: "matches",
|
|
628
|
+
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
629
|
+
}
|
|
630
|
+
],
|
|
631
|
+
verdict: "review",
|
|
632
|
+
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
633
|
+
}
|
|
634
|
+
],
|
|
635
|
+
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
636
|
+
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
637
|
+
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
638
|
+
dangerousWords: ["wipefs"]
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
function resolveShieldName(input) {
|
|
642
|
+
const lower = input.toLowerCase();
|
|
643
|
+
if (SHIELDS[lower]) return lower;
|
|
644
|
+
for (const [name, def] of Object.entries(SHIELDS)) {
|
|
645
|
+
if (def.aliases.includes(lower)) return name;
|
|
646
|
+
}
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
function getShield(name) {
|
|
650
|
+
const resolved = resolveShieldName(name);
|
|
651
|
+
return resolved ? SHIELDS[resolved] : null;
|
|
652
|
+
}
|
|
653
|
+
var SHIELDS_STATE_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "shields.json");
|
|
654
|
+
function readActiveShields() {
|
|
655
|
+
try {
|
|
656
|
+
const raw = import_fs.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
657
|
+
if (!raw.trim()) return [];
|
|
658
|
+
const parsed = JSON.parse(raw);
|
|
659
|
+
if (Array.isArray(parsed.active)) {
|
|
660
|
+
return parsed.active.filter(
|
|
661
|
+
(e) => typeof e === "string" && e.length > 0 && e in SHIELDS
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
} catch (err) {
|
|
665
|
+
if (err.code !== "ENOENT") {
|
|
666
|
+
process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
|
|
667
|
+
`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return [];
|
|
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
|
+
|
|
469
743
|
// src/core.ts
|
|
470
|
-
var PAUSED_FILE =
|
|
471
|
-
var TRUST_FILE =
|
|
472
|
-
var LOCAL_AUDIT_LOG =
|
|
473
|
-
var 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");
|
|
474
748
|
function checkPause() {
|
|
475
749
|
try {
|
|
476
|
-
if (!
|
|
477
|
-
const state = JSON.parse(
|
|
750
|
+
if (!import_fs2.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
751
|
+
const state = JSON.parse(import_fs2.default.readFileSync(PAUSED_FILE, "utf-8"));
|
|
478
752
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
479
753
|
try {
|
|
480
|
-
|
|
754
|
+
import_fs2.default.unlinkSync(PAUSED_FILE);
|
|
481
755
|
} catch {
|
|
482
756
|
}
|
|
483
757
|
return { paused: false };
|
|
@@ -488,20 +762,20 @@ function checkPause() {
|
|
|
488
762
|
}
|
|
489
763
|
}
|
|
490
764
|
function atomicWriteSync(filePath, data, options) {
|
|
491
|
-
const dir =
|
|
492
|
-
if (!
|
|
493
|
-
const tmpPath = `${filePath}.${
|
|
494
|
-
|
|
495
|
-
|
|
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);
|
|
496
770
|
}
|
|
497
771
|
function getActiveTrustSession(toolName) {
|
|
498
772
|
try {
|
|
499
|
-
if (!
|
|
500
|
-
const trust = JSON.parse(
|
|
773
|
+
if (!import_fs2.default.existsSync(TRUST_FILE)) return false;
|
|
774
|
+
const trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
501
775
|
const now = Date.now();
|
|
502
776
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
503
777
|
if (active.length !== trust.entries.length) {
|
|
504
|
-
|
|
778
|
+
import_fs2.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
505
779
|
}
|
|
506
780
|
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
507
781
|
} catch {
|
|
@@ -512,8 +786,8 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
512
786
|
try {
|
|
513
787
|
let trust = { entries: [] };
|
|
514
788
|
try {
|
|
515
|
-
if (
|
|
516
|
-
trust = JSON.parse(
|
|
789
|
+
if (import_fs2.default.existsSync(TRUST_FILE)) {
|
|
790
|
+
trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
517
791
|
}
|
|
518
792
|
} catch {
|
|
519
793
|
}
|
|
@@ -529,9 +803,9 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
529
803
|
}
|
|
530
804
|
function appendToLog(logPath, entry) {
|
|
531
805
|
try {
|
|
532
|
-
const dir =
|
|
533
|
-
if (!
|
|
534
|
-
|
|
806
|
+
const dir = import_path4.default.dirname(logPath);
|
|
807
|
+
if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
|
|
808
|
+
import_fs2.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
535
809
|
} catch {
|
|
536
810
|
}
|
|
537
811
|
}
|
|
@@ -543,7 +817,7 @@ function appendHookDebug(toolName, args, meta) {
|
|
|
543
817
|
args: safeArgs,
|
|
544
818
|
agent: meta?.agent,
|
|
545
819
|
mcpServer: meta?.mcpServer,
|
|
546
|
-
hostname:
|
|
820
|
+
hostname: import_os2.default.hostname(),
|
|
547
821
|
cwd: process.cwd()
|
|
548
822
|
});
|
|
549
823
|
}
|
|
@@ -557,7 +831,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
|
557
831
|
checkedBy,
|
|
558
832
|
agent: meta?.agent,
|
|
559
833
|
mcpServer: meta?.mcpServer,
|
|
560
|
-
hostname:
|
|
834
|
+
hostname: import_os2.default.hostname()
|
|
561
835
|
});
|
|
562
836
|
}
|
|
563
837
|
function tokenize(toolName) {
|
|
@@ -573,9 +847,9 @@ function matchesPattern(text, patterns) {
|
|
|
573
847
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
574
848
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
575
849
|
}
|
|
576
|
-
function getNestedValue(obj,
|
|
850
|
+
function getNestedValue(obj, path5) {
|
|
577
851
|
if (!obj || typeof obj !== "object") return null;
|
|
578
|
-
return
|
|
852
|
+
return path5.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
579
853
|
}
|
|
580
854
|
function evaluateSmartConditions(args, rule) {
|
|
581
855
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -608,6 +882,10 @@ function evaluateSmartConditions(args, rule) {
|
|
|
608
882
|
return true;
|
|
609
883
|
}
|
|
610
884
|
}
|
|
885
|
+
case "matchesGlob":
|
|
886
|
+
return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
|
|
887
|
+
case "notMatchesGlob":
|
|
888
|
+
return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : true;
|
|
611
889
|
default:
|
|
612
890
|
return false;
|
|
613
891
|
}
|
|
@@ -771,25 +1049,27 @@ var DEFAULT_CONFIG = {
|
|
|
771
1049
|
onlyPaths: [],
|
|
772
1050
|
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
773
1051
|
},
|
|
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
1052
|
smartRules: [
|
|
1053
|
+
// ── rm safety (critical — always evaluated first) ──────────────────────
|
|
1054
|
+
{
|
|
1055
|
+
name: "block-rm-rf-home",
|
|
1056
|
+
tool: "bash",
|
|
1057
|
+
conditionMode: "all",
|
|
1058
|
+
conditions: [
|
|
1059
|
+
{
|
|
1060
|
+
field: "command",
|
|
1061
|
+
op: "matches",
|
|
1062
|
+
value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
field: "command",
|
|
1066
|
+
op: "matches",
|
|
1067
|
+
value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
|
|
1068
|
+
}
|
|
1069
|
+
],
|
|
1070
|
+
verdict: "block",
|
|
1071
|
+
reason: "Recursive delete of home directory is irreversible"
|
|
1072
|
+
},
|
|
793
1073
|
// ── SQL safety ────────────────────────────────────────────────────────
|
|
794
1074
|
{
|
|
795
1075
|
name: "no-delete-without-where",
|
|
@@ -880,16 +1160,42 @@ var DEFAULT_CONFIG = {
|
|
|
880
1160
|
verdict: "block",
|
|
881
1161
|
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
882
1162
|
}
|
|
883
|
-
]
|
|
1163
|
+
],
|
|
1164
|
+
dlp: { enabled: true, scanIgnoredTools: true }
|
|
884
1165
|
},
|
|
885
1166
|
environments: {}
|
|
886
1167
|
};
|
|
1168
|
+
var ADVISORY_SMART_RULES = [
|
|
1169
|
+
{
|
|
1170
|
+
name: "allow-rm-safe-paths",
|
|
1171
|
+
tool: "*",
|
|
1172
|
+
conditionMode: "all",
|
|
1173
|
+
conditions: [
|
|
1174
|
+
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
1175
|
+
{
|
|
1176
|
+
field: "command",
|
|
1177
|
+
op: "matches",
|
|
1178
|
+
// Matches known-safe build artifact paths in the command.
|
|
1179
|
+
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
1180
|
+
}
|
|
1181
|
+
],
|
|
1182
|
+
verdict: "allow",
|
|
1183
|
+
reason: "Deleting a known-safe build artifact path"
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
name: "review-rm",
|
|
1187
|
+
tool: "*",
|
|
1188
|
+
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
1189
|
+
verdict: "review",
|
|
1190
|
+
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
1191
|
+
}
|
|
1192
|
+
];
|
|
887
1193
|
var cachedConfig = null;
|
|
888
1194
|
function getInternalToken() {
|
|
889
1195
|
try {
|
|
890
|
-
const pidFile =
|
|
891
|
-
if (!
|
|
892
|
-
const data = JSON.parse(
|
|
1196
|
+
const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
|
|
1197
|
+
if (!import_fs2.default.existsSync(pidFile)) return null;
|
|
1198
|
+
const data = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
|
|
893
1199
|
process.kill(data.pid, 0);
|
|
894
1200
|
return data.internalToken ?? null;
|
|
895
1201
|
} catch {
|
|
@@ -904,7 +1210,8 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
904
1210
|
(rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
|
|
905
1211
|
);
|
|
906
1212
|
if (matchedRule) {
|
|
907
|
-
if (matchedRule.verdict === "allow")
|
|
1213
|
+
if (matchedRule.verdict === "allow")
|
|
1214
|
+
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
908
1215
|
return {
|
|
909
1216
|
decision: matchedRule.verdict,
|
|
910
1217
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
@@ -915,13 +1222,11 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
915
1222
|
}
|
|
916
1223
|
}
|
|
917
1224
|
let allTokens = [];
|
|
918
|
-
let actionTokens = [];
|
|
919
1225
|
let pathTokens = [];
|
|
920
1226
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
921
1227
|
if (shellCommand) {
|
|
922
1228
|
const analyzed = await analyzeShellCommand(shellCommand);
|
|
923
1229
|
allTokens = analyzed.allTokens;
|
|
924
|
-
actionTokens = analyzed.actions;
|
|
925
1230
|
pathTokens = analyzed.paths;
|
|
926
1231
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
927
1232
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
@@ -929,11 +1234,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
929
1234
|
}
|
|
930
1235
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
931
1236
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
932
|
-
actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
933
1237
|
}
|
|
934
1238
|
} else {
|
|
935
1239
|
allTokens = tokenize(toolName);
|
|
936
|
-
actionTokens = [toolName];
|
|
937
1240
|
if (args && typeof args === "object") {
|
|
938
1241
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
939
1242
|
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
@@ -956,29 +1259,6 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
956
1259
|
const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
|
|
957
1260
|
if (allInSandbox) return { decision: "allow" };
|
|
958
1261
|
}
|
|
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
1262
|
let matchedDangerousWord;
|
|
983
1263
|
const isDangerous = allTokens.some(
|
|
984
1264
|
(token) => config.policy.dangerousWords.some((word) => {
|
|
@@ -1036,9 +1316,9 @@ var DAEMON_PORT = 7391;
|
|
|
1036
1316
|
var DAEMON_HOST = "127.0.0.1";
|
|
1037
1317
|
function isDaemonRunning() {
|
|
1038
1318
|
try {
|
|
1039
|
-
const pidFile =
|
|
1040
|
-
if (!
|
|
1041
|
-
const { pid, port } = JSON.parse(
|
|
1319
|
+
const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
|
|
1320
|
+
if (!import_fs2.default.existsSync(pidFile)) return false;
|
|
1321
|
+
const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
|
|
1042
1322
|
if (port !== DAEMON_PORT) return false;
|
|
1043
1323
|
process.kill(pid, 0);
|
|
1044
1324
|
return true;
|
|
@@ -1048,16 +1328,16 @@ function isDaemonRunning() {
|
|
|
1048
1328
|
}
|
|
1049
1329
|
function getPersistentDecision(toolName) {
|
|
1050
1330
|
try {
|
|
1051
|
-
const file =
|
|
1052
|
-
if (!
|
|
1053
|
-
const decisions = JSON.parse(
|
|
1331
|
+
const file = import_path4.default.join(import_os2.default.homedir(), ".node9", "decisions.json");
|
|
1332
|
+
if (!import_fs2.default.existsSync(file)) return null;
|
|
1333
|
+
const decisions = JSON.parse(import_fs2.default.readFileSync(file, "utf-8"));
|
|
1054
1334
|
const d = decisions[toolName];
|
|
1055
1335
|
if (d === "allow" || d === "deny") return d;
|
|
1056
1336
|
} catch {
|
|
1057
1337
|
}
|
|
1058
1338
|
return null;
|
|
1059
1339
|
}
|
|
1060
|
-
async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
1340
|
+
async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
|
|
1061
1341
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1062
1342
|
const checkCtrl = new AbortController();
|
|
1063
1343
|
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
@@ -1072,6 +1352,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
|
1072
1352
|
args,
|
|
1073
1353
|
agent: meta?.agent,
|
|
1074
1354
|
mcpServer: meta?.mcpServer,
|
|
1355
|
+
fromCLI: true,
|
|
1356
|
+
// Pass the flight-recorder ID so the daemon uses the same UUID for
|
|
1357
|
+
// activity-result as the CLI used for the pending activity event.
|
|
1358
|
+
// Without this, the two UUIDs never match and tail.ts never resolves
|
|
1359
|
+
// the pending item.
|
|
1360
|
+
activityId,
|
|
1075
1361
|
...riskMetadata && { riskMetadata }
|
|
1076
1362
|
}),
|
|
1077
1363
|
signal: checkCtrl.signal
|
|
@@ -1126,7 +1412,45 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1126
1412
|
signal: AbortSignal.timeout(3e3)
|
|
1127
1413
|
});
|
|
1128
1414
|
}
|
|
1415
|
+
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path4.default.join(import_os2.default.tmpdir(), "node9-activity.sock");
|
|
1416
|
+
function notifyActivity(data) {
|
|
1417
|
+
return new Promise((resolve) => {
|
|
1418
|
+
try {
|
|
1419
|
+
const payload = JSON.stringify(data);
|
|
1420
|
+
const sock = import_net.default.createConnection(ACTIVITY_SOCKET_PATH);
|
|
1421
|
+
sock.on("connect", () => {
|
|
1422
|
+
sock.on("close", resolve);
|
|
1423
|
+
sock.end(payload);
|
|
1424
|
+
});
|
|
1425
|
+
sock.on("error", resolve);
|
|
1426
|
+
} catch {
|
|
1427
|
+
resolve();
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1129
1431
|
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1432
|
+
if (!options?.calledFromDaemon) {
|
|
1433
|
+
const actId = (0, import_crypto.randomUUID)();
|
|
1434
|
+
const actTs = Date.now();
|
|
1435
|
+
await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
|
|
1436
|
+
const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
|
|
1437
|
+
...options,
|
|
1438
|
+
activityId: actId
|
|
1439
|
+
});
|
|
1440
|
+
if (!result.noApprovalMechanism) {
|
|
1441
|
+
await notifyActivity({
|
|
1442
|
+
id: actId,
|
|
1443
|
+
tool: toolName,
|
|
1444
|
+
ts: actTs,
|
|
1445
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
1446
|
+
label: result.blockedByLabel
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
return result;
|
|
1450
|
+
}
|
|
1451
|
+
return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
|
|
1452
|
+
}
|
|
1453
|
+
async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1130
1454
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
1131
1455
|
const pauseState = checkPause();
|
|
1132
1456
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
@@ -1149,6 +1473,23 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1149
1473
|
let policyMatchedField;
|
|
1150
1474
|
let policyMatchedWord;
|
|
1151
1475
|
let riskMetadata;
|
|
1476
|
+
if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
1477
|
+
const dlpMatch = scanArgs(args);
|
|
1478
|
+
if (dlpMatch) {
|
|
1479
|
+
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
1480
|
+
if (dlpMatch.severity === "block") {
|
|
1481
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
|
|
1482
|
+
return {
|
|
1483
|
+
approved: false,
|
|
1484
|
+
reason: dlpReason,
|
|
1485
|
+
blockedBy: "local-config",
|
|
1486
|
+
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
|
|
1490
|
+
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1152
1493
|
if (config.settings.mode === "audit") {
|
|
1153
1494
|
if (!isIgnoredTool(toolName)) {
|
|
1154
1495
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
@@ -1368,7 +1709,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1368
1709
|
console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
1369
1710
|
`));
|
|
1370
1711
|
}
|
|
1371
|
-
const daemonDecision = await askDaemon(
|
|
1712
|
+
const daemonDecision = await askDaemon(
|
|
1713
|
+
toolName,
|
|
1714
|
+
args,
|
|
1715
|
+
meta,
|
|
1716
|
+
signal,
|
|
1717
|
+
riskMetadata,
|
|
1718
|
+
options?.activityId
|
|
1719
|
+
);
|
|
1372
1720
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1373
1721
|
const isApproved = daemonDecision === "allow";
|
|
1374
1722
|
return {
|
|
@@ -1388,7 +1736,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1388
1736
|
racePromises.push(
|
|
1389
1737
|
(async () => {
|
|
1390
1738
|
try {
|
|
1391
|
-
|
|
1739
|
+
if (explainableLabel.includes("DLP")) {
|
|
1740
|
+
console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
1741
|
+
console.log(
|
|
1742
|
+
import_chalk2.default.red.bold(` A sensitive secret was detected in the tool arguments!`)
|
|
1743
|
+
);
|
|
1744
|
+
} else {
|
|
1745
|
+
console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
|
|
1746
|
+
}
|
|
1392
1747
|
console.log(`${import_chalk2.default.bold("Action:")} ${import_chalk2.default.red(toolName)}`);
|
|
1393
1748
|
console.log(`${import_chalk2.default.bold("Flagged By:")} ${import_chalk2.default.yellow(explainableLabel)}`);
|
|
1394
1749
|
if (isRemoteLocked) {
|
|
@@ -1493,8 +1848,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1493
1848
|
}
|
|
1494
1849
|
function getConfig() {
|
|
1495
1850
|
if (cachedConfig) return cachedConfig;
|
|
1496
|
-
const globalPath =
|
|
1497
|
-
const projectPath =
|
|
1851
|
+
const globalPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
|
|
1852
|
+
const projectPath = import_path4.default.join(process.cwd(), "node9.config.json");
|
|
1498
1853
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1499
1854
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1500
1855
|
const mergedSettings = {
|
|
@@ -1506,13 +1861,13 @@ function getConfig() {
|
|
|
1506
1861
|
dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
|
|
1507
1862
|
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
1508
1863
|
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
1509
|
-
rules: [...DEFAULT_CONFIG.policy.rules],
|
|
1510
1864
|
smartRules: [...DEFAULT_CONFIG.policy.smartRules],
|
|
1511
1865
|
snapshot: {
|
|
1512
1866
|
tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
|
|
1513
1867
|
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
1514
1868
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
1515
|
-
}
|
|
1869
|
+
},
|
|
1870
|
+
dlp: { ...DEFAULT_CONFIG.policy.dlp }
|
|
1516
1871
|
};
|
|
1517
1872
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
1518
1873
|
const applyLayer = (source) => {
|
|
@@ -1532,7 +1887,6 @@ function getConfig() {
|
|
|
1532
1887
|
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
1533
1888
|
if (p.toolInspection)
|
|
1534
1889
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
1535
|
-
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
1536
1890
|
if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
|
|
1537
1891
|
if (p.snapshot) {
|
|
1538
1892
|
const s2 = p.snapshot;
|
|
@@ -1540,6 +1894,11 @@ function getConfig() {
|
|
|
1540
1894
|
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
1541
1895
|
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
1542
1896
|
}
|
|
1897
|
+
if (p.dlp) {
|
|
1898
|
+
const d = p.dlp;
|
|
1899
|
+
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
1900
|
+
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
1901
|
+
}
|
|
1543
1902
|
const envs = source.environments || {};
|
|
1544
1903
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
1545
1904
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -1554,6 +1913,22 @@ function getConfig() {
|
|
|
1554
1913
|
};
|
|
1555
1914
|
applyLayer(globalConfig);
|
|
1556
1915
|
applyLayer(projectConfig);
|
|
1916
|
+
for (const shieldName of readActiveShields()) {
|
|
1917
|
+
const shield = getShield(shieldName);
|
|
1918
|
+
if (!shield) continue;
|
|
1919
|
+
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
1920
|
+
for (const rule of shield.smartRules) {
|
|
1921
|
+
if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
1922
|
+
}
|
|
1923
|
+
const existingWords = new Set(mergedPolicy.dangerousWords);
|
|
1924
|
+
for (const word of shield.dangerousWords) {
|
|
1925
|
+
if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
1929
|
+
for (const rule of ADVISORY_SMART_RULES) {
|
|
1930
|
+
if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
1931
|
+
}
|
|
1557
1932
|
if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
|
|
1558
1933
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
1559
1934
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
@@ -1569,10 +1944,10 @@ function getConfig() {
|
|
|
1569
1944
|
return cachedConfig;
|
|
1570
1945
|
}
|
|
1571
1946
|
function tryLoadConfig(filePath) {
|
|
1572
|
-
if (!
|
|
1947
|
+
if (!import_fs2.default.existsSync(filePath)) return null;
|
|
1573
1948
|
let raw;
|
|
1574
1949
|
try {
|
|
1575
|
-
raw = JSON.parse(
|
|
1950
|
+
raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
|
|
1576
1951
|
} catch (err) {
|
|
1577
1952
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1578
1953
|
process.stderr.write(
|
|
@@ -1634,9 +2009,9 @@ function getCredentials() {
|
|
|
1634
2009
|
};
|
|
1635
2010
|
}
|
|
1636
2011
|
try {
|
|
1637
|
-
const credPath =
|
|
1638
|
-
if (
|
|
1639
|
-
const creds = JSON.parse(
|
|
2012
|
+
const credPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
|
|
2013
|
+
if (import_fs2.default.existsSync(credPath)) {
|
|
2014
|
+
const creds = JSON.parse(import_fs2.default.readFileSync(credPath, "utf-8"));
|
|
1640
2015
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
1641
2016
|
const profile = creds[profileName];
|
|
1642
2017
|
if (profile?.apiKey) {
|
|
@@ -1671,9 +2046,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1671
2046
|
context: {
|
|
1672
2047
|
agent: meta?.agent,
|
|
1673
2048
|
mcpServer: meta?.mcpServer,
|
|
1674
|
-
hostname:
|
|
2049
|
+
hostname: import_os2.default.hostname(),
|
|
1675
2050
|
cwd: process.cwd(),
|
|
1676
|
-
platform:
|
|
2051
|
+
platform: import_os2.default.platform()
|
|
1677
2052
|
}
|
|
1678
2053
|
}),
|
|
1679
2054
|
signal: AbortSignal.timeout(5e3)
|
|
@@ -1694,9 +2069,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
1694
2069
|
context: {
|
|
1695
2070
|
agent: meta?.agent,
|
|
1696
2071
|
mcpServer: meta?.mcpServer,
|
|
1697
|
-
hostname:
|
|
2072
|
+
hostname: import_os2.default.hostname(),
|
|
1698
2073
|
cwd: process.cwd(),
|
|
1699
|
-
platform:
|
|
2074
|
+
platform: import_os2.default.platform()
|
|
1700
2075
|
},
|
|
1701
2076
|
...riskMetadata && { riskMetadata }
|
|
1702
2077
|
}),
|