@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.js
CHANGED
|
@@ -38,18 +38,18 @@ module.exports = __toCommonJS(src_exports);
|
|
|
38
38
|
var import_chalk2 = __toESM(require("chalk"));
|
|
39
39
|
var import_prompts = require("@inquirer/prompts");
|
|
40
40
|
var import_fs = __toESM(require("fs"));
|
|
41
|
-
var
|
|
41
|
+
var import_path3 = __toESM(require("path"));
|
|
42
42
|
var import_os = __toESM(require("os"));
|
|
43
43
|
var import_picomatch = __toESM(require("picomatch"));
|
|
44
44
|
var import_sh_syntax = require("sh-syntax");
|
|
45
45
|
|
|
46
46
|
// src/ui/native.ts
|
|
47
47
|
var import_child_process = require("child_process");
|
|
48
|
-
var
|
|
48
|
+
var import_path2 = __toESM(require("path"));
|
|
49
49
|
var import_chalk = __toESM(require("chalk"));
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
|
|
51
|
+
// src/context-sniper.ts
|
|
52
|
+
var import_path = __toESM(require("path"));
|
|
53
53
|
function smartTruncate(str, maxLen = 500) {
|
|
54
54
|
if (str.length <= maxLen) return str;
|
|
55
55
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -57,11 +57,13 @@ function smartTruncate(str, maxLen = 500) {
|
|
|
57
57
|
}
|
|
58
58
|
function extractContext(text, matchedWord) {
|
|
59
59
|
const lines = text.split("\n");
|
|
60
|
-
if (lines.length <= 7 || !matchedWord)
|
|
60
|
+
if (lines.length <= 7 || !matchedWord) {
|
|
61
|
+
return { snippet: smartTruncate(text, 500), lineIndex: -1 };
|
|
62
|
+
}
|
|
61
63
|
const escaped = matchedWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
62
64
|
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
63
65
|
const allHits = lines.map((line, i) => ({ i, line })).filter(({ line }) => pattern.test(line));
|
|
64
|
-
if (allHits.length === 0) return smartTruncate(text, 500);
|
|
66
|
+
if (allHits.length === 0) return { snippet: smartTruncate(text, 500), lineIndex: -1 };
|
|
65
67
|
const nonComment = allHits.find(({ line }) => {
|
|
66
68
|
const trimmed = line.trim();
|
|
67
69
|
return !trimmed.startsWith("//") && !trimmed.startsWith("#");
|
|
@@ -69,13 +71,89 @@ function extractContext(text, matchedWord) {
|
|
|
69
71
|
const hitIndex = (nonComment ?? allHits[0]).i;
|
|
70
72
|
const start = Math.max(0, hitIndex - 3);
|
|
71
73
|
const end = Math.min(lines.length, hitIndex + 4);
|
|
74
|
+
const lineIndex = hitIndex - start;
|
|
72
75
|
const snippet = lines.slice(start, end).map((line, i) => `${start + i === hitIndex ? "\u{1F6D1} " : " "}${line}`).join("\n");
|
|
73
76
|
const head = start > 0 ? `... [${start} lines hidden] ...
|
|
74
77
|
` : "";
|
|
75
78
|
const tail = end < lines.length ? `
|
|
76
79
|
... [${lines.length - end} lines hidden] ...` : "";
|
|
77
|
-
return `${head}${snippet}${tail}
|
|
80
|
+
return { snippet: `${head}${snippet}${tail}`, lineIndex };
|
|
78
81
|
}
|
|
82
|
+
var CODE_KEYS = [
|
|
83
|
+
"command",
|
|
84
|
+
"cmd",
|
|
85
|
+
"shell_command",
|
|
86
|
+
"bash_command",
|
|
87
|
+
"script",
|
|
88
|
+
"code",
|
|
89
|
+
"input",
|
|
90
|
+
"sql",
|
|
91
|
+
"query",
|
|
92
|
+
"arguments",
|
|
93
|
+
"args",
|
|
94
|
+
"param",
|
|
95
|
+
"params",
|
|
96
|
+
"text"
|
|
97
|
+
];
|
|
98
|
+
function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
|
|
99
|
+
let intent = "EXEC";
|
|
100
|
+
let contextSnippet;
|
|
101
|
+
let contextLineIndex;
|
|
102
|
+
let editFileName;
|
|
103
|
+
let editFilePath;
|
|
104
|
+
let parsed = args;
|
|
105
|
+
if (typeof args === "string") {
|
|
106
|
+
const trimmed = args.trim();
|
|
107
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(trimmed);
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
115
|
+
const obj = parsed;
|
|
116
|
+
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
117
|
+
intent = "EDIT";
|
|
118
|
+
if (obj.file_path) {
|
|
119
|
+
editFilePath = String(obj.file_path);
|
|
120
|
+
editFileName = import_path.default.basename(editFilePath);
|
|
121
|
+
}
|
|
122
|
+
const result = extractContext(String(obj.new_string), matchedWord);
|
|
123
|
+
contextSnippet = result.snippet;
|
|
124
|
+
if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
|
|
125
|
+
} else if (matchedField && obj[matchedField] !== void 0) {
|
|
126
|
+
const result = extractContext(String(obj[matchedField]), matchedWord);
|
|
127
|
+
contextSnippet = result.snippet;
|
|
128
|
+
if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
|
|
129
|
+
} else {
|
|
130
|
+
const foundKey = Object.keys(obj).find((k) => CODE_KEYS.includes(k.toLowerCase()));
|
|
131
|
+
if (foundKey) {
|
|
132
|
+
const val = obj[foundKey];
|
|
133
|
+
contextSnippet = smartTruncate(typeof val === "string" ? val : JSON.stringify(val), 500);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} else if (typeof parsed === "string") {
|
|
137
|
+
contextSnippet = smartTruncate(parsed, 500);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
intent,
|
|
141
|
+
tier,
|
|
142
|
+
blockedByLabel,
|
|
143
|
+
...matchedWord && { matchedWord },
|
|
144
|
+
...matchedField && { matchedField },
|
|
145
|
+
...contextSnippet !== void 0 && { contextSnippet },
|
|
146
|
+
...contextLineIndex !== void 0 && { contextLineIndex },
|
|
147
|
+
...editFileName && { editFileName },
|
|
148
|
+
...editFilePath && { editFilePath },
|
|
149
|
+
...ruleName && { ruleName }
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/ui/native.ts
|
|
154
|
+
var isTestEnv = () => {
|
|
155
|
+
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";
|
|
156
|
+
};
|
|
79
157
|
function formatArgs(args, matchedField, matchedWord) {
|
|
80
158
|
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
81
159
|
let parsed = args;
|
|
@@ -94,9 +172,9 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
94
172
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
95
173
|
const obj = parsed;
|
|
96
174
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
97
|
-
const file = obj.file_path ?
|
|
175
|
+
const file = obj.file_path ? import_path2.default.basename(String(obj.file_path)) : "file";
|
|
98
176
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
99
|
-
const newPreview = extractContext(String(obj.new_string), matchedWord);
|
|
177
|
+
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
100
178
|
return {
|
|
101
179
|
intent: "EDIT",
|
|
102
180
|
message: `\u{1F4DD} EDITING: ${file}
|
|
@@ -114,7 +192,7 @@ ${newPreview}`
|
|
|
114
192
|
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(", ")}
|
|
115
193
|
|
|
116
194
|
` : "";
|
|
117
|
-
const content = extractContext(String(obj[matchedField]), matchedWord);
|
|
195
|
+
const content = extractContext(String(obj[matchedField]), matchedWord).snippet;
|
|
118
196
|
return {
|
|
119
197
|
intent: "EXEC",
|
|
120
198
|
message: `${context}\u{1F6D1} [${matchedField.toUpperCase()}]:
|
|
@@ -286,11 +364,113 @@ end run`;
|
|
|
286
364
|
});
|
|
287
365
|
}
|
|
288
366
|
|
|
367
|
+
// src/config-schema.ts
|
|
368
|
+
var import_zod = require("zod");
|
|
369
|
+
var noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
370
|
+
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
371
|
+
});
|
|
372
|
+
var validRegex = noNewlines.refine(
|
|
373
|
+
(s) => {
|
|
374
|
+
try {
|
|
375
|
+
new RegExp(s);
|
|
376
|
+
return true;
|
|
377
|
+
} catch {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
{ message: "Value must be a valid regular expression" }
|
|
382
|
+
);
|
|
383
|
+
var SmartConditionSchema = import_zod.z.object({
|
|
384
|
+
field: import_zod.z.string().min(1, "Condition field must not be empty"),
|
|
385
|
+
op: import_zod.z.enum(["matches", "notMatches", "contains", "notContains", "exists", "notExists"], {
|
|
386
|
+
errorMap: () => ({
|
|
387
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists"
|
|
388
|
+
})
|
|
389
|
+
}),
|
|
390
|
+
value: validRegex.optional(),
|
|
391
|
+
flags: import_zod.z.string().optional()
|
|
392
|
+
});
|
|
393
|
+
var SmartRuleSchema = import_zod.z.object({
|
|
394
|
+
name: import_zod.z.string().optional(),
|
|
395
|
+
tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
|
|
396
|
+
conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
397
|
+
conditionMode: import_zod.z.enum(["all", "any"]).optional(),
|
|
398
|
+
verdict: import_zod.z.enum(["allow", "review", "block"], {
|
|
399
|
+
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
400
|
+
}),
|
|
401
|
+
reason: import_zod.z.string().optional()
|
|
402
|
+
});
|
|
403
|
+
var PolicyRuleSchema = import_zod.z.object({
|
|
404
|
+
action: import_zod.z.string().min(1),
|
|
405
|
+
allowPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
406
|
+
blockPaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
407
|
+
});
|
|
408
|
+
var ConfigFileSchema = import_zod.z.object({
|
|
409
|
+
version: import_zod.z.string().optional(),
|
|
410
|
+
settings: import_zod.z.object({
|
|
411
|
+
mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
|
|
412
|
+
autoStartDaemon: import_zod.z.boolean().optional(),
|
|
413
|
+
enableUndo: import_zod.z.boolean().optional(),
|
|
414
|
+
enableHookLogDebug: import_zod.z.boolean().optional(),
|
|
415
|
+
approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
|
|
416
|
+
approvers: import_zod.z.object({
|
|
417
|
+
native: import_zod.z.boolean().optional(),
|
|
418
|
+
browser: import_zod.z.boolean().optional(),
|
|
419
|
+
cloud: import_zod.z.boolean().optional(),
|
|
420
|
+
terminal: import_zod.z.boolean().optional()
|
|
421
|
+
}).optional(),
|
|
422
|
+
environment: import_zod.z.string().optional(),
|
|
423
|
+
slackEnabled: import_zod.z.boolean().optional(),
|
|
424
|
+
enableTrustSessions: import_zod.z.boolean().optional(),
|
|
425
|
+
allowGlobalPause: import_zod.z.boolean().optional()
|
|
426
|
+
}).optional(),
|
|
427
|
+
policy: import_zod.z.object({
|
|
428
|
+
sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
429
|
+
dangerousWords: import_zod.z.array(noNewlines).optional(),
|
|
430
|
+
ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
431
|
+
toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
|
|
432
|
+
rules: import_zod.z.array(PolicyRuleSchema).optional(),
|
|
433
|
+
smartRules: import_zod.z.array(SmartRuleSchema).optional(),
|
|
434
|
+
snapshot: import_zod.z.object({
|
|
435
|
+
tools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
436
|
+
onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
437
|
+
ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
438
|
+
}).optional()
|
|
439
|
+
}).optional(),
|
|
440
|
+
environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
|
|
441
|
+
}).strict({ message: "Config contains unknown top-level keys" });
|
|
442
|
+
function sanitizeConfig(raw) {
|
|
443
|
+
const result = ConfigFileSchema.safeParse(raw);
|
|
444
|
+
if (result.success) {
|
|
445
|
+
return { sanitized: result.data, error: null };
|
|
446
|
+
}
|
|
447
|
+
const invalidTopLevelKeys = new Set(
|
|
448
|
+
result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
|
|
449
|
+
);
|
|
450
|
+
const sanitized = {};
|
|
451
|
+
if (typeof raw === "object" && raw !== null) {
|
|
452
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
453
|
+
if (!invalidTopLevelKeys.has(key)) {
|
|
454
|
+
sanitized[key] = value;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const lines = result.error.issues.map((issue) => {
|
|
459
|
+
const path4 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
460
|
+
return ` \u2022 ${path4}: ${issue.message}`;
|
|
461
|
+
});
|
|
462
|
+
return {
|
|
463
|
+
sanitized,
|
|
464
|
+
error: `Invalid config:
|
|
465
|
+
${lines.join("\n")}`
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
289
469
|
// src/core.ts
|
|
290
|
-
var PAUSED_FILE =
|
|
291
|
-
var TRUST_FILE =
|
|
292
|
-
var LOCAL_AUDIT_LOG =
|
|
293
|
-
var HOOK_DEBUG_LOG =
|
|
470
|
+
var PAUSED_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "PAUSED");
|
|
471
|
+
var TRUST_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "trust.json");
|
|
472
|
+
var LOCAL_AUDIT_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
473
|
+
var HOOK_DEBUG_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
|
|
294
474
|
function checkPause() {
|
|
295
475
|
try {
|
|
296
476
|
if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -308,7 +488,7 @@ function checkPause() {
|
|
|
308
488
|
}
|
|
309
489
|
}
|
|
310
490
|
function atomicWriteSync(filePath, data, options) {
|
|
311
|
-
const dir =
|
|
491
|
+
const dir = import_path3.default.dirname(filePath);
|
|
312
492
|
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
313
493
|
const tmpPath = `${filePath}.${import_os.default.hostname()}.${process.pid}.tmp`;
|
|
314
494
|
import_fs.default.writeFileSync(tmpPath, data, options);
|
|
@@ -349,7 +529,7 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
349
529
|
}
|
|
350
530
|
function appendToLog(logPath, entry) {
|
|
351
531
|
try {
|
|
352
|
-
const dir =
|
|
532
|
+
const dir = import_path3.default.dirname(logPath);
|
|
353
533
|
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
354
534
|
import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
355
535
|
} catch {
|
|
@@ -393,9 +573,9 @@ function matchesPattern(text, patterns) {
|
|
|
393
573
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
394
574
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
395
575
|
}
|
|
396
|
-
function getNestedValue(obj,
|
|
576
|
+
function getNestedValue(obj, path4) {
|
|
397
577
|
if (!obj || typeof obj !== "object") return null;
|
|
398
|
-
return
|
|
578
|
+
return path4.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
399
579
|
}
|
|
400
580
|
function evaluateSmartConditions(args, rule) {
|
|
401
581
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -532,15 +712,10 @@ function redactSecrets(text) {
|
|
|
532
712
|
return redacted;
|
|
533
713
|
}
|
|
534
714
|
var DANGEROUS_WORDS = [
|
|
535
|
-
"
|
|
536
|
-
|
|
537
|
-
"
|
|
538
|
-
|
|
539
|
-
"destroy",
|
|
540
|
-
"terminate",
|
|
541
|
-
"revoke",
|
|
542
|
-
"docker",
|
|
543
|
-
"psql"
|
|
715
|
+
"mkfs",
|
|
716
|
+
// formats/wipes a filesystem partition
|
|
717
|
+
"shred"
|
|
718
|
+
// permanently overwrites file contents (unrecoverable)
|
|
544
719
|
];
|
|
545
720
|
var DEFAULT_CONFIG = {
|
|
546
721
|
settings: {
|
|
@@ -597,6 +772,8 @@ var DEFAULT_CONFIG = {
|
|
|
597
772
|
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
598
773
|
},
|
|
599
774
|
rules: [
|
|
775
|
+
// Only use the legacy rules format for simple path-based rm control.
|
|
776
|
+
// All other command-level enforcement lives in smartRules below.
|
|
600
777
|
{
|
|
601
778
|
action: "rm",
|
|
602
779
|
allowPaths: [
|
|
@@ -613,6 +790,7 @@ var DEFAULT_CONFIG = {
|
|
|
613
790
|
}
|
|
614
791
|
],
|
|
615
792
|
smartRules: [
|
|
793
|
+
// ── SQL safety ────────────────────────────────────────────────────────
|
|
616
794
|
{
|
|
617
795
|
name: "no-delete-without-where",
|
|
618
796
|
tool: "*",
|
|
@@ -623,6 +801,84 @@ var DEFAULT_CONFIG = {
|
|
|
623
801
|
conditionMode: "all",
|
|
624
802
|
verdict: "review",
|
|
625
803
|
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
name: "review-drop-truncate-shell",
|
|
807
|
+
tool: "bash",
|
|
808
|
+
conditions: [
|
|
809
|
+
{
|
|
810
|
+
field: "command",
|
|
811
|
+
op: "matches",
|
|
812
|
+
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
813
|
+
flags: "i"
|
|
814
|
+
}
|
|
815
|
+
],
|
|
816
|
+
conditionMode: "all",
|
|
817
|
+
verdict: "review",
|
|
818
|
+
reason: "SQL DDL destructive statement inside a shell command"
|
|
819
|
+
},
|
|
820
|
+
// ── Git safety ────────────────────────────────────────────────────────
|
|
821
|
+
{
|
|
822
|
+
name: "block-force-push",
|
|
823
|
+
tool: "bash",
|
|
824
|
+
conditions: [
|
|
825
|
+
{
|
|
826
|
+
field: "command",
|
|
827
|
+
op: "matches",
|
|
828
|
+
value: "git push.*(--force|--force-with-lease|-f\\b)",
|
|
829
|
+
flags: "i"
|
|
830
|
+
}
|
|
831
|
+
],
|
|
832
|
+
conditionMode: "all",
|
|
833
|
+
verdict: "block",
|
|
834
|
+
reason: "Force push overwrites remote history and cannot be undone"
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
name: "review-git-push",
|
|
838
|
+
tool: "bash",
|
|
839
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
|
|
840
|
+
conditionMode: "all",
|
|
841
|
+
verdict: "review",
|
|
842
|
+
reason: "git push sends changes to a shared remote"
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
name: "review-git-destructive",
|
|
846
|
+
tool: "bash",
|
|
847
|
+
conditions: [
|
|
848
|
+
{
|
|
849
|
+
field: "command",
|
|
850
|
+
op: "matches",
|
|
851
|
+
value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
|
|
852
|
+
flags: "i"
|
|
853
|
+
}
|
|
854
|
+
],
|
|
855
|
+
conditionMode: "all",
|
|
856
|
+
verdict: "review",
|
|
857
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
858
|
+
},
|
|
859
|
+
// ── Shell safety ──────────────────────────────────────────────────────
|
|
860
|
+
{
|
|
861
|
+
name: "review-sudo",
|
|
862
|
+
tool: "bash",
|
|
863
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
|
|
864
|
+
conditionMode: "all",
|
|
865
|
+
verdict: "review",
|
|
866
|
+
reason: "Command requires elevated privileges"
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
name: "review-curl-pipe-shell",
|
|
870
|
+
tool: "bash",
|
|
871
|
+
conditions: [
|
|
872
|
+
{
|
|
873
|
+
field: "command",
|
|
874
|
+
op: "matches",
|
|
875
|
+
value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
876
|
+
flags: "i"
|
|
877
|
+
}
|
|
878
|
+
],
|
|
879
|
+
conditionMode: "all",
|
|
880
|
+
verdict: "block",
|
|
881
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
626
882
|
}
|
|
627
883
|
]
|
|
628
884
|
},
|
|
@@ -631,7 +887,7 @@ var DEFAULT_CONFIG = {
|
|
|
631
887
|
var cachedConfig = null;
|
|
632
888
|
function getInternalToken() {
|
|
633
889
|
try {
|
|
634
|
-
const pidFile =
|
|
890
|
+
const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
|
|
635
891
|
if (!import_fs.default.existsSync(pidFile)) return null;
|
|
636
892
|
const data = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
|
|
637
893
|
process.kill(data.pid, 0);
|
|
@@ -652,7 +908,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
652
908
|
return {
|
|
653
909
|
decision: matchedRule.verdict,
|
|
654
910
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
655
|
-
reason: matchedRule.reason
|
|
911
|
+
reason: matchedRule.reason,
|
|
912
|
+
tier: 2,
|
|
913
|
+
ruleName: matchedRule.name ?? matchedRule.tool
|
|
656
914
|
};
|
|
657
915
|
}
|
|
658
916
|
}
|
|
@@ -667,7 +925,7 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
667
925
|
pathTokens = analyzed.paths;
|
|
668
926
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
669
927
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
670
|
-
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)" };
|
|
928
|
+
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
671
929
|
}
|
|
672
930
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
673
931
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
@@ -690,7 +948,7 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
690
948
|
);
|
|
691
949
|
const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
|
|
692
950
|
if (hasSystemDisaster || isRootWipe) {
|
|
693
|
-
return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
|
|
951
|
+
return { decision: "review", blockedByLabel: "Manual Nuclear Protection", tier: 3 };
|
|
694
952
|
}
|
|
695
953
|
return { decision: "allow" };
|
|
696
954
|
}
|
|
@@ -708,14 +966,16 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
708
966
|
if (anyBlocked)
|
|
709
967
|
return {
|
|
710
968
|
decision: "review",
|
|
711
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)
|
|
969
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
|
|
970
|
+
tier: 5
|
|
712
971
|
};
|
|
713
972
|
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
714
973
|
if (allAllowed) return { decision: "allow" };
|
|
715
974
|
}
|
|
716
975
|
return {
|
|
717
976
|
decision: "review",
|
|
718
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)
|
|
977
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
|
|
978
|
+
tier: 5
|
|
719
979
|
};
|
|
720
980
|
}
|
|
721
981
|
}
|
|
@@ -757,13 +1017,14 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
757
1017
|
decision: "review",
|
|
758
1018
|
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
|
|
759
1019
|
matchedWord: matchedDangerousWord,
|
|
760
|
-
matchedField
|
|
1020
|
+
matchedField,
|
|
1021
|
+
tier: 6
|
|
761
1022
|
};
|
|
762
1023
|
}
|
|
763
1024
|
if (config.settings.mode === "strict") {
|
|
764
1025
|
const envConfig = getActiveEnvironment(config);
|
|
765
1026
|
if (envConfig?.requireApproval === false) return { decision: "allow" };
|
|
766
|
-
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)" };
|
|
1027
|
+
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)", tier: 7 };
|
|
767
1028
|
}
|
|
768
1029
|
return { decision: "allow" };
|
|
769
1030
|
}
|
|
@@ -775,7 +1036,7 @@ var DAEMON_PORT = 7391;
|
|
|
775
1036
|
var DAEMON_HOST = "127.0.0.1";
|
|
776
1037
|
function isDaemonRunning() {
|
|
777
1038
|
try {
|
|
778
|
-
const pidFile =
|
|
1039
|
+
const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
|
|
779
1040
|
if (!import_fs.default.existsSync(pidFile)) return false;
|
|
780
1041
|
const { pid, port } = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
|
|
781
1042
|
if (port !== DAEMON_PORT) return false;
|
|
@@ -787,7 +1048,7 @@ function isDaemonRunning() {
|
|
|
787
1048
|
}
|
|
788
1049
|
function getPersistentDecision(toolName) {
|
|
789
1050
|
try {
|
|
790
|
-
const file =
|
|
1051
|
+
const file = import_path3.default.join(import_os.default.homedir(), ".node9", "decisions.json");
|
|
791
1052
|
if (!import_fs.default.existsSync(file)) return null;
|
|
792
1053
|
const decisions = JSON.parse(import_fs.default.readFileSync(file, "utf-8"));
|
|
793
1054
|
const d = decisions[toolName];
|
|
@@ -796,7 +1057,7 @@ function getPersistentDecision(toolName) {
|
|
|
796
1057
|
}
|
|
797
1058
|
return null;
|
|
798
1059
|
}
|
|
799
|
-
async function askDaemon(toolName, args, meta, signal) {
|
|
1060
|
+
async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
800
1061
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
801
1062
|
const checkCtrl = new AbortController();
|
|
802
1063
|
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
@@ -806,7 +1067,13 @@ async function askDaemon(toolName, args, meta, signal) {
|
|
|
806
1067
|
const checkRes = await fetch(`${base}/check`, {
|
|
807
1068
|
method: "POST",
|
|
808
1069
|
headers: { "Content-Type": "application/json" },
|
|
809
|
-
body: JSON.stringify({
|
|
1070
|
+
body: JSON.stringify({
|
|
1071
|
+
toolName,
|
|
1072
|
+
args,
|
|
1073
|
+
agent: meta?.agent,
|
|
1074
|
+
mcpServer: meta?.mcpServer,
|
|
1075
|
+
...riskMetadata && { riskMetadata }
|
|
1076
|
+
}),
|
|
810
1077
|
signal: checkCtrl.signal
|
|
811
1078
|
});
|
|
812
1079
|
if (!checkRes.ok) throw new Error("Daemon fail");
|
|
@@ -831,7 +1098,7 @@ async function askDaemon(toolName, args, meta, signal) {
|
|
|
831
1098
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
832
1099
|
}
|
|
833
1100
|
}
|
|
834
|
-
async function notifyDaemonViewer(toolName, args, meta) {
|
|
1101
|
+
async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
835
1102
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
836
1103
|
const res = await fetch(`${base}/check`, {
|
|
837
1104
|
method: "POST",
|
|
@@ -841,7 +1108,8 @@ async function notifyDaemonViewer(toolName, args, meta) {
|
|
|
841
1108
|
args,
|
|
842
1109
|
slackDelegated: true,
|
|
843
1110
|
agent: meta?.agent,
|
|
844
|
-
mcpServer: meta?.mcpServer
|
|
1111
|
+
mcpServer: meta?.mcpServer,
|
|
1112
|
+
...riskMetadata && { riskMetadata }
|
|
845
1113
|
}),
|
|
846
1114
|
signal: AbortSignal.timeout(3e3)
|
|
847
1115
|
});
|
|
@@ -880,11 +1148,15 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
880
1148
|
let explainableLabel = "Local Config";
|
|
881
1149
|
let policyMatchedField;
|
|
882
1150
|
let policyMatchedWord;
|
|
1151
|
+
let riskMetadata;
|
|
883
1152
|
if (config.settings.mode === "audit") {
|
|
884
1153
|
if (!isIgnoredTool(toolName)) {
|
|
885
1154
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
886
1155
|
if (policyResult.decision === "review") {
|
|
887
1156
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
1157
|
+
if (approvers.cloud && creds?.apiKey) {
|
|
1158
|
+
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
1159
|
+
}
|
|
888
1160
|
sendDesktopNotification(
|
|
889
1161
|
"Node9 Audit Mode",
|
|
890
1162
|
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
@@ -895,13 +1167,15 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
895
1167
|
}
|
|
896
1168
|
if (!isIgnoredTool(toolName)) {
|
|
897
1169
|
if (getActiveTrustSession(toolName)) {
|
|
898
|
-
if (creds?.apiKey)
|
|
1170
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1171
|
+
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
899
1172
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
900
1173
|
return { approved: true, checkedBy: "trust" };
|
|
901
1174
|
}
|
|
902
1175
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
903
1176
|
if (policyResult.decision === "allow") {
|
|
904
|
-
if (creds?.apiKey)
|
|
1177
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1178
|
+
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
905
1179
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
906
1180
|
return { approved: true, checkedBy: "local-policy" };
|
|
907
1181
|
}
|
|
@@ -917,9 +1191,18 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
917
1191
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
918
1192
|
policyMatchedField = policyResult.matchedField;
|
|
919
1193
|
policyMatchedWord = policyResult.matchedWord;
|
|
1194
|
+
riskMetadata = computeRiskMetadata(
|
|
1195
|
+
args,
|
|
1196
|
+
policyResult.tier ?? 6,
|
|
1197
|
+
explainableLabel,
|
|
1198
|
+
policyMatchedField,
|
|
1199
|
+
policyMatchedWord,
|
|
1200
|
+
policyResult.ruleName
|
|
1201
|
+
);
|
|
920
1202
|
const persistent = getPersistentDecision(toolName);
|
|
921
1203
|
if (persistent === "allow") {
|
|
922
|
-
if (creds?.apiKey)
|
|
1204
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1205
|
+
await auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
923
1206
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
924
1207
|
return { approved: true, checkedBy: "persistent" };
|
|
925
1208
|
}
|
|
@@ -933,7 +1216,6 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
933
1216
|
};
|
|
934
1217
|
}
|
|
935
1218
|
} else {
|
|
936
|
-
if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
|
|
937
1219
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
938
1220
|
return { approved: true };
|
|
939
1221
|
}
|
|
@@ -942,8 +1224,21 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
942
1224
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
943
1225
|
if (cloudEnforced) {
|
|
944
1226
|
try {
|
|
945
|
-
const initResult = await initNode9SaaS(toolName, args, creds, meta);
|
|
1227
|
+
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
946
1228
|
if (!initResult.pending) {
|
|
1229
|
+
if (initResult.shadowMode) {
|
|
1230
|
+
console.error(
|
|
1231
|
+
import_chalk2.default.yellow(
|
|
1232
|
+
`
|
|
1233
|
+
\u26A0\uFE0F Node9 Shadow Mode: Action allowed, but would have been blocked by company policy.`
|
|
1234
|
+
)
|
|
1235
|
+
);
|
|
1236
|
+
if (initResult.shadowReason) {
|
|
1237
|
+
console.error(import_chalk2.default.dim(` Reason: ${initResult.shadowReason}
|
|
1238
|
+
`));
|
|
1239
|
+
}
|
|
1240
|
+
return { approved: true, checkedBy: "cloud" };
|
|
1241
|
+
}
|
|
947
1242
|
return {
|
|
948
1243
|
approved: !!initResult.approved,
|
|
949
1244
|
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
@@ -968,18 +1263,22 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
968
1263
|
);
|
|
969
1264
|
}
|
|
970
1265
|
}
|
|
971
|
-
if (
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1266
|
+
if (!options?.calledFromDaemon) {
|
|
1267
|
+
if (cloudEnforced && cloudRequestId) {
|
|
1268
|
+
console.error(
|
|
1269
|
+
import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
|
|
1270
|
+
);
|
|
1271
|
+
console.error(
|
|
1272
|
+
import_chalk2.default.cyan(" Dashboard \u2192 ") + import_chalk2.default.bold("Mission Control > Activity Feed\n")
|
|
1273
|
+
);
|
|
1274
|
+
} else if (!cloudEnforced) {
|
|
1275
|
+
const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
|
|
1276
|
+
console.error(
|
|
1277
|
+
import_chalk2.default.dim(`
|
|
980
1278
|
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
981
1279
|
`)
|
|
982
|
-
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
983
1282
|
}
|
|
984
1283
|
const abortController = new AbortController();
|
|
985
1284
|
const { signal } = abortController;
|
|
@@ -1010,7 +1309,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1010
1309
|
(async () => {
|
|
1011
1310
|
try {
|
|
1012
1311
|
if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
|
|
1013
|
-
viewerId = await notifyDaemonViewer(toolName, args, meta).catch(
|
|
1312
|
+
viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
|
|
1313
|
+
() => null
|
|
1314
|
+
);
|
|
1014
1315
|
}
|
|
1015
1316
|
const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
|
|
1016
1317
|
return {
|
|
@@ -1028,7 +1329,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1028
1329
|
})()
|
|
1029
1330
|
);
|
|
1030
1331
|
}
|
|
1031
|
-
if (approvers.native && !isManual) {
|
|
1332
|
+
if (approvers.native && !isManual && !options?.calledFromDaemon) {
|
|
1032
1333
|
racePromises.push(
|
|
1033
1334
|
(async () => {
|
|
1034
1335
|
const decision = await askNativePopup(
|
|
@@ -1056,7 +1357,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1056
1357
|
})()
|
|
1057
1358
|
);
|
|
1058
1359
|
}
|
|
1059
|
-
if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
1360
|
+
if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon && !cloudEnforced) {
|
|
1060
1361
|
racePromises.push(
|
|
1061
1362
|
(async () => {
|
|
1062
1363
|
try {
|
|
@@ -1067,7 +1368,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1067
1368
|
console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
1068
1369
|
`));
|
|
1069
1370
|
}
|
|
1070
|
-
const daemonDecision = await askDaemon(toolName, args, meta, signal);
|
|
1371
|
+
const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
|
|
1071
1372
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1072
1373
|
const isApproved = daemonDecision === "allow";
|
|
1073
1374
|
return {
|
|
@@ -1192,8 +1493,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1192
1493
|
}
|
|
1193
1494
|
function getConfig() {
|
|
1194
1495
|
if (cachedConfig) return cachedConfig;
|
|
1195
|
-
const globalPath =
|
|
1196
|
-
const projectPath =
|
|
1496
|
+
const globalPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
|
|
1497
|
+
const projectPath = import_path3.default.join(process.cwd(), "node9.config.json");
|
|
1197
1498
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1198
1499
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1199
1500
|
const mergedSettings = {
|
|
@@ -1213,6 +1514,7 @@ function getConfig() {
|
|
|
1213
1514
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
1214
1515
|
}
|
|
1215
1516
|
};
|
|
1517
|
+
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
1216
1518
|
const applyLayer = (source) => {
|
|
1217
1519
|
if (!source) return;
|
|
1218
1520
|
const s = source.settings || {};
|
|
@@ -1238,6 +1540,17 @@ function getConfig() {
|
|
|
1238
1540
|
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
1239
1541
|
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
1240
1542
|
}
|
|
1543
|
+
const envs = source.environments || {};
|
|
1544
|
+
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
1545
|
+
if (envConfig && typeof envConfig === "object") {
|
|
1546
|
+
const ec = envConfig;
|
|
1547
|
+
mergedEnvironments[envName] = {
|
|
1548
|
+
...mergedEnvironments[envName],
|
|
1549
|
+
// Validate field types before merging — do not blindly spread user input
|
|
1550
|
+
...typeof ec.requireApproval === "boolean" ? { requireApproval: ec.requireApproval } : {}
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1241
1554
|
};
|
|
1242
1555
|
applyLayer(globalConfig);
|
|
1243
1556
|
applyLayer(projectConfig);
|
|
@@ -1251,17 +1564,62 @@ function getConfig() {
|
|
|
1251
1564
|
cachedConfig = {
|
|
1252
1565
|
settings: mergedSettings,
|
|
1253
1566
|
policy: mergedPolicy,
|
|
1254
|
-
environments:
|
|
1567
|
+
environments: mergedEnvironments
|
|
1255
1568
|
};
|
|
1256
1569
|
return cachedConfig;
|
|
1257
1570
|
}
|
|
1258
1571
|
function tryLoadConfig(filePath) {
|
|
1259
1572
|
if (!import_fs.default.existsSync(filePath)) return null;
|
|
1573
|
+
let raw;
|
|
1260
1574
|
try {
|
|
1261
|
-
|
|
1262
|
-
} catch {
|
|
1575
|
+
raw = JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
|
|
1576
|
+
} catch (err) {
|
|
1577
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1578
|
+
process.stderr.write(
|
|
1579
|
+
`
|
|
1580
|
+
\u26A0\uFE0F Node9: Failed to parse ${filePath}
|
|
1581
|
+
${msg}
|
|
1582
|
+
\u2192 Using default config
|
|
1583
|
+
|
|
1584
|
+
`
|
|
1585
|
+
);
|
|
1263
1586
|
return null;
|
|
1264
1587
|
}
|
|
1588
|
+
const SUPPORTED_VERSION = "1.0";
|
|
1589
|
+
const SUPPORTED_MAJOR = SUPPORTED_VERSION.split(".")[0];
|
|
1590
|
+
const fileVersion = raw?.version;
|
|
1591
|
+
if (fileVersion !== void 0) {
|
|
1592
|
+
const vStr = String(fileVersion);
|
|
1593
|
+
const fileMajor = vStr.split(".")[0];
|
|
1594
|
+
if (fileMajor !== SUPPORTED_MAJOR) {
|
|
1595
|
+
process.stderr.write(
|
|
1596
|
+
`
|
|
1597
|
+
\u274C Node9: Config at ${filePath} has version "${vStr}" \u2014 major version is incompatible with this release (expected "${SUPPORTED_VERSION}"). Config will not be loaded.
|
|
1598
|
+
|
|
1599
|
+
`
|
|
1600
|
+
);
|
|
1601
|
+
return null;
|
|
1602
|
+
} else if (vStr !== SUPPORTED_VERSION) {
|
|
1603
|
+
process.stderr.write(
|
|
1604
|
+
`
|
|
1605
|
+
\u26A0\uFE0F Node9: Config at ${filePath} declares version "${vStr}" \u2014 expected "${SUPPORTED_VERSION}". Continuing with best-effort parsing.
|
|
1606
|
+
|
|
1607
|
+
`
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
const { sanitized, error } = sanitizeConfig(raw);
|
|
1612
|
+
if (error) {
|
|
1613
|
+
process.stderr.write(
|
|
1614
|
+
`
|
|
1615
|
+
\u26A0\uFE0F Node9: Invalid config at ${filePath}:
|
|
1616
|
+
${error.replace("Invalid config:\n", "")}
|
|
1617
|
+
\u2192 Invalid fields ignored, using defaults for those keys
|
|
1618
|
+
|
|
1619
|
+
`
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
return sanitized;
|
|
1265
1623
|
}
|
|
1266
1624
|
function getActiveEnvironment(config) {
|
|
1267
1625
|
const env = config.settings.environment || process.env.NODE_ENV || "development";
|
|
@@ -1276,7 +1634,7 @@ function getCredentials() {
|
|
|
1276
1634
|
};
|
|
1277
1635
|
}
|
|
1278
1636
|
try {
|
|
1279
|
-
const credPath =
|
|
1637
|
+
const credPath = import_path3.default.join(import_os.default.homedir(), ".node9", "credentials.json");
|
|
1280
1638
|
if (import_fs.default.existsSync(credPath)) {
|
|
1281
1639
|
const creds = JSON.parse(import_fs.default.readFileSync(credPath, "utf-8"));
|
|
1282
1640
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
@@ -1303,9 +1661,7 @@ async function authorizeAction(toolName, args) {
|
|
|
1303
1661
|
return result.approved;
|
|
1304
1662
|
}
|
|
1305
1663
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
1306
|
-
|
|
1307
|
-
setTimeout(() => controller.abort(), 5e3);
|
|
1308
|
-
fetch(`${creds.apiUrl}/audit`, {
|
|
1664
|
+
return fetch(`${creds.apiUrl}/audit`, {
|
|
1309
1665
|
method: "POST",
|
|
1310
1666
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
1311
1667
|
body: JSON.stringify({
|
|
@@ -1320,11 +1676,12 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1320
1676
|
platform: import_os.default.platform()
|
|
1321
1677
|
}
|
|
1322
1678
|
}),
|
|
1323
|
-
signal:
|
|
1679
|
+
signal: AbortSignal.timeout(5e3)
|
|
1680
|
+
}).then(() => {
|
|
1324
1681
|
}).catch(() => {
|
|
1325
1682
|
});
|
|
1326
1683
|
}
|
|
1327
|
-
async function initNode9SaaS(toolName, args, creds, meta) {
|
|
1684
|
+
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
1328
1685
|
const controller = new AbortController();
|
|
1329
1686
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
1330
1687
|
try {
|
|
@@ -1340,7 +1697,8 @@ async function initNode9SaaS(toolName, args, creds, meta) {
|
|
|
1340
1697
|
hostname: import_os.default.hostname(),
|
|
1341
1698
|
cwd: process.cwd(),
|
|
1342
1699
|
platform: import_os.default.platform()
|
|
1343
|
-
}
|
|
1700
|
+
},
|
|
1701
|
+
...riskMetadata && { riskMetadata }
|
|
1344
1702
|
}),
|
|
1345
1703
|
signal: controller.signal
|
|
1346
1704
|
});
|