@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.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// src/core.ts
|
|
2
2
|
import chalk2 from "chalk";
|
|
3
3
|
import { confirm } from "@inquirer/prompts";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
4
|
+
import fs2 from "fs";
|
|
5
|
+
import path4 from "path";
|
|
6
|
+
import os2 from "os";
|
|
7
|
+
import net from "net";
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
7
9
|
import pm from "picomatch";
|
|
8
10
|
import { parse } from "sh-syntax";
|
|
9
11
|
|
|
@@ -333,25 +335,26 @@ import { z } from "zod";
|
|
|
333
335
|
var noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
334
336
|
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
335
337
|
});
|
|
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
338
|
var SmartConditionSchema = z.object({
|
|
348
339
|
field: z.string().min(1, "Condition field must not be empty"),
|
|
349
|
-
op: z.enum(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
340
|
+
op: z.enum(
|
|
341
|
+
[
|
|
342
|
+
"matches",
|
|
343
|
+
"notMatches",
|
|
344
|
+
"contains",
|
|
345
|
+
"notContains",
|
|
346
|
+
"exists",
|
|
347
|
+
"notExists",
|
|
348
|
+
"matchesGlob",
|
|
349
|
+
"notMatchesGlob"
|
|
350
|
+
],
|
|
351
|
+
{
|
|
352
|
+
errorMap: () => ({
|
|
353
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
),
|
|
357
|
+
value: z.string().optional(),
|
|
355
358
|
flags: z.string().optional()
|
|
356
359
|
});
|
|
357
360
|
var SmartRuleSchema = z.object({
|
|
@@ -364,11 +367,6 @@ var SmartRuleSchema = z.object({
|
|
|
364
367
|
}),
|
|
365
368
|
reason: z.string().optional()
|
|
366
369
|
});
|
|
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
370
|
var ConfigFileSchema = z.object({
|
|
373
371
|
version: z.string().optional(),
|
|
374
372
|
settings: z.object({
|
|
@@ -393,12 +391,15 @@ var ConfigFileSchema = z.object({
|
|
|
393
391
|
dangerousWords: z.array(noNewlines).optional(),
|
|
394
392
|
ignoredTools: z.array(z.string()).optional(),
|
|
395
393
|
toolInspection: z.record(z.string()).optional(),
|
|
396
|
-
rules: z.array(PolicyRuleSchema).optional(),
|
|
397
394
|
smartRules: z.array(SmartRuleSchema).optional(),
|
|
398
395
|
snapshot: z.object({
|
|
399
396
|
tools: z.array(z.string()).optional(),
|
|
400
397
|
onlyPaths: z.array(z.string()).optional(),
|
|
401
398
|
ignorePaths: z.array(z.string()).optional()
|
|
399
|
+
}).optional(),
|
|
400
|
+
dlp: z.object({
|
|
401
|
+
enabled: z.boolean().optional(),
|
|
402
|
+
scanIgnoredTools: z.boolean().optional()
|
|
402
403
|
}).optional()
|
|
403
404
|
}).optional(),
|
|
404
405
|
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
@@ -420,8 +421,8 @@ function sanitizeConfig(raw) {
|
|
|
420
421
|
}
|
|
421
422
|
}
|
|
422
423
|
const lines = result.error.issues.map((issue) => {
|
|
423
|
-
const
|
|
424
|
-
return ` \u2022 ${
|
|
424
|
+
const path5 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
425
|
+
return ` \u2022 ${path5}: ${issue.message}`;
|
|
425
426
|
});
|
|
426
427
|
return {
|
|
427
428
|
sanitized,
|
|
@@ -430,18 +431,291 @@ ${lines.join("\n")}`
|
|
|
430
431
|
};
|
|
431
432
|
}
|
|
432
433
|
|
|
434
|
+
// src/shields.ts
|
|
435
|
+
import fs from "fs";
|
|
436
|
+
import path3 from "path";
|
|
437
|
+
import os from "os";
|
|
438
|
+
var SHIELDS = {
|
|
439
|
+
postgres: {
|
|
440
|
+
name: "postgres",
|
|
441
|
+
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
442
|
+
aliases: ["pg", "postgresql"],
|
|
443
|
+
smartRules: [
|
|
444
|
+
{
|
|
445
|
+
name: "shield:postgres:block-drop-table",
|
|
446
|
+
tool: "*",
|
|
447
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
448
|
+
verdict: "block",
|
|
449
|
+
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: "shield:postgres:block-truncate",
|
|
453
|
+
tool: "*",
|
|
454
|
+
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
455
|
+
verdict: "block",
|
|
456
|
+
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
name: "shield:postgres:block-drop-column",
|
|
460
|
+
tool: "*",
|
|
461
|
+
conditions: [
|
|
462
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
463
|
+
],
|
|
464
|
+
verdict: "block",
|
|
465
|
+
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: "shield:postgres:review-grant-revoke",
|
|
469
|
+
tool: "*",
|
|
470
|
+
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
471
|
+
verdict: "review",
|
|
472
|
+
reason: "Permission changes require human approval (Postgres shield)"
|
|
473
|
+
}
|
|
474
|
+
],
|
|
475
|
+
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
476
|
+
},
|
|
477
|
+
github: {
|
|
478
|
+
name: "github",
|
|
479
|
+
description: "Protects GitHub repositories from destructive AI operations",
|
|
480
|
+
aliases: ["git"],
|
|
481
|
+
smartRules: [
|
|
482
|
+
{
|
|
483
|
+
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
484
|
+
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
485
|
+
name: "shield:github:review-delete-branch-remote",
|
|
486
|
+
tool: "bash",
|
|
487
|
+
conditions: [
|
|
488
|
+
{
|
|
489
|
+
field: "command",
|
|
490
|
+
op: "matches",
|
|
491
|
+
value: "git\\s+push\\s+.*--delete",
|
|
492
|
+
flags: "i"
|
|
493
|
+
}
|
|
494
|
+
],
|
|
495
|
+
verdict: "review",
|
|
496
|
+
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
name: "shield:github:block-delete-repo",
|
|
500
|
+
tool: "*",
|
|
501
|
+
conditions: [
|
|
502
|
+
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
503
|
+
],
|
|
504
|
+
verdict: "block",
|
|
505
|
+
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
506
|
+
}
|
|
507
|
+
],
|
|
508
|
+
dangerousWords: []
|
|
509
|
+
},
|
|
510
|
+
aws: {
|
|
511
|
+
name: "aws",
|
|
512
|
+
description: "Protects AWS infrastructure from destructive AI operations",
|
|
513
|
+
aliases: ["amazon"],
|
|
514
|
+
smartRules: [
|
|
515
|
+
{
|
|
516
|
+
name: "shield:aws:block-delete-s3-bucket",
|
|
517
|
+
tool: "*",
|
|
518
|
+
conditions: [
|
|
519
|
+
{
|
|
520
|
+
field: "command",
|
|
521
|
+
op: "matches",
|
|
522
|
+
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
523
|
+
flags: "i"
|
|
524
|
+
}
|
|
525
|
+
],
|
|
526
|
+
verdict: "block",
|
|
527
|
+
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
name: "shield:aws:review-iam-changes",
|
|
531
|
+
tool: "*",
|
|
532
|
+
conditions: [
|
|
533
|
+
{
|
|
534
|
+
field: "command",
|
|
535
|
+
op: "matches",
|
|
536
|
+
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
537
|
+
flags: "i"
|
|
538
|
+
}
|
|
539
|
+
],
|
|
540
|
+
verdict: "review",
|
|
541
|
+
reason: "IAM changes require human approval (AWS shield)"
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
name: "shield:aws:block-ec2-terminate",
|
|
545
|
+
tool: "*",
|
|
546
|
+
conditions: [
|
|
547
|
+
{
|
|
548
|
+
field: "command",
|
|
549
|
+
op: "matches",
|
|
550
|
+
value: "aws\\s+ec2\\s+terminate-instances",
|
|
551
|
+
flags: "i"
|
|
552
|
+
}
|
|
553
|
+
],
|
|
554
|
+
verdict: "block",
|
|
555
|
+
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
name: "shield:aws:review-rds-delete",
|
|
559
|
+
tool: "*",
|
|
560
|
+
conditions: [
|
|
561
|
+
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
562
|
+
],
|
|
563
|
+
verdict: "review",
|
|
564
|
+
reason: "RDS deletion requires human approval (AWS shield)"
|
|
565
|
+
}
|
|
566
|
+
],
|
|
567
|
+
dangerousWords: []
|
|
568
|
+
},
|
|
569
|
+
filesystem: {
|
|
570
|
+
name: "filesystem",
|
|
571
|
+
description: "Protects the local filesystem from dangerous AI operations",
|
|
572
|
+
aliases: ["fs"],
|
|
573
|
+
smartRules: [
|
|
574
|
+
{
|
|
575
|
+
name: "shield:filesystem:review-chmod-777",
|
|
576
|
+
tool: "bash",
|
|
577
|
+
conditions: [
|
|
578
|
+
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
579
|
+
],
|
|
580
|
+
verdict: "review",
|
|
581
|
+
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
name: "shield:filesystem:review-write-etc",
|
|
585
|
+
tool: "bash",
|
|
586
|
+
conditions: [
|
|
587
|
+
{
|
|
588
|
+
field: "command",
|
|
589
|
+
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
590
|
+
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
591
|
+
op: "matches",
|
|
592
|
+
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
593
|
+
}
|
|
594
|
+
],
|
|
595
|
+
verdict: "review",
|
|
596
|
+
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
597
|
+
}
|
|
598
|
+
],
|
|
599
|
+
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
600
|
+
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
601
|
+
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
602
|
+
dangerousWords: ["wipefs"]
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
function resolveShieldName(input) {
|
|
606
|
+
const lower = input.toLowerCase();
|
|
607
|
+
if (SHIELDS[lower]) return lower;
|
|
608
|
+
for (const [name, def] of Object.entries(SHIELDS)) {
|
|
609
|
+
if (def.aliases.includes(lower)) return name;
|
|
610
|
+
}
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
function getShield(name) {
|
|
614
|
+
const resolved = resolveShieldName(name);
|
|
615
|
+
return resolved ? SHIELDS[resolved] : null;
|
|
616
|
+
}
|
|
617
|
+
var SHIELDS_STATE_FILE = path3.join(os.homedir(), ".node9", "shields.json");
|
|
618
|
+
function readActiveShields() {
|
|
619
|
+
try {
|
|
620
|
+
const raw = fs.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
621
|
+
if (!raw.trim()) return [];
|
|
622
|
+
const parsed = JSON.parse(raw);
|
|
623
|
+
if (Array.isArray(parsed.active)) {
|
|
624
|
+
return parsed.active.filter(
|
|
625
|
+
(e) => typeof e === "string" && e.length > 0 && e in SHIELDS
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
} catch (err) {
|
|
629
|
+
if (err.code !== "ENOENT") {
|
|
630
|
+
process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
|
|
631
|
+
`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return [];
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/dlp.ts
|
|
638
|
+
var DLP_PATTERNS = [
|
|
639
|
+
{ name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
|
|
640
|
+
{ name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
|
|
641
|
+
{ name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
|
|
642
|
+
{ name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
|
|
643
|
+
{ name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
|
|
644
|
+
{
|
|
645
|
+
name: "Private Key (PEM)",
|
|
646
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
647
|
+
severity: "block"
|
|
648
|
+
},
|
|
649
|
+
{ name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
|
|
650
|
+
];
|
|
651
|
+
function maskSecret(raw, pattern) {
|
|
652
|
+
const match = raw.match(pattern);
|
|
653
|
+
if (!match) return "****";
|
|
654
|
+
const secret = match[0];
|
|
655
|
+
if (secret.length < 8) return "****";
|
|
656
|
+
const prefix = secret.slice(0, 4);
|
|
657
|
+
const suffix = secret.slice(-4);
|
|
658
|
+
const stars = "*".repeat(Math.min(secret.length - 8, 12));
|
|
659
|
+
return `${prefix}${stars}${suffix}`;
|
|
660
|
+
}
|
|
661
|
+
var MAX_DEPTH = 5;
|
|
662
|
+
var MAX_STRING_BYTES = 1e5;
|
|
663
|
+
var MAX_JSON_PARSE_BYTES = 1e4;
|
|
664
|
+
function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
665
|
+
if (depth > MAX_DEPTH || args === null || args === void 0) return null;
|
|
666
|
+
if (Array.isArray(args)) {
|
|
667
|
+
for (let i = 0; i < args.length; i++) {
|
|
668
|
+
const match = scanArgs(args[i], depth + 1, `${fieldPath}[${i}]`);
|
|
669
|
+
if (match) return match;
|
|
670
|
+
}
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
if (typeof args === "object") {
|
|
674
|
+
for (const [key, value] of Object.entries(args)) {
|
|
675
|
+
const match = scanArgs(value, depth + 1, `${fieldPath}.${key}`);
|
|
676
|
+
if (match) return match;
|
|
677
|
+
}
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
if (typeof args === "string") {
|
|
681
|
+
const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
|
|
682
|
+
for (const pattern of DLP_PATTERNS) {
|
|
683
|
+
if (pattern.regex.test(text)) {
|
|
684
|
+
return {
|
|
685
|
+
patternName: pattern.name,
|
|
686
|
+
fieldPath,
|
|
687
|
+
redactedSample: maskSecret(text, pattern.regex),
|
|
688
|
+
severity: pattern.severity
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (text.length < MAX_JSON_PARSE_BYTES) {
|
|
693
|
+
const trimmed = text.trim();
|
|
694
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
695
|
+
try {
|
|
696
|
+
const parsed = JSON.parse(text);
|
|
697
|
+
const inner = scanArgs(parsed, depth + 1, fieldPath);
|
|
698
|
+
if (inner) return inner;
|
|
699
|
+
} catch {
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
|
|
433
707
|
// src/core.ts
|
|
434
|
-
var PAUSED_FILE =
|
|
435
|
-
var TRUST_FILE =
|
|
436
|
-
var LOCAL_AUDIT_LOG =
|
|
437
|
-
var HOOK_DEBUG_LOG =
|
|
708
|
+
var PAUSED_FILE = path4.join(os2.homedir(), ".node9", "PAUSED");
|
|
709
|
+
var TRUST_FILE = path4.join(os2.homedir(), ".node9", "trust.json");
|
|
710
|
+
var LOCAL_AUDIT_LOG = path4.join(os2.homedir(), ".node9", "audit.log");
|
|
711
|
+
var HOOK_DEBUG_LOG = path4.join(os2.homedir(), ".node9", "hook-debug.log");
|
|
438
712
|
function checkPause() {
|
|
439
713
|
try {
|
|
440
|
-
if (!
|
|
441
|
-
const state = JSON.parse(
|
|
714
|
+
if (!fs2.existsSync(PAUSED_FILE)) return { paused: false };
|
|
715
|
+
const state = JSON.parse(fs2.readFileSync(PAUSED_FILE, "utf-8"));
|
|
442
716
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
443
717
|
try {
|
|
444
|
-
|
|
718
|
+
fs2.unlinkSync(PAUSED_FILE);
|
|
445
719
|
} catch {
|
|
446
720
|
}
|
|
447
721
|
return { paused: false };
|
|
@@ -452,20 +726,20 @@ function checkPause() {
|
|
|
452
726
|
}
|
|
453
727
|
}
|
|
454
728
|
function atomicWriteSync(filePath, data, options) {
|
|
455
|
-
const dir =
|
|
456
|
-
if (!
|
|
457
|
-
const tmpPath = `${filePath}.${
|
|
458
|
-
|
|
459
|
-
|
|
729
|
+
const dir = path4.dirname(filePath);
|
|
730
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
731
|
+
const tmpPath = `${filePath}.${os2.hostname()}.${process.pid}.tmp`;
|
|
732
|
+
fs2.writeFileSync(tmpPath, data, options);
|
|
733
|
+
fs2.renameSync(tmpPath, filePath);
|
|
460
734
|
}
|
|
461
735
|
function getActiveTrustSession(toolName) {
|
|
462
736
|
try {
|
|
463
|
-
if (!
|
|
464
|
-
const trust = JSON.parse(
|
|
737
|
+
if (!fs2.existsSync(TRUST_FILE)) return false;
|
|
738
|
+
const trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
|
|
465
739
|
const now = Date.now();
|
|
466
740
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
467
741
|
if (active.length !== trust.entries.length) {
|
|
468
|
-
|
|
742
|
+
fs2.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
469
743
|
}
|
|
470
744
|
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
471
745
|
} catch {
|
|
@@ -476,8 +750,8 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
476
750
|
try {
|
|
477
751
|
let trust = { entries: [] };
|
|
478
752
|
try {
|
|
479
|
-
if (
|
|
480
|
-
trust = JSON.parse(
|
|
753
|
+
if (fs2.existsSync(TRUST_FILE)) {
|
|
754
|
+
trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
|
|
481
755
|
}
|
|
482
756
|
} catch {
|
|
483
757
|
}
|
|
@@ -493,9 +767,9 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
493
767
|
}
|
|
494
768
|
function appendToLog(logPath, entry) {
|
|
495
769
|
try {
|
|
496
|
-
const dir =
|
|
497
|
-
if (!
|
|
498
|
-
|
|
770
|
+
const dir = path4.dirname(logPath);
|
|
771
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
772
|
+
fs2.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
499
773
|
} catch {
|
|
500
774
|
}
|
|
501
775
|
}
|
|
@@ -507,7 +781,7 @@ function appendHookDebug(toolName, args, meta) {
|
|
|
507
781
|
args: safeArgs,
|
|
508
782
|
agent: meta?.agent,
|
|
509
783
|
mcpServer: meta?.mcpServer,
|
|
510
|
-
hostname:
|
|
784
|
+
hostname: os2.hostname(),
|
|
511
785
|
cwd: process.cwd()
|
|
512
786
|
});
|
|
513
787
|
}
|
|
@@ -521,7 +795,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
|
521
795
|
checkedBy,
|
|
522
796
|
agent: meta?.agent,
|
|
523
797
|
mcpServer: meta?.mcpServer,
|
|
524
|
-
hostname:
|
|
798
|
+
hostname: os2.hostname()
|
|
525
799
|
});
|
|
526
800
|
}
|
|
527
801
|
function tokenize(toolName) {
|
|
@@ -537,9 +811,9 @@ function matchesPattern(text, patterns) {
|
|
|
537
811
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
538
812
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
539
813
|
}
|
|
540
|
-
function getNestedValue(obj,
|
|
814
|
+
function getNestedValue(obj, path5) {
|
|
541
815
|
if (!obj || typeof obj !== "object") return null;
|
|
542
|
-
return
|
|
816
|
+
return path5.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
543
817
|
}
|
|
544
818
|
function evaluateSmartConditions(args, rule) {
|
|
545
819
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -572,6 +846,10 @@ function evaluateSmartConditions(args, rule) {
|
|
|
572
846
|
return true;
|
|
573
847
|
}
|
|
574
848
|
}
|
|
849
|
+
case "matchesGlob":
|
|
850
|
+
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
851
|
+
case "notMatchesGlob":
|
|
852
|
+
return val !== null && cond.value ? !pm.isMatch(val, cond.value) : true;
|
|
575
853
|
default:
|
|
576
854
|
return false;
|
|
577
855
|
}
|
|
@@ -735,25 +1013,27 @@ var DEFAULT_CONFIG = {
|
|
|
735
1013
|
onlyPaths: [],
|
|
736
1014
|
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
737
1015
|
},
|
|
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
1016
|
smartRules: [
|
|
1017
|
+
// ── rm safety (critical — always evaluated first) ──────────────────────
|
|
1018
|
+
{
|
|
1019
|
+
name: "block-rm-rf-home",
|
|
1020
|
+
tool: "bash",
|
|
1021
|
+
conditionMode: "all",
|
|
1022
|
+
conditions: [
|
|
1023
|
+
{
|
|
1024
|
+
field: "command",
|
|
1025
|
+
op: "matches",
|
|
1026
|
+
value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
field: "command",
|
|
1030
|
+
op: "matches",
|
|
1031
|
+
value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
|
|
1032
|
+
}
|
|
1033
|
+
],
|
|
1034
|
+
verdict: "block",
|
|
1035
|
+
reason: "Recursive delete of home directory is irreversible"
|
|
1036
|
+
},
|
|
757
1037
|
// ── SQL safety ────────────────────────────────────────────────────────
|
|
758
1038
|
{
|
|
759
1039
|
name: "no-delete-without-where",
|
|
@@ -844,16 +1124,42 @@ var DEFAULT_CONFIG = {
|
|
|
844
1124
|
verdict: "block",
|
|
845
1125
|
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
846
1126
|
}
|
|
847
|
-
]
|
|
1127
|
+
],
|
|
1128
|
+
dlp: { enabled: true, scanIgnoredTools: true }
|
|
848
1129
|
},
|
|
849
1130
|
environments: {}
|
|
850
1131
|
};
|
|
1132
|
+
var ADVISORY_SMART_RULES = [
|
|
1133
|
+
{
|
|
1134
|
+
name: "allow-rm-safe-paths",
|
|
1135
|
+
tool: "*",
|
|
1136
|
+
conditionMode: "all",
|
|
1137
|
+
conditions: [
|
|
1138
|
+
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
1139
|
+
{
|
|
1140
|
+
field: "command",
|
|
1141
|
+
op: "matches",
|
|
1142
|
+
// Matches known-safe build artifact paths in the command.
|
|
1143
|
+
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
1144
|
+
}
|
|
1145
|
+
],
|
|
1146
|
+
verdict: "allow",
|
|
1147
|
+
reason: "Deleting a known-safe build artifact path"
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
name: "review-rm",
|
|
1151
|
+
tool: "*",
|
|
1152
|
+
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
1153
|
+
verdict: "review",
|
|
1154
|
+
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
1155
|
+
}
|
|
1156
|
+
];
|
|
851
1157
|
var cachedConfig = null;
|
|
852
1158
|
function getInternalToken() {
|
|
853
1159
|
try {
|
|
854
|
-
const pidFile =
|
|
855
|
-
if (!
|
|
856
|
-
const data = JSON.parse(
|
|
1160
|
+
const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
|
|
1161
|
+
if (!fs2.existsSync(pidFile)) return null;
|
|
1162
|
+
const data = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
|
|
857
1163
|
process.kill(data.pid, 0);
|
|
858
1164
|
return data.internalToken ?? null;
|
|
859
1165
|
} catch {
|
|
@@ -868,7 +1174,8 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
868
1174
|
(rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
|
|
869
1175
|
);
|
|
870
1176
|
if (matchedRule) {
|
|
871
|
-
if (matchedRule.verdict === "allow")
|
|
1177
|
+
if (matchedRule.verdict === "allow")
|
|
1178
|
+
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
872
1179
|
return {
|
|
873
1180
|
decision: matchedRule.verdict,
|
|
874
1181
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
@@ -879,13 +1186,11 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
879
1186
|
}
|
|
880
1187
|
}
|
|
881
1188
|
let allTokens = [];
|
|
882
|
-
let actionTokens = [];
|
|
883
1189
|
let pathTokens = [];
|
|
884
1190
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
885
1191
|
if (shellCommand) {
|
|
886
1192
|
const analyzed = await analyzeShellCommand(shellCommand);
|
|
887
1193
|
allTokens = analyzed.allTokens;
|
|
888
|
-
actionTokens = analyzed.actions;
|
|
889
1194
|
pathTokens = analyzed.paths;
|
|
890
1195
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
891
1196
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
@@ -893,11 +1198,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
893
1198
|
}
|
|
894
1199
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
895
1200
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
896
|
-
actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
897
1201
|
}
|
|
898
1202
|
} else {
|
|
899
1203
|
allTokens = tokenize(toolName);
|
|
900
|
-
actionTokens = [toolName];
|
|
901
1204
|
if (args && typeof args === "object") {
|
|
902
1205
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
903
1206
|
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
@@ -920,29 +1223,6 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
920
1223
|
const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
|
|
921
1224
|
if (allInSandbox) return { decision: "allow" };
|
|
922
1225
|
}
|
|
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
1226
|
let matchedDangerousWord;
|
|
947
1227
|
const isDangerous = allTokens.some(
|
|
948
1228
|
(token) => config.policy.dangerousWords.some((word) => {
|
|
@@ -1000,9 +1280,9 @@ var DAEMON_PORT = 7391;
|
|
|
1000
1280
|
var DAEMON_HOST = "127.0.0.1";
|
|
1001
1281
|
function isDaemonRunning() {
|
|
1002
1282
|
try {
|
|
1003
|
-
const pidFile =
|
|
1004
|
-
if (!
|
|
1005
|
-
const { pid, port } = JSON.parse(
|
|
1283
|
+
const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
|
|
1284
|
+
if (!fs2.existsSync(pidFile)) return false;
|
|
1285
|
+
const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
|
|
1006
1286
|
if (port !== DAEMON_PORT) return false;
|
|
1007
1287
|
process.kill(pid, 0);
|
|
1008
1288
|
return true;
|
|
@@ -1012,16 +1292,16 @@ function isDaemonRunning() {
|
|
|
1012
1292
|
}
|
|
1013
1293
|
function getPersistentDecision(toolName) {
|
|
1014
1294
|
try {
|
|
1015
|
-
const file =
|
|
1016
|
-
if (!
|
|
1017
|
-
const decisions = JSON.parse(
|
|
1295
|
+
const file = path4.join(os2.homedir(), ".node9", "decisions.json");
|
|
1296
|
+
if (!fs2.existsSync(file)) return null;
|
|
1297
|
+
const decisions = JSON.parse(fs2.readFileSync(file, "utf-8"));
|
|
1018
1298
|
const d = decisions[toolName];
|
|
1019
1299
|
if (d === "allow" || d === "deny") return d;
|
|
1020
1300
|
} catch {
|
|
1021
1301
|
}
|
|
1022
1302
|
return null;
|
|
1023
1303
|
}
|
|
1024
|
-
async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
1304
|
+
async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
|
|
1025
1305
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1026
1306
|
const checkCtrl = new AbortController();
|
|
1027
1307
|
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
@@ -1036,6 +1316,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
|
1036
1316
|
args,
|
|
1037
1317
|
agent: meta?.agent,
|
|
1038
1318
|
mcpServer: meta?.mcpServer,
|
|
1319
|
+
fromCLI: true,
|
|
1320
|
+
// Pass the flight-recorder ID so the daemon uses the same UUID for
|
|
1321
|
+
// activity-result as the CLI used for the pending activity event.
|
|
1322
|
+
// Without this, the two UUIDs never match and tail.ts never resolves
|
|
1323
|
+
// the pending item.
|
|
1324
|
+
activityId,
|
|
1039
1325
|
...riskMetadata && { riskMetadata }
|
|
1040
1326
|
}),
|
|
1041
1327
|
signal: checkCtrl.signal
|
|
@@ -1090,7 +1376,45 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1090
1376
|
signal: AbortSignal.timeout(3e3)
|
|
1091
1377
|
});
|
|
1092
1378
|
}
|
|
1379
|
+
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path4.join(os2.tmpdir(), "node9-activity.sock");
|
|
1380
|
+
function notifyActivity(data) {
|
|
1381
|
+
return new Promise((resolve) => {
|
|
1382
|
+
try {
|
|
1383
|
+
const payload = JSON.stringify(data);
|
|
1384
|
+
const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
|
|
1385
|
+
sock.on("connect", () => {
|
|
1386
|
+
sock.on("close", resolve);
|
|
1387
|
+
sock.end(payload);
|
|
1388
|
+
});
|
|
1389
|
+
sock.on("error", resolve);
|
|
1390
|
+
} catch {
|
|
1391
|
+
resolve();
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1093
1395
|
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1396
|
+
if (!options?.calledFromDaemon) {
|
|
1397
|
+
const actId = randomUUID();
|
|
1398
|
+
const actTs = Date.now();
|
|
1399
|
+
await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
|
|
1400
|
+
const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
|
|
1401
|
+
...options,
|
|
1402
|
+
activityId: actId
|
|
1403
|
+
});
|
|
1404
|
+
if (!result.noApprovalMechanism) {
|
|
1405
|
+
await notifyActivity({
|
|
1406
|
+
id: actId,
|
|
1407
|
+
tool: toolName,
|
|
1408
|
+
ts: actTs,
|
|
1409
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
1410
|
+
label: result.blockedByLabel
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
return result;
|
|
1414
|
+
}
|
|
1415
|
+
return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
|
|
1416
|
+
}
|
|
1417
|
+
async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1094
1418
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
1095
1419
|
const pauseState = checkPause();
|
|
1096
1420
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
@@ -1113,6 +1437,23 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1113
1437
|
let policyMatchedField;
|
|
1114
1438
|
let policyMatchedWord;
|
|
1115
1439
|
let riskMetadata;
|
|
1440
|
+
if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
1441
|
+
const dlpMatch = scanArgs(args);
|
|
1442
|
+
if (dlpMatch) {
|
|
1443
|
+
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
1444
|
+
if (dlpMatch.severity === "block") {
|
|
1445
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
|
|
1446
|
+
return {
|
|
1447
|
+
approved: false,
|
|
1448
|
+
reason: dlpReason,
|
|
1449
|
+
blockedBy: "local-config",
|
|
1450
|
+
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
|
|
1454
|
+
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1116
1457
|
if (config.settings.mode === "audit") {
|
|
1117
1458
|
if (!isIgnoredTool(toolName)) {
|
|
1118
1459
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
@@ -1332,7 +1673,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1332
1673
|
console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
1333
1674
|
`));
|
|
1334
1675
|
}
|
|
1335
|
-
const daemonDecision = await askDaemon(
|
|
1676
|
+
const daemonDecision = await askDaemon(
|
|
1677
|
+
toolName,
|
|
1678
|
+
args,
|
|
1679
|
+
meta,
|
|
1680
|
+
signal,
|
|
1681
|
+
riskMetadata,
|
|
1682
|
+
options?.activityId
|
|
1683
|
+
);
|
|
1336
1684
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1337
1685
|
const isApproved = daemonDecision === "allow";
|
|
1338
1686
|
return {
|
|
@@ -1352,7 +1700,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1352
1700
|
racePromises.push(
|
|
1353
1701
|
(async () => {
|
|
1354
1702
|
try {
|
|
1355
|
-
|
|
1703
|
+
if (explainableLabel.includes("DLP")) {
|
|
1704
|
+
console.log(chalk2.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
1705
|
+
console.log(
|
|
1706
|
+
chalk2.red.bold(` A sensitive secret was detected in the tool arguments!`)
|
|
1707
|
+
);
|
|
1708
|
+
} else {
|
|
1709
|
+
console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
|
|
1710
|
+
}
|
|
1356
1711
|
console.log(`${chalk2.bold("Action:")} ${chalk2.red(toolName)}`);
|
|
1357
1712
|
console.log(`${chalk2.bold("Flagged By:")} ${chalk2.yellow(explainableLabel)}`);
|
|
1358
1713
|
if (isRemoteLocked) {
|
|
@@ -1457,8 +1812,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1457
1812
|
}
|
|
1458
1813
|
function getConfig() {
|
|
1459
1814
|
if (cachedConfig) return cachedConfig;
|
|
1460
|
-
const globalPath =
|
|
1461
|
-
const projectPath =
|
|
1815
|
+
const globalPath = path4.join(os2.homedir(), ".node9", "config.json");
|
|
1816
|
+
const projectPath = path4.join(process.cwd(), "node9.config.json");
|
|
1462
1817
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1463
1818
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1464
1819
|
const mergedSettings = {
|
|
@@ -1470,13 +1825,13 @@ function getConfig() {
|
|
|
1470
1825
|
dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
|
|
1471
1826
|
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
1472
1827
|
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
1473
|
-
rules: [...DEFAULT_CONFIG.policy.rules],
|
|
1474
1828
|
smartRules: [...DEFAULT_CONFIG.policy.smartRules],
|
|
1475
1829
|
snapshot: {
|
|
1476
1830
|
tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
|
|
1477
1831
|
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
1478
1832
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
1479
|
-
}
|
|
1833
|
+
},
|
|
1834
|
+
dlp: { ...DEFAULT_CONFIG.policy.dlp }
|
|
1480
1835
|
};
|
|
1481
1836
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
1482
1837
|
const applyLayer = (source) => {
|
|
@@ -1496,7 +1851,6 @@ function getConfig() {
|
|
|
1496
1851
|
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
1497
1852
|
if (p.toolInspection)
|
|
1498
1853
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
1499
|
-
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
1500
1854
|
if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
|
|
1501
1855
|
if (p.snapshot) {
|
|
1502
1856
|
const s2 = p.snapshot;
|
|
@@ -1504,6 +1858,11 @@ function getConfig() {
|
|
|
1504
1858
|
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
1505
1859
|
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
1506
1860
|
}
|
|
1861
|
+
if (p.dlp) {
|
|
1862
|
+
const d = p.dlp;
|
|
1863
|
+
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
1864
|
+
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
1865
|
+
}
|
|
1507
1866
|
const envs = source.environments || {};
|
|
1508
1867
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
1509
1868
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -1518,6 +1877,22 @@ function getConfig() {
|
|
|
1518
1877
|
};
|
|
1519
1878
|
applyLayer(globalConfig);
|
|
1520
1879
|
applyLayer(projectConfig);
|
|
1880
|
+
for (const shieldName of readActiveShields()) {
|
|
1881
|
+
const shield = getShield(shieldName);
|
|
1882
|
+
if (!shield) continue;
|
|
1883
|
+
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
1884
|
+
for (const rule of shield.smartRules) {
|
|
1885
|
+
if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
1886
|
+
}
|
|
1887
|
+
const existingWords = new Set(mergedPolicy.dangerousWords);
|
|
1888
|
+
for (const word of shield.dangerousWords) {
|
|
1889
|
+
if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
1893
|
+
for (const rule of ADVISORY_SMART_RULES) {
|
|
1894
|
+
if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
1895
|
+
}
|
|
1521
1896
|
if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
|
|
1522
1897
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
1523
1898
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
@@ -1533,10 +1908,10 @@ function getConfig() {
|
|
|
1533
1908
|
return cachedConfig;
|
|
1534
1909
|
}
|
|
1535
1910
|
function tryLoadConfig(filePath) {
|
|
1536
|
-
if (!
|
|
1911
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
1537
1912
|
let raw;
|
|
1538
1913
|
try {
|
|
1539
|
-
raw = JSON.parse(
|
|
1914
|
+
raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
|
|
1540
1915
|
} catch (err) {
|
|
1541
1916
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1542
1917
|
process.stderr.write(
|
|
@@ -1598,9 +1973,9 @@ function getCredentials() {
|
|
|
1598
1973
|
};
|
|
1599
1974
|
}
|
|
1600
1975
|
try {
|
|
1601
|
-
const credPath =
|
|
1602
|
-
if (
|
|
1603
|
-
const creds = JSON.parse(
|
|
1976
|
+
const credPath = path4.join(os2.homedir(), ".node9", "credentials.json");
|
|
1977
|
+
if (fs2.existsSync(credPath)) {
|
|
1978
|
+
const creds = JSON.parse(fs2.readFileSync(credPath, "utf-8"));
|
|
1604
1979
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
1605
1980
|
const profile = creds[profileName];
|
|
1606
1981
|
if (profile?.apiKey) {
|
|
@@ -1635,9 +2010,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1635
2010
|
context: {
|
|
1636
2011
|
agent: meta?.agent,
|
|
1637
2012
|
mcpServer: meta?.mcpServer,
|
|
1638
|
-
hostname:
|
|
2013
|
+
hostname: os2.hostname(),
|
|
1639
2014
|
cwd: process.cwd(),
|
|
1640
|
-
platform:
|
|
2015
|
+
platform: os2.platform()
|
|
1641
2016
|
}
|
|
1642
2017
|
}),
|
|
1643
2018
|
signal: AbortSignal.timeout(5e3)
|
|
@@ -1658,9 +2033,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
1658
2033
|
context: {
|
|
1659
2034
|
agent: meta?.agent,
|
|
1660
2035
|
mcpServer: meta?.mcpServer,
|
|
1661
|
-
hostname:
|
|
2036
|
+
hostname: os2.hostname(),
|
|
1662
2037
|
cwd: process.cwd(),
|
|
1663
|
-
platform:
|
|
2038
|
+
platform: os2.platform()
|
|
1664
2039
|
},
|
|
1665
2040
|
...riskMetadata && { riskMetadata }
|
|
1666
2041
|
}),
|