@node9/proxy 1.0.7 → 1.0.9
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/LICENSE +183 -21
- package/README.md +1 -106
- package/dist/cli.js +750 -182
- package/dist/cli.mjs +750 -182
- package/dist/index.js +510 -79
- package/dist/index.mjs +510 -79
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -38,24 +38,124 @@ 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 import_path2 = __toESM(require("path"));
|
|
48
49
|
var import_chalk = __toESM(require("chalk"));
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
|
|
51
|
+
// src/context-sniper.ts
|
|
52
|
+
var import_path = __toESM(require("path"));
|
|
52
53
|
function smartTruncate(str, maxLen = 500) {
|
|
53
54
|
if (str.length <= maxLen) return str;
|
|
54
55
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
55
56
|
return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
|
|
56
57
|
}
|
|
57
|
-
function
|
|
58
|
-
|
|
58
|
+
function extractContext(text, matchedWord) {
|
|
59
|
+
const lines = text.split("\n");
|
|
60
|
+
if (lines.length <= 7 || !matchedWord) {
|
|
61
|
+
return { snippet: smartTruncate(text, 500), lineIndex: -1 };
|
|
62
|
+
}
|
|
63
|
+
const escaped = matchedWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
64
|
+
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
65
|
+
const allHits = lines.map((line, i) => ({ i, line })).filter(({ line }) => pattern.test(line));
|
|
66
|
+
if (allHits.length === 0) return { snippet: smartTruncate(text, 500), lineIndex: -1 };
|
|
67
|
+
const nonComment = allHits.find(({ line }) => {
|
|
68
|
+
const trimmed = line.trim();
|
|
69
|
+
return !trimmed.startsWith("//") && !trimmed.startsWith("#");
|
|
70
|
+
});
|
|
71
|
+
const hitIndex = (nonComment ?? allHits[0]).i;
|
|
72
|
+
const start = Math.max(0, hitIndex - 3);
|
|
73
|
+
const end = Math.min(lines.length, hitIndex + 4);
|
|
74
|
+
const lineIndex = hitIndex - start;
|
|
75
|
+
const snippet = lines.slice(start, end).map((line, i) => `${start + i === hitIndex ? "\u{1F6D1} " : " "}${line}`).join("\n");
|
|
76
|
+
const head = start > 0 ? `... [${start} lines hidden] ...
|
|
77
|
+
` : "";
|
|
78
|
+
const tail = end < lines.length ? `
|
|
79
|
+
... [${lines.length - end} lines hidden] ...` : "";
|
|
80
|
+
return { snippet: `${head}${snippet}${tail}`, lineIndex };
|
|
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
|
+
};
|
|
157
|
+
function formatArgs(args, matchedField, matchedWord) {
|
|
158
|
+
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
59
159
|
let parsed = args;
|
|
60
160
|
if (typeof args === "string") {
|
|
61
161
|
const trimmed = args.trim();
|
|
@@ -66,11 +166,39 @@ function formatArgs(args) {
|
|
|
66
166
|
parsed = args;
|
|
67
167
|
}
|
|
68
168
|
} else {
|
|
69
|
-
return smartTruncate(args, 600);
|
|
169
|
+
return { message: smartTruncate(args, 600), intent: "EXEC" };
|
|
70
170
|
}
|
|
71
171
|
}
|
|
72
172
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
73
173
|
const obj = parsed;
|
|
174
|
+
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
175
|
+
const file = obj.file_path ? import_path2.default.basename(String(obj.file_path)) : "file";
|
|
176
|
+
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
177
|
+
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
178
|
+
return {
|
|
179
|
+
intent: "EDIT",
|
|
180
|
+
message: `\u{1F4DD} EDITING: ${file}
|
|
181
|
+
\u{1F4C2} PATH: ${obj.file_path}
|
|
182
|
+
|
|
183
|
+
--- REPLACING ---
|
|
184
|
+
${oldPreview}
|
|
185
|
+
|
|
186
|
+
+++ NEW CODE +++
|
|
187
|
+
${newPreview}`
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (matchedField && obj[matchedField] !== void 0) {
|
|
191
|
+
const otherKeys = Object.keys(obj).filter((k) => k !== matchedField);
|
|
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(", ")}
|
|
193
|
+
|
|
194
|
+
` : "";
|
|
195
|
+
const content = extractContext(String(obj[matchedField]), matchedWord).snippet;
|
|
196
|
+
return {
|
|
197
|
+
intent: "EXEC",
|
|
198
|
+
message: `${context}\u{1F6D1} [${matchedField.toUpperCase()}]:
|
|
199
|
+
${content}`
|
|
200
|
+
};
|
|
201
|
+
}
|
|
74
202
|
const codeKeys = [
|
|
75
203
|
"command",
|
|
76
204
|
"cmd",
|
|
@@ -91,14 +219,18 @@ function formatArgs(args) {
|
|
|
91
219
|
if (foundKey) {
|
|
92
220
|
const val = obj[foundKey];
|
|
93
221
|
const str = typeof val === "string" ? val : JSON.stringify(val);
|
|
94
|
-
return
|
|
95
|
-
|
|
222
|
+
return {
|
|
223
|
+
intent: "EXEC",
|
|
224
|
+
message: `[${foundKey.toUpperCase()}]:
|
|
225
|
+
${smartTruncate(str, 500)}`
|
|
226
|
+
};
|
|
96
227
|
}
|
|
97
|
-
|
|
228
|
+
const msg = Object.entries(obj).slice(0, 5).map(
|
|
98
229
|
([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
|
|
99
230
|
).join("\n");
|
|
231
|
+
return { intent: "EXEC", message: msg };
|
|
100
232
|
}
|
|
101
|
-
return smartTruncate(JSON.stringify(parsed), 200);
|
|
233
|
+
return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
|
|
102
234
|
}
|
|
103
235
|
function sendDesktopNotification(title, body) {
|
|
104
236
|
if (isTestEnv()) return;
|
|
@@ -151,10 +283,11 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
151
283
|
}
|
|
152
284
|
return lines.join("\n");
|
|
153
285
|
}
|
|
154
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
|
|
286
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
|
|
155
287
|
if (isTestEnv()) return "deny";
|
|
156
|
-
const formattedArgs = formatArgs(args);
|
|
157
|
-
const
|
|
288
|
+
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
289
|
+
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
290
|
+
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
158
291
|
const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
|
|
159
292
|
process.stderr.write(import_chalk.default.yellow(`
|
|
160
293
|
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
@@ -231,11 +364,113 @@ end run`;
|
|
|
231
364
|
});
|
|
232
365
|
}
|
|
233
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
|
+
|
|
234
469
|
// src/core.ts
|
|
235
|
-
var PAUSED_FILE =
|
|
236
|
-
var TRUST_FILE =
|
|
237
|
-
var LOCAL_AUDIT_LOG =
|
|
238
|
-
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");
|
|
239
474
|
function checkPause() {
|
|
240
475
|
try {
|
|
241
476
|
if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -253,7 +488,7 @@ function checkPause() {
|
|
|
253
488
|
}
|
|
254
489
|
}
|
|
255
490
|
function atomicWriteSync(filePath, data, options) {
|
|
256
|
-
const dir =
|
|
491
|
+
const dir = import_path3.default.dirname(filePath);
|
|
257
492
|
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
258
493
|
const tmpPath = `${filePath}.${import_os.default.hostname()}.${process.pid}.tmp`;
|
|
259
494
|
import_fs.default.writeFileSync(tmpPath, data, options);
|
|
@@ -294,7 +529,7 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
294
529
|
}
|
|
295
530
|
function appendToLog(logPath, entry) {
|
|
296
531
|
try {
|
|
297
|
-
const dir =
|
|
532
|
+
const dir = import_path3.default.dirname(logPath);
|
|
298
533
|
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
299
534
|
import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
300
535
|
} catch {
|
|
@@ -338,9 +573,9 @@ function matchesPattern(text, patterns) {
|
|
|
338
573
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
339
574
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
340
575
|
}
|
|
341
|
-
function getNestedValue(obj,
|
|
576
|
+
function getNestedValue(obj, path4) {
|
|
342
577
|
if (!obj || typeof obj !== "object") return null;
|
|
343
|
-
return
|
|
578
|
+
return path4.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
344
579
|
}
|
|
345
580
|
function evaluateSmartConditions(args, rule) {
|
|
346
581
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -477,15 +712,10 @@ function redactSecrets(text) {
|
|
|
477
712
|
return redacted;
|
|
478
713
|
}
|
|
479
714
|
var DANGEROUS_WORDS = [
|
|
480
|
-
"
|
|
481
|
-
|
|
482
|
-
"
|
|
483
|
-
|
|
484
|
-
"destroy",
|
|
485
|
-
"terminate",
|
|
486
|
-
"revoke",
|
|
487
|
-
"docker",
|
|
488
|
-
"psql"
|
|
715
|
+
"mkfs",
|
|
716
|
+
// formats/wipes a filesystem partition
|
|
717
|
+
"shred"
|
|
718
|
+
// permanently overwrites file contents (unrecoverable)
|
|
489
719
|
];
|
|
490
720
|
var DEFAULT_CONFIG = {
|
|
491
721
|
settings: {
|
|
@@ -529,7 +759,21 @@ var DEFAULT_CONFIG = {
|
|
|
529
759
|
"terminal.execute": "command",
|
|
530
760
|
"postgres:query": "sql"
|
|
531
761
|
},
|
|
762
|
+
snapshot: {
|
|
763
|
+
tools: [
|
|
764
|
+
"str_replace_based_edit_tool",
|
|
765
|
+
"write_file",
|
|
766
|
+
"edit_file",
|
|
767
|
+
"create_file",
|
|
768
|
+
"edit",
|
|
769
|
+
"replace"
|
|
770
|
+
],
|
|
771
|
+
onlyPaths: [],
|
|
772
|
+
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
773
|
+
},
|
|
532
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.
|
|
533
777
|
{
|
|
534
778
|
action: "rm",
|
|
535
779
|
allowPaths: [
|
|
@@ -546,6 +790,7 @@ var DEFAULT_CONFIG = {
|
|
|
546
790
|
}
|
|
547
791
|
],
|
|
548
792
|
smartRules: [
|
|
793
|
+
// ── SQL safety ────────────────────────────────────────────────────────
|
|
549
794
|
{
|
|
550
795
|
name: "no-delete-without-where",
|
|
551
796
|
tool: "*",
|
|
@@ -556,6 +801,84 @@ var DEFAULT_CONFIG = {
|
|
|
556
801
|
conditionMode: "all",
|
|
557
802
|
verdict: "review",
|
|
558
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 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"
|
|
559
882
|
}
|
|
560
883
|
]
|
|
561
884
|
},
|
|
@@ -564,7 +887,7 @@ var DEFAULT_CONFIG = {
|
|
|
564
887
|
var cachedConfig = null;
|
|
565
888
|
function getInternalToken() {
|
|
566
889
|
try {
|
|
567
|
-
const pidFile =
|
|
890
|
+
const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
|
|
568
891
|
if (!import_fs.default.existsSync(pidFile)) return null;
|
|
569
892
|
const data = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
|
|
570
893
|
process.kill(data.pid, 0);
|
|
@@ -585,7 +908,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
585
908
|
return {
|
|
586
909
|
decision: matchedRule.verdict,
|
|
587
910
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
588
|
-
reason: matchedRule.reason
|
|
911
|
+
reason: matchedRule.reason,
|
|
912
|
+
tier: 2,
|
|
913
|
+
ruleName: matchedRule.name ?? matchedRule.tool
|
|
589
914
|
};
|
|
590
915
|
}
|
|
591
916
|
}
|
|
@@ -600,7 +925,7 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
600
925
|
pathTokens = analyzed.paths;
|
|
601
926
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
602
927
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
603
|
-
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)" };
|
|
928
|
+
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
604
929
|
}
|
|
605
930
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
606
931
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
@@ -623,7 +948,7 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
623
948
|
);
|
|
624
949
|
const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
|
|
625
950
|
if (hasSystemDisaster || isRootWipe) {
|
|
626
|
-
return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
|
|
951
|
+
return { decision: "review", blockedByLabel: "Manual Nuclear Protection", tier: 3 };
|
|
627
952
|
}
|
|
628
953
|
return { decision: "allow" };
|
|
629
954
|
}
|
|
@@ -641,14 +966,16 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
641
966
|
if (anyBlocked)
|
|
642
967
|
return {
|
|
643
968
|
decision: "review",
|
|
644
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)
|
|
969
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
|
|
970
|
+
tier: 5
|
|
645
971
|
};
|
|
646
972
|
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
647
973
|
if (allAllowed) return { decision: "allow" };
|
|
648
974
|
}
|
|
649
975
|
return {
|
|
650
976
|
decision: "review",
|
|
651
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)
|
|
977
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
|
|
978
|
+
tier: 5
|
|
652
979
|
};
|
|
653
980
|
}
|
|
654
981
|
}
|
|
@@ -668,15 +995,36 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
668
995
|
})
|
|
669
996
|
);
|
|
670
997
|
if (isDangerous) {
|
|
998
|
+
let matchedField;
|
|
999
|
+
if (matchedDangerousWord && args && typeof args === "object" && !Array.isArray(args)) {
|
|
1000
|
+
const obj = args;
|
|
1001
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1002
|
+
if (typeof value === "string") {
|
|
1003
|
+
try {
|
|
1004
|
+
if (new RegExp(
|
|
1005
|
+
`\\b${matchedDangerousWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
|
|
1006
|
+
"i"
|
|
1007
|
+
).test(value)) {
|
|
1008
|
+
matchedField = key;
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
} catch {
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
671
1016
|
return {
|
|
672
1017
|
decision: "review",
|
|
673
|
-
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"
|
|
1018
|
+
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
|
|
1019
|
+
matchedWord: matchedDangerousWord,
|
|
1020
|
+
matchedField,
|
|
1021
|
+
tier: 6
|
|
674
1022
|
};
|
|
675
1023
|
}
|
|
676
1024
|
if (config.settings.mode === "strict") {
|
|
677
1025
|
const envConfig = getActiveEnvironment(config);
|
|
678
1026
|
if (envConfig?.requireApproval === false) return { decision: "allow" };
|
|
679
|
-
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)" };
|
|
1027
|
+
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)", tier: 7 };
|
|
680
1028
|
}
|
|
681
1029
|
return { decision: "allow" };
|
|
682
1030
|
}
|
|
@@ -688,7 +1036,7 @@ var DAEMON_PORT = 7391;
|
|
|
688
1036
|
var DAEMON_HOST = "127.0.0.1";
|
|
689
1037
|
function isDaemonRunning() {
|
|
690
1038
|
try {
|
|
691
|
-
const pidFile =
|
|
1039
|
+
const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
|
|
692
1040
|
if (!import_fs.default.existsSync(pidFile)) return false;
|
|
693
1041
|
const { pid, port } = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
|
|
694
1042
|
if (port !== DAEMON_PORT) return false;
|
|
@@ -700,7 +1048,7 @@ function isDaemonRunning() {
|
|
|
700
1048
|
}
|
|
701
1049
|
function getPersistentDecision(toolName) {
|
|
702
1050
|
try {
|
|
703
|
-
const file =
|
|
1051
|
+
const file = import_path3.default.join(import_os.default.homedir(), ".node9", "decisions.json");
|
|
704
1052
|
if (!import_fs.default.existsSync(file)) return null;
|
|
705
1053
|
const decisions = JSON.parse(import_fs.default.readFileSync(file, "utf-8"));
|
|
706
1054
|
const d = decisions[toolName];
|
|
@@ -709,7 +1057,7 @@ function getPersistentDecision(toolName) {
|
|
|
709
1057
|
}
|
|
710
1058
|
return null;
|
|
711
1059
|
}
|
|
712
|
-
async function askDaemon(toolName, args, meta, signal) {
|
|
1060
|
+
async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
713
1061
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
714
1062
|
const checkCtrl = new AbortController();
|
|
715
1063
|
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
@@ -719,7 +1067,13 @@ async function askDaemon(toolName, args, meta, signal) {
|
|
|
719
1067
|
const checkRes = await fetch(`${base}/check`, {
|
|
720
1068
|
method: "POST",
|
|
721
1069
|
headers: { "Content-Type": "application/json" },
|
|
722
|
-
body: JSON.stringify({
|
|
1070
|
+
body: JSON.stringify({
|
|
1071
|
+
toolName,
|
|
1072
|
+
args,
|
|
1073
|
+
agent: meta?.agent,
|
|
1074
|
+
mcpServer: meta?.mcpServer,
|
|
1075
|
+
...riskMetadata && { riskMetadata }
|
|
1076
|
+
}),
|
|
723
1077
|
signal: checkCtrl.signal
|
|
724
1078
|
});
|
|
725
1079
|
if (!checkRes.ok) throw new Error("Daemon fail");
|
|
@@ -744,7 +1098,7 @@ async function askDaemon(toolName, args, meta, signal) {
|
|
|
744
1098
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
745
1099
|
}
|
|
746
1100
|
}
|
|
747
|
-
async function notifyDaemonViewer(toolName, args, meta) {
|
|
1101
|
+
async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
748
1102
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
749
1103
|
const res = await fetch(`${base}/check`, {
|
|
750
1104
|
method: "POST",
|
|
@@ -754,7 +1108,8 @@ async function notifyDaemonViewer(toolName, args, meta) {
|
|
|
754
1108
|
args,
|
|
755
1109
|
slackDelegated: true,
|
|
756
1110
|
agent: meta?.agent,
|
|
757
|
-
mcpServer: meta?.mcpServer
|
|
1111
|
+
mcpServer: meta?.mcpServer,
|
|
1112
|
+
...riskMetadata && { riskMetadata }
|
|
758
1113
|
}),
|
|
759
1114
|
signal: AbortSignal.timeout(3e3)
|
|
760
1115
|
});
|
|
@@ -771,7 +1126,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
771
1126
|
signal: AbortSignal.timeout(3e3)
|
|
772
1127
|
});
|
|
773
1128
|
}
|
|
774
|
-
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta) {
|
|
1129
|
+
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
775
1130
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
776
1131
|
const pauseState = checkPause();
|
|
777
1132
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
@@ -791,11 +1146,17 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
791
1146
|
}
|
|
792
1147
|
const isManual = meta?.agent === "Terminal";
|
|
793
1148
|
let explainableLabel = "Local Config";
|
|
1149
|
+
let policyMatchedField;
|
|
1150
|
+
let policyMatchedWord;
|
|
1151
|
+
let riskMetadata;
|
|
794
1152
|
if (config.settings.mode === "audit") {
|
|
795
1153
|
if (!isIgnoredTool(toolName)) {
|
|
796
1154
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
797
1155
|
if (policyResult.decision === "review") {
|
|
798
1156
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
1157
|
+
if (approvers.cloud && creds?.apiKey) {
|
|
1158
|
+
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
1159
|
+
}
|
|
799
1160
|
sendDesktopNotification(
|
|
800
1161
|
"Node9 Audit Mode",
|
|
801
1162
|
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
@@ -806,13 +1167,15 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
806
1167
|
}
|
|
807
1168
|
if (!isIgnoredTool(toolName)) {
|
|
808
1169
|
if (getActiveTrustSession(toolName)) {
|
|
809
|
-
if (creds?.apiKey)
|
|
1170
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1171
|
+
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
810
1172
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
811
1173
|
return { approved: true, checkedBy: "trust" };
|
|
812
1174
|
}
|
|
813
1175
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
814
1176
|
if (policyResult.decision === "allow") {
|
|
815
|
-
if (creds?.apiKey)
|
|
1177
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1178
|
+
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
816
1179
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
817
1180
|
return { approved: true, checkedBy: "local-policy" };
|
|
818
1181
|
}
|
|
@@ -826,9 +1189,20 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
826
1189
|
};
|
|
827
1190
|
}
|
|
828
1191
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
1192
|
+
policyMatchedField = policyResult.matchedField;
|
|
1193
|
+
policyMatchedWord = policyResult.matchedWord;
|
|
1194
|
+
riskMetadata = computeRiskMetadata(
|
|
1195
|
+
args,
|
|
1196
|
+
policyResult.tier ?? 6,
|
|
1197
|
+
explainableLabel,
|
|
1198
|
+
policyMatchedField,
|
|
1199
|
+
policyMatchedWord,
|
|
1200
|
+
policyResult.ruleName
|
|
1201
|
+
);
|
|
829
1202
|
const persistent = getPersistentDecision(toolName);
|
|
830
1203
|
if (persistent === "allow") {
|
|
831
|
-
if (creds?.apiKey)
|
|
1204
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1205
|
+
await auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
832
1206
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
833
1207
|
return { approved: true, checkedBy: "persistent" };
|
|
834
1208
|
}
|
|
@@ -842,7 +1216,6 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
842
1216
|
};
|
|
843
1217
|
}
|
|
844
1218
|
} else {
|
|
845
|
-
if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
|
|
846
1219
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
847
1220
|
return { approved: true };
|
|
848
1221
|
}
|
|
@@ -851,8 +1224,21 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
851
1224
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
852
1225
|
if (cloudEnforced) {
|
|
853
1226
|
try {
|
|
854
|
-
const initResult = await initNode9SaaS(toolName, args, creds, meta);
|
|
1227
|
+
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
855
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
|
+
}
|
|
856
1242
|
return {
|
|
857
1243
|
approved: !!initResult.approved,
|
|
858
1244
|
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
@@ -877,18 +1263,22 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
877
1263
|
);
|
|
878
1264
|
}
|
|
879
1265
|
}
|
|
880
|
-
if (
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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(`
|
|
889
1278
|
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
890
1279
|
`)
|
|
891
|
-
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
892
1282
|
}
|
|
893
1283
|
const abortController = new AbortController();
|
|
894
1284
|
const { signal } = abortController;
|
|
@@ -918,8 +1308,10 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
918
1308
|
racePromises.push(
|
|
919
1309
|
(async () => {
|
|
920
1310
|
try {
|
|
921
|
-
if (isDaemonRunning() && internalToken) {
|
|
922
|
-
viewerId = await notifyDaemonViewer(toolName, args, meta).catch(
|
|
1311
|
+
if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
|
|
1312
|
+
viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
|
|
1313
|
+
() => null
|
|
1314
|
+
);
|
|
923
1315
|
}
|
|
924
1316
|
const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
|
|
925
1317
|
return {
|
|
@@ -937,7 +1329,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
937
1329
|
})()
|
|
938
1330
|
);
|
|
939
1331
|
}
|
|
940
|
-
if (approvers.native && !isManual) {
|
|
1332
|
+
if (approvers.native && !isManual && !options?.calledFromDaemon) {
|
|
941
1333
|
racePromises.push(
|
|
942
1334
|
(async () => {
|
|
943
1335
|
const decision = await askNativePopup(
|
|
@@ -946,7 +1338,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
946
1338
|
meta?.agent,
|
|
947
1339
|
explainableLabel,
|
|
948
1340
|
isRemoteLocked,
|
|
949
|
-
signal
|
|
1341
|
+
signal,
|
|
1342
|
+
policyMatchedField,
|
|
1343
|
+
policyMatchedWord
|
|
950
1344
|
);
|
|
951
1345
|
if (decision === "always_allow") {
|
|
952
1346
|
writeTrustSession(toolName, 36e5);
|
|
@@ -963,7 +1357,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
963
1357
|
})()
|
|
964
1358
|
);
|
|
965
1359
|
}
|
|
966
|
-
if (approvers.browser && isDaemonRunning()) {
|
|
1360
|
+
if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon && !cloudEnforced) {
|
|
967
1361
|
racePromises.push(
|
|
968
1362
|
(async () => {
|
|
969
1363
|
try {
|
|
@@ -974,7 +1368,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
974
1368
|
console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
975
1369
|
`));
|
|
976
1370
|
}
|
|
977
|
-
const daemonDecision = await askDaemon(toolName, args, meta, signal);
|
|
1371
|
+
const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
|
|
978
1372
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
979
1373
|
const isApproved = daemonDecision === "allow";
|
|
980
1374
|
return {
|
|
@@ -1099,8 +1493,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1099
1493
|
}
|
|
1100
1494
|
function getConfig() {
|
|
1101
1495
|
if (cachedConfig) return cachedConfig;
|
|
1102
|
-
const globalPath =
|
|
1103
|
-
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");
|
|
1104
1498
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1105
1499
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1106
1500
|
const mergedSettings = {
|
|
@@ -1113,7 +1507,12 @@ function getConfig() {
|
|
|
1113
1507
|
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
1114
1508
|
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
1115
1509
|
rules: [...DEFAULT_CONFIG.policy.rules],
|
|
1116
|
-
smartRules: [...DEFAULT_CONFIG.policy.smartRules]
|
|
1510
|
+
smartRules: [...DEFAULT_CONFIG.policy.smartRules],
|
|
1511
|
+
snapshot: {
|
|
1512
|
+
tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
|
|
1513
|
+
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
1514
|
+
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
1515
|
+
}
|
|
1117
1516
|
};
|
|
1118
1517
|
const applyLayer = (source) => {
|
|
1119
1518
|
if (!source) return;
|
|
@@ -1125,6 +1524,7 @@ function getConfig() {
|
|
|
1125
1524
|
if (s.enableHookLogDebug !== void 0)
|
|
1126
1525
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
1127
1526
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
1527
|
+
if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
|
|
1128
1528
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
1129
1529
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
1130
1530
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
@@ -1133,6 +1533,12 @@ function getConfig() {
|
|
|
1133
1533
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
1134
1534
|
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
1135
1535
|
if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
|
|
1536
|
+
if (p.snapshot) {
|
|
1537
|
+
const s2 = p.snapshot;
|
|
1538
|
+
if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
|
|
1539
|
+
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
1540
|
+
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
1541
|
+
}
|
|
1136
1542
|
};
|
|
1137
1543
|
applyLayer(globalConfig);
|
|
1138
1544
|
applyLayer(projectConfig);
|
|
@@ -1140,6 +1546,9 @@ function getConfig() {
|
|
|
1140
1546
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
1141
1547
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
1142
1548
|
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
1549
|
+
mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
|
|
1550
|
+
mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
|
|
1551
|
+
mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
|
|
1143
1552
|
cachedConfig = {
|
|
1144
1553
|
settings: mergedSettings,
|
|
1145
1554
|
policy: mergedPolicy,
|
|
@@ -1149,11 +1558,33 @@ function getConfig() {
|
|
|
1149
1558
|
}
|
|
1150
1559
|
function tryLoadConfig(filePath) {
|
|
1151
1560
|
if (!import_fs.default.existsSync(filePath)) return null;
|
|
1561
|
+
let raw;
|
|
1152
1562
|
try {
|
|
1153
|
-
|
|
1154
|
-
} catch {
|
|
1563
|
+
raw = JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
|
|
1564
|
+
} catch (err) {
|
|
1565
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1566
|
+
process.stderr.write(
|
|
1567
|
+
`
|
|
1568
|
+
\u26A0\uFE0F Node9: Failed to parse ${filePath}
|
|
1569
|
+
${msg}
|
|
1570
|
+
\u2192 Using default config
|
|
1571
|
+
|
|
1572
|
+
`
|
|
1573
|
+
);
|
|
1155
1574
|
return null;
|
|
1156
1575
|
}
|
|
1576
|
+
const { sanitized, error } = sanitizeConfig(raw);
|
|
1577
|
+
if (error) {
|
|
1578
|
+
process.stderr.write(
|
|
1579
|
+
`
|
|
1580
|
+
\u26A0\uFE0F Node9: Invalid config at ${filePath}:
|
|
1581
|
+
${error.replace("Invalid config:\n", "")}
|
|
1582
|
+
\u2192 Invalid fields ignored, using defaults for those keys
|
|
1583
|
+
|
|
1584
|
+
`
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
return sanitized;
|
|
1157
1588
|
}
|
|
1158
1589
|
function getActiveEnvironment(config) {
|
|
1159
1590
|
const env = config.settings.environment || process.env.NODE_ENV || "development";
|
|
@@ -1168,7 +1599,7 @@ function getCredentials() {
|
|
|
1168
1599
|
};
|
|
1169
1600
|
}
|
|
1170
1601
|
try {
|
|
1171
|
-
const credPath =
|
|
1602
|
+
const credPath = import_path3.default.join(import_os.default.homedir(), ".node9", "credentials.json");
|
|
1172
1603
|
if (import_fs.default.existsSync(credPath)) {
|
|
1173
1604
|
const creds = JSON.parse(import_fs.default.readFileSync(credPath, "utf-8"));
|
|
1174
1605
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
@@ -1195,9 +1626,7 @@ async function authorizeAction(toolName, args) {
|
|
|
1195
1626
|
return result.approved;
|
|
1196
1627
|
}
|
|
1197
1628
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
1198
|
-
|
|
1199
|
-
setTimeout(() => controller.abort(), 5e3);
|
|
1200
|
-
fetch(`${creds.apiUrl}/audit`, {
|
|
1629
|
+
return fetch(`${creds.apiUrl}/audit`, {
|
|
1201
1630
|
method: "POST",
|
|
1202
1631
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
1203
1632
|
body: JSON.stringify({
|
|
@@ -1212,11 +1641,12 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1212
1641
|
platform: import_os.default.platform()
|
|
1213
1642
|
}
|
|
1214
1643
|
}),
|
|
1215
|
-
signal:
|
|
1644
|
+
signal: AbortSignal.timeout(5e3)
|
|
1645
|
+
}).then(() => {
|
|
1216
1646
|
}).catch(() => {
|
|
1217
1647
|
});
|
|
1218
1648
|
}
|
|
1219
|
-
async function initNode9SaaS(toolName, args, creds, meta) {
|
|
1649
|
+
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
1220
1650
|
const controller = new AbortController();
|
|
1221
1651
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
1222
1652
|
try {
|
|
@@ -1232,7 +1662,8 @@ async function initNode9SaaS(toolName, args, creds, meta) {
|
|
|
1232
1662
|
hostname: import_os.default.hostname(),
|
|
1233
1663
|
cwd: process.cwd(),
|
|
1234
1664
|
platform: import_os.default.platform()
|
|
1235
|
-
}
|
|
1665
|
+
},
|
|
1666
|
+
...riskMetadata && { riskMetadata }
|
|
1236
1667
|
}),
|
|
1237
1668
|
signal: controller.signal
|
|
1238
1669
|
});
|