@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/cli.js
CHANGED
|
@@ -30,24 +30,124 @@ var import_commander = require("commander");
|
|
|
30
30
|
var import_chalk2 = __toESM(require("chalk"));
|
|
31
31
|
var import_prompts = require("@inquirer/prompts");
|
|
32
32
|
var import_fs = __toESM(require("fs"));
|
|
33
|
-
var
|
|
33
|
+
var import_path3 = __toESM(require("path"));
|
|
34
34
|
var import_os = __toESM(require("os"));
|
|
35
35
|
var import_picomatch = __toESM(require("picomatch"));
|
|
36
36
|
var import_sh_syntax = require("sh-syntax");
|
|
37
37
|
|
|
38
38
|
// src/ui/native.ts
|
|
39
39
|
var import_child_process = require("child_process");
|
|
40
|
+
var import_path2 = __toESM(require("path"));
|
|
40
41
|
var import_chalk = __toESM(require("chalk"));
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
|
|
43
|
+
// src/context-sniper.ts
|
|
44
|
+
var import_path = __toESM(require("path"));
|
|
44
45
|
function smartTruncate(str, maxLen = 500) {
|
|
45
46
|
if (str.length <= maxLen) return str;
|
|
46
47
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
47
48
|
return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
|
|
48
49
|
}
|
|
49
|
-
function
|
|
50
|
-
|
|
50
|
+
function extractContext(text, matchedWord) {
|
|
51
|
+
const lines = text.split("\n");
|
|
52
|
+
if (lines.length <= 7 || !matchedWord) {
|
|
53
|
+
return { snippet: smartTruncate(text, 500), lineIndex: -1 };
|
|
54
|
+
}
|
|
55
|
+
const escaped = matchedWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
56
|
+
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
57
|
+
const allHits = lines.map((line, i) => ({ i, line })).filter(({ line }) => pattern.test(line));
|
|
58
|
+
if (allHits.length === 0) return { snippet: smartTruncate(text, 500), lineIndex: -1 };
|
|
59
|
+
const nonComment = allHits.find(({ line }) => {
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
return !trimmed.startsWith("//") && !trimmed.startsWith("#");
|
|
62
|
+
});
|
|
63
|
+
const hitIndex = (nonComment ?? allHits[0]).i;
|
|
64
|
+
const start = Math.max(0, hitIndex - 3);
|
|
65
|
+
const end = Math.min(lines.length, hitIndex + 4);
|
|
66
|
+
const lineIndex = hitIndex - start;
|
|
67
|
+
const snippet = lines.slice(start, end).map((line, i) => `${start + i === hitIndex ? "\u{1F6D1} " : " "}${line}`).join("\n");
|
|
68
|
+
const head = start > 0 ? `... [${start} lines hidden] ...
|
|
69
|
+
` : "";
|
|
70
|
+
const tail = end < lines.length ? `
|
|
71
|
+
... [${lines.length - end} lines hidden] ...` : "";
|
|
72
|
+
return { snippet: `${head}${snippet}${tail}`, lineIndex };
|
|
73
|
+
}
|
|
74
|
+
var CODE_KEYS = [
|
|
75
|
+
"command",
|
|
76
|
+
"cmd",
|
|
77
|
+
"shell_command",
|
|
78
|
+
"bash_command",
|
|
79
|
+
"script",
|
|
80
|
+
"code",
|
|
81
|
+
"input",
|
|
82
|
+
"sql",
|
|
83
|
+
"query",
|
|
84
|
+
"arguments",
|
|
85
|
+
"args",
|
|
86
|
+
"param",
|
|
87
|
+
"params",
|
|
88
|
+
"text"
|
|
89
|
+
];
|
|
90
|
+
function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
|
|
91
|
+
let intent = "EXEC";
|
|
92
|
+
let contextSnippet;
|
|
93
|
+
let contextLineIndex;
|
|
94
|
+
let editFileName;
|
|
95
|
+
let editFilePath;
|
|
96
|
+
let parsed = args;
|
|
97
|
+
if (typeof args === "string") {
|
|
98
|
+
const trimmed = args.trim();
|
|
99
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(trimmed);
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
107
|
+
const obj = parsed;
|
|
108
|
+
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
109
|
+
intent = "EDIT";
|
|
110
|
+
if (obj.file_path) {
|
|
111
|
+
editFilePath = String(obj.file_path);
|
|
112
|
+
editFileName = import_path.default.basename(editFilePath);
|
|
113
|
+
}
|
|
114
|
+
const result = extractContext(String(obj.new_string), matchedWord);
|
|
115
|
+
contextSnippet = result.snippet;
|
|
116
|
+
if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
|
|
117
|
+
} else if (matchedField && obj[matchedField] !== void 0) {
|
|
118
|
+
const result = extractContext(String(obj[matchedField]), matchedWord);
|
|
119
|
+
contextSnippet = result.snippet;
|
|
120
|
+
if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
|
|
121
|
+
} else {
|
|
122
|
+
const foundKey = Object.keys(obj).find((k) => CODE_KEYS.includes(k.toLowerCase()));
|
|
123
|
+
if (foundKey) {
|
|
124
|
+
const val = obj[foundKey];
|
|
125
|
+
contextSnippet = smartTruncate(typeof val === "string" ? val : JSON.stringify(val), 500);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else if (typeof parsed === "string") {
|
|
129
|
+
contextSnippet = smartTruncate(parsed, 500);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
intent,
|
|
133
|
+
tier,
|
|
134
|
+
blockedByLabel,
|
|
135
|
+
...matchedWord && { matchedWord },
|
|
136
|
+
...matchedField && { matchedField },
|
|
137
|
+
...contextSnippet !== void 0 && { contextSnippet },
|
|
138
|
+
...contextLineIndex !== void 0 && { contextLineIndex },
|
|
139
|
+
...editFileName && { editFileName },
|
|
140
|
+
...editFilePath && { editFilePath },
|
|
141
|
+
...ruleName && { ruleName }
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/ui/native.ts
|
|
146
|
+
var isTestEnv = () => {
|
|
147
|
+
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";
|
|
148
|
+
};
|
|
149
|
+
function formatArgs(args, matchedField, matchedWord) {
|
|
150
|
+
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
51
151
|
let parsed = args;
|
|
52
152
|
if (typeof args === "string") {
|
|
53
153
|
const trimmed = args.trim();
|
|
@@ -58,11 +158,39 @@ function formatArgs(args) {
|
|
|
58
158
|
parsed = args;
|
|
59
159
|
}
|
|
60
160
|
} else {
|
|
61
|
-
return smartTruncate(args, 600);
|
|
161
|
+
return { message: smartTruncate(args, 600), intent: "EXEC" };
|
|
62
162
|
}
|
|
63
163
|
}
|
|
64
164
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
65
165
|
const obj = parsed;
|
|
166
|
+
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
167
|
+
const file = obj.file_path ? import_path2.default.basename(String(obj.file_path)) : "file";
|
|
168
|
+
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
169
|
+
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
170
|
+
return {
|
|
171
|
+
intent: "EDIT",
|
|
172
|
+
message: `\u{1F4DD} EDITING: ${file}
|
|
173
|
+
\u{1F4C2} PATH: ${obj.file_path}
|
|
174
|
+
|
|
175
|
+
--- REPLACING ---
|
|
176
|
+
${oldPreview}
|
|
177
|
+
|
|
178
|
+
+++ NEW CODE +++
|
|
179
|
+
${newPreview}`
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (matchedField && obj[matchedField] !== void 0) {
|
|
183
|
+
const otherKeys = Object.keys(obj).filter((k) => k !== matchedField);
|
|
184
|
+
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(", ")}
|
|
185
|
+
|
|
186
|
+
` : "";
|
|
187
|
+
const content = extractContext(String(obj[matchedField]), matchedWord).snippet;
|
|
188
|
+
return {
|
|
189
|
+
intent: "EXEC",
|
|
190
|
+
message: `${context}\u{1F6D1} [${matchedField.toUpperCase()}]:
|
|
191
|
+
${content}`
|
|
192
|
+
};
|
|
193
|
+
}
|
|
66
194
|
const codeKeys = [
|
|
67
195
|
"command",
|
|
68
196
|
"cmd",
|
|
@@ -83,14 +211,18 @@ function formatArgs(args) {
|
|
|
83
211
|
if (foundKey) {
|
|
84
212
|
const val = obj[foundKey];
|
|
85
213
|
const str = typeof val === "string" ? val : JSON.stringify(val);
|
|
86
|
-
return
|
|
87
|
-
|
|
214
|
+
return {
|
|
215
|
+
intent: "EXEC",
|
|
216
|
+
message: `[${foundKey.toUpperCase()}]:
|
|
217
|
+
${smartTruncate(str, 500)}`
|
|
218
|
+
};
|
|
88
219
|
}
|
|
89
|
-
|
|
220
|
+
const msg = Object.entries(obj).slice(0, 5).map(
|
|
90
221
|
([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
|
|
91
222
|
).join("\n");
|
|
223
|
+
return { intent: "EXEC", message: msg };
|
|
92
224
|
}
|
|
93
|
-
return smartTruncate(JSON.stringify(parsed), 200);
|
|
225
|
+
return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
|
|
94
226
|
}
|
|
95
227
|
function sendDesktopNotification(title, body) {
|
|
96
228
|
if (isTestEnv()) return;
|
|
@@ -143,10 +275,11 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
143
275
|
}
|
|
144
276
|
return lines.join("\n");
|
|
145
277
|
}
|
|
146
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
|
|
278
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
|
|
147
279
|
if (isTestEnv()) return "deny";
|
|
148
|
-
const formattedArgs = formatArgs(args);
|
|
149
|
-
const
|
|
280
|
+
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
281
|
+
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
282
|
+
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
150
283
|
const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
|
|
151
284
|
process.stderr.write(import_chalk.default.yellow(`
|
|
152
285
|
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
@@ -223,11 +356,113 @@ end run`;
|
|
|
223
356
|
});
|
|
224
357
|
}
|
|
225
358
|
|
|
359
|
+
// src/config-schema.ts
|
|
360
|
+
var import_zod = require("zod");
|
|
361
|
+
var noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
362
|
+
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
363
|
+
});
|
|
364
|
+
var validRegex = noNewlines.refine(
|
|
365
|
+
(s) => {
|
|
366
|
+
try {
|
|
367
|
+
new RegExp(s);
|
|
368
|
+
return true;
|
|
369
|
+
} catch {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
{ message: "Value must be a valid regular expression" }
|
|
374
|
+
);
|
|
375
|
+
var SmartConditionSchema = import_zod.z.object({
|
|
376
|
+
field: import_zod.z.string().min(1, "Condition field must not be empty"),
|
|
377
|
+
op: import_zod.z.enum(["matches", "notMatches", "contains", "notContains", "exists", "notExists"], {
|
|
378
|
+
errorMap: () => ({
|
|
379
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists"
|
|
380
|
+
})
|
|
381
|
+
}),
|
|
382
|
+
value: validRegex.optional(),
|
|
383
|
+
flags: import_zod.z.string().optional()
|
|
384
|
+
});
|
|
385
|
+
var SmartRuleSchema = import_zod.z.object({
|
|
386
|
+
name: import_zod.z.string().optional(),
|
|
387
|
+
tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
|
|
388
|
+
conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
389
|
+
conditionMode: import_zod.z.enum(["all", "any"]).optional(),
|
|
390
|
+
verdict: import_zod.z.enum(["allow", "review", "block"], {
|
|
391
|
+
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
392
|
+
}),
|
|
393
|
+
reason: import_zod.z.string().optional()
|
|
394
|
+
});
|
|
395
|
+
var PolicyRuleSchema = import_zod.z.object({
|
|
396
|
+
action: import_zod.z.string().min(1),
|
|
397
|
+
allowPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
398
|
+
blockPaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
399
|
+
});
|
|
400
|
+
var ConfigFileSchema = import_zod.z.object({
|
|
401
|
+
version: import_zod.z.string().optional(),
|
|
402
|
+
settings: import_zod.z.object({
|
|
403
|
+
mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
|
|
404
|
+
autoStartDaemon: import_zod.z.boolean().optional(),
|
|
405
|
+
enableUndo: import_zod.z.boolean().optional(),
|
|
406
|
+
enableHookLogDebug: import_zod.z.boolean().optional(),
|
|
407
|
+
approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
|
|
408
|
+
approvers: import_zod.z.object({
|
|
409
|
+
native: import_zod.z.boolean().optional(),
|
|
410
|
+
browser: import_zod.z.boolean().optional(),
|
|
411
|
+
cloud: import_zod.z.boolean().optional(),
|
|
412
|
+
terminal: import_zod.z.boolean().optional()
|
|
413
|
+
}).optional(),
|
|
414
|
+
environment: import_zod.z.string().optional(),
|
|
415
|
+
slackEnabled: import_zod.z.boolean().optional(),
|
|
416
|
+
enableTrustSessions: import_zod.z.boolean().optional(),
|
|
417
|
+
allowGlobalPause: import_zod.z.boolean().optional()
|
|
418
|
+
}).optional(),
|
|
419
|
+
policy: import_zod.z.object({
|
|
420
|
+
sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
421
|
+
dangerousWords: import_zod.z.array(noNewlines).optional(),
|
|
422
|
+
ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
423
|
+
toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
|
|
424
|
+
rules: import_zod.z.array(PolicyRuleSchema).optional(),
|
|
425
|
+
smartRules: import_zod.z.array(SmartRuleSchema).optional(),
|
|
426
|
+
snapshot: import_zod.z.object({
|
|
427
|
+
tools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
428
|
+
onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
429
|
+
ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
430
|
+
}).optional()
|
|
431
|
+
}).optional(),
|
|
432
|
+
environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
|
|
433
|
+
}).strict({ message: "Config contains unknown top-level keys" });
|
|
434
|
+
function sanitizeConfig(raw) {
|
|
435
|
+
const result = ConfigFileSchema.safeParse(raw);
|
|
436
|
+
if (result.success) {
|
|
437
|
+
return { sanitized: result.data, error: null };
|
|
438
|
+
}
|
|
439
|
+
const invalidTopLevelKeys = new Set(
|
|
440
|
+
result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
|
|
441
|
+
);
|
|
442
|
+
const sanitized = {};
|
|
443
|
+
if (typeof raw === "object" && raw !== null) {
|
|
444
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
445
|
+
if (!invalidTopLevelKeys.has(key)) {
|
|
446
|
+
sanitized[key] = value;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const lines = result.error.issues.map((issue) => {
|
|
451
|
+
const path8 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
452
|
+
return ` \u2022 ${path8}: ${issue.message}`;
|
|
453
|
+
});
|
|
454
|
+
return {
|
|
455
|
+
sanitized,
|
|
456
|
+
error: `Invalid config:
|
|
457
|
+
${lines.join("\n")}`
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
226
461
|
// src/core.ts
|
|
227
|
-
var PAUSED_FILE =
|
|
228
|
-
var TRUST_FILE =
|
|
229
|
-
var LOCAL_AUDIT_LOG =
|
|
230
|
-
var HOOK_DEBUG_LOG =
|
|
462
|
+
var PAUSED_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "PAUSED");
|
|
463
|
+
var TRUST_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "trust.json");
|
|
464
|
+
var LOCAL_AUDIT_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
465
|
+
var HOOK_DEBUG_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
|
|
231
466
|
function checkPause() {
|
|
232
467
|
try {
|
|
233
468
|
if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -245,7 +480,7 @@ function checkPause() {
|
|
|
245
480
|
}
|
|
246
481
|
}
|
|
247
482
|
function atomicWriteSync(filePath, data, options) {
|
|
248
|
-
const dir =
|
|
483
|
+
const dir = import_path3.default.dirname(filePath);
|
|
249
484
|
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
250
485
|
const tmpPath = `${filePath}.${import_os.default.hostname()}.${process.pid}.tmp`;
|
|
251
486
|
import_fs.default.writeFileSync(tmpPath, data, options);
|
|
@@ -296,7 +531,7 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
296
531
|
}
|
|
297
532
|
function appendToLog(logPath, entry) {
|
|
298
533
|
try {
|
|
299
|
-
const dir =
|
|
534
|
+
const dir = import_path3.default.dirname(logPath);
|
|
300
535
|
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
301
536
|
import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
302
537
|
} catch {
|
|
@@ -340,9 +575,21 @@ function matchesPattern(text, patterns) {
|
|
|
340
575
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
341
576
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
342
577
|
}
|
|
343
|
-
function getNestedValue(obj,
|
|
578
|
+
function getNestedValue(obj, path8) {
|
|
344
579
|
if (!obj || typeof obj !== "object") return null;
|
|
345
|
-
return
|
|
580
|
+
return path8.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
581
|
+
}
|
|
582
|
+
function shouldSnapshot(toolName, args, config) {
|
|
583
|
+
if (!config.settings.enableUndo) return false;
|
|
584
|
+
const snap = config.policy.snapshot;
|
|
585
|
+
if (!snap.tools.includes(toolName.toLowerCase())) return false;
|
|
586
|
+
const a = args && typeof args === "object" ? args : {};
|
|
587
|
+
const filePath = String(a.file_path ?? a.path ?? a.filename ?? "");
|
|
588
|
+
if (filePath) {
|
|
589
|
+
if (snap.ignorePaths.length && (0, import_picomatch.default)(snap.ignorePaths)(filePath)) return false;
|
|
590
|
+
if (snap.onlyPaths.length && !(0, import_picomatch.default)(snap.onlyPaths)(filePath)) return false;
|
|
591
|
+
}
|
|
592
|
+
return true;
|
|
346
593
|
}
|
|
347
594
|
function evaluateSmartConditions(args, rule) {
|
|
348
595
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -479,15 +726,10 @@ function redactSecrets(text) {
|
|
|
479
726
|
return redacted;
|
|
480
727
|
}
|
|
481
728
|
var DANGEROUS_WORDS = [
|
|
482
|
-
"
|
|
483
|
-
|
|
484
|
-
"
|
|
485
|
-
|
|
486
|
-
"destroy",
|
|
487
|
-
"terminate",
|
|
488
|
-
"revoke",
|
|
489
|
-
"docker",
|
|
490
|
-
"psql"
|
|
729
|
+
"mkfs",
|
|
730
|
+
// formats/wipes a filesystem partition
|
|
731
|
+
"shred"
|
|
732
|
+
// permanently overwrites file contents (unrecoverable)
|
|
491
733
|
];
|
|
492
734
|
var DEFAULT_CONFIG = {
|
|
493
735
|
settings: {
|
|
@@ -531,7 +773,21 @@ var DEFAULT_CONFIG = {
|
|
|
531
773
|
"terminal.execute": "command",
|
|
532
774
|
"postgres:query": "sql"
|
|
533
775
|
},
|
|
776
|
+
snapshot: {
|
|
777
|
+
tools: [
|
|
778
|
+
"str_replace_based_edit_tool",
|
|
779
|
+
"write_file",
|
|
780
|
+
"edit_file",
|
|
781
|
+
"create_file",
|
|
782
|
+
"edit",
|
|
783
|
+
"replace"
|
|
784
|
+
],
|
|
785
|
+
onlyPaths: [],
|
|
786
|
+
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
787
|
+
},
|
|
534
788
|
rules: [
|
|
789
|
+
// Only use the legacy rules format for simple path-based rm control.
|
|
790
|
+
// All other command-level enforcement lives in smartRules below.
|
|
535
791
|
{
|
|
536
792
|
action: "rm",
|
|
537
793
|
allowPaths: [
|
|
@@ -548,6 +804,7 @@ var DEFAULT_CONFIG = {
|
|
|
548
804
|
}
|
|
549
805
|
],
|
|
550
806
|
smartRules: [
|
|
807
|
+
// ── SQL safety ────────────────────────────────────────────────────────
|
|
551
808
|
{
|
|
552
809
|
name: "no-delete-without-where",
|
|
553
810
|
tool: "*",
|
|
@@ -558,6 +815,84 @@ var DEFAULT_CONFIG = {
|
|
|
558
815
|
conditionMode: "all",
|
|
559
816
|
verdict: "review",
|
|
560
817
|
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
name: "review-drop-truncate-shell",
|
|
821
|
+
tool: "bash",
|
|
822
|
+
conditions: [
|
|
823
|
+
{
|
|
824
|
+
field: "command",
|
|
825
|
+
op: "matches",
|
|
826
|
+
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
827
|
+
flags: "i"
|
|
828
|
+
}
|
|
829
|
+
],
|
|
830
|
+
conditionMode: "all",
|
|
831
|
+
verdict: "review",
|
|
832
|
+
reason: "SQL DDL destructive statement inside a shell command"
|
|
833
|
+
},
|
|
834
|
+
// ── Git safety ────────────────────────────────────────────────────────
|
|
835
|
+
{
|
|
836
|
+
name: "block-force-push",
|
|
837
|
+
tool: "bash",
|
|
838
|
+
conditions: [
|
|
839
|
+
{
|
|
840
|
+
field: "command",
|
|
841
|
+
op: "matches",
|
|
842
|
+
value: "git push.*(--force|--force-with-lease|-f\\b)",
|
|
843
|
+
flags: "i"
|
|
844
|
+
}
|
|
845
|
+
],
|
|
846
|
+
conditionMode: "all",
|
|
847
|
+
verdict: "block",
|
|
848
|
+
reason: "Force push overwrites remote history and cannot be undone"
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
name: "review-git-push",
|
|
852
|
+
tool: "bash",
|
|
853
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*git push\\b", flags: "i" }],
|
|
854
|
+
conditionMode: "all",
|
|
855
|
+
verdict: "review",
|
|
856
|
+
reason: "git push sends changes to a shared remote"
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
name: "review-git-destructive",
|
|
860
|
+
tool: "bash",
|
|
861
|
+
conditions: [
|
|
862
|
+
{
|
|
863
|
+
field: "command",
|
|
864
|
+
op: "matches",
|
|
865
|
+
value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
|
|
866
|
+
flags: "i"
|
|
867
|
+
}
|
|
868
|
+
],
|
|
869
|
+
conditionMode: "all",
|
|
870
|
+
verdict: "review",
|
|
871
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
872
|
+
},
|
|
873
|
+
// ── Shell safety ──────────────────────────────────────────────────────
|
|
874
|
+
{
|
|
875
|
+
name: "review-sudo",
|
|
876
|
+
tool: "bash",
|
|
877
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
|
|
878
|
+
conditionMode: "all",
|
|
879
|
+
verdict: "review",
|
|
880
|
+
reason: "Command requires elevated privileges"
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
name: "review-curl-pipe-shell",
|
|
884
|
+
tool: "bash",
|
|
885
|
+
conditions: [
|
|
886
|
+
{
|
|
887
|
+
field: "command",
|
|
888
|
+
op: "matches",
|
|
889
|
+
value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
890
|
+
flags: "i"
|
|
891
|
+
}
|
|
892
|
+
],
|
|
893
|
+
conditionMode: "all",
|
|
894
|
+
verdict: "block",
|
|
895
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
561
896
|
}
|
|
562
897
|
]
|
|
563
898
|
},
|
|
@@ -569,7 +904,7 @@ function _resetConfigCache() {
|
|
|
569
904
|
}
|
|
570
905
|
function getGlobalSettings() {
|
|
571
906
|
try {
|
|
572
|
-
const globalConfigPath =
|
|
907
|
+
const globalConfigPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
|
|
573
908
|
if (import_fs.default.existsSync(globalConfigPath)) {
|
|
574
909
|
const parsed = JSON.parse(import_fs.default.readFileSync(globalConfigPath, "utf-8"));
|
|
575
910
|
const settings = parsed.settings || {};
|
|
@@ -593,7 +928,7 @@ function getGlobalSettings() {
|
|
|
593
928
|
}
|
|
594
929
|
function getInternalToken() {
|
|
595
930
|
try {
|
|
596
|
-
const pidFile =
|
|
931
|
+
const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
|
|
597
932
|
if (!import_fs.default.existsSync(pidFile)) return null;
|
|
598
933
|
const data = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
|
|
599
934
|
process.kill(data.pid, 0);
|
|
@@ -614,7 +949,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
614
949
|
return {
|
|
615
950
|
decision: matchedRule.verdict,
|
|
616
951
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
617
|
-
reason: matchedRule.reason
|
|
952
|
+
reason: matchedRule.reason,
|
|
953
|
+
tier: 2,
|
|
954
|
+
ruleName: matchedRule.name ?? matchedRule.tool
|
|
618
955
|
};
|
|
619
956
|
}
|
|
620
957
|
}
|
|
@@ -629,7 +966,7 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
629
966
|
pathTokens = analyzed.paths;
|
|
630
967
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
631
968
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
632
|
-
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)" };
|
|
969
|
+
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
633
970
|
}
|
|
634
971
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
635
972
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
@@ -652,7 +989,7 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
652
989
|
);
|
|
653
990
|
const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
|
|
654
991
|
if (hasSystemDisaster || isRootWipe) {
|
|
655
|
-
return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
|
|
992
|
+
return { decision: "review", blockedByLabel: "Manual Nuclear Protection", tier: 3 };
|
|
656
993
|
}
|
|
657
994
|
return { decision: "allow" };
|
|
658
995
|
}
|
|
@@ -670,14 +1007,16 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
670
1007
|
if (anyBlocked)
|
|
671
1008
|
return {
|
|
672
1009
|
decision: "review",
|
|
673
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)
|
|
1010
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
|
|
1011
|
+
tier: 5
|
|
674
1012
|
};
|
|
675
1013
|
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
676
1014
|
if (allAllowed) return { decision: "allow" };
|
|
677
1015
|
}
|
|
678
1016
|
return {
|
|
679
1017
|
decision: "review",
|
|
680
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)
|
|
1018
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
|
|
1019
|
+
tier: 5
|
|
681
1020
|
};
|
|
682
1021
|
}
|
|
683
1022
|
}
|
|
@@ -697,23 +1036,44 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
697
1036
|
})
|
|
698
1037
|
);
|
|
699
1038
|
if (isDangerous) {
|
|
1039
|
+
let matchedField;
|
|
1040
|
+
if (matchedDangerousWord && args && typeof args === "object" && !Array.isArray(args)) {
|
|
1041
|
+
const obj = args;
|
|
1042
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1043
|
+
if (typeof value === "string") {
|
|
1044
|
+
try {
|
|
1045
|
+
if (new RegExp(
|
|
1046
|
+
`\\b${matchedDangerousWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
|
|
1047
|
+
"i"
|
|
1048
|
+
).test(value)) {
|
|
1049
|
+
matchedField = key;
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
} catch {
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
700
1057
|
return {
|
|
701
1058
|
decision: "review",
|
|
702
|
-
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"
|
|
1059
|
+
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
|
|
1060
|
+
matchedWord: matchedDangerousWord,
|
|
1061
|
+
matchedField,
|
|
1062
|
+
tier: 6
|
|
703
1063
|
};
|
|
704
1064
|
}
|
|
705
1065
|
if (config.settings.mode === "strict") {
|
|
706
1066
|
const envConfig = getActiveEnvironment(config);
|
|
707
1067
|
if (envConfig?.requireApproval === false) return { decision: "allow" };
|
|
708
|
-
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)" };
|
|
1068
|
+
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)", tier: 7 };
|
|
709
1069
|
}
|
|
710
1070
|
return { decision: "allow" };
|
|
711
1071
|
}
|
|
712
1072
|
async function explainPolicy(toolName, args) {
|
|
713
1073
|
const steps = [];
|
|
714
|
-
const globalPath =
|
|
715
|
-
const projectPath =
|
|
716
|
-
const credsPath =
|
|
1074
|
+
const globalPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
|
|
1075
|
+
const projectPath = import_path3.default.join(process.cwd(), "node9.config.json");
|
|
1076
|
+
const credsPath = import_path3.default.join(import_os.default.homedir(), ".node9", "credentials.json");
|
|
717
1077
|
const waterfall = [
|
|
718
1078
|
{
|
|
719
1079
|
tier: 1,
|
|
@@ -1017,7 +1377,7 @@ var DAEMON_PORT = 7391;
|
|
|
1017
1377
|
var DAEMON_HOST = "127.0.0.1";
|
|
1018
1378
|
function isDaemonRunning() {
|
|
1019
1379
|
try {
|
|
1020
|
-
const pidFile =
|
|
1380
|
+
const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
|
|
1021
1381
|
if (!import_fs.default.existsSync(pidFile)) return false;
|
|
1022
1382
|
const { pid, port } = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
|
|
1023
1383
|
if (port !== DAEMON_PORT) return false;
|
|
@@ -1029,7 +1389,7 @@ function isDaemonRunning() {
|
|
|
1029
1389
|
}
|
|
1030
1390
|
function getPersistentDecision(toolName) {
|
|
1031
1391
|
try {
|
|
1032
|
-
const file =
|
|
1392
|
+
const file = import_path3.default.join(import_os.default.homedir(), ".node9", "decisions.json");
|
|
1033
1393
|
if (!import_fs.default.existsSync(file)) return null;
|
|
1034
1394
|
const decisions = JSON.parse(import_fs.default.readFileSync(file, "utf-8"));
|
|
1035
1395
|
const d = decisions[toolName];
|
|
@@ -1038,7 +1398,7 @@ function getPersistentDecision(toolName) {
|
|
|
1038
1398
|
}
|
|
1039
1399
|
return null;
|
|
1040
1400
|
}
|
|
1041
|
-
async function askDaemon(toolName, args, meta, signal) {
|
|
1401
|
+
async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
1042
1402
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1043
1403
|
const checkCtrl = new AbortController();
|
|
1044
1404
|
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
@@ -1048,7 +1408,13 @@ async function askDaemon(toolName, args, meta, signal) {
|
|
|
1048
1408
|
const checkRes = await fetch(`${base}/check`, {
|
|
1049
1409
|
method: "POST",
|
|
1050
1410
|
headers: { "Content-Type": "application/json" },
|
|
1051
|
-
body: JSON.stringify({
|
|
1411
|
+
body: JSON.stringify({
|
|
1412
|
+
toolName,
|
|
1413
|
+
args,
|
|
1414
|
+
agent: meta?.agent,
|
|
1415
|
+
mcpServer: meta?.mcpServer,
|
|
1416
|
+
...riskMetadata && { riskMetadata }
|
|
1417
|
+
}),
|
|
1052
1418
|
signal: checkCtrl.signal
|
|
1053
1419
|
});
|
|
1054
1420
|
if (!checkRes.ok) throw new Error("Daemon fail");
|
|
@@ -1073,7 +1439,7 @@ async function askDaemon(toolName, args, meta, signal) {
|
|
|
1073
1439
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1074
1440
|
}
|
|
1075
1441
|
}
|
|
1076
|
-
async function notifyDaemonViewer(toolName, args, meta) {
|
|
1442
|
+
async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
1077
1443
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1078
1444
|
const res = await fetch(`${base}/check`, {
|
|
1079
1445
|
method: "POST",
|
|
@@ -1083,7 +1449,8 @@ async function notifyDaemonViewer(toolName, args, meta) {
|
|
|
1083
1449
|
args,
|
|
1084
1450
|
slackDelegated: true,
|
|
1085
1451
|
agent: meta?.agent,
|
|
1086
|
-
mcpServer: meta?.mcpServer
|
|
1452
|
+
mcpServer: meta?.mcpServer,
|
|
1453
|
+
...riskMetadata && { riskMetadata }
|
|
1087
1454
|
}),
|
|
1088
1455
|
signal: AbortSignal.timeout(3e3)
|
|
1089
1456
|
});
|
|
@@ -1100,7 +1467,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1100
1467
|
signal: AbortSignal.timeout(3e3)
|
|
1101
1468
|
});
|
|
1102
1469
|
}
|
|
1103
|
-
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta) {
|
|
1470
|
+
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1104
1471
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
1105
1472
|
const pauseState = checkPause();
|
|
1106
1473
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
@@ -1120,11 +1487,17 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1120
1487
|
}
|
|
1121
1488
|
const isManual = meta?.agent === "Terminal";
|
|
1122
1489
|
let explainableLabel = "Local Config";
|
|
1490
|
+
let policyMatchedField;
|
|
1491
|
+
let policyMatchedWord;
|
|
1492
|
+
let riskMetadata;
|
|
1123
1493
|
if (config.settings.mode === "audit") {
|
|
1124
1494
|
if (!isIgnoredTool(toolName)) {
|
|
1125
1495
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
1126
1496
|
if (policyResult.decision === "review") {
|
|
1127
1497
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
1498
|
+
if (approvers.cloud && creds?.apiKey) {
|
|
1499
|
+
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
1500
|
+
}
|
|
1128
1501
|
sendDesktopNotification(
|
|
1129
1502
|
"Node9 Audit Mode",
|
|
1130
1503
|
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
@@ -1135,13 +1508,15 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1135
1508
|
}
|
|
1136
1509
|
if (!isIgnoredTool(toolName)) {
|
|
1137
1510
|
if (getActiveTrustSession(toolName)) {
|
|
1138
|
-
if (creds?.apiKey)
|
|
1511
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1512
|
+
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
1139
1513
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
1140
1514
|
return { approved: true, checkedBy: "trust" };
|
|
1141
1515
|
}
|
|
1142
1516
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
1143
1517
|
if (policyResult.decision === "allow") {
|
|
1144
|
-
if (creds?.apiKey)
|
|
1518
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1519
|
+
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
1145
1520
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
1146
1521
|
return { approved: true, checkedBy: "local-policy" };
|
|
1147
1522
|
}
|
|
@@ -1155,9 +1530,20 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1155
1530
|
};
|
|
1156
1531
|
}
|
|
1157
1532
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
1533
|
+
policyMatchedField = policyResult.matchedField;
|
|
1534
|
+
policyMatchedWord = policyResult.matchedWord;
|
|
1535
|
+
riskMetadata = computeRiskMetadata(
|
|
1536
|
+
args,
|
|
1537
|
+
policyResult.tier ?? 6,
|
|
1538
|
+
explainableLabel,
|
|
1539
|
+
policyMatchedField,
|
|
1540
|
+
policyMatchedWord,
|
|
1541
|
+
policyResult.ruleName
|
|
1542
|
+
);
|
|
1158
1543
|
const persistent = getPersistentDecision(toolName);
|
|
1159
1544
|
if (persistent === "allow") {
|
|
1160
|
-
if (creds?.apiKey)
|
|
1545
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1546
|
+
await auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
1161
1547
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
1162
1548
|
return { approved: true, checkedBy: "persistent" };
|
|
1163
1549
|
}
|
|
@@ -1171,7 +1557,6 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1171
1557
|
};
|
|
1172
1558
|
}
|
|
1173
1559
|
} else {
|
|
1174
|
-
if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
|
|
1175
1560
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
1176
1561
|
return { approved: true };
|
|
1177
1562
|
}
|
|
@@ -1180,8 +1565,21 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1180
1565
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
1181
1566
|
if (cloudEnforced) {
|
|
1182
1567
|
try {
|
|
1183
|
-
const initResult = await initNode9SaaS(toolName, args, creds, meta);
|
|
1568
|
+
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
1184
1569
|
if (!initResult.pending) {
|
|
1570
|
+
if (initResult.shadowMode) {
|
|
1571
|
+
console.error(
|
|
1572
|
+
import_chalk2.default.yellow(
|
|
1573
|
+
`
|
|
1574
|
+
\u26A0\uFE0F Node9 Shadow Mode: Action allowed, but would have been blocked by company policy.`
|
|
1575
|
+
)
|
|
1576
|
+
);
|
|
1577
|
+
if (initResult.shadowReason) {
|
|
1578
|
+
console.error(import_chalk2.default.dim(` Reason: ${initResult.shadowReason}
|
|
1579
|
+
`));
|
|
1580
|
+
}
|
|
1581
|
+
return { approved: true, checkedBy: "cloud" };
|
|
1582
|
+
}
|
|
1185
1583
|
return {
|
|
1186
1584
|
approved: !!initResult.approved,
|
|
1187
1585
|
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
@@ -1206,18 +1604,22 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1206
1604
|
);
|
|
1207
1605
|
}
|
|
1208
1606
|
}
|
|
1209
|
-
if (
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1607
|
+
if (!options?.calledFromDaemon) {
|
|
1608
|
+
if (cloudEnforced && cloudRequestId) {
|
|
1609
|
+
console.error(
|
|
1610
|
+
import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
|
|
1611
|
+
);
|
|
1612
|
+
console.error(
|
|
1613
|
+
import_chalk2.default.cyan(" Dashboard \u2192 ") + import_chalk2.default.bold("Mission Control > Activity Feed\n")
|
|
1614
|
+
);
|
|
1615
|
+
} else if (!cloudEnforced) {
|
|
1616
|
+
const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
|
|
1617
|
+
console.error(
|
|
1618
|
+
import_chalk2.default.dim(`
|
|
1218
1619
|
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
1219
1620
|
`)
|
|
1220
|
-
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1221
1623
|
}
|
|
1222
1624
|
const abortController = new AbortController();
|
|
1223
1625
|
const { signal } = abortController;
|
|
@@ -1247,8 +1649,10 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1247
1649
|
racePromises.push(
|
|
1248
1650
|
(async () => {
|
|
1249
1651
|
try {
|
|
1250
|
-
if (isDaemonRunning() && internalToken) {
|
|
1251
|
-
viewerId = await notifyDaemonViewer(toolName, args, meta).catch(
|
|
1652
|
+
if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
|
|
1653
|
+
viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
|
|
1654
|
+
() => null
|
|
1655
|
+
);
|
|
1252
1656
|
}
|
|
1253
1657
|
const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
|
|
1254
1658
|
return {
|
|
@@ -1266,7 +1670,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1266
1670
|
})()
|
|
1267
1671
|
);
|
|
1268
1672
|
}
|
|
1269
|
-
if (approvers.native && !isManual) {
|
|
1673
|
+
if (approvers.native && !isManual && !options?.calledFromDaemon) {
|
|
1270
1674
|
racePromises.push(
|
|
1271
1675
|
(async () => {
|
|
1272
1676
|
const decision = await askNativePopup(
|
|
@@ -1275,7 +1679,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1275
1679
|
meta?.agent,
|
|
1276
1680
|
explainableLabel,
|
|
1277
1681
|
isRemoteLocked,
|
|
1278
|
-
signal
|
|
1682
|
+
signal,
|
|
1683
|
+
policyMatchedField,
|
|
1684
|
+
policyMatchedWord
|
|
1279
1685
|
);
|
|
1280
1686
|
if (decision === "always_allow") {
|
|
1281
1687
|
writeTrustSession(toolName, 36e5);
|
|
@@ -1292,7 +1698,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1292
1698
|
})()
|
|
1293
1699
|
);
|
|
1294
1700
|
}
|
|
1295
|
-
if (approvers.browser && isDaemonRunning()) {
|
|
1701
|
+
if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon && !cloudEnforced) {
|
|
1296
1702
|
racePromises.push(
|
|
1297
1703
|
(async () => {
|
|
1298
1704
|
try {
|
|
@@ -1303,7 +1709,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1303
1709
|
console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
1304
1710
|
`));
|
|
1305
1711
|
}
|
|
1306
|
-
const daemonDecision = await askDaemon(toolName, args, meta, signal);
|
|
1712
|
+
const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
|
|
1307
1713
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1308
1714
|
const isApproved = daemonDecision === "allow";
|
|
1309
1715
|
return {
|
|
@@ -1428,8 +1834,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1428
1834
|
}
|
|
1429
1835
|
function getConfig() {
|
|
1430
1836
|
if (cachedConfig) return cachedConfig;
|
|
1431
|
-
const globalPath =
|
|
1432
|
-
const projectPath =
|
|
1837
|
+
const globalPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
|
|
1838
|
+
const projectPath = import_path3.default.join(process.cwd(), "node9.config.json");
|
|
1433
1839
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1434
1840
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1435
1841
|
const mergedSettings = {
|
|
@@ -1442,7 +1848,12 @@ function getConfig() {
|
|
|
1442
1848
|
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
1443
1849
|
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
1444
1850
|
rules: [...DEFAULT_CONFIG.policy.rules],
|
|
1445
|
-
smartRules: [...DEFAULT_CONFIG.policy.smartRules]
|
|
1851
|
+
smartRules: [...DEFAULT_CONFIG.policy.smartRules],
|
|
1852
|
+
snapshot: {
|
|
1853
|
+
tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
|
|
1854
|
+
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
1855
|
+
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
1856
|
+
}
|
|
1446
1857
|
};
|
|
1447
1858
|
const applyLayer = (source) => {
|
|
1448
1859
|
if (!source) return;
|
|
@@ -1454,6 +1865,7 @@ function getConfig() {
|
|
|
1454
1865
|
if (s.enableHookLogDebug !== void 0)
|
|
1455
1866
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
1456
1867
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
1868
|
+
if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
|
|
1457
1869
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
1458
1870
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
1459
1871
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
@@ -1462,6 +1874,12 @@ function getConfig() {
|
|
|
1462
1874
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
1463
1875
|
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
1464
1876
|
if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
|
|
1877
|
+
if (p.snapshot) {
|
|
1878
|
+
const s2 = p.snapshot;
|
|
1879
|
+
if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
|
|
1880
|
+
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
1881
|
+
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
1882
|
+
}
|
|
1465
1883
|
};
|
|
1466
1884
|
applyLayer(globalConfig);
|
|
1467
1885
|
applyLayer(projectConfig);
|
|
@@ -1469,6 +1887,9 @@ function getConfig() {
|
|
|
1469
1887
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
1470
1888
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
1471
1889
|
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
1890
|
+
mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
|
|
1891
|
+
mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
|
|
1892
|
+
mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
|
|
1472
1893
|
cachedConfig = {
|
|
1473
1894
|
settings: mergedSettings,
|
|
1474
1895
|
policy: mergedPolicy,
|
|
@@ -1478,11 +1899,33 @@ function getConfig() {
|
|
|
1478
1899
|
}
|
|
1479
1900
|
function tryLoadConfig(filePath) {
|
|
1480
1901
|
if (!import_fs.default.existsSync(filePath)) return null;
|
|
1902
|
+
let raw;
|
|
1481
1903
|
try {
|
|
1482
|
-
|
|
1483
|
-
} catch {
|
|
1904
|
+
raw = JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
|
|
1905
|
+
} catch (err) {
|
|
1906
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1907
|
+
process.stderr.write(
|
|
1908
|
+
`
|
|
1909
|
+
\u26A0\uFE0F Node9: Failed to parse ${filePath}
|
|
1910
|
+
${msg}
|
|
1911
|
+
\u2192 Using default config
|
|
1912
|
+
|
|
1913
|
+
`
|
|
1914
|
+
);
|
|
1484
1915
|
return null;
|
|
1485
1916
|
}
|
|
1917
|
+
const { sanitized, error } = sanitizeConfig(raw);
|
|
1918
|
+
if (error) {
|
|
1919
|
+
process.stderr.write(
|
|
1920
|
+
`
|
|
1921
|
+
\u26A0\uFE0F Node9: Invalid config at ${filePath}:
|
|
1922
|
+
${error.replace("Invalid config:\n", "")}
|
|
1923
|
+
\u2192 Invalid fields ignored, using defaults for those keys
|
|
1924
|
+
|
|
1925
|
+
`
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
return sanitized;
|
|
1486
1929
|
}
|
|
1487
1930
|
function getActiveEnvironment(config) {
|
|
1488
1931
|
const env = config.settings.environment || process.env.NODE_ENV || "development";
|
|
@@ -1497,7 +1940,7 @@ function getCredentials() {
|
|
|
1497
1940
|
};
|
|
1498
1941
|
}
|
|
1499
1942
|
try {
|
|
1500
|
-
const credPath =
|
|
1943
|
+
const credPath = import_path3.default.join(import_os.default.homedir(), ".node9", "credentials.json");
|
|
1501
1944
|
if (import_fs.default.existsSync(credPath)) {
|
|
1502
1945
|
const creds = JSON.parse(import_fs.default.readFileSync(credPath, "utf-8"));
|
|
1503
1946
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
@@ -1520,9 +1963,7 @@ function getCredentials() {
|
|
|
1520
1963
|
return null;
|
|
1521
1964
|
}
|
|
1522
1965
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
1523
|
-
|
|
1524
|
-
setTimeout(() => controller.abort(), 5e3);
|
|
1525
|
-
fetch(`${creds.apiUrl}/audit`, {
|
|
1966
|
+
return fetch(`${creds.apiUrl}/audit`, {
|
|
1526
1967
|
method: "POST",
|
|
1527
1968
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
1528
1969
|
body: JSON.stringify({
|
|
@@ -1537,11 +1978,12 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1537
1978
|
platform: import_os.default.platform()
|
|
1538
1979
|
}
|
|
1539
1980
|
}),
|
|
1540
|
-
signal:
|
|
1981
|
+
signal: AbortSignal.timeout(5e3)
|
|
1982
|
+
}).then(() => {
|
|
1541
1983
|
}).catch(() => {
|
|
1542
1984
|
});
|
|
1543
1985
|
}
|
|
1544
|
-
async function initNode9SaaS(toolName, args, creds, meta) {
|
|
1986
|
+
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
1545
1987
|
const controller = new AbortController();
|
|
1546
1988
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
1547
1989
|
try {
|
|
@@ -1557,7 +1999,8 @@ async function initNode9SaaS(toolName, args, creds, meta) {
|
|
|
1557
1999
|
hostname: import_os.default.hostname(),
|
|
1558
2000
|
cwd: process.cwd(),
|
|
1559
2001
|
platform: import_os.default.platform()
|
|
1560
|
-
}
|
|
2002
|
+
},
|
|
2003
|
+
...riskMetadata && { riskMetadata }
|
|
1561
2004
|
}),
|
|
1562
2005
|
signal: controller.signal
|
|
1563
2006
|
});
|
|
@@ -1615,7 +2058,7 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
1615
2058
|
|
|
1616
2059
|
// src/setup.ts
|
|
1617
2060
|
var import_fs2 = __toESM(require("fs"));
|
|
1618
|
-
var
|
|
2061
|
+
var import_path4 = __toESM(require("path"));
|
|
1619
2062
|
var import_os2 = __toESM(require("os"));
|
|
1620
2063
|
var import_chalk3 = __toESM(require("chalk"));
|
|
1621
2064
|
var import_prompts2 = require("@inquirer/prompts");
|
|
@@ -1640,14 +2083,14 @@ function readJson(filePath) {
|
|
|
1640
2083
|
return null;
|
|
1641
2084
|
}
|
|
1642
2085
|
function writeJson(filePath, data) {
|
|
1643
|
-
const dir =
|
|
2086
|
+
const dir = import_path4.default.dirname(filePath);
|
|
1644
2087
|
if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
|
|
1645
2088
|
import_fs2.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
1646
2089
|
}
|
|
1647
2090
|
async function setupClaude() {
|
|
1648
2091
|
const homeDir2 = import_os2.default.homedir();
|
|
1649
|
-
const mcpPath =
|
|
1650
|
-
const hooksPath =
|
|
2092
|
+
const mcpPath = import_path4.default.join(homeDir2, ".claude.json");
|
|
2093
|
+
const hooksPath = import_path4.default.join(homeDir2, ".claude", "settings.json");
|
|
1651
2094
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
1652
2095
|
const settings = readJson(hooksPath) ?? {};
|
|
1653
2096
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -1722,7 +2165,7 @@ async function setupClaude() {
|
|
|
1722
2165
|
}
|
|
1723
2166
|
async function setupGemini() {
|
|
1724
2167
|
const homeDir2 = import_os2.default.homedir();
|
|
1725
|
-
const settingsPath =
|
|
2168
|
+
const settingsPath = import_path4.default.join(homeDir2, ".gemini", "settings.json");
|
|
1726
2169
|
const settings = readJson(settingsPath) ?? {};
|
|
1727
2170
|
const servers = settings.mcpServers ?? {};
|
|
1728
2171
|
let anythingChanged = false;
|
|
@@ -1805,8 +2248,8 @@ async function setupGemini() {
|
|
|
1805
2248
|
}
|
|
1806
2249
|
async function setupCursor() {
|
|
1807
2250
|
const homeDir2 = import_os2.default.homedir();
|
|
1808
|
-
const mcpPath =
|
|
1809
|
-
const hooksPath =
|
|
2251
|
+
const mcpPath = import_path4.default.join(homeDir2, ".cursor", "mcp.json");
|
|
2252
|
+
const hooksPath = import_path4.default.join(homeDir2, ".cursor", "hooks.json");
|
|
1810
2253
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
1811
2254
|
const hooksFile = readJson(hooksPath) ?? { version: 1 };
|
|
1812
2255
|
const servers = mcpConfig.mcpServers ?? {};
|
|
@@ -2102,6 +2545,55 @@ var ui_default = `<!doctype html>
|
|
|
2102
2545
|
white-space: pre-wrap;
|
|
2103
2546
|
word-break: break-all;
|
|
2104
2547
|
}
|
|
2548
|
+
/* \u2500\u2500 Context Sniper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2549
|
+
.sniper-header {
|
|
2550
|
+
display: flex;
|
|
2551
|
+
align-items: center;
|
|
2552
|
+
gap: 8px;
|
|
2553
|
+
flex-wrap: wrap;
|
|
2554
|
+
margin-bottom: 8px;
|
|
2555
|
+
}
|
|
2556
|
+
.sniper-badge {
|
|
2557
|
+
font-size: 11px;
|
|
2558
|
+
font-weight: 600;
|
|
2559
|
+
padding: 3px 8px;
|
|
2560
|
+
border-radius: 5px;
|
|
2561
|
+
letter-spacing: 0.02em;
|
|
2562
|
+
}
|
|
2563
|
+
.sniper-badge-edit {
|
|
2564
|
+
background: rgba(59, 130, 246, 0.15);
|
|
2565
|
+
color: #60a5fa;
|
|
2566
|
+
border: 1px solid rgba(59, 130, 246, 0.3);
|
|
2567
|
+
}
|
|
2568
|
+
.sniper-badge-exec {
|
|
2569
|
+
background: rgba(239, 68, 68, 0.12);
|
|
2570
|
+
color: #f87171;
|
|
2571
|
+
border: 1px solid rgba(239, 68, 68, 0.25);
|
|
2572
|
+
}
|
|
2573
|
+
.sniper-tier {
|
|
2574
|
+
font-size: 10px;
|
|
2575
|
+
color: var(--muted);
|
|
2576
|
+
font-family: 'Fira Code', monospace;
|
|
2577
|
+
}
|
|
2578
|
+
.sniper-filepath {
|
|
2579
|
+
font-size: 11px;
|
|
2580
|
+
color: #a8b3c4;
|
|
2581
|
+
font-family: 'Fira Code', monospace;
|
|
2582
|
+
margin-bottom: 6px;
|
|
2583
|
+
word-break: break-all;
|
|
2584
|
+
}
|
|
2585
|
+
.sniper-match {
|
|
2586
|
+
font-size: 11px;
|
|
2587
|
+
color: #a8b3c4;
|
|
2588
|
+
margin-bottom: 6px;
|
|
2589
|
+
}
|
|
2590
|
+
.sniper-match code {
|
|
2591
|
+
background: rgba(239, 68, 68, 0.15);
|
|
2592
|
+
color: #f87171;
|
|
2593
|
+
padding: 1px 5px;
|
|
2594
|
+
border-radius: 3px;
|
|
2595
|
+
font-family: 'Fira Code', monospace;
|
|
2596
|
+
}
|
|
2105
2597
|
.actions {
|
|
2106
2598
|
display: grid;
|
|
2107
2599
|
grid-template-columns: 1fr 1fr;
|
|
@@ -2608,20 +3100,47 @@ var ui_default = `<!doctype html>
|
|
|
2608
3100
|
}, 200);
|
|
2609
3101
|
}
|
|
2610
3102
|
|
|
3103
|
+
function renderPayload(req) {
|
|
3104
|
+
const rm = req.riskMetadata;
|
|
3105
|
+
if (!rm) {
|
|
3106
|
+
// Fallback: raw args for requests without context sniper data
|
|
3107
|
+
const cmd = esc(
|
|
3108
|
+
String(
|
|
3109
|
+
req.args &&
|
|
3110
|
+
(req.args.command ||
|
|
3111
|
+
req.args.cmd ||
|
|
3112
|
+
req.args.script ||
|
|
3113
|
+
JSON.stringify(req.args, null, 2))
|
|
3114
|
+
)
|
|
3115
|
+
);
|
|
3116
|
+
return \`<span class="label">Input Payload</span><pre>\${cmd}</pre>\`;
|
|
3117
|
+
}
|
|
3118
|
+
const isEdit = rm.intent === 'EDIT';
|
|
3119
|
+
const badgeClass = isEdit ? 'sniper-badge-edit' : 'sniper-badge-exec';
|
|
3120
|
+
const badgeLabel = isEdit ? '\u{1F4DD} Code Edit' : '\u{1F6D1} Execution';
|
|
3121
|
+
const tierLabel = \`Tier \${rm.tier} \xB7 \${esc(rm.blockedByLabel)}\`;
|
|
3122
|
+
const fileLine =
|
|
3123
|
+
isEdit && rm.editFilePath
|
|
3124
|
+
? \`<div class="sniper-filepath">\u{1F4C2} \${esc(rm.editFilePath)}</div>\`
|
|
3125
|
+
: !isEdit && rm.matchedWord
|
|
3126
|
+
? \`<div class="sniper-match">Matched: <code>\${esc(rm.matchedWord)}</code>\${rm.matchedField ? \` in <code>\${esc(rm.matchedField)}</code>\` : ''}</div>\`
|
|
3127
|
+
: '';
|
|
3128
|
+
const snippetHtml = rm.contextSnippet ? \`<pre>\${esc(rm.contextSnippet)}</pre>\` : '';
|
|
3129
|
+
return \`
|
|
3130
|
+
<div class="sniper-header">
|
|
3131
|
+
<span class="sniper-badge \${badgeClass}">\${badgeLabel}</span>
|
|
3132
|
+
<span class="sniper-tier">\${tierLabel}</span>
|
|
3133
|
+
</div>
|
|
3134
|
+
\${fileLine}
|
|
3135
|
+
\${snippetHtml}
|
|
3136
|
+
\`;
|
|
3137
|
+
}
|
|
3138
|
+
|
|
2611
3139
|
function addCard(req) {
|
|
2612
3140
|
if (requests.has(req.id)) return;
|
|
2613
3141
|
requests.add(req.id);
|
|
2614
3142
|
refresh();
|
|
2615
3143
|
const isSlack = !!req.slackDelegated;
|
|
2616
|
-
const cmd = esc(
|
|
2617
|
-
String(
|
|
2618
|
-
req.args &&
|
|
2619
|
-
(req.args.command ||
|
|
2620
|
-
req.args.cmd ||
|
|
2621
|
-
req.args.script ||
|
|
2622
|
-
JSON.stringify(req.args, null, 2))
|
|
2623
|
-
)
|
|
2624
|
-
);
|
|
2625
3144
|
const card = document.createElement('div');
|
|
2626
3145
|
card.className = 'card' + (isSlack ? ' slack-viewer' : '');
|
|
2627
3146
|
card.id = 'c-' + req.id;
|
|
@@ -2635,8 +3154,7 @@ var ui_default = `<!doctype html>
|
|
|
2635
3154
|
</div>
|
|
2636
3155
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
2637
3156
|
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Slack approval \u2014 view only</div>' : ''}
|
|
2638
|
-
|
|
2639
|
-
<pre>\${cmd}</pre>
|
|
3157
|
+
\${renderPayload(req)}
|
|
2640
3158
|
<div class="actions" id="act-\${req.id}">
|
|
2641
3159
|
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>Approve Execution</button>
|
|
2642
3160
|
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>Block Action</button>
|
|
@@ -2844,7 +3362,7 @@ var UI_HTML_TEMPLATE = ui_default;
|
|
|
2844
3362
|
// src/daemon/index.ts
|
|
2845
3363
|
var import_http = __toESM(require("http"));
|
|
2846
3364
|
var import_fs3 = __toESM(require("fs"));
|
|
2847
|
-
var
|
|
3365
|
+
var import_path5 = __toESM(require("path"));
|
|
2848
3366
|
var import_os3 = __toESM(require("os"));
|
|
2849
3367
|
var import_child_process2 = require("child_process");
|
|
2850
3368
|
var import_crypto = require("crypto");
|
|
@@ -2852,14 +3370,14 @@ var import_chalk4 = __toESM(require("chalk"));
|
|
|
2852
3370
|
var DAEMON_PORT2 = 7391;
|
|
2853
3371
|
var DAEMON_HOST2 = "127.0.0.1";
|
|
2854
3372
|
var homeDir = import_os3.default.homedir();
|
|
2855
|
-
var DAEMON_PID_FILE =
|
|
2856
|
-
var DECISIONS_FILE =
|
|
2857
|
-
var GLOBAL_CONFIG_FILE =
|
|
2858
|
-
var CREDENTIALS_FILE =
|
|
2859
|
-
var AUDIT_LOG_FILE =
|
|
2860
|
-
var TRUST_FILE2 =
|
|
3373
|
+
var DAEMON_PID_FILE = import_path5.default.join(homeDir, ".node9", "daemon.pid");
|
|
3374
|
+
var DECISIONS_FILE = import_path5.default.join(homeDir, ".node9", "decisions.json");
|
|
3375
|
+
var GLOBAL_CONFIG_FILE = import_path5.default.join(homeDir, ".node9", "config.json");
|
|
3376
|
+
var CREDENTIALS_FILE = import_path5.default.join(homeDir, ".node9", "credentials.json");
|
|
3377
|
+
var AUDIT_LOG_FILE = import_path5.default.join(homeDir, ".node9", "audit.log");
|
|
3378
|
+
var TRUST_FILE2 = import_path5.default.join(homeDir, ".node9", "trust.json");
|
|
2861
3379
|
function atomicWriteSync2(filePath, data, options) {
|
|
2862
|
-
const dir =
|
|
3380
|
+
const dir = import_path5.default.dirname(filePath);
|
|
2863
3381
|
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
2864
3382
|
const tmpPath = `${filePath}.${(0, import_crypto.randomUUID)()}.tmp`;
|
|
2865
3383
|
import_fs3.default.writeFileSync(tmpPath, data, options);
|
|
@@ -2903,7 +3421,7 @@ function appendAuditLog(data) {
|
|
|
2903
3421
|
decision: data.decision,
|
|
2904
3422
|
source: "daemon"
|
|
2905
3423
|
};
|
|
2906
|
-
const dir =
|
|
3424
|
+
const dir = import_path5.default.dirname(AUDIT_LOG_FILE);
|
|
2907
3425
|
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
2908
3426
|
import_fs3.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
2909
3427
|
} catch {
|
|
@@ -3061,6 +3579,7 @@ data: ${JSON.stringify({
|
|
|
3061
3579
|
id: e.id,
|
|
3062
3580
|
toolName: e.toolName,
|
|
3063
3581
|
args: e.args,
|
|
3582
|
+
riskMetadata: e.riskMetadata,
|
|
3064
3583
|
slackDelegated: e.slackDelegated,
|
|
3065
3584
|
timestamp: e.timestamp,
|
|
3066
3585
|
agent: e.agent,
|
|
@@ -3086,14 +3605,23 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3086
3605
|
if (req.method === "POST" && pathname === "/check") {
|
|
3087
3606
|
try {
|
|
3088
3607
|
resetIdleTimer();
|
|
3608
|
+
_resetConfigCache();
|
|
3089
3609
|
const body = await readBody(req);
|
|
3090
3610
|
if (body.length > 65536) return res.writeHead(413).end();
|
|
3091
|
-
const {
|
|
3611
|
+
const {
|
|
3612
|
+
toolName,
|
|
3613
|
+
args,
|
|
3614
|
+
slackDelegated = false,
|
|
3615
|
+
agent,
|
|
3616
|
+
mcpServer,
|
|
3617
|
+
riskMetadata
|
|
3618
|
+
} = JSON.parse(body);
|
|
3092
3619
|
const id = (0, import_crypto.randomUUID)();
|
|
3093
3620
|
const entry = {
|
|
3094
3621
|
id,
|
|
3095
3622
|
toolName,
|
|
3096
3623
|
args,
|
|
3624
|
+
riskMetadata: riskMetadata ?? void 0,
|
|
3097
3625
|
agent: typeof agent === "string" ? agent : void 0,
|
|
3098
3626
|
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0,
|
|
3099
3627
|
slackDelegated: !!slackDelegated,
|
|
@@ -3108,25 +3636,71 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3108
3636
|
args: e.args,
|
|
3109
3637
|
decision: "auto-deny"
|
|
3110
3638
|
});
|
|
3111
|
-
if (e.waiter) e.waiter("deny");
|
|
3112
|
-
else
|
|
3639
|
+
if (e.waiter) e.waiter("deny", "No response \u2014 auto-denied after timeout");
|
|
3640
|
+
else {
|
|
3641
|
+
e.earlyDecision = "deny";
|
|
3642
|
+
e.earlyReason = "No response \u2014 auto-denied after timeout";
|
|
3643
|
+
}
|
|
3113
3644
|
pending.delete(id);
|
|
3114
3645
|
broadcast("remove", { id });
|
|
3115
3646
|
}
|
|
3116
3647
|
}, AUTO_DENY_MS)
|
|
3117
3648
|
};
|
|
3118
3649
|
pending.set(id, entry);
|
|
3119
|
-
|
|
3120
|
-
|
|
3650
|
+
const browserEnabled = getConfig().settings.approvers?.browser !== false;
|
|
3651
|
+
if (browserEnabled) {
|
|
3652
|
+
broadcast("add", {
|
|
3653
|
+
id,
|
|
3654
|
+
toolName,
|
|
3655
|
+
args,
|
|
3656
|
+
riskMetadata: entry.riskMetadata,
|
|
3657
|
+
slackDelegated: entry.slackDelegated,
|
|
3658
|
+
agent: entry.agent,
|
|
3659
|
+
mcpServer: entry.mcpServer
|
|
3660
|
+
});
|
|
3661
|
+
if (sseClients.size === 0 && !autoStarted)
|
|
3662
|
+
openBrowser(`http://127.0.0.1:${DAEMON_PORT2}/`);
|
|
3663
|
+
}
|
|
3664
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3665
|
+
res.end(JSON.stringify({ id }));
|
|
3666
|
+
authorizeHeadless(
|
|
3121
3667
|
toolName,
|
|
3122
3668
|
args,
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3669
|
+
false,
|
|
3670
|
+
{
|
|
3671
|
+
agent: typeof agent === "string" ? agent : void 0,
|
|
3672
|
+
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
|
|
3673
|
+
},
|
|
3674
|
+
{ calledFromDaemon: true }
|
|
3675
|
+
).then((result) => {
|
|
3676
|
+
const e = pending.get(id);
|
|
3677
|
+
if (!e) return;
|
|
3678
|
+
if (result.noApprovalMechanism) return;
|
|
3679
|
+
clearTimeout(e.timer);
|
|
3680
|
+
const decision = result.approved ? "allow" : "deny";
|
|
3681
|
+
appendAuditLog({ toolName: e.toolName, args: e.args, decision });
|
|
3682
|
+
if (e.waiter) {
|
|
3683
|
+
e.waiter(decision, result.reason);
|
|
3684
|
+
pending.delete(id);
|
|
3685
|
+
broadcast("remove", { id });
|
|
3686
|
+
} else {
|
|
3687
|
+
e.earlyDecision = decision;
|
|
3688
|
+
e.earlyReason = result.reason;
|
|
3689
|
+
}
|
|
3690
|
+
}).catch((err) => {
|
|
3691
|
+
const e = pending.get(id);
|
|
3692
|
+
if (!e) return;
|
|
3693
|
+
clearTimeout(e.timer);
|
|
3694
|
+
const reason = err?.reason || "No response \u2014 request timed out";
|
|
3695
|
+
if (e.waiter) e.waiter("deny", reason);
|
|
3696
|
+
else {
|
|
3697
|
+
e.earlyDecision = "deny";
|
|
3698
|
+
e.earlyReason = reason;
|
|
3699
|
+
}
|
|
3700
|
+
pending.delete(id);
|
|
3701
|
+
broadcast("remove", { id });
|
|
3126
3702
|
});
|
|
3127
|
-
|
|
3128
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3129
|
-
return res.end(JSON.stringify({ id }));
|
|
3703
|
+
return;
|
|
3130
3704
|
} catch {
|
|
3131
3705
|
res.writeHead(400).end();
|
|
3132
3706
|
}
|
|
@@ -3136,12 +3710,18 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3136
3710
|
const entry = pending.get(id);
|
|
3137
3711
|
if (!entry) return res.writeHead(404).end();
|
|
3138
3712
|
if (entry.earlyDecision) {
|
|
3713
|
+
pending.delete(id);
|
|
3714
|
+
broadcast("remove", { id });
|
|
3139
3715
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3140
|
-
|
|
3716
|
+
const body = { decision: entry.earlyDecision };
|
|
3717
|
+
if (entry.earlyReason) body.reason = entry.earlyReason;
|
|
3718
|
+
return res.end(JSON.stringify(body));
|
|
3141
3719
|
}
|
|
3142
|
-
entry.waiter = (d) => {
|
|
3720
|
+
entry.waiter = (d, reason) => {
|
|
3143
3721
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3144
|
-
|
|
3722
|
+
const body = { decision: d };
|
|
3723
|
+
if (reason) body.reason = reason;
|
|
3724
|
+
res.end(JSON.stringify(body));
|
|
3145
3725
|
};
|
|
3146
3726
|
return;
|
|
3147
3727
|
}
|
|
@@ -3151,7 +3731,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3151
3731
|
const id = pathname.split("/").pop();
|
|
3152
3732
|
const entry = pending.get(id);
|
|
3153
3733
|
if (!entry) return res.writeHead(404).end();
|
|
3154
|
-
const { decision, persist, trustDuration } = JSON.parse(await readBody(req));
|
|
3734
|
+
const { decision, persist, trustDuration, reason } = JSON.parse(await readBody(req));
|
|
3155
3735
|
if (decision === "trust" && trustDuration) {
|
|
3156
3736
|
const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
|
|
3157
3737
|
writeTrustEntry(entry.toolName, ms);
|
|
@@ -3176,8 +3756,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3176
3756
|
decision: resolvedDecision
|
|
3177
3757
|
});
|
|
3178
3758
|
clearTimeout(entry.timer);
|
|
3179
|
-
if (entry.waiter) entry.waiter(resolvedDecision);
|
|
3180
|
-
else
|
|
3759
|
+
if (entry.waiter) entry.waiter(resolvedDecision, reason);
|
|
3760
|
+
else {
|
|
3761
|
+
entry.earlyDecision = resolvedDecision;
|
|
3762
|
+
entry.earlyReason = reason;
|
|
3763
|
+
}
|
|
3181
3764
|
pending.delete(id);
|
|
3182
3765
|
broadcast("remove", { id });
|
|
3183
3766
|
res.writeHead(200);
|
|
@@ -3340,16 +3923,16 @@ var import_execa2 = require("execa");
|
|
|
3340
3923
|
var import_chalk5 = __toESM(require("chalk"));
|
|
3341
3924
|
var import_readline = __toESM(require("readline"));
|
|
3342
3925
|
var import_fs5 = __toESM(require("fs"));
|
|
3343
|
-
var
|
|
3926
|
+
var import_path7 = __toESM(require("path"));
|
|
3344
3927
|
var import_os5 = __toESM(require("os"));
|
|
3345
3928
|
|
|
3346
3929
|
// src/undo.ts
|
|
3347
3930
|
var import_child_process3 = require("child_process");
|
|
3348
3931
|
var import_fs4 = __toESM(require("fs"));
|
|
3349
|
-
var
|
|
3932
|
+
var import_path6 = __toESM(require("path"));
|
|
3350
3933
|
var import_os4 = __toESM(require("os"));
|
|
3351
|
-
var SNAPSHOT_STACK_PATH =
|
|
3352
|
-
var UNDO_LATEST_PATH =
|
|
3934
|
+
var SNAPSHOT_STACK_PATH = import_path6.default.join(import_os4.default.homedir(), ".node9", "snapshots.json");
|
|
3935
|
+
var UNDO_LATEST_PATH = import_path6.default.join(import_os4.default.homedir(), ".node9", "undo_latest.txt");
|
|
3353
3936
|
var MAX_SNAPSHOTS = 10;
|
|
3354
3937
|
function readStack() {
|
|
3355
3938
|
try {
|
|
@@ -3360,7 +3943,7 @@ function readStack() {
|
|
|
3360
3943
|
return [];
|
|
3361
3944
|
}
|
|
3362
3945
|
function writeStack(stack) {
|
|
3363
|
-
const dir =
|
|
3946
|
+
const dir = import_path6.default.dirname(SNAPSHOT_STACK_PATH);
|
|
3364
3947
|
if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
|
|
3365
3948
|
import_fs4.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
3366
3949
|
}
|
|
@@ -3378,8 +3961,8 @@ function buildArgsSummary(tool, args) {
|
|
|
3378
3961
|
async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
3379
3962
|
try {
|
|
3380
3963
|
const cwd = process.cwd();
|
|
3381
|
-
if (!import_fs4.default.existsSync(
|
|
3382
|
-
const tempIndex =
|
|
3964
|
+
if (!import_fs4.default.existsSync(import_path6.default.join(cwd, ".git"))) return null;
|
|
3965
|
+
const tempIndex = import_path6.default.join(cwd, ".git", `node9_index_${Date.now()}`);
|
|
3383
3966
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
3384
3967
|
(0, import_child_process3.spawnSync)("git", ["add", "-A"], { env });
|
|
3385
3968
|
const treeRes = (0, import_child_process3.spawnSync)("git", ["write-tree"], { env });
|
|
@@ -3443,7 +4026,7 @@ function applyUndo(hash, cwd) {
|
|
|
3443
4026
|
const tracked = (0, import_child_process3.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
3444
4027
|
const untracked = (0, import_child_process3.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
3445
4028
|
for (const file of [...tracked, ...untracked]) {
|
|
3446
|
-
const fullPath =
|
|
4029
|
+
const fullPath = import_path6.default.join(dir, file);
|
|
3447
4030
|
if (!snapshotFiles.has(file) && import_fs4.default.existsSync(fullPath)) {
|
|
3448
4031
|
import_fs4.default.unlinkSync(fullPath);
|
|
3449
4032
|
}
|
|
@@ -3457,7 +4040,7 @@ function applyUndo(hash, cwd) {
|
|
|
3457
4040
|
// src/cli.ts
|
|
3458
4041
|
var import_prompts3 = require("@inquirer/prompts");
|
|
3459
4042
|
var { version } = JSON.parse(
|
|
3460
|
-
import_fs5.default.readFileSync(
|
|
4043
|
+
import_fs5.default.readFileSync(import_path7.default.join(__dirname, "../package.json"), "utf-8")
|
|
3461
4044
|
);
|
|
3462
4045
|
function parseDuration(str) {
|
|
3463
4046
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -3650,9 +4233,9 @@ async function runProxy(targetCommand) {
|
|
|
3650
4233
|
}
|
|
3651
4234
|
program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
|
|
3652
4235
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
3653
|
-
const credPath =
|
|
3654
|
-
if (!import_fs5.default.existsSync(
|
|
3655
|
-
import_fs5.default.mkdirSync(
|
|
4236
|
+
const credPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "credentials.json");
|
|
4237
|
+
if (!import_fs5.default.existsSync(import_path7.default.dirname(credPath)))
|
|
4238
|
+
import_fs5.default.mkdirSync(import_path7.default.dirname(credPath), { recursive: true });
|
|
3656
4239
|
const profileName = options.profile || "default";
|
|
3657
4240
|
let existingCreds = {};
|
|
3658
4241
|
try {
|
|
@@ -3671,7 +4254,7 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3671
4254
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
3672
4255
|
import_fs5.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
3673
4256
|
if (profileName === "default") {
|
|
3674
|
-
const configPath =
|
|
4257
|
+
const configPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
3675
4258
|
let config = {};
|
|
3676
4259
|
try {
|
|
3677
4260
|
if (import_fs5.default.existsSync(configPath))
|
|
@@ -3686,10 +4269,12 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3686
4269
|
cloud: true,
|
|
3687
4270
|
terminal: true
|
|
3688
4271
|
};
|
|
3689
|
-
|
|
4272
|
+
if (options.local) {
|
|
4273
|
+
approvers.cloud = false;
|
|
4274
|
+
}
|
|
3690
4275
|
s.approvers = approvers;
|
|
3691
|
-
if (!import_fs5.default.existsSync(
|
|
3692
|
-
import_fs5.default.mkdirSync(
|
|
4276
|
+
if (!import_fs5.default.existsSync(import_path7.default.dirname(configPath)))
|
|
4277
|
+
import_fs5.default.mkdirSync(import_path7.default.dirname(configPath), { recursive: true });
|
|
3693
4278
|
import_fs5.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3694
4279
|
}
|
|
3695
4280
|
if (options.profile && profileName !== "default") {
|
|
@@ -3775,7 +4360,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3775
4360
|
);
|
|
3776
4361
|
}
|
|
3777
4362
|
section("Configuration");
|
|
3778
|
-
const globalConfigPath =
|
|
4363
|
+
const globalConfigPath = import_path7.default.join(homeDir2, ".node9", "config.json");
|
|
3779
4364
|
if (import_fs5.default.existsSync(globalConfigPath)) {
|
|
3780
4365
|
try {
|
|
3781
4366
|
JSON.parse(import_fs5.default.readFileSync(globalConfigPath, "utf-8"));
|
|
@@ -3786,7 +4371,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3786
4371
|
} else {
|
|
3787
4372
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
3788
4373
|
}
|
|
3789
|
-
const projectConfigPath =
|
|
4374
|
+
const projectConfigPath = import_path7.default.join(process.cwd(), "node9.config.json");
|
|
3790
4375
|
if (import_fs5.default.existsSync(projectConfigPath)) {
|
|
3791
4376
|
try {
|
|
3792
4377
|
JSON.parse(import_fs5.default.readFileSync(projectConfigPath, "utf-8"));
|
|
@@ -3795,7 +4380,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3795
4380
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
3796
4381
|
}
|
|
3797
4382
|
}
|
|
3798
|
-
const credsPath =
|
|
4383
|
+
const credsPath = import_path7.default.join(homeDir2, ".node9", "credentials.json");
|
|
3799
4384
|
if (import_fs5.default.existsSync(credsPath)) {
|
|
3800
4385
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
3801
4386
|
} else {
|
|
@@ -3805,7 +4390,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3805
4390
|
);
|
|
3806
4391
|
}
|
|
3807
4392
|
section("Agent Hooks");
|
|
3808
|
-
const claudeSettingsPath =
|
|
4393
|
+
const claudeSettingsPath = import_path7.default.join(homeDir2, ".claude", "settings.json");
|
|
3809
4394
|
if (import_fs5.default.existsSync(claudeSettingsPath)) {
|
|
3810
4395
|
try {
|
|
3811
4396
|
const cs = JSON.parse(import_fs5.default.readFileSync(claudeSettingsPath, "utf-8"));
|
|
@@ -3821,7 +4406,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3821
4406
|
} else {
|
|
3822
4407
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
3823
4408
|
}
|
|
3824
|
-
const geminiSettingsPath =
|
|
4409
|
+
const geminiSettingsPath = import_path7.default.join(homeDir2, ".gemini", "settings.json");
|
|
3825
4410
|
if (import_fs5.default.existsSync(geminiSettingsPath)) {
|
|
3826
4411
|
try {
|
|
3827
4412
|
const gs = JSON.parse(import_fs5.default.readFileSync(geminiSettingsPath, "utf-8"));
|
|
@@ -3837,7 +4422,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3837
4422
|
} else {
|
|
3838
4423
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
3839
4424
|
}
|
|
3840
|
-
const cursorHooksPath =
|
|
4425
|
+
const cursorHooksPath = import_path7.default.join(homeDir2, ".cursor", "hooks.json");
|
|
3841
4426
|
if (import_fs5.default.existsSync(cursorHooksPath)) {
|
|
3842
4427
|
try {
|
|
3843
4428
|
const cur = JSON.parse(import_fs5.default.readFileSync(cursorHooksPath, "utf-8"));
|
|
@@ -3942,7 +4527,7 @@ program.command("explain").description(
|
|
|
3942
4527
|
console.log("");
|
|
3943
4528
|
});
|
|
3944
4529
|
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
|
|
3945
|
-
const configPath =
|
|
4530
|
+
const configPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
3946
4531
|
if (import_fs5.default.existsSync(configPath) && !options.force) {
|
|
3947
4532
|
console.log(import_chalk5.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
3948
4533
|
console.log(import_chalk5.default.gray(` Run with --force to overwrite.`));
|
|
@@ -3957,7 +4542,7 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
3957
4542
|
mode: safeMode
|
|
3958
4543
|
}
|
|
3959
4544
|
};
|
|
3960
|
-
const dir =
|
|
4545
|
+
const dir = import_path7.default.dirname(configPath);
|
|
3961
4546
|
if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
|
|
3962
4547
|
import_fs5.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
3963
4548
|
console.log(import_chalk5.default.green(`\u2705 Global config created: ${configPath}`));
|
|
@@ -3977,7 +4562,7 @@ function formatRelativeTime(timestamp) {
|
|
|
3977
4562
|
return new Date(timestamp).toLocaleDateString();
|
|
3978
4563
|
}
|
|
3979
4564
|
program.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
|
|
3980
|
-
const logPath =
|
|
4565
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "audit.log");
|
|
3981
4566
|
if (!import_fs5.default.existsSync(logPath)) {
|
|
3982
4567
|
console.log(
|
|
3983
4568
|
import_chalk5.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
@@ -4067,8 +4652,8 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4067
4652
|
console.log("");
|
|
4068
4653
|
const modeLabel = settings.mode === "audit" ? import_chalk5.default.blue("audit") : settings.mode === "strict" ? import_chalk5.default.red("strict") : import_chalk5.default.white("standard");
|
|
4069
4654
|
console.log(` Mode: ${modeLabel}`);
|
|
4070
|
-
const projectConfig =
|
|
4071
|
-
const globalConfig =
|
|
4655
|
+
const projectConfig = import_path7.default.join(process.cwd(), "node9.config.json");
|
|
4656
|
+
const globalConfig = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
4072
4657
|
console.log(
|
|
4073
4658
|
` Local: ${import_fs5.default.existsSync(projectConfig) ? import_chalk5.default.green("Active (node9.config.json)") : import_chalk5.default.gray("Not present")}`
|
|
4074
4659
|
);
|
|
@@ -4136,7 +4721,7 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
4136
4721
|
} catch (err) {
|
|
4137
4722
|
const tempConfig = getConfig();
|
|
4138
4723
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
4139
|
-
const logPath =
|
|
4724
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4140
4725
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4141
4726
|
import_fs5.default.appendFileSync(
|
|
4142
4727
|
logPath,
|
|
@@ -4156,9 +4741,9 @@ RAW: ${raw}
|
|
|
4156
4741
|
}
|
|
4157
4742
|
const config = getConfig();
|
|
4158
4743
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
4159
|
-
const logPath =
|
|
4160
|
-
if (!import_fs5.default.existsSync(
|
|
4161
|
-
import_fs5.default.mkdirSync(
|
|
4744
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4745
|
+
if (!import_fs5.default.existsSync(import_path7.default.dirname(logPath)))
|
|
4746
|
+
import_fs5.default.mkdirSync(import_path7.default.dirname(logPath), { recursive: true });
|
|
4162
4747
|
import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
4163
4748
|
`);
|
|
4164
4749
|
}
|
|
@@ -4196,16 +4781,7 @@ RAW: ${raw}
|
|
|
4196
4781
|
return;
|
|
4197
4782
|
}
|
|
4198
4783
|
const meta = { agent, mcpServer };
|
|
4199
|
-
|
|
4200
|
-
"write_file",
|
|
4201
|
-
"edit_file",
|
|
4202
|
-
"edit",
|
|
4203
|
-
"replace",
|
|
4204
|
-
"terminal.execute",
|
|
4205
|
-
"str_replace_based_edit_tool",
|
|
4206
|
-
"create_file"
|
|
4207
|
-
];
|
|
4208
|
-
if (config.settings.enableUndo && STATE_CHANGING_TOOLS_PRE.includes(toolName.toLowerCase())) {
|
|
4784
|
+
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
4209
4785
|
await createShadowSnapshot(toolName, toolInput);
|
|
4210
4786
|
}
|
|
4211
4787
|
const result = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
@@ -4239,7 +4815,7 @@ RAW: ${raw}
|
|
|
4239
4815
|
});
|
|
4240
4816
|
} catch (err) {
|
|
4241
4817
|
if (process.env.NODE9_DEBUG === "1") {
|
|
4242
|
-
const logPath =
|
|
4818
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4243
4819
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4244
4820
|
import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
4245
4821
|
`);
|
|
@@ -4286,20 +4862,12 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
4286
4862
|
decision: "allowed",
|
|
4287
4863
|
source: "post-hook"
|
|
4288
4864
|
};
|
|
4289
|
-
const logPath =
|
|
4290
|
-
if (!import_fs5.default.existsSync(
|
|
4291
|
-
import_fs5.default.mkdirSync(
|
|
4865
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "audit.log");
|
|
4866
|
+
if (!import_fs5.default.existsSync(import_path7.default.dirname(logPath)))
|
|
4867
|
+
import_fs5.default.mkdirSync(import_path7.default.dirname(logPath), { recursive: true });
|
|
4292
4868
|
import_fs5.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
4293
4869
|
const config = getConfig();
|
|
4294
|
-
|
|
4295
|
-
"bash",
|
|
4296
|
-
"shell",
|
|
4297
|
-
"write_file",
|
|
4298
|
-
"edit_file",
|
|
4299
|
-
"replace",
|
|
4300
|
-
"terminal.execute"
|
|
4301
|
-
];
|
|
4302
|
-
if (config.settings.enableUndo && STATE_CHANGING_TOOLS.includes(tool.toLowerCase())) {
|
|
4870
|
+
if (shouldSnapshot(tool, {}, config)) {
|
|
4303
4871
|
await createShadowSnapshot();
|
|
4304
4872
|
}
|
|
4305
4873
|
} catch {
|
|
@@ -4471,7 +5039,7 @@ process.on("unhandledRejection", (reason) => {
|
|
|
4471
5039
|
const isCheckHook = process.argv[2] === "check";
|
|
4472
5040
|
if (isCheckHook) {
|
|
4473
5041
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
4474
|
-
const logPath =
|
|
5042
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4475
5043
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
4476
5044
|
import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
4477
5045
|
`);
|