@node9/proxy 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +587 -141
- package/dist/cli.mjs +587 -141
- package/dist/index.js +431 -73
- package/dist/index.mjs +431 -73
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
import chalk2 from "chalk";
|
|
3
3
|
import { confirm } from "@inquirer/prompts";
|
|
4
4
|
import fs from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path3 from "path";
|
|
6
6
|
import os from "os";
|
|
7
7
|
import pm from "picomatch";
|
|
8
8
|
import { parse } from "sh-syntax";
|
|
9
9
|
|
|
10
10
|
// src/ui/native.ts
|
|
11
11
|
import { spawn } from "child_process";
|
|
12
|
-
import
|
|
12
|
+
import path2 from "path";
|
|
13
13
|
import chalk from "chalk";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
|
|
15
|
+
// src/context-sniper.ts
|
|
16
|
+
import path from "path";
|
|
17
17
|
function smartTruncate(str, maxLen = 500) {
|
|
18
18
|
if (str.length <= maxLen) return str;
|
|
19
19
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -21,11 +21,13 @@ function smartTruncate(str, maxLen = 500) {
|
|
|
21
21
|
}
|
|
22
22
|
function extractContext(text, matchedWord) {
|
|
23
23
|
const lines = text.split("\n");
|
|
24
|
-
if (lines.length <= 7 || !matchedWord)
|
|
24
|
+
if (lines.length <= 7 || !matchedWord) {
|
|
25
|
+
return { snippet: smartTruncate(text, 500), lineIndex: -1 };
|
|
26
|
+
}
|
|
25
27
|
const escaped = matchedWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
26
28
|
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
27
29
|
const allHits = lines.map((line, i) => ({ i, line })).filter(({ line }) => pattern.test(line));
|
|
28
|
-
if (allHits.length === 0) return smartTruncate(text, 500);
|
|
30
|
+
if (allHits.length === 0) return { snippet: smartTruncate(text, 500), lineIndex: -1 };
|
|
29
31
|
const nonComment = allHits.find(({ line }) => {
|
|
30
32
|
const trimmed = line.trim();
|
|
31
33
|
return !trimmed.startsWith("//") && !trimmed.startsWith("#");
|
|
@@ -33,13 +35,89 @@ function extractContext(text, matchedWord) {
|
|
|
33
35
|
const hitIndex = (nonComment ?? allHits[0]).i;
|
|
34
36
|
const start = Math.max(0, hitIndex - 3);
|
|
35
37
|
const end = Math.min(lines.length, hitIndex + 4);
|
|
38
|
+
const lineIndex = hitIndex - start;
|
|
36
39
|
const snippet = lines.slice(start, end).map((line, i) => `${start + i === hitIndex ? "\u{1F6D1} " : " "}${line}`).join("\n");
|
|
37
40
|
const head = start > 0 ? `... [${start} lines hidden] ...
|
|
38
41
|
` : "";
|
|
39
42
|
const tail = end < lines.length ? `
|
|
40
43
|
... [${lines.length - end} lines hidden] ...` : "";
|
|
41
|
-
return `${head}${snippet}${tail}
|
|
44
|
+
return { snippet: `${head}${snippet}${tail}`, lineIndex };
|
|
45
|
+
}
|
|
46
|
+
var CODE_KEYS = [
|
|
47
|
+
"command",
|
|
48
|
+
"cmd",
|
|
49
|
+
"shell_command",
|
|
50
|
+
"bash_command",
|
|
51
|
+
"script",
|
|
52
|
+
"code",
|
|
53
|
+
"input",
|
|
54
|
+
"sql",
|
|
55
|
+
"query",
|
|
56
|
+
"arguments",
|
|
57
|
+
"args",
|
|
58
|
+
"param",
|
|
59
|
+
"params",
|
|
60
|
+
"text"
|
|
61
|
+
];
|
|
62
|
+
function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
|
|
63
|
+
let intent = "EXEC";
|
|
64
|
+
let contextSnippet;
|
|
65
|
+
let contextLineIndex;
|
|
66
|
+
let editFileName;
|
|
67
|
+
let editFilePath;
|
|
68
|
+
let parsed = args;
|
|
69
|
+
if (typeof args === "string") {
|
|
70
|
+
const trimmed = args.trim();
|
|
71
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(trimmed);
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
79
|
+
const obj = parsed;
|
|
80
|
+
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
81
|
+
intent = "EDIT";
|
|
82
|
+
if (obj.file_path) {
|
|
83
|
+
editFilePath = String(obj.file_path);
|
|
84
|
+
editFileName = path.basename(editFilePath);
|
|
85
|
+
}
|
|
86
|
+
const result = extractContext(String(obj.new_string), matchedWord);
|
|
87
|
+
contextSnippet = result.snippet;
|
|
88
|
+
if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
|
|
89
|
+
} else if (matchedField && obj[matchedField] !== void 0) {
|
|
90
|
+
const result = extractContext(String(obj[matchedField]), matchedWord);
|
|
91
|
+
contextSnippet = result.snippet;
|
|
92
|
+
if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
|
|
93
|
+
} else {
|
|
94
|
+
const foundKey = Object.keys(obj).find((k) => CODE_KEYS.includes(k.toLowerCase()));
|
|
95
|
+
if (foundKey) {
|
|
96
|
+
const val = obj[foundKey];
|
|
97
|
+
contextSnippet = smartTruncate(typeof val === "string" ? val : JSON.stringify(val), 500);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} else if (typeof parsed === "string") {
|
|
101
|
+
contextSnippet = smartTruncate(parsed, 500);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
intent,
|
|
105
|
+
tier,
|
|
106
|
+
blockedByLabel,
|
|
107
|
+
...matchedWord && { matchedWord },
|
|
108
|
+
...matchedField && { matchedField },
|
|
109
|
+
...contextSnippet !== void 0 && { contextSnippet },
|
|
110
|
+
...contextLineIndex !== void 0 && { contextLineIndex },
|
|
111
|
+
...editFileName && { editFileName },
|
|
112
|
+
...editFilePath && { editFilePath },
|
|
113
|
+
...ruleName && { ruleName }
|
|
114
|
+
};
|
|
42
115
|
}
|
|
116
|
+
|
|
117
|
+
// src/ui/native.ts
|
|
118
|
+
var isTestEnv = () => {
|
|
119
|
+
return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
|
|
120
|
+
};
|
|
43
121
|
function formatArgs(args, matchedField, matchedWord) {
|
|
44
122
|
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
45
123
|
let parsed = args;
|
|
@@ -58,9 +136,9 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
58
136
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
59
137
|
const obj = parsed;
|
|
60
138
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
61
|
-
const file = obj.file_path ?
|
|
139
|
+
const file = obj.file_path ? path2.basename(String(obj.file_path)) : "file";
|
|
62
140
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
63
|
-
const newPreview = extractContext(String(obj.new_string), matchedWord);
|
|
141
|
+
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
64
142
|
return {
|
|
65
143
|
intent: "EDIT",
|
|
66
144
|
message: `\u{1F4DD} EDITING: ${file}
|
|
@@ -78,7 +156,7 @@ ${newPreview}`
|
|
|
78
156
|
const context = otherKeys.length > 0 ? `\u2699\uFE0F Context: ${otherKeys.map((k) => `${k}=${smartTruncate(typeof obj[k] === "object" ? JSON.stringify(obj[k]) : String(obj[k]), 30)}`).join(", ")}
|
|
79
157
|
|
|
80
158
|
` : "";
|
|
81
|
-
const content = extractContext(String(obj[matchedField]), matchedWord);
|
|
159
|
+
const content = extractContext(String(obj[matchedField]), matchedWord).snippet;
|
|
82
160
|
return {
|
|
83
161
|
intent: "EXEC",
|
|
84
162
|
message: `${context}\u{1F6D1} [${matchedField.toUpperCase()}]:
|
|
@@ -250,11 +328,113 @@ end run`;
|
|
|
250
328
|
});
|
|
251
329
|
}
|
|
252
330
|
|
|
331
|
+
// src/config-schema.ts
|
|
332
|
+
import { z } from "zod";
|
|
333
|
+
var noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
334
|
+
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
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
|
+
var SmartConditionSchema = z.object({
|
|
348
|
+
field: z.string().min(1, "Condition field must not be empty"),
|
|
349
|
+
op: z.enum(["matches", "notMatches", "contains", "notContains", "exists", "notExists"], {
|
|
350
|
+
errorMap: () => ({
|
|
351
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists"
|
|
352
|
+
})
|
|
353
|
+
}),
|
|
354
|
+
value: validRegex.optional(),
|
|
355
|
+
flags: z.string().optional()
|
|
356
|
+
});
|
|
357
|
+
var SmartRuleSchema = z.object({
|
|
358
|
+
name: z.string().optional(),
|
|
359
|
+
tool: z.string().min(1, "Smart rule tool must not be empty"),
|
|
360
|
+
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
361
|
+
conditionMode: z.enum(["all", "any"]).optional(),
|
|
362
|
+
verdict: z.enum(["allow", "review", "block"], {
|
|
363
|
+
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
364
|
+
}),
|
|
365
|
+
reason: z.string().optional()
|
|
366
|
+
});
|
|
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
|
+
var ConfigFileSchema = z.object({
|
|
373
|
+
version: z.string().optional(),
|
|
374
|
+
settings: z.object({
|
|
375
|
+
mode: z.enum(["standard", "strict", "audit"]).optional(),
|
|
376
|
+
autoStartDaemon: z.boolean().optional(),
|
|
377
|
+
enableUndo: z.boolean().optional(),
|
|
378
|
+
enableHookLogDebug: z.boolean().optional(),
|
|
379
|
+
approvalTimeoutMs: z.number().nonnegative().optional(),
|
|
380
|
+
approvers: z.object({
|
|
381
|
+
native: z.boolean().optional(),
|
|
382
|
+
browser: z.boolean().optional(),
|
|
383
|
+
cloud: z.boolean().optional(),
|
|
384
|
+
terminal: z.boolean().optional()
|
|
385
|
+
}).optional(),
|
|
386
|
+
environment: z.string().optional(),
|
|
387
|
+
slackEnabled: z.boolean().optional(),
|
|
388
|
+
enableTrustSessions: z.boolean().optional(),
|
|
389
|
+
allowGlobalPause: z.boolean().optional()
|
|
390
|
+
}).optional(),
|
|
391
|
+
policy: z.object({
|
|
392
|
+
sandboxPaths: z.array(z.string()).optional(),
|
|
393
|
+
dangerousWords: z.array(noNewlines).optional(),
|
|
394
|
+
ignoredTools: z.array(z.string()).optional(),
|
|
395
|
+
toolInspection: z.record(z.string()).optional(),
|
|
396
|
+
rules: z.array(PolicyRuleSchema).optional(),
|
|
397
|
+
smartRules: z.array(SmartRuleSchema).optional(),
|
|
398
|
+
snapshot: z.object({
|
|
399
|
+
tools: z.array(z.string()).optional(),
|
|
400
|
+
onlyPaths: z.array(z.string()).optional(),
|
|
401
|
+
ignorePaths: z.array(z.string()).optional()
|
|
402
|
+
}).optional()
|
|
403
|
+
}).optional(),
|
|
404
|
+
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
405
|
+
}).strict({ message: "Config contains unknown top-level keys" });
|
|
406
|
+
function sanitizeConfig(raw) {
|
|
407
|
+
const result = ConfigFileSchema.safeParse(raw);
|
|
408
|
+
if (result.success) {
|
|
409
|
+
return { sanitized: result.data, error: null };
|
|
410
|
+
}
|
|
411
|
+
const invalidTopLevelKeys = new Set(
|
|
412
|
+
result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
|
|
413
|
+
);
|
|
414
|
+
const sanitized = {};
|
|
415
|
+
if (typeof raw === "object" && raw !== null) {
|
|
416
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
417
|
+
if (!invalidTopLevelKeys.has(key)) {
|
|
418
|
+
sanitized[key] = value;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const lines = result.error.issues.map((issue) => {
|
|
423
|
+
const path4 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
424
|
+
return ` \u2022 ${path4}: ${issue.message}`;
|
|
425
|
+
});
|
|
426
|
+
return {
|
|
427
|
+
sanitized,
|
|
428
|
+
error: `Invalid config:
|
|
429
|
+
${lines.join("\n")}`
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
253
433
|
// src/core.ts
|
|
254
|
-
var PAUSED_FILE =
|
|
255
|
-
var TRUST_FILE =
|
|
256
|
-
var LOCAL_AUDIT_LOG =
|
|
257
|
-
var HOOK_DEBUG_LOG =
|
|
434
|
+
var PAUSED_FILE = path3.join(os.homedir(), ".node9", "PAUSED");
|
|
435
|
+
var TRUST_FILE = path3.join(os.homedir(), ".node9", "trust.json");
|
|
436
|
+
var LOCAL_AUDIT_LOG = path3.join(os.homedir(), ".node9", "audit.log");
|
|
437
|
+
var HOOK_DEBUG_LOG = path3.join(os.homedir(), ".node9", "hook-debug.log");
|
|
258
438
|
function checkPause() {
|
|
259
439
|
try {
|
|
260
440
|
if (!fs.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -272,7 +452,7 @@ function checkPause() {
|
|
|
272
452
|
}
|
|
273
453
|
}
|
|
274
454
|
function atomicWriteSync(filePath, data, options) {
|
|
275
|
-
const dir =
|
|
455
|
+
const dir = path3.dirname(filePath);
|
|
276
456
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
277
457
|
const tmpPath = `${filePath}.${os.hostname()}.${process.pid}.tmp`;
|
|
278
458
|
fs.writeFileSync(tmpPath, data, options);
|
|
@@ -313,7 +493,7 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
313
493
|
}
|
|
314
494
|
function appendToLog(logPath, entry) {
|
|
315
495
|
try {
|
|
316
|
-
const dir =
|
|
496
|
+
const dir = path3.dirname(logPath);
|
|
317
497
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
318
498
|
fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
319
499
|
} catch {
|
|
@@ -357,9 +537,9 @@ function matchesPattern(text, patterns) {
|
|
|
357
537
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
358
538
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
359
539
|
}
|
|
360
|
-
function getNestedValue(obj,
|
|
540
|
+
function getNestedValue(obj, path4) {
|
|
361
541
|
if (!obj || typeof obj !== "object") return null;
|
|
362
|
-
return
|
|
542
|
+
return path4.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
363
543
|
}
|
|
364
544
|
function evaluateSmartConditions(args, rule) {
|
|
365
545
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -496,15 +676,10 @@ function redactSecrets(text) {
|
|
|
496
676
|
return redacted;
|
|
497
677
|
}
|
|
498
678
|
var DANGEROUS_WORDS = [
|
|
499
|
-
"
|
|
500
|
-
|
|
501
|
-
"
|
|
502
|
-
|
|
503
|
-
"destroy",
|
|
504
|
-
"terminate",
|
|
505
|
-
"revoke",
|
|
506
|
-
"docker",
|
|
507
|
-
"psql"
|
|
679
|
+
"mkfs",
|
|
680
|
+
// formats/wipes a filesystem partition
|
|
681
|
+
"shred"
|
|
682
|
+
// permanently overwrites file contents (unrecoverable)
|
|
508
683
|
];
|
|
509
684
|
var DEFAULT_CONFIG = {
|
|
510
685
|
settings: {
|
|
@@ -561,6 +736,8 @@ var DEFAULT_CONFIG = {
|
|
|
561
736
|
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
562
737
|
},
|
|
563
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.
|
|
564
741
|
{
|
|
565
742
|
action: "rm",
|
|
566
743
|
allowPaths: [
|
|
@@ -577,6 +754,7 @@ var DEFAULT_CONFIG = {
|
|
|
577
754
|
}
|
|
578
755
|
],
|
|
579
756
|
smartRules: [
|
|
757
|
+
// ── SQL safety ────────────────────────────────────────────────────────
|
|
580
758
|
{
|
|
581
759
|
name: "no-delete-without-where",
|
|
582
760
|
tool: "*",
|
|
@@ -587,6 +765,84 @@ var DEFAULT_CONFIG = {
|
|
|
587
765
|
conditionMode: "all",
|
|
588
766
|
verdict: "review",
|
|
589
767
|
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
name: "review-drop-truncate-shell",
|
|
771
|
+
tool: "bash",
|
|
772
|
+
conditions: [
|
|
773
|
+
{
|
|
774
|
+
field: "command",
|
|
775
|
+
op: "matches",
|
|
776
|
+
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
777
|
+
flags: "i"
|
|
778
|
+
}
|
|
779
|
+
],
|
|
780
|
+
conditionMode: "all",
|
|
781
|
+
verdict: "review",
|
|
782
|
+
reason: "SQL DDL destructive statement inside a shell command"
|
|
783
|
+
},
|
|
784
|
+
// ── Git safety ────────────────────────────────────────────────────────
|
|
785
|
+
{
|
|
786
|
+
name: "block-force-push",
|
|
787
|
+
tool: "bash",
|
|
788
|
+
conditions: [
|
|
789
|
+
{
|
|
790
|
+
field: "command",
|
|
791
|
+
op: "matches",
|
|
792
|
+
value: "git push.*(--force|--force-with-lease|-f\\b)",
|
|
793
|
+
flags: "i"
|
|
794
|
+
}
|
|
795
|
+
],
|
|
796
|
+
conditionMode: "all",
|
|
797
|
+
verdict: "block",
|
|
798
|
+
reason: "Force push overwrites remote history and cannot be undone"
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
name: "review-git-push",
|
|
802
|
+
tool: "bash",
|
|
803
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
|
|
804
|
+
conditionMode: "all",
|
|
805
|
+
verdict: "review",
|
|
806
|
+
reason: "git push sends changes to a shared remote"
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
name: "review-git-destructive",
|
|
810
|
+
tool: "bash",
|
|
811
|
+
conditions: [
|
|
812
|
+
{
|
|
813
|
+
field: "command",
|
|
814
|
+
op: "matches",
|
|
815
|
+
value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
|
|
816
|
+
flags: "i"
|
|
817
|
+
}
|
|
818
|
+
],
|
|
819
|
+
conditionMode: "all",
|
|
820
|
+
verdict: "review",
|
|
821
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
822
|
+
},
|
|
823
|
+
// ── Shell safety ──────────────────────────────────────────────────────
|
|
824
|
+
{
|
|
825
|
+
name: "review-sudo",
|
|
826
|
+
tool: "bash",
|
|
827
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
|
|
828
|
+
conditionMode: "all",
|
|
829
|
+
verdict: "review",
|
|
830
|
+
reason: "Command requires elevated privileges"
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
name: "review-curl-pipe-shell",
|
|
834
|
+
tool: "bash",
|
|
835
|
+
conditions: [
|
|
836
|
+
{
|
|
837
|
+
field: "command",
|
|
838
|
+
op: "matches",
|
|
839
|
+
value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
840
|
+
flags: "i"
|
|
841
|
+
}
|
|
842
|
+
],
|
|
843
|
+
conditionMode: "all",
|
|
844
|
+
verdict: "block",
|
|
845
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
590
846
|
}
|
|
591
847
|
]
|
|
592
848
|
},
|
|
@@ -595,7 +851,7 @@ var DEFAULT_CONFIG = {
|
|
|
595
851
|
var cachedConfig = null;
|
|
596
852
|
function getInternalToken() {
|
|
597
853
|
try {
|
|
598
|
-
const pidFile =
|
|
854
|
+
const pidFile = path3.join(os.homedir(), ".node9", "daemon.pid");
|
|
599
855
|
if (!fs.existsSync(pidFile)) return null;
|
|
600
856
|
const data = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
|
|
601
857
|
process.kill(data.pid, 0);
|
|
@@ -616,7 +872,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
616
872
|
return {
|
|
617
873
|
decision: matchedRule.verdict,
|
|
618
874
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
619
|
-
reason: matchedRule.reason
|
|
875
|
+
reason: matchedRule.reason,
|
|
876
|
+
tier: 2,
|
|
877
|
+
ruleName: matchedRule.name ?? matchedRule.tool
|
|
620
878
|
};
|
|
621
879
|
}
|
|
622
880
|
}
|
|
@@ -631,7 +889,7 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
631
889
|
pathTokens = analyzed.paths;
|
|
632
890
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
633
891
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
634
|
-
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)" };
|
|
892
|
+
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
635
893
|
}
|
|
636
894
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
637
895
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
@@ -654,7 +912,7 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
654
912
|
);
|
|
655
913
|
const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
|
|
656
914
|
if (hasSystemDisaster || isRootWipe) {
|
|
657
|
-
return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
|
|
915
|
+
return { decision: "review", blockedByLabel: "Manual Nuclear Protection", tier: 3 };
|
|
658
916
|
}
|
|
659
917
|
return { decision: "allow" };
|
|
660
918
|
}
|
|
@@ -672,14 +930,16 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
672
930
|
if (anyBlocked)
|
|
673
931
|
return {
|
|
674
932
|
decision: "review",
|
|
675
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)
|
|
933
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
|
|
934
|
+
tier: 5
|
|
676
935
|
};
|
|
677
936
|
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
678
937
|
if (allAllowed) return { decision: "allow" };
|
|
679
938
|
}
|
|
680
939
|
return {
|
|
681
940
|
decision: "review",
|
|
682
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)
|
|
941
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
|
|
942
|
+
tier: 5
|
|
683
943
|
};
|
|
684
944
|
}
|
|
685
945
|
}
|
|
@@ -721,13 +981,14 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
721
981
|
decision: "review",
|
|
722
982
|
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
|
|
723
983
|
matchedWord: matchedDangerousWord,
|
|
724
|
-
matchedField
|
|
984
|
+
matchedField,
|
|
985
|
+
tier: 6
|
|
725
986
|
};
|
|
726
987
|
}
|
|
727
988
|
if (config.settings.mode === "strict") {
|
|
728
989
|
const envConfig = getActiveEnvironment(config);
|
|
729
990
|
if (envConfig?.requireApproval === false) return { decision: "allow" };
|
|
730
|
-
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)" };
|
|
991
|
+
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)", tier: 7 };
|
|
731
992
|
}
|
|
732
993
|
return { decision: "allow" };
|
|
733
994
|
}
|
|
@@ -739,7 +1000,7 @@ var DAEMON_PORT = 7391;
|
|
|
739
1000
|
var DAEMON_HOST = "127.0.0.1";
|
|
740
1001
|
function isDaemonRunning() {
|
|
741
1002
|
try {
|
|
742
|
-
const pidFile =
|
|
1003
|
+
const pidFile = path3.join(os.homedir(), ".node9", "daemon.pid");
|
|
743
1004
|
if (!fs.existsSync(pidFile)) return false;
|
|
744
1005
|
const { pid, port } = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
|
|
745
1006
|
if (port !== DAEMON_PORT) return false;
|
|
@@ -751,7 +1012,7 @@ function isDaemonRunning() {
|
|
|
751
1012
|
}
|
|
752
1013
|
function getPersistentDecision(toolName) {
|
|
753
1014
|
try {
|
|
754
|
-
const file =
|
|
1015
|
+
const file = path3.join(os.homedir(), ".node9", "decisions.json");
|
|
755
1016
|
if (!fs.existsSync(file)) return null;
|
|
756
1017
|
const decisions = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
757
1018
|
const d = decisions[toolName];
|
|
@@ -760,7 +1021,7 @@ function getPersistentDecision(toolName) {
|
|
|
760
1021
|
}
|
|
761
1022
|
return null;
|
|
762
1023
|
}
|
|
763
|
-
async function askDaemon(toolName, args, meta, signal) {
|
|
1024
|
+
async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
764
1025
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
765
1026
|
const checkCtrl = new AbortController();
|
|
766
1027
|
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
@@ -770,7 +1031,13 @@ async function askDaemon(toolName, args, meta, signal) {
|
|
|
770
1031
|
const checkRes = await fetch(`${base}/check`, {
|
|
771
1032
|
method: "POST",
|
|
772
1033
|
headers: { "Content-Type": "application/json" },
|
|
773
|
-
body: JSON.stringify({
|
|
1034
|
+
body: JSON.stringify({
|
|
1035
|
+
toolName,
|
|
1036
|
+
args,
|
|
1037
|
+
agent: meta?.agent,
|
|
1038
|
+
mcpServer: meta?.mcpServer,
|
|
1039
|
+
...riskMetadata && { riskMetadata }
|
|
1040
|
+
}),
|
|
774
1041
|
signal: checkCtrl.signal
|
|
775
1042
|
});
|
|
776
1043
|
if (!checkRes.ok) throw new Error("Daemon fail");
|
|
@@ -795,7 +1062,7 @@ async function askDaemon(toolName, args, meta, signal) {
|
|
|
795
1062
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
796
1063
|
}
|
|
797
1064
|
}
|
|
798
|
-
async function notifyDaemonViewer(toolName, args, meta) {
|
|
1065
|
+
async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
799
1066
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
800
1067
|
const res = await fetch(`${base}/check`, {
|
|
801
1068
|
method: "POST",
|
|
@@ -805,7 +1072,8 @@ async function notifyDaemonViewer(toolName, args, meta) {
|
|
|
805
1072
|
args,
|
|
806
1073
|
slackDelegated: true,
|
|
807
1074
|
agent: meta?.agent,
|
|
808
|
-
mcpServer: meta?.mcpServer
|
|
1075
|
+
mcpServer: meta?.mcpServer,
|
|
1076
|
+
...riskMetadata && { riskMetadata }
|
|
809
1077
|
}),
|
|
810
1078
|
signal: AbortSignal.timeout(3e3)
|
|
811
1079
|
});
|
|
@@ -844,11 +1112,15 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
844
1112
|
let explainableLabel = "Local Config";
|
|
845
1113
|
let policyMatchedField;
|
|
846
1114
|
let policyMatchedWord;
|
|
1115
|
+
let riskMetadata;
|
|
847
1116
|
if (config.settings.mode === "audit") {
|
|
848
1117
|
if (!isIgnoredTool(toolName)) {
|
|
849
1118
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
850
1119
|
if (policyResult.decision === "review") {
|
|
851
1120
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
1121
|
+
if (approvers.cloud && creds?.apiKey) {
|
|
1122
|
+
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
1123
|
+
}
|
|
852
1124
|
sendDesktopNotification(
|
|
853
1125
|
"Node9 Audit Mode",
|
|
854
1126
|
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
@@ -859,13 +1131,15 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
859
1131
|
}
|
|
860
1132
|
if (!isIgnoredTool(toolName)) {
|
|
861
1133
|
if (getActiveTrustSession(toolName)) {
|
|
862
|
-
if (creds?.apiKey)
|
|
1134
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1135
|
+
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
863
1136
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
864
1137
|
return { approved: true, checkedBy: "trust" };
|
|
865
1138
|
}
|
|
866
1139
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
867
1140
|
if (policyResult.decision === "allow") {
|
|
868
|
-
if (creds?.apiKey)
|
|
1141
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1142
|
+
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
869
1143
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
870
1144
|
return { approved: true, checkedBy: "local-policy" };
|
|
871
1145
|
}
|
|
@@ -881,9 +1155,18 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
881
1155
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
882
1156
|
policyMatchedField = policyResult.matchedField;
|
|
883
1157
|
policyMatchedWord = policyResult.matchedWord;
|
|
1158
|
+
riskMetadata = computeRiskMetadata(
|
|
1159
|
+
args,
|
|
1160
|
+
policyResult.tier ?? 6,
|
|
1161
|
+
explainableLabel,
|
|
1162
|
+
policyMatchedField,
|
|
1163
|
+
policyMatchedWord,
|
|
1164
|
+
policyResult.ruleName
|
|
1165
|
+
);
|
|
884
1166
|
const persistent = getPersistentDecision(toolName);
|
|
885
1167
|
if (persistent === "allow") {
|
|
886
|
-
if (creds?.apiKey)
|
|
1168
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1169
|
+
await auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
887
1170
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
888
1171
|
return { approved: true, checkedBy: "persistent" };
|
|
889
1172
|
}
|
|
@@ -897,7 +1180,6 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
897
1180
|
};
|
|
898
1181
|
}
|
|
899
1182
|
} else {
|
|
900
|
-
if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
|
|
901
1183
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
902
1184
|
return { approved: true };
|
|
903
1185
|
}
|
|
@@ -906,8 +1188,21 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
906
1188
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
907
1189
|
if (cloudEnforced) {
|
|
908
1190
|
try {
|
|
909
|
-
const initResult = await initNode9SaaS(toolName, args, creds, meta);
|
|
1191
|
+
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
910
1192
|
if (!initResult.pending) {
|
|
1193
|
+
if (initResult.shadowMode) {
|
|
1194
|
+
console.error(
|
|
1195
|
+
chalk2.yellow(
|
|
1196
|
+
`
|
|
1197
|
+
\u26A0\uFE0F Node9 Shadow Mode: Action allowed, but would have been blocked by company policy.`
|
|
1198
|
+
)
|
|
1199
|
+
);
|
|
1200
|
+
if (initResult.shadowReason) {
|
|
1201
|
+
console.error(chalk2.dim(` Reason: ${initResult.shadowReason}
|
|
1202
|
+
`));
|
|
1203
|
+
}
|
|
1204
|
+
return { approved: true, checkedBy: "cloud" };
|
|
1205
|
+
}
|
|
911
1206
|
return {
|
|
912
1207
|
approved: !!initResult.approved,
|
|
913
1208
|
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
@@ -932,18 +1227,22 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
932
1227
|
);
|
|
933
1228
|
}
|
|
934
1229
|
}
|
|
935
|
-
if (
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1230
|
+
if (!options?.calledFromDaemon) {
|
|
1231
|
+
if (cloudEnforced && cloudRequestId) {
|
|
1232
|
+
console.error(
|
|
1233
|
+
chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
|
|
1234
|
+
);
|
|
1235
|
+
console.error(
|
|
1236
|
+
chalk2.cyan(" Dashboard \u2192 ") + chalk2.bold("Mission Control > Activity Feed\n")
|
|
1237
|
+
);
|
|
1238
|
+
} else if (!cloudEnforced) {
|
|
1239
|
+
const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
|
|
1240
|
+
console.error(
|
|
1241
|
+
chalk2.dim(`
|
|
944
1242
|
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
945
1243
|
`)
|
|
946
|
-
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
947
1246
|
}
|
|
948
1247
|
const abortController = new AbortController();
|
|
949
1248
|
const { signal } = abortController;
|
|
@@ -974,7 +1273,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
974
1273
|
(async () => {
|
|
975
1274
|
try {
|
|
976
1275
|
if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
|
|
977
|
-
viewerId = await notifyDaemonViewer(toolName, args, meta).catch(
|
|
1276
|
+
viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
|
|
1277
|
+
() => null
|
|
1278
|
+
);
|
|
978
1279
|
}
|
|
979
1280
|
const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
|
|
980
1281
|
return {
|
|
@@ -992,7 +1293,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
992
1293
|
})()
|
|
993
1294
|
);
|
|
994
1295
|
}
|
|
995
|
-
if (approvers.native && !isManual) {
|
|
1296
|
+
if (approvers.native && !isManual && !options?.calledFromDaemon) {
|
|
996
1297
|
racePromises.push(
|
|
997
1298
|
(async () => {
|
|
998
1299
|
const decision = await askNativePopup(
|
|
@@ -1020,7 +1321,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1020
1321
|
})()
|
|
1021
1322
|
);
|
|
1022
1323
|
}
|
|
1023
|
-
if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
1324
|
+
if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon && !cloudEnforced) {
|
|
1024
1325
|
racePromises.push(
|
|
1025
1326
|
(async () => {
|
|
1026
1327
|
try {
|
|
@@ -1031,7 +1332,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1031
1332
|
console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
1032
1333
|
`));
|
|
1033
1334
|
}
|
|
1034
|
-
const daemonDecision = await askDaemon(toolName, args, meta, signal);
|
|
1335
|
+
const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
|
|
1035
1336
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1036
1337
|
const isApproved = daemonDecision === "allow";
|
|
1037
1338
|
return {
|
|
@@ -1156,8 +1457,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1156
1457
|
}
|
|
1157
1458
|
function getConfig() {
|
|
1158
1459
|
if (cachedConfig) return cachedConfig;
|
|
1159
|
-
const globalPath =
|
|
1160
|
-
const projectPath =
|
|
1460
|
+
const globalPath = path3.join(os.homedir(), ".node9", "config.json");
|
|
1461
|
+
const projectPath = path3.join(process.cwd(), "node9.config.json");
|
|
1161
1462
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1162
1463
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1163
1464
|
const mergedSettings = {
|
|
@@ -1177,6 +1478,7 @@ function getConfig() {
|
|
|
1177
1478
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
1178
1479
|
}
|
|
1179
1480
|
};
|
|
1481
|
+
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
1180
1482
|
const applyLayer = (source) => {
|
|
1181
1483
|
if (!source) return;
|
|
1182
1484
|
const s = source.settings || {};
|
|
@@ -1202,6 +1504,17 @@ function getConfig() {
|
|
|
1202
1504
|
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
1203
1505
|
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
1204
1506
|
}
|
|
1507
|
+
const envs = source.environments || {};
|
|
1508
|
+
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
1509
|
+
if (envConfig && typeof envConfig === "object") {
|
|
1510
|
+
const ec = envConfig;
|
|
1511
|
+
mergedEnvironments[envName] = {
|
|
1512
|
+
...mergedEnvironments[envName],
|
|
1513
|
+
// Validate field types before merging — do not blindly spread user input
|
|
1514
|
+
...typeof ec.requireApproval === "boolean" ? { requireApproval: ec.requireApproval } : {}
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1205
1518
|
};
|
|
1206
1519
|
applyLayer(globalConfig);
|
|
1207
1520
|
applyLayer(projectConfig);
|
|
@@ -1215,17 +1528,62 @@ function getConfig() {
|
|
|
1215
1528
|
cachedConfig = {
|
|
1216
1529
|
settings: mergedSettings,
|
|
1217
1530
|
policy: mergedPolicy,
|
|
1218
|
-
environments:
|
|
1531
|
+
environments: mergedEnvironments
|
|
1219
1532
|
};
|
|
1220
1533
|
return cachedConfig;
|
|
1221
1534
|
}
|
|
1222
1535
|
function tryLoadConfig(filePath) {
|
|
1223
1536
|
if (!fs.existsSync(filePath)) return null;
|
|
1537
|
+
let raw;
|
|
1224
1538
|
try {
|
|
1225
|
-
|
|
1226
|
-
} catch {
|
|
1539
|
+
raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1540
|
+
} catch (err) {
|
|
1541
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1542
|
+
process.stderr.write(
|
|
1543
|
+
`
|
|
1544
|
+
\u26A0\uFE0F Node9: Failed to parse ${filePath}
|
|
1545
|
+
${msg}
|
|
1546
|
+
\u2192 Using default config
|
|
1547
|
+
|
|
1548
|
+
`
|
|
1549
|
+
);
|
|
1227
1550
|
return null;
|
|
1228
1551
|
}
|
|
1552
|
+
const SUPPORTED_VERSION = "1.0";
|
|
1553
|
+
const SUPPORTED_MAJOR = SUPPORTED_VERSION.split(".")[0];
|
|
1554
|
+
const fileVersion = raw?.version;
|
|
1555
|
+
if (fileVersion !== void 0) {
|
|
1556
|
+
const vStr = String(fileVersion);
|
|
1557
|
+
const fileMajor = vStr.split(".")[0];
|
|
1558
|
+
if (fileMajor !== SUPPORTED_MAJOR) {
|
|
1559
|
+
process.stderr.write(
|
|
1560
|
+
`
|
|
1561
|
+
\u274C Node9: Config at ${filePath} has version "${vStr}" \u2014 major version is incompatible with this release (expected "${SUPPORTED_VERSION}"). Config will not be loaded.
|
|
1562
|
+
|
|
1563
|
+
`
|
|
1564
|
+
);
|
|
1565
|
+
return null;
|
|
1566
|
+
} else if (vStr !== SUPPORTED_VERSION) {
|
|
1567
|
+
process.stderr.write(
|
|
1568
|
+
`
|
|
1569
|
+
\u26A0\uFE0F Node9: Config at ${filePath} declares version "${vStr}" \u2014 expected "${SUPPORTED_VERSION}". Continuing with best-effort parsing.
|
|
1570
|
+
|
|
1571
|
+
`
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
const { sanitized, error } = sanitizeConfig(raw);
|
|
1576
|
+
if (error) {
|
|
1577
|
+
process.stderr.write(
|
|
1578
|
+
`
|
|
1579
|
+
\u26A0\uFE0F Node9: Invalid config at ${filePath}:
|
|
1580
|
+
${error.replace("Invalid config:\n", "")}
|
|
1581
|
+
\u2192 Invalid fields ignored, using defaults for those keys
|
|
1582
|
+
|
|
1583
|
+
`
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
return sanitized;
|
|
1229
1587
|
}
|
|
1230
1588
|
function getActiveEnvironment(config) {
|
|
1231
1589
|
const env = config.settings.environment || process.env.NODE_ENV || "development";
|
|
@@ -1240,7 +1598,7 @@ function getCredentials() {
|
|
|
1240
1598
|
};
|
|
1241
1599
|
}
|
|
1242
1600
|
try {
|
|
1243
|
-
const credPath =
|
|
1601
|
+
const credPath = path3.join(os.homedir(), ".node9", "credentials.json");
|
|
1244
1602
|
if (fs.existsSync(credPath)) {
|
|
1245
1603
|
const creds = JSON.parse(fs.readFileSync(credPath, "utf-8"));
|
|
1246
1604
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
@@ -1267,9 +1625,7 @@ async function authorizeAction(toolName, args) {
|
|
|
1267
1625
|
return result.approved;
|
|
1268
1626
|
}
|
|
1269
1627
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
1270
|
-
|
|
1271
|
-
setTimeout(() => controller.abort(), 5e3);
|
|
1272
|
-
fetch(`${creds.apiUrl}/audit`, {
|
|
1628
|
+
return fetch(`${creds.apiUrl}/audit`, {
|
|
1273
1629
|
method: "POST",
|
|
1274
1630
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
1275
1631
|
body: JSON.stringify({
|
|
@@ -1284,11 +1640,12 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1284
1640
|
platform: os.platform()
|
|
1285
1641
|
}
|
|
1286
1642
|
}),
|
|
1287
|
-
signal:
|
|
1643
|
+
signal: AbortSignal.timeout(5e3)
|
|
1644
|
+
}).then(() => {
|
|
1288
1645
|
}).catch(() => {
|
|
1289
1646
|
});
|
|
1290
1647
|
}
|
|
1291
|
-
async function initNode9SaaS(toolName, args, creds, meta) {
|
|
1648
|
+
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
1292
1649
|
const controller = new AbortController();
|
|
1293
1650
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
1294
1651
|
try {
|
|
@@ -1304,7 +1661,8 @@ async function initNode9SaaS(toolName, args, creds, meta) {
|
|
|
1304
1661
|
hostname: os.hostname(),
|
|
1305
1662
|
cwd: process.cwd(),
|
|
1306
1663
|
platform: os.platform()
|
|
1307
|
-
}
|
|
1664
|
+
},
|
|
1665
|
+
...riskMetadata && { riskMetadata }
|
|
1308
1666
|
}),
|
|
1309
1667
|
signal: controller.signal
|
|
1310
1668
|
});
|