@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/README.md +117 -119
- package/dist/cli.js +736 -360
- package/dist/cli.mjs +731 -355
- package/dist/index.js +441 -123
- package/dist/index.mjs +441 -123
- package/package.json +1 -1
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
|
|
5
|
-
import
|
|
6
|
-
import
|
|
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(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
424
|
-
return ` \u2022 ${
|
|
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 =
|
|
435
|
-
var TRUST_FILE =
|
|
436
|
-
var LOCAL_AUDIT_LOG =
|
|
437
|
-
var 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 (!
|
|
441
|
-
const state = JSON.parse(
|
|
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
|
-
|
|
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 =
|
|
456
|
-
if (!
|
|
457
|
-
const tmpPath = `${filePath}.${
|
|
458
|
-
|
|
459
|
-
|
|
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 (!
|
|
464
|
-
const trust = JSON.parse(
|
|
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
|
-
|
|
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 (
|
|
480
|
-
trust = JSON.parse(
|
|
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 =
|
|
497
|
-
if (!
|
|
498
|
-
|
|
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:
|
|
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:
|
|
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,
|
|
812
|
+
function getNestedValue(obj, path5) {
|
|
541
813
|
if (!obj || typeof obj !== "object") return null;
|
|
542
|
-
return
|
|
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 =
|
|
855
|
-
if (!
|
|
856
|
-
const data = JSON.parse(
|
|
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")
|
|
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 =
|
|
1004
|
-
if (!
|
|
1005
|
-
const { pid, port } = JSON.parse(
|
|
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 =
|
|
1016
|
-
if (!
|
|
1017
|
-
const decisions = JSON.parse(
|
|
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
|
-
|
|
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 =
|
|
1461
|
-
const projectPath =
|
|
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 (!
|
|
1854
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
1537
1855
|
let raw;
|
|
1538
1856
|
try {
|
|
1539
|
-
raw = JSON.parse(
|
|
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 =
|
|
1602
|
-
if (
|
|
1603
|
-
const creds = JSON.parse(
|
|
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:
|
|
1956
|
+
hostname: os2.hostname(),
|
|
1639
1957
|
cwd: process.cwd(),
|
|
1640
|
-
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:
|
|
1979
|
+
hostname: os2.hostname(),
|
|
1662
1980
|
cwd: process.cwd(),
|
|
1663
|
-
platform:
|
|
1981
|
+
platform: os2.platform()
|
|
1664
1982
|
},
|
|
1665
1983
|
...riskMetadata && { riskMetadata }
|
|
1666
1984
|
}),
|