@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.mjs
CHANGED
|
@@ -7,24 +7,124 @@ import { Command } from "commander";
|
|
|
7
7
|
import chalk2 from "chalk";
|
|
8
8
|
import { confirm } from "@inquirer/prompts";
|
|
9
9
|
import fs from "fs";
|
|
10
|
-
import
|
|
10
|
+
import path3 from "path";
|
|
11
11
|
import os from "os";
|
|
12
12
|
import pm from "picomatch";
|
|
13
13
|
import { parse } from "sh-syntax";
|
|
14
14
|
|
|
15
15
|
// src/ui/native.ts
|
|
16
16
|
import { spawn } from "child_process";
|
|
17
|
+
import path2 from "path";
|
|
17
18
|
import chalk from "chalk";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
|
|
20
|
+
// src/context-sniper.ts
|
|
21
|
+
import path from "path";
|
|
21
22
|
function smartTruncate(str, maxLen = 500) {
|
|
22
23
|
if (str.length <= maxLen) return str;
|
|
23
24
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
24
25
|
return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
|
|
25
26
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
27
|
+
function extractContext(text, matchedWord) {
|
|
28
|
+
const lines = text.split("\n");
|
|
29
|
+
if (lines.length <= 7 || !matchedWord) {
|
|
30
|
+
return { snippet: smartTruncate(text, 500), lineIndex: -1 };
|
|
31
|
+
}
|
|
32
|
+
const escaped = matchedWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
33
|
+
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
34
|
+
const allHits = lines.map((line, i) => ({ i, line })).filter(({ line }) => pattern.test(line));
|
|
35
|
+
if (allHits.length === 0) return { snippet: smartTruncate(text, 500), lineIndex: -1 };
|
|
36
|
+
const nonComment = allHits.find(({ line }) => {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
return !trimmed.startsWith("//") && !trimmed.startsWith("#");
|
|
39
|
+
});
|
|
40
|
+
const hitIndex = (nonComment ?? allHits[0]).i;
|
|
41
|
+
const start = Math.max(0, hitIndex - 3);
|
|
42
|
+
const end = Math.min(lines.length, hitIndex + 4);
|
|
43
|
+
const lineIndex = hitIndex - start;
|
|
44
|
+
const snippet = lines.slice(start, end).map((line, i) => `${start + i === hitIndex ? "\u{1F6D1} " : " "}${line}`).join("\n");
|
|
45
|
+
const head = start > 0 ? `... [${start} lines hidden] ...
|
|
46
|
+
` : "";
|
|
47
|
+
const tail = end < lines.length ? `
|
|
48
|
+
... [${lines.length - end} lines hidden] ...` : "";
|
|
49
|
+
return { snippet: `${head}${snippet}${tail}`, lineIndex };
|
|
50
|
+
}
|
|
51
|
+
var CODE_KEYS = [
|
|
52
|
+
"command",
|
|
53
|
+
"cmd",
|
|
54
|
+
"shell_command",
|
|
55
|
+
"bash_command",
|
|
56
|
+
"script",
|
|
57
|
+
"code",
|
|
58
|
+
"input",
|
|
59
|
+
"sql",
|
|
60
|
+
"query",
|
|
61
|
+
"arguments",
|
|
62
|
+
"args",
|
|
63
|
+
"param",
|
|
64
|
+
"params",
|
|
65
|
+
"text"
|
|
66
|
+
];
|
|
67
|
+
function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
|
|
68
|
+
let intent = "EXEC";
|
|
69
|
+
let contextSnippet;
|
|
70
|
+
let contextLineIndex;
|
|
71
|
+
let editFileName;
|
|
72
|
+
let editFilePath;
|
|
73
|
+
let parsed = args;
|
|
74
|
+
if (typeof args === "string") {
|
|
75
|
+
const trimmed = args.trim();
|
|
76
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(trimmed);
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
84
|
+
const obj = parsed;
|
|
85
|
+
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
86
|
+
intent = "EDIT";
|
|
87
|
+
if (obj.file_path) {
|
|
88
|
+
editFilePath = String(obj.file_path);
|
|
89
|
+
editFileName = path.basename(editFilePath);
|
|
90
|
+
}
|
|
91
|
+
const result = extractContext(String(obj.new_string), matchedWord);
|
|
92
|
+
contextSnippet = result.snippet;
|
|
93
|
+
if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
|
|
94
|
+
} else if (matchedField && obj[matchedField] !== void 0) {
|
|
95
|
+
const result = extractContext(String(obj[matchedField]), matchedWord);
|
|
96
|
+
contextSnippet = result.snippet;
|
|
97
|
+
if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
|
|
98
|
+
} else {
|
|
99
|
+
const foundKey = Object.keys(obj).find((k) => CODE_KEYS.includes(k.toLowerCase()));
|
|
100
|
+
if (foundKey) {
|
|
101
|
+
const val = obj[foundKey];
|
|
102
|
+
contextSnippet = smartTruncate(typeof val === "string" ? val : JSON.stringify(val), 500);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else if (typeof parsed === "string") {
|
|
106
|
+
contextSnippet = smartTruncate(parsed, 500);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
intent,
|
|
110
|
+
tier,
|
|
111
|
+
blockedByLabel,
|
|
112
|
+
...matchedWord && { matchedWord },
|
|
113
|
+
...matchedField && { matchedField },
|
|
114
|
+
...contextSnippet !== void 0 && { contextSnippet },
|
|
115
|
+
...contextLineIndex !== void 0 && { contextLineIndex },
|
|
116
|
+
...editFileName && { editFileName },
|
|
117
|
+
...editFilePath && { editFilePath },
|
|
118
|
+
...ruleName && { ruleName }
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/ui/native.ts
|
|
123
|
+
var isTestEnv = () => {
|
|
124
|
+
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";
|
|
125
|
+
};
|
|
126
|
+
function formatArgs(args, matchedField, matchedWord) {
|
|
127
|
+
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
28
128
|
let parsed = args;
|
|
29
129
|
if (typeof args === "string") {
|
|
30
130
|
const trimmed = args.trim();
|
|
@@ -35,11 +135,39 @@ function formatArgs(args) {
|
|
|
35
135
|
parsed = args;
|
|
36
136
|
}
|
|
37
137
|
} else {
|
|
38
|
-
return smartTruncate(args, 600);
|
|
138
|
+
return { message: smartTruncate(args, 600), intent: "EXEC" };
|
|
39
139
|
}
|
|
40
140
|
}
|
|
41
141
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
42
142
|
const obj = parsed;
|
|
143
|
+
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
144
|
+
const file = obj.file_path ? path2.basename(String(obj.file_path)) : "file";
|
|
145
|
+
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
146
|
+
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
147
|
+
return {
|
|
148
|
+
intent: "EDIT",
|
|
149
|
+
message: `\u{1F4DD} EDITING: ${file}
|
|
150
|
+
\u{1F4C2} PATH: ${obj.file_path}
|
|
151
|
+
|
|
152
|
+
--- REPLACING ---
|
|
153
|
+
${oldPreview}
|
|
154
|
+
|
|
155
|
+
+++ NEW CODE +++
|
|
156
|
+
${newPreview}`
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (matchedField && obj[matchedField] !== void 0) {
|
|
160
|
+
const otherKeys = Object.keys(obj).filter((k) => k !== matchedField);
|
|
161
|
+
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(", ")}
|
|
162
|
+
|
|
163
|
+
` : "";
|
|
164
|
+
const content = extractContext(String(obj[matchedField]), matchedWord).snippet;
|
|
165
|
+
return {
|
|
166
|
+
intent: "EXEC",
|
|
167
|
+
message: `${context}\u{1F6D1} [${matchedField.toUpperCase()}]:
|
|
168
|
+
${content}`
|
|
169
|
+
};
|
|
170
|
+
}
|
|
43
171
|
const codeKeys = [
|
|
44
172
|
"command",
|
|
45
173
|
"cmd",
|
|
@@ -60,14 +188,18 @@ function formatArgs(args) {
|
|
|
60
188
|
if (foundKey) {
|
|
61
189
|
const val = obj[foundKey];
|
|
62
190
|
const str = typeof val === "string" ? val : JSON.stringify(val);
|
|
63
|
-
return
|
|
64
|
-
|
|
191
|
+
return {
|
|
192
|
+
intent: "EXEC",
|
|
193
|
+
message: `[${foundKey.toUpperCase()}]:
|
|
194
|
+
${smartTruncate(str, 500)}`
|
|
195
|
+
};
|
|
65
196
|
}
|
|
66
|
-
|
|
197
|
+
const msg = Object.entries(obj).slice(0, 5).map(
|
|
67
198
|
([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
|
|
68
199
|
).join("\n");
|
|
200
|
+
return { intent: "EXEC", message: msg };
|
|
69
201
|
}
|
|
70
|
-
return smartTruncate(JSON.stringify(parsed), 200);
|
|
202
|
+
return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
|
|
71
203
|
}
|
|
72
204
|
function sendDesktopNotification(title, body) {
|
|
73
205
|
if (isTestEnv()) return;
|
|
@@ -120,10 +252,11 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
120
252
|
}
|
|
121
253
|
return lines.join("\n");
|
|
122
254
|
}
|
|
123
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
|
|
255
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
|
|
124
256
|
if (isTestEnv()) return "deny";
|
|
125
|
-
const formattedArgs = formatArgs(args);
|
|
126
|
-
const
|
|
257
|
+
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
258
|
+
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
259
|
+
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
127
260
|
const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
|
|
128
261
|
process.stderr.write(chalk.yellow(`
|
|
129
262
|
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
@@ -200,11 +333,113 @@ end run`;
|
|
|
200
333
|
});
|
|
201
334
|
}
|
|
202
335
|
|
|
336
|
+
// src/config-schema.ts
|
|
337
|
+
import { z } from "zod";
|
|
338
|
+
var noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
339
|
+
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
340
|
+
});
|
|
341
|
+
var validRegex = noNewlines.refine(
|
|
342
|
+
(s) => {
|
|
343
|
+
try {
|
|
344
|
+
new RegExp(s);
|
|
345
|
+
return true;
|
|
346
|
+
} catch {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
{ message: "Value must be a valid regular expression" }
|
|
351
|
+
);
|
|
352
|
+
var SmartConditionSchema = z.object({
|
|
353
|
+
field: z.string().min(1, "Condition field must not be empty"),
|
|
354
|
+
op: z.enum(["matches", "notMatches", "contains", "notContains", "exists", "notExists"], {
|
|
355
|
+
errorMap: () => ({
|
|
356
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists"
|
|
357
|
+
})
|
|
358
|
+
}),
|
|
359
|
+
value: validRegex.optional(),
|
|
360
|
+
flags: z.string().optional()
|
|
361
|
+
});
|
|
362
|
+
var SmartRuleSchema = z.object({
|
|
363
|
+
name: z.string().optional(),
|
|
364
|
+
tool: z.string().min(1, "Smart rule tool must not be empty"),
|
|
365
|
+
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
366
|
+
conditionMode: z.enum(["all", "any"]).optional(),
|
|
367
|
+
verdict: z.enum(["allow", "review", "block"], {
|
|
368
|
+
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
369
|
+
}),
|
|
370
|
+
reason: z.string().optional()
|
|
371
|
+
});
|
|
372
|
+
var PolicyRuleSchema = z.object({
|
|
373
|
+
action: z.string().min(1),
|
|
374
|
+
allowPaths: z.array(z.string()).optional(),
|
|
375
|
+
blockPaths: z.array(z.string()).optional()
|
|
376
|
+
});
|
|
377
|
+
var ConfigFileSchema = z.object({
|
|
378
|
+
version: z.string().optional(),
|
|
379
|
+
settings: z.object({
|
|
380
|
+
mode: z.enum(["standard", "strict", "audit"]).optional(),
|
|
381
|
+
autoStartDaemon: z.boolean().optional(),
|
|
382
|
+
enableUndo: z.boolean().optional(),
|
|
383
|
+
enableHookLogDebug: z.boolean().optional(),
|
|
384
|
+
approvalTimeoutMs: z.number().nonnegative().optional(),
|
|
385
|
+
approvers: z.object({
|
|
386
|
+
native: z.boolean().optional(),
|
|
387
|
+
browser: z.boolean().optional(),
|
|
388
|
+
cloud: z.boolean().optional(),
|
|
389
|
+
terminal: z.boolean().optional()
|
|
390
|
+
}).optional(),
|
|
391
|
+
environment: z.string().optional(),
|
|
392
|
+
slackEnabled: z.boolean().optional(),
|
|
393
|
+
enableTrustSessions: z.boolean().optional(),
|
|
394
|
+
allowGlobalPause: z.boolean().optional()
|
|
395
|
+
}).optional(),
|
|
396
|
+
policy: z.object({
|
|
397
|
+
sandboxPaths: z.array(z.string()).optional(),
|
|
398
|
+
dangerousWords: z.array(noNewlines).optional(),
|
|
399
|
+
ignoredTools: z.array(z.string()).optional(),
|
|
400
|
+
toolInspection: z.record(z.string()).optional(),
|
|
401
|
+
rules: z.array(PolicyRuleSchema).optional(),
|
|
402
|
+
smartRules: z.array(SmartRuleSchema).optional(),
|
|
403
|
+
snapshot: z.object({
|
|
404
|
+
tools: z.array(z.string()).optional(),
|
|
405
|
+
onlyPaths: z.array(z.string()).optional(),
|
|
406
|
+
ignorePaths: z.array(z.string()).optional()
|
|
407
|
+
}).optional()
|
|
408
|
+
}).optional(),
|
|
409
|
+
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
410
|
+
}).strict({ message: "Config contains unknown top-level keys" });
|
|
411
|
+
function sanitizeConfig(raw) {
|
|
412
|
+
const result = ConfigFileSchema.safeParse(raw);
|
|
413
|
+
if (result.success) {
|
|
414
|
+
return { sanitized: result.data, error: null };
|
|
415
|
+
}
|
|
416
|
+
const invalidTopLevelKeys = new Set(
|
|
417
|
+
result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
|
|
418
|
+
);
|
|
419
|
+
const sanitized = {};
|
|
420
|
+
if (typeof raw === "object" && raw !== null) {
|
|
421
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
422
|
+
if (!invalidTopLevelKeys.has(key)) {
|
|
423
|
+
sanitized[key] = value;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const lines = result.error.issues.map((issue) => {
|
|
428
|
+
const path8 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
429
|
+
return ` \u2022 ${path8}: ${issue.message}`;
|
|
430
|
+
});
|
|
431
|
+
return {
|
|
432
|
+
sanitized,
|
|
433
|
+
error: `Invalid config:
|
|
434
|
+
${lines.join("\n")}`
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
203
438
|
// src/core.ts
|
|
204
|
-
var PAUSED_FILE =
|
|
205
|
-
var TRUST_FILE =
|
|
206
|
-
var LOCAL_AUDIT_LOG =
|
|
207
|
-
var HOOK_DEBUG_LOG =
|
|
439
|
+
var PAUSED_FILE = path3.join(os.homedir(), ".node9", "PAUSED");
|
|
440
|
+
var TRUST_FILE = path3.join(os.homedir(), ".node9", "trust.json");
|
|
441
|
+
var LOCAL_AUDIT_LOG = path3.join(os.homedir(), ".node9", "audit.log");
|
|
442
|
+
var HOOK_DEBUG_LOG = path3.join(os.homedir(), ".node9", "hook-debug.log");
|
|
208
443
|
function checkPause() {
|
|
209
444
|
try {
|
|
210
445
|
if (!fs.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -222,7 +457,7 @@ function checkPause() {
|
|
|
222
457
|
}
|
|
223
458
|
}
|
|
224
459
|
function atomicWriteSync(filePath, data, options) {
|
|
225
|
-
const dir =
|
|
460
|
+
const dir = path3.dirname(filePath);
|
|
226
461
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
227
462
|
const tmpPath = `${filePath}.${os.hostname()}.${process.pid}.tmp`;
|
|
228
463
|
fs.writeFileSync(tmpPath, data, options);
|
|
@@ -273,7 +508,7 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
273
508
|
}
|
|
274
509
|
function appendToLog(logPath, entry) {
|
|
275
510
|
try {
|
|
276
|
-
const dir =
|
|
511
|
+
const dir = path3.dirname(logPath);
|
|
277
512
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
278
513
|
fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
279
514
|
} catch {
|
|
@@ -317,9 +552,21 @@ function matchesPattern(text, patterns) {
|
|
|
317
552
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
318
553
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
319
554
|
}
|
|
320
|
-
function getNestedValue(obj,
|
|
555
|
+
function getNestedValue(obj, path8) {
|
|
321
556
|
if (!obj || typeof obj !== "object") return null;
|
|
322
|
-
return
|
|
557
|
+
return path8.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
558
|
+
}
|
|
559
|
+
function shouldSnapshot(toolName, args, config) {
|
|
560
|
+
if (!config.settings.enableUndo) return false;
|
|
561
|
+
const snap = config.policy.snapshot;
|
|
562
|
+
if (!snap.tools.includes(toolName.toLowerCase())) return false;
|
|
563
|
+
const a = args && typeof args === "object" ? args : {};
|
|
564
|
+
const filePath = String(a.file_path ?? a.path ?? a.filename ?? "");
|
|
565
|
+
if (filePath) {
|
|
566
|
+
if (snap.ignorePaths.length && pm(snap.ignorePaths)(filePath)) return false;
|
|
567
|
+
if (snap.onlyPaths.length && !pm(snap.onlyPaths)(filePath)) return false;
|
|
568
|
+
}
|
|
569
|
+
return true;
|
|
323
570
|
}
|
|
324
571
|
function evaluateSmartConditions(args, rule) {
|
|
325
572
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -456,15 +703,10 @@ function redactSecrets(text) {
|
|
|
456
703
|
return redacted;
|
|
457
704
|
}
|
|
458
705
|
var DANGEROUS_WORDS = [
|
|
459
|
-
"
|
|
460
|
-
|
|
461
|
-
"
|
|
462
|
-
|
|
463
|
-
"destroy",
|
|
464
|
-
"terminate",
|
|
465
|
-
"revoke",
|
|
466
|
-
"docker",
|
|
467
|
-
"psql"
|
|
706
|
+
"mkfs",
|
|
707
|
+
// formats/wipes a filesystem partition
|
|
708
|
+
"shred"
|
|
709
|
+
// permanently overwrites file contents (unrecoverable)
|
|
468
710
|
];
|
|
469
711
|
var DEFAULT_CONFIG = {
|
|
470
712
|
settings: {
|
|
@@ -508,7 +750,21 @@ var DEFAULT_CONFIG = {
|
|
|
508
750
|
"terminal.execute": "command",
|
|
509
751
|
"postgres:query": "sql"
|
|
510
752
|
},
|
|
753
|
+
snapshot: {
|
|
754
|
+
tools: [
|
|
755
|
+
"str_replace_based_edit_tool",
|
|
756
|
+
"write_file",
|
|
757
|
+
"edit_file",
|
|
758
|
+
"create_file",
|
|
759
|
+
"edit",
|
|
760
|
+
"replace"
|
|
761
|
+
],
|
|
762
|
+
onlyPaths: [],
|
|
763
|
+
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
764
|
+
},
|
|
511
765
|
rules: [
|
|
766
|
+
// Only use the legacy rules format for simple path-based rm control.
|
|
767
|
+
// All other command-level enforcement lives in smartRules below.
|
|
512
768
|
{
|
|
513
769
|
action: "rm",
|
|
514
770
|
allowPaths: [
|
|
@@ -525,6 +781,7 @@ var DEFAULT_CONFIG = {
|
|
|
525
781
|
}
|
|
526
782
|
],
|
|
527
783
|
smartRules: [
|
|
784
|
+
// ── SQL safety ────────────────────────────────────────────────────────
|
|
528
785
|
{
|
|
529
786
|
name: "no-delete-without-where",
|
|
530
787
|
tool: "*",
|
|
@@ -535,6 +792,84 @@ var DEFAULT_CONFIG = {
|
|
|
535
792
|
conditionMode: "all",
|
|
536
793
|
verdict: "review",
|
|
537
794
|
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
name: "review-drop-truncate-shell",
|
|
798
|
+
tool: "bash",
|
|
799
|
+
conditions: [
|
|
800
|
+
{
|
|
801
|
+
field: "command",
|
|
802
|
+
op: "matches",
|
|
803
|
+
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
804
|
+
flags: "i"
|
|
805
|
+
}
|
|
806
|
+
],
|
|
807
|
+
conditionMode: "all",
|
|
808
|
+
verdict: "review",
|
|
809
|
+
reason: "SQL DDL destructive statement inside a shell command"
|
|
810
|
+
},
|
|
811
|
+
// ── Git safety ────────────────────────────────────────────────────────
|
|
812
|
+
{
|
|
813
|
+
name: "block-force-push",
|
|
814
|
+
tool: "bash",
|
|
815
|
+
conditions: [
|
|
816
|
+
{
|
|
817
|
+
field: "command",
|
|
818
|
+
op: "matches",
|
|
819
|
+
value: "git push.*(--force|--force-with-lease|-f\\b)",
|
|
820
|
+
flags: "i"
|
|
821
|
+
}
|
|
822
|
+
],
|
|
823
|
+
conditionMode: "all",
|
|
824
|
+
verdict: "block",
|
|
825
|
+
reason: "Force push overwrites remote history and cannot be undone"
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
name: "review-git-push",
|
|
829
|
+
tool: "bash",
|
|
830
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*git push\\b", flags: "i" }],
|
|
831
|
+
conditionMode: "all",
|
|
832
|
+
verdict: "review",
|
|
833
|
+
reason: "git push sends changes to a shared remote"
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
name: "review-git-destructive",
|
|
837
|
+
tool: "bash",
|
|
838
|
+
conditions: [
|
|
839
|
+
{
|
|
840
|
+
field: "command",
|
|
841
|
+
op: "matches",
|
|
842
|
+
value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
|
|
843
|
+
flags: "i"
|
|
844
|
+
}
|
|
845
|
+
],
|
|
846
|
+
conditionMode: "all",
|
|
847
|
+
verdict: "review",
|
|
848
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
849
|
+
},
|
|
850
|
+
// ── Shell safety ──────────────────────────────────────────────────────
|
|
851
|
+
{
|
|
852
|
+
name: "review-sudo",
|
|
853
|
+
tool: "bash",
|
|
854
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
|
|
855
|
+
conditionMode: "all",
|
|
856
|
+
verdict: "review",
|
|
857
|
+
reason: "Command requires elevated privileges"
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
name: "review-curl-pipe-shell",
|
|
861
|
+
tool: "bash",
|
|
862
|
+
conditions: [
|
|
863
|
+
{
|
|
864
|
+
field: "command",
|
|
865
|
+
op: "matches",
|
|
866
|
+
value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
867
|
+
flags: "i"
|
|
868
|
+
}
|
|
869
|
+
],
|
|
870
|
+
conditionMode: "all",
|
|
871
|
+
verdict: "block",
|
|
872
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
538
873
|
}
|
|
539
874
|
]
|
|
540
875
|
},
|
|
@@ -546,7 +881,7 @@ function _resetConfigCache() {
|
|
|
546
881
|
}
|
|
547
882
|
function getGlobalSettings() {
|
|
548
883
|
try {
|
|
549
|
-
const globalConfigPath =
|
|
884
|
+
const globalConfigPath = path3.join(os.homedir(), ".node9", "config.json");
|
|
550
885
|
if (fs.existsSync(globalConfigPath)) {
|
|
551
886
|
const parsed = JSON.parse(fs.readFileSync(globalConfigPath, "utf-8"));
|
|
552
887
|
const settings = parsed.settings || {};
|
|
@@ -570,7 +905,7 @@ function getGlobalSettings() {
|
|
|
570
905
|
}
|
|
571
906
|
function getInternalToken() {
|
|
572
907
|
try {
|
|
573
|
-
const pidFile =
|
|
908
|
+
const pidFile = path3.join(os.homedir(), ".node9", "daemon.pid");
|
|
574
909
|
if (!fs.existsSync(pidFile)) return null;
|
|
575
910
|
const data = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
|
|
576
911
|
process.kill(data.pid, 0);
|
|
@@ -591,7 +926,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
591
926
|
return {
|
|
592
927
|
decision: matchedRule.verdict,
|
|
593
928
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
594
|
-
reason: matchedRule.reason
|
|
929
|
+
reason: matchedRule.reason,
|
|
930
|
+
tier: 2,
|
|
931
|
+
ruleName: matchedRule.name ?? matchedRule.tool
|
|
595
932
|
};
|
|
596
933
|
}
|
|
597
934
|
}
|
|
@@ -606,7 +943,7 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
606
943
|
pathTokens = analyzed.paths;
|
|
607
944
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
608
945
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
609
|
-
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)" };
|
|
946
|
+
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
610
947
|
}
|
|
611
948
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
612
949
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
@@ -629,7 +966,7 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
629
966
|
);
|
|
630
967
|
const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
|
|
631
968
|
if (hasSystemDisaster || isRootWipe) {
|
|
632
|
-
return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
|
|
969
|
+
return { decision: "review", blockedByLabel: "Manual Nuclear Protection", tier: 3 };
|
|
633
970
|
}
|
|
634
971
|
return { decision: "allow" };
|
|
635
972
|
}
|
|
@@ -647,14 +984,16 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
647
984
|
if (anyBlocked)
|
|
648
985
|
return {
|
|
649
986
|
decision: "review",
|
|
650
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)
|
|
987
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
|
|
988
|
+
tier: 5
|
|
651
989
|
};
|
|
652
990
|
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
653
991
|
if (allAllowed) return { decision: "allow" };
|
|
654
992
|
}
|
|
655
993
|
return {
|
|
656
994
|
decision: "review",
|
|
657
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)
|
|
995
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
|
|
996
|
+
tier: 5
|
|
658
997
|
};
|
|
659
998
|
}
|
|
660
999
|
}
|
|
@@ -674,23 +1013,44 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
674
1013
|
})
|
|
675
1014
|
);
|
|
676
1015
|
if (isDangerous) {
|
|
1016
|
+
let matchedField;
|
|
1017
|
+
if (matchedDangerousWord && args && typeof args === "object" && !Array.isArray(args)) {
|
|
1018
|
+
const obj = args;
|
|
1019
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1020
|
+
if (typeof value === "string") {
|
|
1021
|
+
try {
|
|
1022
|
+
if (new RegExp(
|
|
1023
|
+
`\\b${matchedDangerousWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
|
|
1024
|
+
"i"
|
|
1025
|
+
).test(value)) {
|
|
1026
|
+
matchedField = key;
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
} catch {
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
677
1034
|
return {
|
|
678
1035
|
decision: "review",
|
|
679
|
-
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"
|
|
1036
|
+
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
|
|
1037
|
+
matchedWord: matchedDangerousWord,
|
|
1038
|
+
matchedField,
|
|
1039
|
+
tier: 6
|
|
680
1040
|
};
|
|
681
1041
|
}
|
|
682
1042
|
if (config.settings.mode === "strict") {
|
|
683
1043
|
const envConfig = getActiveEnvironment(config);
|
|
684
1044
|
if (envConfig?.requireApproval === false) return { decision: "allow" };
|
|
685
|
-
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)" };
|
|
1045
|
+
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)", tier: 7 };
|
|
686
1046
|
}
|
|
687
1047
|
return { decision: "allow" };
|
|
688
1048
|
}
|
|
689
1049
|
async function explainPolicy(toolName, args) {
|
|
690
1050
|
const steps = [];
|
|
691
|
-
const globalPath =
|
|
692
|
-
const projectPath =
|
|
693
|
-
const credsPath =
|
|
1051
|
+
const globalPath = path3.join(os.homedir(), ".node9", "config.json");
|
|
1052
|
+
const projectPath = path3.join(process.cwd(), "node9.config.json");
|
|
1053
|
+
const credsPath = path3.join(os.homedir(), ".node9", "credentials.json");
|
|
694
1054
|
const waterfall = [
|
|
695
1055
|
{
|
|
696
1056
|
tier: 1,
|
|
@@ -994,7 +1354,7 @@ var DAEMON_PORT = 7391;
|
|
|
994
1354
|
var DAEMON_HOST = "127.0.0.1";
|
|
995
1355
|
function isDaemonRunning() {
|
|
996
1356
|
try {
|
|
997
|
-
const pidFile =
|
|
1357
|
+
const pidFile = path3.join(os.homedir(), ".node9", "daemon.pid");
|
|
998
1358
|
if (!fs.existsSync(pidFile)) return false;
|
|
999
1359
|
const { pid, port } = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
|
|
1000
1360
|
if (port !== DAEMON_PORT) return false;
|
|
@@ -1006,7 +1366,7 @@ function isDaemonRunning() {
|
|
|
1006
1366
|
}
|
|
1007
1367
|
function getPersistentDecision(toolName) {
|
|
1008
1368
|
try {
|
|
1009
|
-
const file =
|
|
1369
|
+
const file = path3.join(os.homedir(), ".node9", "decisions.json");
|
|
1010
1370
|
if (!fs.existsSync(file)) return null;
|
|
1011
1371
|
const decisions = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
1012
1372
|
const d = decisions[toolName];
|
|
@@ -1015,7 +1375,7 @@ function getPersistentDecision(toolName) {
|
|
|
1015
1375
|
}
|
|
1016
1376
|
return null;
|
|
1017
1377
|
}
|
|
1018
|
-
async function askDaemon(toolName, args, meta, signal) {
|
|
1378
|
+
async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
1019
1379
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1020
1380
|
const checkCtrl = new AbortController();
|
|
1021
1381
|
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
@@ -1025,7 +1385,13 @@ async function askDaemon(toolName, args, meta, signal) {
|
|
|
1025
1385
|
const checkRes = await fetch(`${base}/check`, {
|
|
1026
1386
|
method: "POST",
|
|
1027
1387
|
headers: { "Content-Type": "application/json" },
|
|
1028
|
-
body: JSON.stringify({
|
|
1388
|
+
body: JSON.stringify({
|
|
1389
|
+
toolName,
|
|
1390
|
+
args,
|
|
1391
|
+
agent: meta?.agent,
|
|
1392
|
+
mcpServer: meta?.mcpServer,
|
|
1393
|
+
...riskMetadata && { riskMetadata }
|
|
1394
|
+
}),
|
|
1029
1395
|
signal: checkCtrl.signal
|
|
1030
1396
|
});
|
|
1031
1397
|
if (!checkRes.ok) throw new Error("Daemon fail");
|
|
@@ -1050,7 +1416,7 @@ async function askDaemon(toolName, args, meta, signal) {
|
|
|
1050
1416
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1051
1417
|
}
|
|
1052
1418
|
}
|
|
1053
|
-
async function notifyDaemonViewer(toolName, args, meta) {
|
|
1419
|
+
async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
1054
1420
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1055
1421
|
const res = await fetch(`${base}/check`, {
|
|
1056
1422
|
method: "POST",
|
|
@@ -1060,7 +1426,8 @@ async function notifyDaemonViewer(toolName, args, meta) {
|
|
|
1060
1426
|
args,
|
|
1061
1427
|
slackDelegated: true,
|
|
1062
1428
|
agent: meta?.agent,
|
|
1063
|
-
mcpServer: meta?.mcpServer
|
|
1429
|
+
mcpServer: meta?.mcpServer,
|
|
1430
|
+
...riskMetadata && { riskMetadata }
|
|
1064
1431
|
}),
|
|
1065
1432
|
signal: AbortSignal.timeout(3e3)
|
|
1066
1433
|
});
|
|
@@ -1077,7 +1444,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1077
1444
|
signal: AbortSignal.timeout(3e3)
|
|
1078
1445
|
});
|
|
1079
1446
|
}
|
|
1080
|
-
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta) {
|
|
1447
|
+
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1081
1448
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
1082
1449
|
const pauseState = checkPause();
|
|
1083
1450
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
@@ -1097,11 +1464,17 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1097
1464
|
}
|
|
1098
1465
|
const isManual = meta?.agent === "Terminal";
|
|
1099
1466
|
let explainableLabel = "Local Config";
|
|
1467
|
+
let policyMatchedField;
|
|
1468
|
+
let policyMatchedWord;
|
|
1469
|
+
let riskMetadata;
|
|
1100
1470
|
if (config.settings.mode === "audit") {
|
|
1101
1471
|
if (!isIgnoredTool(toolName)) {
|
|
1102
1472
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
1103
1473
|
if (policyResult.decision === "review") {
|
|
1104
1474
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
1475
|
+
if (approvers.cloud && creds?.apiKey) {
|
|
1476
|
+
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
1477
|
+
}
|
|
1105
1478
|
sendDesktopNotification(
|
|
1106
1479
|
"Node9 Audit Mode",
|
|
1107
1480
|
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
@@ -1112,13 +1485,15 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1112
1485
|
}
|
|
1113
1486
|
if (!isIgnoredTool(toolName)) {
|
|
1114
1487
|
if (getActiveTrustSession(toolName)) {
|
|
1115
|
-
if (creds?.apiKey)
|
|
1488
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1489
|
+
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
1116
1490
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
1117
1491
|
return { approved: true, checkedBy: "trust" };
|
|
1118
1492
|
}
|
|
1119
1493
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
1120
1494
|
if (policyResult.decision === "allow") {
|
|
1121
|
-
if (creds?.apiKey)
|
|
1495
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1496
|
+
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
1122
1497
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
1123
1498
|
return { approved: true, checkedBy: "local-policy" };
|
|
1124
1499
|
}
|
|
@@ -1132,9 +1507,20 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1132
1507
|
};
|
|
1133
1508
|
}
|
|
1134
1509
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
1510
|
+
policyMatchedField = policyResult.matchedField;
|
|
1511
|
+
policyMatchedWord = policyResult.matchedWord;
|
|
1512
|
+
riskMetadata = computeRiskMetadata(
|
|
1513
|
+
args,
|
|
1514
|
+
policyResult.tier ?? 6,
|
|
1515
|
+
explainableLabel,
|
|
1516
|
+
policyMatchedField,
|
|
1517
|
+
policyMatchedWord,
|
|
1518
|
+
policyResult.ruleName
|
|
1519
|
+
);
|
|
1135
1520
|
const persistent = getPersistentDecision(toolName);
|
|
1136
1521
|
if (persistent === "allow") {
|
|
1137
|
-
if (creds?.apiKey)
|
|
1522
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1523
|
+
await auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
1138
1524
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
1139
1525
|
return { approved: true, checkedBy: "persistent" };
|
|
1140
1526
|
}
|
|
@@ -1148,7 +1534,6 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1148
1534
|
};
|
|
1149
1535
|
}
|
|
1150
1536
|
} else {
|
|
1151
|
-
if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
|
|
1152
1537
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
1153
1538
|
return { approved: true };
|
|
1154
1539
|
}
|
|
@@ -1157,8 +1542,21 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1157
1542
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
1158
1543
|
if (cloudEnforced) {
|
|
1159
1544
|
try {
|
|
1160
|
-
const initResult = await initNode9SaaS(toolName, args, creds, meta);
|
|
1545
|
+
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
1161
1546
|
if (!initResult.pending) {
|
|
1547
|
+
if (initResult.shadowMode) {
|
|
1548
|
+
console.error(
|
|
1549
|
+
chalk2.yellow(
|
|
1550
|
+
`
|
|
1551
|
+
\u26A0\uFE0F Node9 Shadow Mode: Action allowed, but would have been blocked by company policy.`
|
|
1552
|
+
)
|
|
1553
|
+
);
|
|
1554
|
+
if (initResult.shadowReason) {
|
|
1555
|
+
console.error(chalk2.dim(` Reason: ${initResult.shadowReason}
|
|
1556
|
+
`));
|
|
1557
|
+
}
|
|
1558
|
+
return { approved: true, checkedBy: "cloud" };
|
|
1559
|
+
}
|
|
1162
1560
|
return {
|
|
1163
1561
|
approved: !!initResult.approved,
|
|
1164
1562
|
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
@@ -1183,18 +1581,22 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1183
1581
|
);
|
|
1184
1582
|
}
|
|
1185
1583
|
}
|
|
1186
|
-
if (
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1584
|
+
if (!options?.calledFromDaemon) {
|
|
1585
|
+
if (cloudEnforced && cloudRequestId) {
|
|
1586
|
+
console.error(
|
|
1587
|
+
chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
|
|
1588
|
+
);
|
|
1589
|
+
console.error(
|
|
1590
|
+
chalk2.cyan(" Dashboard \u2192 ") + chalk2.bold("Mission Control > Activity Feed\n")
|
|
1591
|
+
);
|
|
1592
|
+
} else if (!cloudEnforced) {
|
|
1593
|
+
const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
|
|
1594
|
+
console.error(
|
|
1595
|
+
chalk2.dim(`
|
|
1195
1596
|
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
1196
1597
|
`)
|
|
1197
|
-
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1198
1600
|
}
|
|
1199
1601
|
const abortController = new AbortController();
|
|
1200
1602
|
const { signal } = abortController;
|
|
@@ -1224,8 +1626,10 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1224
1626
|
racePromises.push(
|
|
1225
1627
|
(async () => {
|
|
1226
1628
|
try {
|
|
1227
|
-
if (isDaemonRunning() && internalToken) {
|
|
1228
|
-
viewerId = await notifyDaemonViewer(toolName, args, meta).catch(
|
|
1629
|
+
if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
|
|
1630
|
+
viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
|
|
1631
|
+
() => null
|
|
1632
|
+
);
|
|
1229
1633
|
}
|
|
1230
1634
|
const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
|
|
1231
1635
|
return {
|
|
@@ -1243,7 +1647,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1243
1647
|
})()
|
|
1244
1648
|
);
|
|
1245
1649
|
}
|
|
1246
|
-
if (approvers.native && !isManual) {
|
|
1650
|
+
if (approvers.native && !isManual && !options?.calledFromDaemon) {
|
|
1247
1651
|
racePromises.push(
|
|
1248
1652
|
(async () => {
|
|
1249
1653
|
const decision = await askNativePopup(
|
|
@@ -1252,7 +1656,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1252
1656
|
meta?.agent,
|
|
1253
1657
|
explainableLabel,
|
|
1254
1658
|
isRemoteLocked,
|
|
1255
|
-
signal
|
|
1659
|
+
signal,
|
|
1660
|
+
policyMatchedField,
|
|
1661
|
+
policyMatchedWord
|
|
1256
1662
|
);
|
|
1257
1663
|
if (decision === "always_allow") {
|
|
1258
1664
|
writeTrustSession(toolName, 36e5);
|
|
@@ -1269,7 +1675,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1269
1675
|
})()
|
|
1270
1676
|
);
|
|
1271
1677
|
}
|
|
1272
|
-
if (approvers.browser && isDaemonRunning()) {
|
|
1678
|
+
if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon && !cloudEnforced) {
|
|
1273
1679
|
racePromises.push(
|
|
1274
1680
|
(async () => {
|
|
1275
1681
|
try {
|
|
@@ -1280,7 +1686,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1280
1686
|
console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
1281
1687
|
`));
|
|
1282
1688
|
}
|
|
1283
|
-
const daemonDecision = await askDaemon(toolName, args, meta, signal);
|
|
1689
|
+
const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
|
|
1284
1690
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1285
1691
|
const isApproved = daemonDecision === "allow";
|
|
1286
1692
|
return {
|
|
@@ -1405,8 +1811,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1405
1811
|
}
|
|
1406
1812
|
function getConfig() {
|
|
1407
1813
|
if (cachedConfig) return cachedConfig;
|
|
1408
|
-
const globalPath =
|
|
1409
|
-
const projectPath =
|
|
1814
|
+
const globalPath = path3.join(os.homedir(), ".node9", "config.json");
|
|
1815
|
+
const projectPath = path3.join(process.cwd(), "node9.config.json");
|
|
1410
1816
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1411
1817
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1412
1818
|
const mergedSettings = {
|
|
@@ -1419,7 +1825,12 @@ function getConfig() {
|
|
|
1419
1825
|
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
1420
1826
|
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
1421
1827
|
rules: [...DEFAULT_CONFIG.policy.rules],
|
|
1422
|
-
smartRules: [...DEFAULT_CONFIG.policy.smartRules]
|
|
1828
|
+
smartRules: [...DEFAULT_CONFIG.policy.smartRules],
|
|
1829
|
+
snapshot: {
|
|
1830
|
+
tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
|
|
1831
|
+
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
1832
|
+
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
1833
|
+
}
|
|
1423
1834
|
};
|
|
1424
1835
|
const applyLayer = (source) => {
|
|
1425
1836
|
if (!source) return;
|
|
@@ -1431,6 +1842,7 @@ function getConfig() {
|
|
|
1431
1842
|
if (s.enableHookLogDebug !== void 0)
|
|
1432
1843
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
1433
1844
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
1845
|
+
if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
|
|
1434
1846
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
1435
1847
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
1436
1848
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
@@ -1439,6 +1851,12 @@ function getConfig() {
|
|
|
1439
1851
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
1440
1852
|
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
1441
1853
|
if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
|
|
1854
|
+
if (p.snapshot) {
|
|
1855
|
+
const s2 = p.snapshot;
|
|
1856
|
+
if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
|
|
1857
|
+
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
1858
|
+
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
1859
|
+
}
|
|
1442
1860
|
};
|
|
1443
1861
|
applyLayer(globalConfig);
|
|
1444
1862
|
applyLayer(projectConfig);
|
|
@@ -1446,6 +1864,9 @@ function getConfig() {
|
|
|
1446
1864
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
1447
1865
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
1448
1866
|
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
1867
|
+
mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
|
|
1868
|
+
mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
|
|
1869
|
+
mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
|
|
1449
1870
|
cachedConfig = {
|
|
1450
1871
|
settings: mergedSettings,
|
|
1451
1872
|
policy: mergedPolicy,
|
|
@@ -1455,11 +1876,33 @@ function getConfig() {
|
|
|
1455
1876
|
}
|
|
1456
1877
|
function tryLoadConfig(filePath) {
|
|
1457
1878
|
if (!fs.existsSync(filePath)) return null;
|
|
1879
|
+
let raw;
|
|
1458
1880
|
try {
|
|
1459
|
-
|
|
1460
|
-
} catch {
|
|
1881
|
+
raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1882
|
+
} catch (err) {
|
|
1883
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1884
|
+
process.stderr.write(
|
|
1885
|
+
`
|
|
1886
|
+
\u26A0\uFE0F Node9: Failed to parse ${filePath}
|
|
1887
|
+
${msg}
|
|
1888
|
+
\u2192 Using default config
|
|
1889
|
+
|
|
1890
|
+
`
|
|
1891
|
+
);
|
|
1461
1892
|
return null;
|
|
1462
1893
|
}
|
|
1894
|
+
const { sanitized, error } = sanitizeConfig(raw);
|
|
1895
|
+
if (error) {
|
|
1896
|
+
process.stderr.write(
|
|
1897
|
+
`
|
|
1898
|
+
\u26A0\uFE0F Node9: Invalid config at ${filePath}:
|
|
1899
|
+
${error.replace("Invalid config:\n", "")}
|
|
1900
|
+
\u2192 Invalid fields ignored, using defaults for those keys
|
|
1901
|
+
|
|
1902
|
+
`
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
return sanitized;
|
|
1463
1906
|
}
|
|
1464
1907
|
function getActiveEnvironment(config) {
|
|
1465
1908
|
const env = config.settings.environment || process.env.NODE_ENV || "development";
|
|
@@ -1474,7 +1917,7 @@ function getCredentials() {
|
|
|
1474
1917
|
};
|
|
1475
1918
|
}
|
|
1476
1919
|
try {
|
|
1477
|
-
const credPath =
|
|
1920
|
+
const credPath = path3.join(os.homedir(), ".node9", "credentials.json");
|
|
1478
1921
|
if (fs.existsSync(credPath)) {
|
|
1479
1922
|
const creds = JSON.parse(fs.readFileSync(credPath, "utf-8"));
|
|
1480
1923
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
@@ -1497,9 +1940,7 @@ function getCredentials() {
|
|
|
1497
1940
|
return null;
|
|
1498
1941
|
}
|
|
1499
1942
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
1500
|
-
|
|
1501
|
-
setTimeout(() => controller.abort(), 5e3);
|
|
1502
|
-
fetch(`${creds.apiUrl}/audit`, {
|
|
1943
|
+
return fetch(`${creds.apiUrl}/audit`, {
|
|
1503
1944
|
method: "POST",
|
|
1504
1945
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
1505
1946
|
body: JSON.stringify({
|
|
@@ -1514,11 +1955,12 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1514
1955
|
platform: os.platform()
|
|
1515
1956
|
}
|
|
1516
1957
|
}),
|
|
1517
|
-
signal:
|
|
1958
|
+
signal: AbortSignal.timeout(5e3)
|
|
1959
|
+
}).then(() => {
|
|
1518
1960
|
}).catch(() => {
|
|
1519
1961
|
});
|
|
1520
1962
|
}
|
|
1521
|
-
async function initNode9SaaS(toolName, args, creds, meta) {
|
|
1963
|
+
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
1522
1964
|
const controller = new AbortController();
|
|
1523
1965
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
1524
1966
|
try {
|
|
@@ -1534,7 +1976,8 @@ async function initNode9SaaS(toolName, args, creds, meta) {
|
|
|
1534
1976
|
hostname: os.hostname(),
|
|
1535
1977
|
cwd: process.cwd(),
|
|
1536
1978
|
platform: os.platform()
|
|
1537
|
-
}
|
|
1979
|
+
},
|
|
1980
|
+
...riskMetadata && { riskMetadata }
|
|
1538
1981
|
}),
|
|
1539
1982
|
signal: controller.signal
|
|
1540
1983
|
});
|
|
@@ -1592,7 +2035,7 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
1592
2035
|
|
|
1593
2036
|
// src/setup.ts
|
|
1594
2037
|
import fs2 from "fs";
|
|
1595
|
-
import
|
|
2038
|
+
import path4 from "path";
|
|
1596
2039
|
import os2 from "os";
|
|
1597
2040
|
import chalk3 from "chalk";
|
|
1598
2041
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
@@ -1617,14 +2060,14 @@ function readJson(filePath) {
|
|
|
1617
2060
|
return null;
|
|
1618
2061
|
}
|
|
1619
2062
|
function writeJson(filePath, data) {
|
|
1620
|
-
const dir =
|
|
2063
|
+
const dir = path4.dirname(filePath);
|
|
1621
2064
|
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
1622
2065
|
fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
1623
2066
|
}
|
|
1624
2067
|
async function setupClaude() {
|
|
1625
2068
|
const homeDir2 = os2.homedir();
|
|
1626
|
-
const mcpPath =
|
|
1627
|
-
const hooksPath =
|
|
2069
|
+
const mcpPath = path4.join(homeDir2, ".claude.json");
|
|
2070
|
+
const hooksPath = path4.join(homeDir2, ".claude", "settings.json");
|
|
1628
2071
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
1629
2072
|
const settings = readJson(hooksPath) ?? {};
|
|
1630
2073
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -1699,7 +2142,7 @@ async function setupClaude() {
|
|
|
1699
2142
|
}
|
|
1700
2143
|
async function setupGemini() {
|
|
1701
2144
|
const homeDir2 = os2.homedir();
|
|
1702
|
-
const settingsPath =
|
|
2145
|
+
const settingsPath = path4.join(homeDir2, ".gemini", "settings.json");
|
|
1703
2146
|
const settings = readJson(settingsPath) ?? {};
|
|
1704
2147
|
const servers = settings.mcpServers ?? {};
|
|
1705
2148
|
let anythingChanged = false;
|
|
@@ -1782,8 +2225,8 @@ async function setupGemini() {
|
|
|
1782
2225
|
}
|
|
1783
2226
|
async function setupCursor() {
|
|
1784
2227
|
const homeDir2 = os2.homedir();
|
|
1785
|
-
const mcpPath =
|
|
1786
|
-
const hooksPath =
|
|
2228
|
+
const mcpPath = path4.join(homeDir2, ".cursor", "mcp.json");
|
|
2229
|
+
const hooksPath = path4.join(homeDir2, ".cursor", "hooks.json");
|
|
1787
2230
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
1788
2231
|
const hooksFile = readJson(hooksPath) ?? { version: 1 };
|
|
1789
2232
|
const servers = mcpConfig.mcpServers ?? {};
|
|
@@ -2079,6 +2522,55 @@ var ui_default = `<!doctype html>
|
|
|
2079
2522
|
white-space: pre-wrap;
|
|
2080
2523
|
word-break: break-all;
|
|
2081
2524
|
}
|
|
2525
|
+
/* \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 */
|
|
2526
|
+
.sniper-header {
|
|
2527
|
+
display: flex;
|
|
2528
|
+
align-items: center;
|
|
2529
|
+
gap: 8px;
|
|
2530
|
+
flex-wrap: wrap;
|
|
2531
|
+
margin-bottom: 8px;
|
|
2532
|
+
}
|
|
2533
|
+
.sniper-badge {
|
|
2534
|
+
font-size: 11px;
|
|
2535
|
+
font-weight: 600;
|
|
2536
|
+
padding: 3px 8px;
|
|
2537
|
+
border-radius: 5px;
|
|
2538
|
+
letter-spacing: 0.02em;
|
|
2539
|
+
}
|
|
2540
|
+
.sniper-badge-edit {
|
|
2541
|
+
background: rgba(59, 130, 246, 0.15);
|
|
2542
|
+
color: #60a5fa;
|
|
2543
|
+
border: 1px solid rgba(59, 130, 246, 0.3);
|
|
2544
|
+
}
|
|
2545
|
+
.sniper-badge-exec {
|
|
2546
|
+
background: rgba(239, 68, 68, 0.12);
|
|
2547
|
+
color: #f87171;
|
|
2548
|
+
border: 1px solid rgba(239, 68, 68, 0.25);
|
|
2549
|
+
}
|
|
2550
|
+
.sniper-tier {
|
|
2551
|
+
font-size: 10px;
|
|
2552
|
+
color: var(--muted);
|
|
2553
|
+
font-family: 'Fira Code', monospace;
|
|
2554
|
+
}
|
|
2555
|
+
.sniper-filepath {
|
|
2556
|
+
font-size: 11px;
|
|
2557
|
+
color: #a8b3c4;
|
|
2558
|
+
font-family: 'Fira Code', monospace;
|
|
2559
|
+
margin-bottom: 6px;
|
|
2560
|
+
word-break: break-all;
|
|
2561
|
+
}
|
|
2562
|
+
.sniper-match {
|
|
2563
|
+
font-size: 11px;
|
|
2564
|
+
color: #a8b3c4;
|
|
2565
|
+
margin-bottom: 6px;
|
|
2566
|
+
}
|
|
2567
|
+
.sniper-match code {
|
|
2568
|
+
background: rgba(239, 68, 68, 0.15);
|
|
2569
|
+
color: #f87171;
|
|
2570
|
+
padding: 1px 5px;
|
|
2571
|
+
border-radius: 3px;
|
|
2572
|
+
font-family: 'Fira Code', monospace;
|
|
2573
|
+
}
|
|
2082
2574
|
.actions {
|
|
2083
2575
|
display: grid;
|
|
2084
2576
|
grid-template-columns: 1fr 1fr;
|
|
@@ -2585,20 +3077,47 @@ var ui_default = `<!doctype html>
|
|
|
2585
3077
|
}, 200);
|
|
2586
3078
|
}
|
|
2587
3079
|
|
|
3080
|
+
function renderPayload(req) {
|
|
3081
|
+
const rm = req.riskMetadata;
|
|
3082
|
+
if (!rm) {
|
|
3083
|
+
// Fallback: raw args for requests without context sniper data
|
|
3084
|
+
const cmd = esc(
|
|
3085
|
+
String(
|
|
3086
|
+
req.args &&
|
|
3087
|
+
(req.args.command ||
|
|
3088
|
+
req.args.cmd ||
|
|
3089
|
+
req.args.script ||
|
|
3090
|
+
JSON.stringify(req.args, null, 2))
|
|
3091
|
+
)
|
|
3092
|
+
);
|
|
3093
|
+
return \`<span class="label">Input Payload</span><pre>\${cmd}</pre>\`;
|
|
3094
|
+
}
|
|
3095
|
+
const isEdit = rm.intent === 'EDIT';
|
|
3096
|
+
const badgeClass = isEdit ? 'sniper-badge-edit' : 'sniper-badge-exec';
|
|
3097
|
+
const badgeLabel = isEdit ? '\u{1F4DD} Code Edit' : '\u{1F6D1} Execution';
|
|
3098
|
+
const tierLabel = \`Tier \${rm.tier} \xB7 \${esc(rm.blockedByLabel)}\`;
|
|
3099
|
+
const fileLine =
|
|
3100
|
+
isEdit && rm.editFilePath
|
|
3101
|
+
? \`<div class="sniper-filepath">\u{1F4C2} \${esc(rm.editFilePath)}</div>\`
|
|
3102
|
+
: !isEdit && rm.matchedWord
|
|
3103
|
+
? \`<div class="sniper-match">Matched: <code>\${esc(rm.matchedWord)}</code>\${rm.matchedField ? \` in <code>\${esc(rm.matchedField)}</code>\` : ''}</div>\`
|
|
3104
|
+
: '';
|
|
3105
|
+
const snippetHtml = rm.contextSnippet ? \`<pre>\${esc(rm.contextSnippet)}</pre>\` : '';
|
|
3106
|
+
return \`
|
|
3107
|
+
<div class="sniper-header">
|
|
3108
|
+
<span class="sniper-badge \${badgeClass}">\${badgeLabel}</span>
|
|
3109
|
+
<span class="sniper-tier">\${tierLabel}</span>
|
|
3110
|
+
</div>
|
|
3111
|
+
\${fileLine}
|
|
3112
|
+
\${snippetHtml}
|
|
3113
|
+
\`;
|
|
3114
|
+
}
|
|
3115
|
+
|
|
2588
3116
|
function addCard(req) {
|
|
2589
3117
|
if (requests.has(req.id)) return;
|
|
2590
3118
|
requests.add(req.id);
|
|
2591
3119
|
refresh();
|
|
2592
3120
|
const isSlack = !!req.slackDelegated;
|
|
2593
|
-
const cmd = esc(
|
|
2594
|
-
String(
|
|
2595
|
-
req.args &&
|
|
2596
|
-
(req.args.command ||
|
|
2597
|
-
req.args.cmd ||
|
|
2598
|
-
req.args.script ||
|
|
2599
|
-
JSON.stringify(req.args, null, 2))
|
|
2600
|
-
)
|
|
2601
|
-
);
|
|
2602
3121
|
const card = document.createElement('div');
|
|
2603
3122
|
card.className = 'card' + (isSlack ? ' slack-viewer' : '');
|
|
2604
3123
|
card.id = 'c-' + req.id;
|
|
@@ -2612,8 +3131,7 @@ var ui_default = `<!doctype html>
|
|
|
2612
3131
|
</div>
|
|
2613
3132
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
2614
3133
|
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Slack approval \u2014 view only</div>' : ''}
|
|
2615
|
-
|
|
2616
|
-
<pre>\${cmd}</pre>
|
|
3134
|
+
\${renderPayload(req)}
|
|
2617
3135
|
<div class="actions" id="act-\${req.id}">
|
|
2618
3136
|
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>Approve Execution</button>
|
|
2619
3137
|
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>Block Action</button>
|
|
@@ -2821,7 +3339,7 @@ var UI_HTML_TEMPLATE = ui_default;
|
|
|
2821
3339
|
// src/daemon/index.ts
|
|
2822
3340
|
import http from "http";
|
|
2823
3341
|
import fs3 from "fs";
|
|
2824
|
-
import
|
|
3342
|
+
import path5 from "path";
|
|
2825
3343
|
import os3 from "os";
|
|
2826
3344
|
import { spawn as spawn2 } from "child_process";
|
|
2827
3345
|
import { randomUUID } from "crypto";
|
|
@@ -2829,14 +3347,14 @@ import chalk4 from "chalk";
|
|
|
2829
3347
|
var DAEMON_PORT2 = 7391;
|
|
2830
3348
|
var DAEMON_HOST2 = "127.0.0.1";
|
|
2831
3349
|
var homeDir = os3.homedir();
|
|
2832
|
-
var DAEMON_PID_FILE =
|
|
2833
|
-
var DECISIONS_FILE =
|
|
2834
|
-
var GLOBAL_CONFIG_FILE =
|
|
2835
|
-
var CREDENTIALS_FILE =
|
|
2836
|
-
var AUDIT_LOG_FILE =
|
|
2837
|
-
var TRUST_FILE2 =
|
|
3350
|
+
var DAEMON_PID_FILE = path5.join(homeDir, ".node9", "daemon.pid");
|
|
3351
|
+
var DECISIONS_FILE = path5.join(homeDir, ".node9", "decisions.json");
|
|
3352
|
+
var GLOBAL_CONFIG_FILE = path5.join(homeDir, ".node9", "config.json");
|
|
3353
|
+
var CREDENTIALS_FILE = path5.join(homeDir, ".node9", "credentials.json");
|
|
3354
|
+
var AUDIT_LOG_FILE = path5.join(homeDir, ".node9", "audit.log");
|
|
3355
|
+
var TRUST_FILE2 = path5.join(homeDir, ".node9", "trust.json");
|
|
2838
3356
|
function atomicWriteSync2(filePath, data, options) {
|
|
2839
|
-
const dir =
|
|
3357
|
+
const dir = path5.dirname(filePath);
|
|
2840
3358
|
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
2841
3359
|
const tmpPath = `${filePath}.${randomUUID()}.tmp`;
|
|
2842
3360
|
fs3.writeFileSync(tmpPath, data, options);
|
|
@@ -2880,7 +3398,7 @@ function appendAuditLog(data) {
|
|
|
2880
3398
|
decision: data.decision,
|
|
2881
3399
|
source: "daemon"
|
|
2882
3400
|
};
|
|
2883
|
-
const dir =
|
|
3401
|
+
const dir = path5.dirname(AUDIT_LOG_FILE);
|
|
2884
3402
|
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
2885
3403
|
fs3.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
2886
3404
|
} catch {
|
|
@@ -3038,6 +3556,7 @@ data: ${JSON.stringify({
|
|
|
3038
3556
|
id: e.id,
|
|
3039
3557
|
toolName: e.toolName,
|
|
3040
3558
|
args: e.args,
|
|
3559
|
+
riskMetadata: e.riskMetadata,
|
|
3041
3560
|
slackDelegated: e.slackDelegated,
|
|
3042
3561
|
timestamp: e.timestamp,
|
|
3043
3562
|
agent: e.agent,
|
|
@@ -3063,14 +3582,23 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3063
3582
|
if (req.method === "POST" && pathname === "/check") {
|
|
3064
3583
|
try {
|
|
3065
3584
|
resetIdleTimer();
|
|
3585
|
+
_resetConfigCache();
|
|
3066
3586
|
const body = await readBody(req);
|
|
3067
3587
|
if (body.length > 65536) return res.writeHead(413).end();
|
|
3068
|
-
const {
|
|
3588
|
+
const {
|
|
3589
|
+
toolName,
|
|
3590
|
+
args,
|
|
3591
|
+
slackDelegated = false,
|
|
3592
|
+
agent,
|
|
3593
|
+
mcpServer,
|
|
3594
|
+
riskMetadata
|
|
3595
|
+
} = JSON.parse(body);
|
|
3069
3596
|
const id = randomUUID();
|
|
3070
3597
|
const entry = {
|
|
3071
3598
|
id,
|
|
3072
3599
|
toolName,
|
|
3073
3600
|
args,
|
|
3601
|
+
riskMetadata: riskMetadata ?? void 0,
|
|
3074
3602
|
agent: typeof agent === "string" ? agent : void 0,
|
|
3075
3603
|
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0,
|
|
3076
3604
|
slackDelegated: !!slackDelegated,
|
|
@@ -3085,25 +3613,71 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3085
3613
|
args: e.args,
|
|
3086
3614
|
decision: "auto-deny"
|
|
3087
3615
|
});
|
|
3088
|
-
if (e.waiter) e.waiter("deny");
|
|
3089
|
-
else
|
|
3616
|
+
if (e.waiter) e.waiter("deny", "No response \u2014 auto-denied after timeout");
|
|
3617
|
+
else {
|
|
3618
|
+
e.earlyDecision = "deny";
|
|
3619
|
+
e.earlyReason = "No response \u2014 auto-denied after timeout";
|
|
3620
|
+
}
|
|
3090
3621
|
pending.delete(id);
|
|
3091
3622
|
broadcast("remove", { id });
|
|
3092
3623
|
}
|
|
3093
3624
|
}, AUTO_DENY_MS)
|
|
3094
3625
|
};
|
|
3095
3626
|
pending.set(id, entry);
|
|
3096
|
-
|
|
3097
|
-
|
|
3627
|
+
const browserEnabled = getConfig().settings.approvers?.browser !== false;
|
|
3628
|
+
if (browserEnabled) {
|
|
3629
|
+
broadcast("add", {
|
|
3630
|
+
id,
|
|
3631
|
+
toolName,
|
|
3632
|
+
args,
|
|
3633
|
+
riskMetadata: entry.riskMetadata,
|
|
3634
|
+
slackDelegated: entry.slackDelegated,
|
|
3635
|
+
agent: entry.agent,
|
|
3636
|
+
mcpServer: entry.mcpServer
|
|
3637
|
+
});
|
|
3638
|
+
if (sseClients.size === 0 && !autoStarted)
|
|
3639
|
+
openBrowser(`http://127.0.0.1:${DAEMON_PORT2}/`);
|
|
3640
|
+
}
|
|
3641
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3642
|
+
res.end(JSON.stringify({ id }));
|
|
3643
|
+
authorizeHeadless(
|
|
3098
3644
|
toolName,
|
|
3099
3645
|
args,
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3646
|
+
false,
|
|
3647
|
+
{
|
|
3648
|
+
agent: typeof agent === "string" ? agent : void 0,
|
|
3649
|
+
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
|
|
3650
|
+
},
|
|
3651
|
+
{ calledFromDaemon: true }
|
|
3652
|
+
).then((result) => {
|
|
3653
|
+
const e = pending.get(id);
|
|
3654
|
+
if (!e) return;
|
|
3655
|
+
if (result.noApprovalMechanism) return;
|
|
3656
|
+
clearTimeout(e.timer);
|
|
3657
|
+
const decision = result.approved ? "allow" : "deny";
|
|
3658
|
+
appendAuditLog({ toolName: e.toolName, args: e.args, decision });
|
|
3659
|
+
if (e.waiter) {
|
|
3660
|
+
e.waiter(decision, result.reason);
|
|
3661
|
+
pending.delete(id);
|
|
3662
|
+
broadcast("remove", { id });
|
|
3663
|
+
} else {
|
|
3664
|
+
e.earlyDecision = decision;
|
|
3665
|
+
e.earlyReason = result.reason;
|
|
3666
|
+
}
|
|
3667
|
+
}).catch((err) => {
|
|
3668
|
+
const e = pending.get(id);
|
|
3669
|
+
if (!e) return;
|
|
3670
|
+
clearTimeout(e.timer);
|
|
3671
|
+
const reason = err?.reason || "No response \u2014 request timed out";
|
|
3672
|
+
if (e.waiter) e.waiter("deny", reason);
|
|
3673
|
+
else {
|
|
3674
|
+
e.earlyDecision = "deny";
|
|
3675
|
+
e.earlyReason = reason;
|
|
3676
|
+
}
|
|
3677
|
+
pending.delete(id);
|
|
3678
|
+
broadcast("remove", { id });
|
|
3103
3679
|
});
|
|
3104
|
-
|
|
3105
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3106
|
-
return res.end(JSON.stringify({ id }));
|
|
3680
|
+
return;
|
|
3107
3681
|
} catch {
|
|
3108
3682
|
res.writeHead(400).end();
|
|
3109
3683
|
}
|
|
@@ -3113,12 +3687,18 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3113
3687
|
const entry = pending.get(id);
|
|
3114
3688
|
if (!entry) return res.writeHead(404).end();
|
|
3115
3689
|
if (entry.earlyDecision) {
|
|
3690
|
+
pending.delete(id);
|
|
3691
|
+
broadcast("remove", { id });
|
|
3116
3692
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3117
|
-
|
|
3693
|
+
const body = { decision: entry.earlyDecision };
|
|
3694
|
+
if (entry.earlyReason) body.reason = entry.earlyReason;
|
|
3695
|
+
return res.end(JSON.stringify(body));
|
|
3118
3696
|
}
|
|
3119
|
-
entry.waiter = (d) => {
|
|
3697
|
+
entry.waiter = (d, reason) => {
|
|
3120
3698
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3121
|
-
|
|
3699
|
+
const body = { decision: d };
|
|
3700
|
+
if (reason) body.reason = reason;
|
|
3701
|
+
res.end(JSON.stringify(body));
|
|
3122
3702
|
};
|
|
3123
3703
|
return;
|
|
3124
3704
|
}
|
|
@@ -3128,7 +3708,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3128
3708
|
const id = pathname.split("/").pop();
|
|
3129
3709
|
const entry = pending.get(id);
|
|
3130
3710
|
if (!entry) return res.writeHead(404).end();
|
|
3131
|
-
const { decision, persist, trustDuration } = JSON.parse(await readBody(req));
|
|
3711
|
+
const { decision, persist, trustDuration, reason } = JSON.parse(await readBody(req));
|
|
3132
3712
|
if (decision === "trust" && trustDuration) {
|
|
3133
3713
|
const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
|
|
3134
3714
|
writeTrustEntry(entry.toolName, ms);
|
|
@@ -3153,8 +3733,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3153
3733
|
decision: resolvedDecision
|
|
3154
3734
|
});
|
|
3155
3735
|
clearTimeout(entry.timer);
|
|
3156
|
-
if (entry.waiter) entry.waiter(resolvedDecision);
|
|
3157
|
-
else
|
|
3736
|
+
if (entry.waiter) entry.waiter(resolvedDecision, reason);
|
|
3737
|
+
else {
|
|
3738
|
+
entry.earlyDecision = resolvedDecision;
|
|
3739
|
+
entry.earlyReason = reason;
|
|
3740
|
+
}
|
|
3158
3741
|
pending.delete(id);
|
|
3159
3742
|
broadcast("remove", { id });
|
|
3160
3743
|
res.writeHead(200);
|
|
@@ -3317,16 +3900,16 @@ import { execa } from "execa";
|
|
|
3317
3900
|
import chalk5 from "chalk";
|
|
3318
3901
|
import readline from "readline";
|
|
3319
3902
|
import fs5 from "fs";
|
|
3320
|
-
import
|
|
3903
|
+
import path7 from "path";
|
|
3321
3904
|
import os5 from "os";
|
|
3322
3905
|
|
|
3323
3906
|
// src/undo.ts
|
|
3324
3907
|
import { spawnSync } from "child_process";
|
|
3325
3908
|
import fs4 from "fs";
|
|
3326
|
-
import
|
|
3909
|
+
import path6 from "path";
|
|
3327
3910
|
import os4 from "os";
|
|
3328
|
-
var SNAPSHOT_STACK_PATH =
|
|
3329
|
-
var UNDO_LATEST_PATH =
|
|
3911
|
+
var SNAPSHOT_STACK_PATH = path6.join(os4.homedir(), ".node9", "snapshots.json");
|
|
3912
|
+
var UNDO_LATEST_PATH = path6.join(os4.homedir(), ".node9", "undo_latest.txt");
|
|
3330
3913
|
var MAX_SNAPSHOTS = 10;
|
|
3331
3914
|
function readStack() {
|
|
3332
3915
|
try {
|
|
@@ -3337,7 +3920,7 @@ function readStack() {
|
|
|
3337
3920
|
return [];
|
|
3338
3921
|
}
|
|
3339
3922
|
function writeStack(stack) {
|
|
3340
|
-
const dir =
|
|
3923
|
+
const dir = path6.dirname(SNAPSHOT_STACK_PATH);
|
|
3341
3924
|
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
3342
3925
|
fs4.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
3343
3926
|
}
|
|
@@ -3355,8 +3938,8 @@ function buildArgsSummary(tool, args) {
|
|
|
3355
3938
|
async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
3356
3939
|
try {
|
|
3357
3940
|
const cwd = process.cwd();
|
|
3358
|
-
if (!fs4.existsSync(
|
|
3359
|
-
const tempIndex =
|
|
3941
|
+
if (!fs4.existsSync(path6.join(cwd, ".git"))) return null;
|
|
3942
|
+
const tempIndex = path6.join(cwd, ".git", `node9_index_${Date.now()}`);
|
|
3360
3943
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
3361
3944
|
spawnSync("git", ["add", "-A"], { env });
|
|
3362
3945
|
const treeRes = spawnSync("git", ["write-tree"], { env });
|
|
@@ -3420,7 +4003,7 @@ function applyUndo(hash, cwd) {
|
|
|
3420
4003
|
const tracked = spawnSync("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
3421
4004
|
const untracked = spawnSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
3422
4005
|
for (const file of [...tracked, ...untracked]) {
|
|
3423
|
-
const fullPath =
|
|
4006
|
+
const fullPath = path6.join(dir, file);
|
|
3424
4007
|
if (!snapshotFiles.has(file) && fs4.existsSync(fullPath)) {
|
|
3425
4008
|
fs4.unlinkSync(fullPath);
|
|
3426
4009
|
}
|
|
@@ -3434,7 +4017,7 @@ function applyUndo(hash, cwd) {
|
|
|
3434
4017
|
// src/cli.ts
|
|
3435
4018
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
3436
4019
|
var { version } = JSON.parse(
|
|
3437
|
-
fs5.readFileSync(
|
|
4020
|
+
fs5.readFileSync(path7.join(__dirname, "../package.json"), "utf-8")
|
|
3438
4021
|
);
|
|
3439
4022
|
function parseDuration(str) {
|
|
3440
4023
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -3627,9 +4210,9 @@ async function runProxy(targetCommand) {
|
|
|
3627
4210
|
}
|
|
3628
4211
|
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) => {
|
|
3629
4212
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
3630
|
-
const credPath =
|
|
3631
|
-
if (!fs5.existsSync(
|
|
3632
|
-
fs5.mkdirSync(
|
|
4213
|
+
const credPath = path7.join(os5.homedir(), ".node9", "credentials.json");
|
|
4214
|
+
if (!fs5.existsSync(path7.dirname(credPath)))
|
|
4215
|
+
fs5.mkdirSync(path7.dirname(credPath), { recursive: true });
|
|
3633
4216
|
const profileName = options.profile || "default";
|
|
3634
4217
|
let existingCreds = {};
|
|
3635
4218
|
try {
|
|
@@ -3648,7 +4231,7 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3648
4231
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
3649
4232
|
fs5.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
3650
4233
|
if (profileName === "default") {
|
|
3651
|
-
const configPath =
|
|
4234
|
+
const configPath = path7.join(os5.homedir(), ".node9", "config.json");
|
|
3652
4235
|
let config = {};
|
|
3653
4236
|
try {
|
|
3654
4237
|
if (fs5.existsSync(configPath))
|
|
@@ -3663,10 +4246,12 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3663
4246
|
cloud: true,
|
|
3664
4247
|
terminal: true
|
|
3665
4248
|
};
|
|
3666
|
-
|
|
4249
|
+
if (options.local) {
|
|
4250
|
+
approvers.cloud = false;
|
|
4251
|
+
}
|
|
3667
4252
|
s.approvers = approvers;
|
|
3668
|
-
if (!fs5.existsSync(
|
|
3669
|
-
fs5.mkdirSync(
|
|
4253
|
+
if (!fs5.existsSync(path7.dirname(configPath)))
|
|
4254
|
+
fs5.mkdirSync(path7.dirname(configPath), { recursive: true });
|
|
3670
4255
|
fs5.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3671
4256
|
}
|
|
3672
4257
|
if (options.profile && profileName !== "default") {
|
|
@@ -3752,7 +4337,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3752
4337
|
);
|
|
3753
4338
|
}
|
|
3754
4339
|
section("Configuration");
|
|
3755
|
-
const globalConfigPath =
|
|
4340
|
+
const globalConfigPath = path7.join(homeDir2, ".node9", "config.json");
|
|
3756
4341
|
if (fs5.existsSync(globalConfigPath)) {
|
|
3757
4342
|
try {
|
|
3758
4343
|
JSON.parse(fs5.readFileSync(globalConfigPath, "utf-8"));
|
|
@@ -3763,7 +4348,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3763
4348
|
} else {
|
|
3764
4349
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
3765
4350
|
}
|
|
3766
|
-
const projectConfigPath =
|
|
4351
|
+
const projectConfigPath = path7.join(process.cwd(), "node9.config.json");
|
|
3767
4352
|
if (fs5.existsSync(projectConfigPath)) {
|
|
3768
4353
|
try {
|
|
3769
4354
|
JSON.parse(fs5.readFileSync(projectConfigPath, "utf-8"));
|
|
@@ -3772,7 +4357,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3772
4357
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
3773
4358
|
}
|
|
3774
4359
|
}
|
|
3775
|
-
const credsPath =
|
|
4360
|
+
const credsPath = path7.join(homeDir2, ".node9", "credentials.json");
|
|
3776
4361
|
if (fs5.existsSync(credsPath)) {
|
|
3777
4362
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
3778
4363
|
} else {
|
|
@@ -3782,7 +4367,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3782
4367
|
);
|
|
3783
4368
|
}
|
|
3784
4369
|
section("Agent Hooks");
|
|
3785
|
-
const claudeSettingsPath =
|
|
4370
|
+
const claudeSettingsPath = path7.join(homeDir2, ".claude", "settings.json");
|
|
3786
4371
|
if (fs5.existsSync(claudeSettingsPath)) {
|
|
3787
4372
|
try {
|
|
3788
4373
|
const cs = JSON.parse(fs5.readFileSync(claudeSettingsPath, "utf-8"));
|
|
@@ -3798,7 +4383,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3798
4383
|
} else {
|
|
3799
4384
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
3800
4385
|
}
|
|
3801
|
-
const geminiSettingsPath =
|
|
4386
|
+
const geminiSettingsPath = path7.join(homeDir2, ".gemini", "settings.json");
|
|
3802
4387
|
if (fs5.existsSync(geminiSettingsPath)) {
|
|
3803
4388
|
try {
|
|
3804
4389
|
const gs = JSON.parse(fs5.readFileSync(geminiSettingsPath, "utf-8"));
|
|
@@ -3814,7 +4399,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3814
4399
|
} else {
|
|
3815
4400
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
3816
4401
|
}
|
|
3817
|
-
const cursorHooksPath =
|
|
4402
|
+
const cursorHooksPath = path7.join(homeDir2, ".cursor", "hooks.json");
|
|
3818
4403
|
if (fs5.existsSync(cursorHooksPath)) {
|
|
3819
4404
|
try {
|
|
3820
4405
|
const cur = JSON.parse(fs5.readFileSync(cursorHooksPath, "utf-8"));
|
|
@@ -3919,7 +4504,7 @@ program.command("explain").description(
|
|
|
3919
4504
|
console.log("");
|
|
3920
4505
|
});
|
|
3921
4506
|
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) => {
|
|
3922
|
-
const configPath =
|
|
4507
|
+
const configPath = path7.join(os5.homedir(), ".node9", "config.json");
|
|
3923
4508
|
if (fs5.existsSync(configPath) && !options.force) {
|
|
3924
4509
|
console.log(chalk5.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
3925
4510
|
console.log(chalk5.gray(` Run with --force to overwrite.`));
|
|
@@ -3934,7 +4519,7 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
3934
4519
|
mode: safeMode
|
|
3935
4520
|
}
|
|
3936
4521
|
};
|
|
3937
|
-
const dir =
|
|
4522
|
+
const dir = path7.dirname(configPath);
|
|
3938
4523
|
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
3939
4524
|
fs5.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
3940
4525
|
console.log(chalk5.green(`\u2705 Global config created: ${configPath}`));
|
|
@@ -3954,7 +4539,7 @@ function formatRelativeTime(timestamp) {
|
|
|
3954
4539
|
return new Date(timestamp).toLocaleDateString();
|
|
3955
4540
|
}
|
|
3956
4541
|
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) => {
|
|
3957
|
-
const logPath =
|
|
4542
|
+
const logPath = path7.join(os5.homedir(), ".node9", "audit.log");
|
|
3958
4543
|
if (!fs5.existsSync(logPath)) {
|
|
3959
4544
|
console.log(
|
|
3960
4545
|
chalk5.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
@@ -4044,8 +4629,8 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4044
4629
|
console.log("");
|
|
4045
4630
|
const modeLabel = settings.mode === "audit" ? chalk5.blue("audit") : settings.mode === "strict" ? chalk5.red("strict") : chalk5.white("standard");
|
|
4046
4631
|
console.log(` Mode: ${modeLabel}`);
|
|
4047
|
-
const projectConfig =
|
|
4048
|
-
const globalConfig =
|
|
4632
|
+
const projectConfig = path7.join(process.cwd(), "node9.config.json");
|
|
4633
|
+
const globalConfig = path7.join(os5.homedir(), ".node9", "config.json");
|
|
4049
4634
|
console.log(
|
|
4050
4635
|
` Local: ${fs5.existsSync(projectConfig) ? chalk5.green("Active (node9.config.json)") : chalk5.gray("Not present")}`
|
|
4051
4636
|
);
|
|
@@ -4113,7 +4698,7 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
4113
4698
|
} catch (err) {
|
|
4114
4699
|
const tempConfig = getConfig();
|
|
4115
4700
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
4116
|
-
const logPath =
|
|
4701
|
+
const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
|
|
4117
4702
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4118
4703
|
fs5.appendFileSync(
|
|
4119
4704
|
logPath,
|
|
@@ -4133,9 +4718,9 @@ RAW: ${raw}
|
|
|
4133
4718
|
}
|
|
4134
4719
|
const config = getConfig();
|
|
4135
4720
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
4136
|
-
const logPath =
|
|
4137
|
-
if (!fs5.existsSync(
|
|
4138
|
-
fs5.mkdirSync(
|
|
4721
|
+
const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
|
|
4722
|
+
if (!fs5.existsSync(path7.dirname(logPath)))
|
|
4723
|
+
fs5.mkdirSync(path7.dirname(logPath), { recursive: true });
|
|
4139
4724
|
fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
4140
4725
|
`);
|
|
4141
4726
|
}
|
|
@@ -4173,16 +4758,7 @@ RAW: ${raw}
|
|
|
4173
4758
|
return;
|
|
4174
4759
|
}
|
|
4175
4760
|
const meta = { agent, mcpServer };
|
|
4176
|
-
|
|
4177
|
-
"write_file",
|
|
4178
|
-
"edit_file",
|
|
4179
|
-
"edit",
|
|
4180
|
-
"replace",
|
|
4181
|
-
"terminal.execute",
|
|
4182
|
-
"str_replace_based_edit_tool",
|
|
4183
|
-
"create_file"
|
|
4184
|
-
];
|
|
4185
|
-
if (config.settings.enableUndo && STATE_CHANGING_TOOLS_PRE.includes(toolName.toLowerCase())) {
|
|
4761
|
+
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
4186
4762
|
await createShadowSnapshot(toolName, toolInput);
|
|
4187
4763
|
}
|
|
4188
4764
|
const result = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
@@ -4216,7 +4792,7 @@ RAW: ${raw}
|
|
|
4216
4792
|
});
|
|
4217
4793
|
} catch (err) {
|
|
4218
4794
|
if (process.env.NODE9_DEBUG === "1") {
|
|
4219
|
-
const logPath =
|
|
4795
|
+
const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
|
|
4220
4796
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4221
4797
|
fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
4222
4798
|
`);
|
|
@@ -4263,20 +4839,12 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
4263
4839
|
decision: "allowed",
|
|
4264
4840
|
source: "post-hook"
|
|
4265
4841
|
};
|
|
4266
|
-
const logPath =
|
|
4267
|
-
if (!fs5.existsSync(
|
|
4268
|
-
fs5.mkdirSync(
|
|
4842
|
+
const logPath = path7.join(os5.homedir(), ".node9", "audit.log");
|
|
4843
|
+
if (!fs5.existsSync(path7.dirname(logPath)))
|
|
4844
|
+
fs5.mkdirSync(path7.dirname(logPath), { recursive: true });
|
|
4269
4845
|
fs5.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
4270
4846
|
const config = getConfig();
|
|
4271
|
-
|
|
4272
|
-
"bash",
|
|
4273
|
-
"shell",
|
|
4274
|
-
"write_file",
|
|
4275
|
-
"edit_file",
|
|
4276
|
-
"replace",
|
|
4277
|
-
"terminal.execute"
|
|
4278
|
-
];
|
|
4279
|
-
if (config.settings.enableUndo && STATE_CHANGING_TOOLS.includes(tool.toLowerCase())) {
|
|
4847
|
+
if (shouldSnapshot(tool, {}, config)) {
|
|
4280
4848
|
await createShadowSnapshot();
|
|
4281
4849
|
}
|
|
4282
4850
|
} catch {
|
|
@@ -4448,7 +5016,7 @@ process.on("unhandledRejection", (reason) => {
|
|
|
4448
5016
|
const isCheckHook = process.argv[2] === "check";
|
|
4449
5017
|
if (isCheckHook) {
|
|
4450
5018
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
4451
|
-
const logPath =
|
|
5019
|
+
const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
|
|
4452
5020
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
4453
5021
|
fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
4454
5022
|
`);
|