@node9/proxy 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +587 -141
- package/dist/cli.mjs +587 -141
- package/dist/index.js +431 -73
- package/dist/index.mjs +431 -73
- package/package.json +3 -2
package/dist/cli.mjs
CHANGED
|
@@ -7,18 +7,18 @@ 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
|
|
17
|
+
import path2 from "path";
|
|
18
18
|
import chalk from "chalk";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
|
|
20
|
+
// src/context-sniper.ts
|
|
21
|
+
import path from "path";
|
|
22
22
|
function smartTruncate(str, maxLen = 500) {
|
|
23
23
|
if (str.length <= maxLen) return str;
|
|
24
24
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -26,11 +26,13 @@ function smartTruncate(str, maxLen = 500) {
|
|
|
26
26
|
}
|
|
27
27
|
function extractContext(text, matchedWord) {
|
|
28
28
|
const lines = text.split("\n");
|
|
29
|
-
if (lines.length <= 7 || !matchedWord)
|
|
29
|
+
if (lines.length <= 7 || !matchedWord) {
|
|
30
|
+
return { snippet: smartTruncate(text, 500), lineIndex: -1 };
|
|
31
|
+
}
|
|
30
32
|
const escaped = matchedWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
31
33
|
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
32
34
|
const allHits = lines.map((line, i) => ({ i, line })).filter(({ line }) => pattern.test(line));
|
|
33
|
-
if (allHits.length === 0) return smartTruncate(text, 500);
|
|
35
|
+
if (allHits.length === 0) return { snippet: smartTruncate(text, 500), lineIndex: -1 };
|
|
34
36
|
const nonComment = allHits.find(({ line }) => {
|
|
35
37
|
const trimmed = line.trim();
|
|
36
38
|
return !trimmed.startsWith("//") && !trimmed.startsWith("#");
|
|
@@ -38,13 +40,89 @@ function extractContext(text, matchedWord) {
|
|
|
38
40
|
const hitIndex = (nonComment ?? allHits[0]).i;
|
|
39
41
|
const start = Math.max(0, hitIndex - 3);
|
|
40
42
|
const end = Math.min(lines.length, hitIndex + 4);
|
|
43
|
+
const lineIndex = hitIndex - start;
|
|
41
44
|
const snippet = lines.slice(start, end).map((line, i) => `${start + i === hitIndex ? "\u{1F6D1} " : " "}${line}`).join("\n");
|
|
42
45
|
const head = start > 0 ? `... [${start} lines hidden] ...
|
|
43
46
|
` : "";
|
|
44
47
|
const tail = end < lines.length ? `
|
|
45
48
|
... [${lines.length - end} lines hidden] ...` : "";
|
|
46
|
-
return `${head}${snippet}${tail}
|
|
49
|
+
return { snippet: `${head}${snippet}${tail}`, lineIndex };
|
|
47
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
|
+
};
|
|
48
126
|
function formatArgs(args, matchedField, matchedWord) {
|
|
49
127
|
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
50
128
|
let parsed = args;
|
|
@@ -63,9 +141,9 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
63
141
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
64
142
|
const obj = parsed;
|
|
65
143
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
66
|
-
const file = obj.file_path ?
|
|
144
|
+
const file = obj.file_path ? path2.basename(String(obj.file_path)) : "file";
|
|
67
145
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
68
|
-
const newPreview = extractContext(String(obj.new_string), matchedWord);
|
|
146
|
+
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
69
147
|
return {
|
|
70
148
|
intent: "EDIT",
|
|
71
149
|
message: `\u{1F4DD} EDITING: ${file}
|
|
@@ -83,7 +161,7 @@ ${newPreview}`
|
|
|
83
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(", ")}
|
|
84
162
|
|
|
85
163
|
` : "";
|
|
86
|
-
const content = extractContext(String(obj[matchedField]), matchedWord);
|
|
164
|
+
const content = extractContext(String(obj[matchedField]), matchedWord).snippet;
|
|
87
165
|
return {
|
|
88
166
|
intent: "EXEC",
|
|
89
167
|
message: `${context}\u{1F6D1} [${matchedField.toUpperCase()}]:
|
|
@@ -255,11 +333,113 @@ end run`;
|
|
|
255
333
|
});
|
|
256
334
|
}
|
|
257
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
|
+
|
|
258
438
|
// src/core.ts
|
|
259
|
-
var PAUSED_FILE =
|
|
260
|
-
var TRUST_FILE =
|
|
261
|
-
var LOCAL_AUDIT_LOG =
|
|
262
|
-
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");
|
|
263
443
|
function checkPause() {
|
|
264
444
|
try {
|
|
265
445
|
if (!fs.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -277,7 +457,7 @@ function checkPause() {
|
|
|
277
457
|
}
|
|
278
458
|
}
|
|
279
459
|
function atomicWriteSync(filePath, data, options) {
|
|
280
|
-
const dir =
|
|
460
|
+
const dir = path3.dirname(filePath);
|
|
281
461
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
282
462
|
const tmpPath = `${filePath}.${os.hostname()}.${process.pid}.tmp`;
|
|
283
463
|
fs.writeFileSync(tmpPath, data, options);
|
|
@@ -328,7 +508,7 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
328
508
|
}
|
|
329
509
|
function appendToLog(logPath, entry) {
|
|
330
510
|
try {
|
|
331
|
-
const dir =
|
|
511
|
+
const dir = path3.dirname(logPath);
|
|
332
512
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
333
513
|
fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
334
514
|
} catch {
|
|
@@ -372,9 +552,9 @@ function matchesPattern(text, patterns) {
|
|
|
372
552
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
373
553
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
374
554
|
}
|
|
375
|
-
function getNestedValue(obj,
|
|
555
|
+
function getNestedValue(obj, path8) {
|
|
376
556
|
if (!obj || typeof obj !== "object") return null;
|
|
377
|
-
return
|
|
557
|
+
return path8.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
378
558
|
}
|
|
379
559
|
function shouldSnapshot(toolName, args, config) {
|
|
380
560
|
if (!config.settings.enableUndo) return false;
|
|
@@ -523,15 +703,10 @@ function redactSecrets(text) {
|
|
|
523
703
|
return redacted;
|
|
524
704
|
}
|
|
525
705
|
var DANGEROUS_WORDS = [
|
|
526
|
-
"
|
|
527
|
-
|
|
528
|
-
"
|
|
529
|
-
|
|
530
|
-
"destroy",
|
|
531
|
-
"terminate",
|
|
532
|
-
"revoke",
|
|
533
|
-
"docker",
|
|
534
|
-
"psql"
|
|
706
|
+
"mkfs",
|
|
707
|
+
// formats/wipes a filesystem partition
|
|
708
|
+
"shred"
|
|
709
|
+
// permanently overwrites file contents (unrecoverable)
|
|
535
710
|
];
|
|
536
711
|
var DEFAULT_CONFIG = {
|
|
537
712
|
settings: {
|
|
@@ -588,6 +763,8 @@ var DEFAULT_CONFIG = {
|
|
|
588
763
|
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
589
764
|
},
|
|
590
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.
|
|
591
768
|
{
|
|
592
769
|
action: "rm",
|
|
593
770
|
allowPaths: [
|
|
@@ -604,6 +781,7 @@ var DEFAULT_CONFIG = {
|
|
|
604
781
|
}
|
|
605
782
|
],
|
|
606
783
|
smartRules: [
|
|
784
|
+
// ── SQL safety ────────────────────────────────────────────────────────
|
|
607
785
|
{
|
|
608
786
|
name: "no-delete-without-where",
|
|
609
787
|
tool: "*",
|
|
@@ -614,6 +792,84 @@ var DEFAULT_CONFIG = {
|
|
|
614
792
|
conditionMode: "all",
|
|
615
793
|
verdict: "review",
|
|
616
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\\s+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"
|
|
617
873
|
}
|
|
618
874
|
]
|
|
619
875
|
},
|
|
@@ -625,7 +881,7 @@ function _resetConfigCache() {
|
|
|
625
881
|
}
|
|
626
882
|
function getGlobalSettings() {
|
|
627
883
|
try {
|
|
628
|
-
const globalConfigPath =
|
|
884
|
+
const globalConfigPath = path3.join(os.homedir(), ".node9", "config.json");
|
|
629
885
|
if (fs.existsSync(globalConfigPath)) {
|
|
630
886
|
const parsed = JSON.parse(fs.readFileSync(globalConfigPath, "utf-8"));
|
|
631
887
|
const settings = parsed.settings || {};
|
|
@@ -649,7 +905,7 @@ function getGlobalSettings() {
|
|
|
649
905
|
}
|
|
650
906
|
function getInternalToken() {
|
|
651
907
|
try {
|
|
652
|
-
const pidFile =
|
|
908
|
+
const pidFile = path3.join(os.homedir(), ".node9", "daemon.pid");
|
|
653
909
|
if (!fs.existsSync(pidFile)) return null;
|
|
654
910
|
const data = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
|
|
655
911
|
process.kill(data.pid, 0);
|
|
@@ -670,7 +926,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
670
926
|
return {
|
|
671
927
|
decision: matchedRule.verdict,
|
|
672
928
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
673
|
-
reason: matchedRule.reason
|
|
929
|
+
reason: matchedRule.reason,
|
|
930
|
+
tier: 2,
|
|
931
|
+
ruleName: matchedRule.name ?? matchedRule.tool
|
|
674
932
|
};
|
|
675
933
|
}
|
|
676
934
|
}
|
|
@@ -685,7 +943,7 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
685
943
|
pathTokens = analyzed.paths;
|
|
686
944
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
687
945
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
688
|
-
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)" };
|
|
946
|
+
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
689
947
|
}
|
|
690
948
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
691
949
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
@@ -708,7 +966,7 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
708
966
|
);
|
|
709
967
|
const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
|
|
710
968
|
if (hasSystemDisaster || isRootWipe) {
|
|
711
|
-
return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
|
|
969
|
+
return { decision: "review", blockedByLabel: "Manual Nuclear Protection", tier: 3 };
|
|
712
970
|
}
|
|
713
971
|
return { decision: "allow" };
|
|
714
972
|
}
|
|
@@ -726,14 +984,16 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
726
984
|
if (anyBlocked)
|
|
727
985
|
return {
|
|
728
986
|
decision: "review",
|
|
729
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)
|
|
987
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
|
|
988
|
+
tier: 5
|
|
730
989
|
};
|
|
731
990
|
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
732
991
|
if (allAllowed) return { decision: "allow" };
|
|
733
992
|
}
|
|
734
993
|
return {
|
|
735
994
|
decision: "review",
|
|
736
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)
|
|
995
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
|
|
996
|
+
tier: 5
|
|
737
997
|
};
|
|
738
998
|
}
|
|
739
999
|
}
|
|
@@ -775,21 +1035,22 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
775
1035
|
decision: "review",
|
|
776
1036
|
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
|
|
777
1037
|
matchedWord: matchedDangerousWord,
|
|
778
|
-
matchedField
|
|
1038
|
+
matchedField,
|
|
1039
|
+
tier: 6
|
|
779
1040
|
};
|
|
780
1041
|
}
|
|
781
1042
|
if (config.settings.mode === "strict") {
|
|
782
1043
|
const envConfig = getActiveEnvironment(config);
|
|
783
1044
|
if (envConfig?.requireApproval === false) return { decision: "allow" };
|
|
784
|
-
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)" };
|
|
1045
|
+
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)", tier: 7 };
|
|
785
1046
|
}
|
|
786
1047
|
return { decision: "allow" };
|
|
787
1048
|
}
|
|
788
1049
|
async function explainPolicy(toolName, args) {
|
|
789
1050
|
const steps = [];
|
|
790
|
-
const globalPath =
|
|
791
|
-
const projectPath =
|
|
792
|
-
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");
|
|
793
1054
|
const waterfall = [
|
|
794
1055
|
{
|
|
795
1056
|
tier: 1,
|
|
@@ -1093,7 +1354,7 @@ var DAEMON_PORT = 7391;
|
|
|
1093
1354
|
var DAEMON_HOST = "127.0.0.1";
|
|
1094
1355
|
function isDaemonRunning() {
|
|
1095
1356
|
try {
|
|
1096
|
-
const pidFile =
|
|
1357
|
+
const pidFile = path3.join(os.homedir(), ".node9", "daemon.pid");
|
|
1097
1358
|
if (!fs.existsSync(pidFile)) return false;
|
|
1098
1359
|
const { pid, port } = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
|
|
1099
1360
|
if (port !== DAEMON_PORT) return false;
|
|
@@ -1105,7 +1366,7 @@ function isDaemonRunning() {
|
|
|
1105
1366
|
}
|
|
1106
1367
|
function getPersistentDecision(toolName) {
|
|
1107
1368
|
try {
|
|
1108
|
-
const file =
|
|
1369
|
+
const file = path3.join(os.homedir(), ".node9", "decisions.json");
|
|
1109
1370
|
if (!fs.existsSync(file)) return null;
|
|
1110
1371
|
const decisions = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
1111
1372
|
const d = decisions[toolName];
|
|
@@ -1114,7 +1375,7 @@ function getPersistentDecision(toolName) {
|
|
|
1114
1375
|
}
|
|
1115
1376
|
return null;
|
|
1116
1377
|
}
|
|
1117
|
-
async function askDaemon(toolName, args, meta, signal) {
|
|
1378
|
+
async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
1118
1379
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1119
1380
|
const checkCtrl = new AbortController();
|
|
1120
1381
|
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
@@ -1124,7 +1385,13 @@ async function askDaemon(toolName, args, meta, signal) {
|
|
|
1124
1385
|
const checkRes = await fetch(`${base}/check`, {
|
|
1125
1386
|
method: "POST",
|
|
1126
1387
|
headers: { "Content-Type": "application/json" },
|
|
1127
|
-
body: JSON.stringify({
|
|
1388
|
+
body: JSON.stringify({
|
|
1389
|
+
toolName,
|
|
1390
|
+
args,
|
|
1391
|
+
agent: meta?.agent,
|
|
1392
|
+
mcpServer: meta?.mcpServer,
|
|
1393
|
+
...riskMetadata && { riskMetadata }
|
|
1394
|
+
}),
|
|
1128
1395
|
signal: checkCtrl.signal
|
|
1129
1396
|
});
|
|
1130
1397
|
if (!checkRes.ok) throw new Error("Daemon fail");
|
|
@@ -1149,7 +1416,7 @@ async function askDaemon(toolName, args, meta, signal) {
|
|
|
1149
1416
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1150
1417
|
}
|
|
1151
1418
|
}
|
|
1152
|
-
async function notifyDaemonViewer(toolName, args, meta) {
|
|
1419
|
+
async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
1153
1420
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1154
1421
|
const res = await fetch(`${base}/check`, {
|
|
1155
1422
|
method: "POST",
|
|
@@ -1159,7 +1426,8 @@ async function notifyDaemonViewer(toolName, args, meta) {
|
|
|
1159
1426
|
args,
|
|
1160
1427
|
slackDelegated: true,
|
|
1161
1428
|
agent: meta?.agent,
|
|
1162
|
-
mcpServer: meta?.mcpServer
|
|
1429
|
+
mcpServer: meta?.mcpServer,
|
|
1430
|
+
...riskMetadata && { riskMetadata }
|
|
1163
1431
|
}),
|
|
1164
1432
|
signal: AbortSignal.timeout(3e3)
|
|
1165
1433
|
});
|
|
@@ -1198,11 +1466,15 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1198
1466
|
let explainableLabel = "Local Config";
|
|
1199
1467
|
let policyMatchedField;
|
|
1200
1468
|
let policyMatchedWord;
|
|
1469
|
+
let riskMetadata;
|
|
1201
1470
|
if (config.settings.mode === "audit") {
|
|
1202
1471
|
if (!isIgnoredTool(toolName)) {
|
|
1203
1472
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
1204
1473
|
if (policyResult.decision === "review") {
|
|
1205
1474
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
1475
|
+
if (approvers.cloud && creds?.apiKey) {
|
|
1476
|
+
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
1477
|
+
}
|
|
1206
1478
|
sendDesktopNotification(
|
|
1207
1479
|
"Node9 Audit Mode",
|
|
1208
1480
|
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
@@ -1213,13 +1485,15 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1213
1485
|
}
|
|
1214
1486
|
if (!isIgnoredTool(toolName)) {
|
|
1215
1487
|
if (getActiveTrustSession(toolName)) {
|
|
1216
|
-
if (creds?.apiKey)
|
|
1488
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1489
|
+
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
1217
1490
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
1218
1491
|
return { approved: true, checkedBy: "trust" };
|
|
1219
1492
|
}
|
|
1220
1493
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
1221
1494
|
if (policyResult.decision === "allow") {
|
|
1222
|
-
if (creds?.apiKey)
|
|
1495
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1496
|
+
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
1223
1497
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
1224
1498
|
return { approved: true, checkedBy: "local-policy" };
|
|
1225
1499
|
}
|
|
@@ -1235,9 +1509,18 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1235
1509
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
1236
1510
|
policyMatchedField = policyResult.matchedField;
|
|
1237
1511
|
policyMatchedWord = policyResult.matchedWord;
|
|
1512
|
+
riskMetadata = computeRiskMetadata(
|
|
1513
|
+
args,
|
|
1514
|
+
policyResult.tier ?? 6,
|
|
1515
|
+
explainableLabel,
|
|
1516
|
+
policyMatchedField,
|
|
1517
|
+
policyMatchedWord,
|
|
1518
|
+
policyResult.ruleName
|
|
1519
|
+
);
|
|
1238
1520
|
const persistent = getPersistentDecision(toolName);
|
|
1239
1521
|
if (persistent === "allow") {
|
|
1240
|
-
if (creds?.apiKey)
|
|
1522
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1523
|
+
await auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
1241
1524
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
1242
1525
|
return { approved: true, checkedBy: "persistent" };
|
|
1243
1526
|
}
|
|
@@ -1251,7 +1534,6 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1251
1534
|
};
|
|
1252
1535
|
}
|
|
1253
1536
|
} else {
|
|
1254
|
-
if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
|
|
1255
1537
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
1256
1538
|
return { approved: true };
|
|
1257
1539
|
}
|
|
@@ -1260,8 +1542,21 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1260
1542
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
1261
1543
|
if (cloudEnforced) {
|
|
1262
1544
|
try {
|
|
1263
|
-
const initResult = await initNode9SaaS(toolName, args, creds, meta);
|
|
1545
|
+
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
1264
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
|
+
}
|
|
1265
1560
|
return {
|
|
1266
1561
|
approved: !!initResult.approved,
|
|
1267
1562
|
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
@@ -1286,18 +1581,22 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1286
1581
|
);
|
|
1287
1582
|
}
|
|
1288
1583
|
}
|
|
1289
|
-
if (
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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(`
|
|
1298
1596
|
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
1299
1597
|
`)
|
|
1300
|
-
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1301
1600
|
}
|
|
1302
1601
|
const abortController = new AbortController();
|
|
1303
1602
|
const { signal } = abortController;
|
|
@@ -1328,7 +1627,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1328
1627
|
(async () => {
|
|
1329
1628
|
try {
|
|
1330
1629
|
if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
|
|
1331
|
-
viewerId = await notifyDaemonViewer(toolName, args, meta).catch(
|
|
1630
|
+
viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
|
|
1631
|
+
() => null
|
|
1632
|
+
);
|
|
1332
1633
|
}
|
|
1333
1634
|
const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
|
|
1334
1635
|
return {
|
|
@@ -1346,7 +1647,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1346
1647
|
})()
|
|
1347
1648
|
);
|
|
1348
1649
|
}
|
|
1349
|
-
if (approvers.native && !isManual) {
|
|
1650
|
+
if (approvers.native && !isManual && !options?.calledFromDaemon) {
|
|
1350
1651
|
racePromises.push(
|
|
1351
1652
|
(async () => {
|
|
1352
1653
|
const decision = await askNativePopup(
|
|
@@ -1374,7 +1675,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1374
1675
|
})()
|
|
1375
1676
|
);
|
|
1376
1677
|
}
|
|
1377
|
-
if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
1678
|
+
if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon && !cloudEnforced) {
|
|
1378
1679
|
racePromises.push(
|
|
1379
1680
|
(async () => {
|
|
1380
1681
|
try {
|
|
@@ -1385,7 +1686,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1385
1686
|
console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
1386
1687
|
`));
|
|
1387
1688
|
}
|
|
1388
|
-
const daemonDecision = await askDaemon(toolName, args, meta, signal);
|
|
1689
|
+
const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
|
|
1389
1690
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1390
1691
|
const isApproved = daemonDecision === "allow";
|
|
1391
1692
|
return {
|
|
@@ -1510,8 +1811,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1510
1811
|
}
|
|
1511
1812
|
function getConfig() {
|
|
1512
1813
|
if (cachedConfig) return cachedConfig;
|
|
1513
|
-
const globalPath =
|
|
1514
|
-
const projectPath =
|
|
1814
|
+
const globalPath = path3.join(os.homedir(), ".node9", "config.json");
|
|
1815
|
+
const projectPath = path3.join(process.cwd(), "node9.config.json");
|
|
1515
1816
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1516
1817
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1517
1818
|
const mergedSettings = {
|
|
@@ -1531,6 +1832,7 @@ function getConfig() {
|
|
|
1531
1832
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
1532
1833
|
}
|
|
1533
1834
|
};
|
|
1835
|
+
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
1534
1836
|
const applyLayer = (source) => {
|
|
1535
1837
|
if (!source) return;
|
|
1536
1838
|
const s = source.settings || {};
|
|
@@ -1556,6 +1858,17 @@ function getConfig() {
|
|
|
1556
1858
|
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
1557
1859
|
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
1558
1860
|
}
|
|
1861
|
+
const envs = source.environments || {};
|
|
1862
|
+
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
1863
|
+
if (envConfig && typeof envConfig === "object") {
|
|
1864
|
+
const ec = envConfig;
|
|
1865
|
+
mergedEnvironments[envName] = {
|
|
1866
|
+
...mergedEnvironments[envName],
|
|
1867
|
+
// Validate field types before merging — do not blindly spread user input
|
|
1868
|
+
...typeof ec.requireApproval === "boolean" ? { requireApproval: ec.requireApproval } : {}
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1559
1872
|
};
|
|
1560
1873
|
applyLayer(globalConfig);
|
|
1561
1874
|
applyLayer(projectConfig);
|
|
@@ -1569,17 +1882,62 @@ function getConfig() {
|
|
|
1569
1882
|
cachedConfig = {
|
|
1570
1883
|
settings: mergedSettings,
|
|
1571
1884
|
policy: mergedPolicy,
|
|
1572
|
-
environments:
|
|
1885
|
+
environments: mergedEnvironments
|
|
1573
1886
|
};
|
|
1574
1887
|
return cachedConfig;
|
|
1575
1888
|
}
|
|
1576
1889
|
function tryLoadConfig(filePath) {
|
|
1577
1890
|
if (!fs.existsSync(filePath)) return null;
|
|
1891
|
+
let raw;
|
|
1578
1892
|
try {
|
|
1579
|
-
|
|
1580
|
-
} catch {
|
|
1893
|
+
raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1894
|
+
} catch (err) {
|
|
1895
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1896
|
+
process.stderr.write(
|
|
1897
|
+
`
|
|
1898
|
+
\u26A0\uFE0F Node9: Failed to parse ${filePath}
|
|
1899
|
+
${msg}
|
|
1900
|
+
\u2192 Using default config
|
|
1901
|
+
|
|
1902
|
+
`
|
|
1903
|
+
);
|
|
1581
1904
|
return null;
|
|
1582
1905
|
}
|
|
1906
|
+
const SUPPORTED_VERSION = "1.0";
|
|
1907
|
+
const SUPPORTED_MAJOR = SUPPORTED_VERSION.split(".")[0];
|
|
1908
|
+
const fileVersion = raw?.version;
|
|
1909
|
+
if (fileVersion !== void 0) {
|
|
1910
|
+
const vStr = String(fileVersion);
|
|
1911
|
+
const fileMajor = vStr.split(".")[0];
|
|
1912
|
+
if (fileMajor !== SUPPORTED_MAJOR) {
|
|
1913
|
+
process.stderr.write(
|
|
1914
|
+
`
|
|
1915
|
+
\u274C Node9: Config at ${filePath} has version "${vStr}" \u2014 major version is incompatible with this release (expected "${SUPPORTED_VERSION}"). Config will not be loaded.
|
|
1916
|
+
|
|
1917
|
+
`
|
|
1918
|
+
);
|
|
1919
|
+
return null;
|
|
1920
|
+
} else if (vStr !== SUPPORTED_VERSION) {
|
|
1921
|
+
process.stderr.write(
|
|
1922
|
+
`
|
|
1923
|
+
\u26A0\uFE0F Node9: Config at ${filePath} declares version "${vStr}" \u2014 expected "${SUPPORTED_VERSION}". Continuing with best-effort parsing.
|
|
1924
|
+
|
|
1925
|
+
`
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
const { sanitized, error } = sanitizeConfig(raw);
|
|
1930
|
+
if (error) {
|
|
1931
|
+
process.stderr.write(
|
|
1932
|
+
`
|
|
1933
|
+
\u26A0\uFE0F Node9: Invalid config at ${filePath}:
|
|
1934
|
+
${error.replace("Invalid config:\n", "")}
|
|
1935
|
+
\u2192 Invalid fields ignored, using defaults for those keys
|
|
1936
|
+
|
|
1937
|
+
`
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
return sanitized;
|
|
1583
1941
|
}
|
|
1584
1942
|
function getActiveEnvironment(config) {
|
|
1585
1943
|
const env = config.settings.environment || process.env.NODE_ENV || "development";
|
|
@@ -1594,7 +1952,7 @@ function getCredentials() {
|
|
|
1594
1952
|
};
|
|
1595
1953
|
}
|
|
1596
1954
|
try {
|
|
1597
|
-
const credPath =
|
|
1955
|
+
const credPath = path3.join(os.homedir(), ".node9", "credentials.json");
|
|
1598
1956
|
if (fs.existsSync(credPath)) {
|
|
1599
1957
|
const creds = JSON.parse(fs.readFileSync(credPath, "utf-8"));
|
|
1600
1958
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
@@ -1617,9 +1975,7 @@ function getCredentials() {
|
|
|
1617
1975
|
return null;
|
|
1618
1976
|
}
|
|
1619
1977
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
1620
|
-
|
|
1621
|
-
setTimeout(() => controller.abort(), 5e3);
|
|
1622
|
-
fetch(`${creds.apiUrl}/audit`, {
|
|
1978
|
+
return fetch(`${creds.apiUrl}/audit`, {
|
|
1623
1979
|
method: "POST",
|
|
1624
1980
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
1625
1981
|
body: JSON.stringify({
|
|
@@ -1634,11 +1990,12 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1634
1990
|
platform: os.platform()
|
|
1635
1991
|
}
|
|
1636
1992
|
}),
|
|
1637
|
-
signal:
|
|
1993
|
+
signal: AbortSignal.timeout(5e3)
|
|
1994
|
+
}).then(() => {
|
|
1638
1995
|
}).catch(() => {
|
|
1639
1996
|
});
|
|
1640
1997
|
}
|
|
1641
|
-
async function initNode9SaaS(toolName, args, creds, meta) {
|
|
1998
|
+
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
1642
1999
|
const controller = new AbortController();
|
|
1643
2000
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
1644
2001
|
try {
|
|
@@ -1654,7 +2011,8 @@ async function initNode9SaaS(toolName, args, creds, meta) {
|
|
|
1654
2011
|
hostname: os.hostname(),
|
|
1655
2012
|
cwd: process.cwd(),
|
|
1656
2013
|
platform: os.platform()
|
|
1657
|
-
}
|
|
2014
|
+
},
|
|
2015
|
+
...riskMetadata && { riskMetadata }
|
|
1658
2016
|
}),
|
|
1659
2017
|
signal: controller.signal
|
|
1660
2018
|
});
|
|
@@ -1712,7 +2070,7 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
1712
2070
|
|
|
1713
2071
|
// src/setup.ts
|
|
1714
2072
|
import fs2 from "fs";
|
|
1715
|
-
import
|
|
2073
|
+
import path4 from "path";
|
|
1716
2074
|
import os2 from "os";
|
|
1717
2075
|
import chalk3 from "chalk";
|
|
1718
2076
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
@@ -1737,14 +2095,14 @@ function readJson(filePath) {
|
|
|
1737
2095
|
return null;
|
|
1738
2096
|
}
|
|
1739
2097
|
function writeJson(filePath, data) {
|
|
1740
|
-
const dir =
|
|
2098
|
+
const dir = path4.dirname(filePath);
|
|
1741
2099
|
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
1742
2100
|
fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
1743
2101
|
}
|
|
1744
2102
|
async function setupClaude() {
|
|
1745
2103
|
const homeDir2 = os2.homedir();
|
|
1746
|
-
const mcpPath =
|
|
1747
|
-
const hooksPath =
|
|
2104
|
+
const mcpPath = path4.join(homeDir2, ".claude.json");
|
|
2105
|
+
const hooksPath = path4.join(homeDir2, ".claude", "settings.json");
|
|
1748
2106
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
1749
2107
|
const settings = readJson(hooksPath) ?? {};
|
|
1750
2108
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -1819,7 +2177,7 @@ async function setupClaude() {
|
|
|
1819
2177
|
}
|
|
1820
2178
|
async function setupGemini() {
|
|
1821
2179
|
const homeDir2 = os2.homedir();
|
|
1822
|
-
const settingsPath =
|
|
2180
|
+
const settingsPath = path4.join(homeDir2, ".gemini", "settings.json");
|
|
1823
2181
|
const settings = readJson(settingsPath) ?? {};
|
|
1824
2182
|
const servers = settings.mcpServers ?? {};
|
|
1825
2183
|
let anythingChanged = false;
|
|
@@ -1902,8 +2260,8 @@ async function setupGemini() {
|
|
|
1902
2260
|
}
|
|
1903
2261
|
async function setupCursor() {
|
|
1904
2262
|
const homeDir2 = os2.homedir();
|
|
1905
|
-
const mcpPath =
|
|
1906
|
-
const hooksPath =
|
|
2263
|
+
const mcpPath = path4.join(homeDir2, ".cursor", "mcp.json");
|
|
2264
|
+
const hooksPath = path4.join(homeDir2, ".cursor", "hooks.json");
|
|
1907
2265
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
1908
2266
|
const hooksFile = readJson(hooksPath) ?? { version: 1 };
|
|
1909
2267
|
const servers = mcpConfig.mcpServers ?? {};
|
|
@@ -2199,6 +2557,55 @@ var ui_default = `<!doctype html>
|
|
|
2199
2557
|
white-space: pre-wrap;
|
|
2200
2558
|
word-break: break-all;
|
|
2201
2559
|
}
|
|
2560
|
+
/* \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 */
|
|
2561
|
+
.sniper-header {
|
|
2562
|
+
display: flex;
|
|
2563
|
+
align-items: center;
|
|
2564
|
+
gap: 8px;
|
|
2565
|
+
flex-wrap: wrap;
|
|
2566
|
+
margin-bottom: 8px;
|
|
2567
|
+
}
|
|
2568
|
+
.sniper-badge {
|
|
2569
|
+
font-size: 11px;
|
|
2570
|
+
font-weight: 600;
|
|
2571
|
+
padding: 3px 8px;
|
|
2572
|
+
border-radius: 5px;
|
|
2573
|
+
letter-spacing: 0.02em;
|
|
2574
|
+
}
|
|
2575
|
+
.sniper-badge-edit {
|
|
2576
|
+
background: rgba(59, 130, 246, 0.15);
|
|
2577
|
+
color: #60a5fa;
|
|
2578
|
+
border: 1px solid rgba(59, 130, 246, 0.3);
|
|
2579
|
+
}
|
|
2580
|
+
.sniper-badge-exec {
|
|
2581
|
+
background: rgba(239, 68, 68, 0.12);
|
|
2582
|
+
color: #f87171;
|
|
2583
|
+
border: 1px solid rgba(239, 68, 68, 0.25);
|
|
2584
|
+
}
|
|
2585
|
+
.sniper-tier {
|
|
2586
|
+
font-size: 10px;
|
|
2587
|
+
color: var(--muted);
|
|
2588
|
+
font-family: 'Fira Code', monospace;
|
|
2589
|
+
}
|
|
2590
|
+
.sniper-filepath {
|
|
2591
|
+
font-size: 11px;
|
|
2592
|
+
color: #a8b3c4;
|
|
2593
|
+
font-family: 'Fira Code', monospace;
|
|
2594
|
+
margin-bottom: 6px;
|
|
2595
|
+
word-break: break-all;
|
|
2596
|
+
}
|
|
2597
|
+
.sniper-match {
|
|
2598
|
+
font-size: 11px;
|
|
2599
|
+
color: #a8b3c4;
|
|
2600
|
+
margin-bottom: 6px;
|
|
2601
|
+
}
|
|
2602
|
+
.sniper-match code {
|
|
2603
|
+
background: rgba(239, 68, 68, 0.15);
|
|
2604
|
+
color: #f87171;
|
|
2605
|
+
padding: 1px 5px;
|
|
2606
|
+
border-radius: 3px;
|
|
2607
|
+
font-family: 'Fira Code', monospace;
|
|
2608
|
+
}
|
|
2202
2609
|
.actions {
|
|
2203
2610
|
display: grid;
|
|
2204
2611
|
grid-template-columns: 1fr 1fr;
|
|
@@ -2705,20 +3112,47 @@ var ui_default = `<!doctype html>
|
|
|
2705
3112
|
}, 200);
|
|
2706
3113
|
}
|
|
2707
3114
|
|
|
3115
|
+
function renderPayload(req) {
|
|
3116
|
+
const rm = req.riskMetadata;
|
|
3117
|
+
if (!rm) {
|
|
3118
|
+
// Fallback: raw args for requests without context sniper data
|
|
3119
|
+
const cmd = esc(
|
|
3120
|
+
String(
|
|
3121
|
+
req.args &&
|
|
3122
|
+
(req.args.command ||
|
|
3123
|
+
req.args.cmd ||
|
|
3124
|
+
req.args.script ||
|
|
3125
|
+
JSON.stringify(req.args, null, 2))
|
|
3126
|
+
)
|
|
3127
|
+
);
|
|
3128
|
+
return \`<span class="label">Input Payload</span><pre>\${cmd}</pre>\`;
|
|
3129
|
+
}
|
|
3130
|
+
const isEdit = rm.intent === 'EDIT';
|
|
3131
|
+
const badgeClass = isEdit ? 'sniper-badge-edit' : 'sniper-badge-exec';
|
|
3132
|
+
const badgeLabel = isEdit ? '\u{1F4DD} Code Edit' : '\u{1F6D1} Execution';
|
|
3133
|
+
const tierLabel = \`Tier \${rm.tier} \xB7 \${esc(rm.blockedByLabel)}\`;
|
|
3134
|
+
const fileLine =
|
|
3135
|
+
isEdit && rm.editFilePath
|
|
3136
|
+
? \`<div class="sniper-filepath">\u{1F4C2} \${esc(rm.editFilePath)}</div>\`
|
|
3137
|
+
: !isEdit && rm.matchedWord
|
|
3138
|
+
? \`<div class="sniper-match">Matched: <code>\${esc(rm.matchedWord)}</code>\${rm.matchedField ? \` in <code>\${esc(rm.matchedField)}</code>\` : ''}</div>\`
|
|
3139
|
+
: '';
|
|
3140
|
+
const snippetHtml = rm.contextSnippet ? \`<pre>\${esc(rm.contextSnippet)}</pre>\` : '';
|
|
3141
|
+
return \`
|
|
3142
|
+
<div class="sniper-header">
|
|
3143
|
+
<span class="sniper-badge \${badgeClass}">\${badgeLabel}</span>
|
|
3144
|
+
<span class="sniper-tier">\${tierLabel}</span>
|
|
3145
|
+
</div>
|
|
3146
|
+
\${fileLine}
|
|
3147
|
+
\${snippetHtml}
|
|
3148
|
+
\`;
|
|
3149
|
+
}
|
|
3150
|
+
|
|
2708
3151
|
function addCard(req) {
|
|
2709
3152
|
if (requests.has(req.id)) return;
|
|
2710
3153
|
requests.add(req.id);
|
|
2711
3154
|
refresh();
|
|
2712
3155
|
const isSlack = !!req.slackDelegated;
|
|
2713
|
-
const cmd = esc(
|
|
2714
|
-
String(
|
|
2715
|
-
req.args &&
|
|
2716
|
-
(req.args.command ||
|
|
2717
|
-
req.args.cmd ||
|
|
2718
|
-
req.args.script ||
|
|
2719
|
-
JSON.stringify(req.args, null, 2))
|
|
2720
|
-
)
|
|
2721
|
-
);
|
|
2722
3156
|
const card = document.createElement('div');
|
|
2723
3157
|
card.className = 'card' + (isSlack ? ' slack-viewer' : '');
|
|
2724
3158
|
card.id = 'c-' + req.id;
|
|
@@ -2732,8 +3166,7 @@ var ui_default = `<!doctype html>
|
|
|
2732
3166
|
</div>
|
|
2733
3167
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
2734
3168
|
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Slack approval \u2014 view only</div>' : ''}
|
|
2735
|
-
|
|
2736
|
-
<pre>\${cmd}</pre>
|
|
3169
|
+
\${renderPayload(req)}
|
|
2737
3170
|
<div class="actions" id="act-\${req.id}">
|
|
2738
3171
|
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>Approve Execution</button>
|
|
2739
3172
|
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>Block Action</button>
|
|
@@ -2941,7 +3374,7 @@ var UI_HTML_TEMPLATE = ui_default;
|
|
|
2941
3374
|
// src/daemon/index.ts
|
|
2942
3375
|
import http from "http";
|
|
2943
3376
|
import fs3 from "fs";
|
|
2944
|
-
import
|
|
3377
|
+
import path5 from "path";
|
|
2945
3378
|
import os3 from "os";
|
|
2946
3379
|
import { spawn as spawn2 } from "child_process";
|
|
2947
3380
|
import { randomUUID } from "crypto";
|
|
@@ -2949,14 +3382,14 @@ import chalk4 from "chalk";
|
|
|
2949
3382
|
var DAEMON_PORT2 = 7391;
|
|
2950
3383
|
var DAEMON_HOST2 = "127.0.0.1";
|
|
2951
3384
|
var homeDir = os3.homedir();
|
|
2952
|
-
var DAEMON_PID_FILE =
|
|
2953
|
-
var DECISIONS_FILE =
|
|
2954
|
-
var GLOBAL_CONFIG_FILE =
|
|
2955
|
-
var CREDENTIALS_FILE =
|
|
2956
|
-
var AUDIT_LOG_FILE =
|
|
2957
|
-
var TRUST_FILE2 =
|
|
3385
|
+
var DAEMON_PID_FILE = path5.join(homeDir, ".node9", "daemon.pid");
|
|
3386
|
+
var DECISIONS_FILE = path5.join(homeDir, ".node9", "decisions.json");
|
|
3387
|
+
var GLOBAL_CONFIG_FILE = path5.join(homeDir, ".node9", "config.json");
|
|
3388
|
+
var CREDENTIALS_FILE = path5.join(homeDir, ".node9", "credentials.json");
|
|
3389
|
+
var AUDIT_LOG_FILE = path5.join(homeDir, ".node9", "audit.log");
|
|
3390
|
+
var TRUST_FILE2 = path5.join(homeDir, ".node9", "trust.json");
|
|
2958
3391
|
function atomicWriteSync2(filePath, data, options) {
|
|
2959
|
-
const dir =
|
|
3392
|
+
const dir = path5.dirname(filePath);
|
|
2960
3393
|
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
2961
3394
|
const tmpPath = `${filePath}.${randomUUID()}.tmp`;
|
|
2962
3395
|
fs3.writeFileSync(tmpPath, data, options);
|
|
@@ -3000,7 +3433,7 @@ function appendAuditLog(data) {
|
|
|
3000
3433
|
decision: data.decision,
|
|
3001
3434
|
source: "daemon"
|
|
3002
3435
|
};
|
|
3003
|
-
const dir =
|
|
3436
|
+
const dir = path5.dirname(AUDIT_LOG_FILE);
|
|
3004
3437
|
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
3005
3438
|
fs3.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
3006
3439
|
} catch {
|
|
@@ -3158,6 +3591,7 @@ data: ${JSON.stringify({
|
|
|
3158
3591
|
id: e.id,
|
|
3159
3592
|
toolName: e.toolName,
|
|
3160
3593
|
args: e.args,
|
|
3594
|
+
riskMetadata: e.riskMetadata,
|
|
3161
3595
|
slackDelegated: e.slackDelegated,
|
|
3162
3596
|
timestamp: e.timestamp,
|
|
3163
3597
|
agent: e.agent,
|
|
@@ -3183,14 +3617,23 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3183
3617
|
if (req.method === "POST" && pathname === "/check") {
|
|
3184
3618
|
try {
|
|
3185
3619
|
resetIdleTimer();
|
|
3620
|
+
_resetConfigCache();
|
|
3186
3621
|
const body = await readBody(req);
|
|
3187
3622
|
if (body.length > 65536) return res.writeHead(413).end();
|
|
3188
|
-
const {
|
|
3623
|
+
const {
|
|
3624
|
+
toolName,
|
|
3625
|
+
args,
|
|
3626
|
+
slackDelegated = false,
|
|
3627
|
+
agent,
|
|
3628
|
+
mcpServer,
|
|
3629
|
+
riskMetadata
|
|
3630
|
+
} = JSON.parse(body);
|
|
3189
3631
|
const id = randomUUID();
|
|
3190
3632
|
const entry = {
|
|
3191
3633
|
id,
|
|
3192
3634
|
toolName,
|
|
3193
3635
|
args,
|
|
3636
|
+
riskMetadata: riskMetadata ?? void 0,
|
|
3194
3637
|
agent: typeof agent === "string" ? agent : void 0,
|
|
3195
3638
|
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0,
|
|
3196
3639
|
slackDelegated: !!slackDelegated,
|
|
@@ -3222,6 +3665,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3222
3665
|
id,
|
|
3223
3666
|
toolName,
|
|
3224
3667
|
args,
|
|
3668
|
+
riskMetadata: entry.riskMetadata,
|
|
3225
3669
|
slackDelegated: entry.slackDelegated,
|
|
3226
3670
|
agent: entry.agent,
|
|
3227
3671
|
mcpServer: entry.mcpServer
|
|
@@ -3491,16 +3935,16 @@ import { execa } from "execa";
|
|
|
3491
3935
|
import chalk5 from "chalk";
|
|
3492
3936
|
import readline from "readline";
|
|
3493
3937
|
import fs5 from "fs";
|
|
3494
|
-
import
|
|
3938
|
+
import path7 from "path";
|
|
3495
3939
|
import os5 from "os";
|
|
3496
3940
|
|
|
3497
3941
|
// src/undo.ts
|
|
3498
3942
|
import { spawnSync } from "child_process";
|
|
3499
3943
|
import fs4 from "fs";
|
|
3500
|
-
import
|
|
3944
|
+
import path6 from "path";
|
|
3501
3945
|
import os4 from "os";
|
|
3502
|
-
var SNAPSHOT_STACK_PATH =
|
|
3503
|
-
var UNDO_LATEST_PATH =
|
|
3946
|
+
var SNAPSHOT_STACK_PATH = path6.join(os4.homedir(), ".node9", "snapshots.json");
|
|
3947
|
+
var UNDO_LATEST_PATH = path6.join(os4.homedir(), ".node9", "undo_latest.txt");
|
|
3504
3948
|
var MAX_SNAPSHOTS = 10;
|
|
3505
3949
|
function readStack() {
|
|
3506
3950
|
try {
|
|
@@ -3511,7 +3955,7 @@ function readStack() {
|
|
|
3511
3955
|
return [];
|
|
3512
3956
|
}
|
|
3513
3957
|
function writeStack(stack) {
|
|
3514
|
-
const dir =
|
|
3958
|
+
const dir = path6.dirname(SNAPSHOT_STACK_PATH);
|
|
3515
3959
|
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
3516
3960
|
fs4.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
3517
3961
|
}
|
|
@@ -3529,8 +3973,8 @@ function buildArgsSummary(tool, args) {
|
|
|
3529
3973
|
async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
3530
3974
|
try {
|
|
3531
3975
|
const cwd = process.cwd();
|
|
3532
|
-
if (!fs4.existsSync(
|
|
3533
|
-
const tempIndex =
|
|
3976
|
+
if (!fs4.existsSync(path6.join(cwd, ".git"))) return null;
|
|
3977
|
+
const tempIndex = path6.join(cwd, ".git", `node9_index_${Date.now()}`);
|
|
3534
3978
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
3535
3979
|
spawnSync("git", ["add", "-A"], { env });
|
|
3536
3980
|
const treeRes = spawnSync("git", ["write-tree"], { env });
|
|
@@ -3594,7 +4038,7 @@ function applyUndo(hash, cwd) {
|
|
|
3594
4038
|
const tracked = spawnSync("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
3595
4039
|
const untracked = spawnSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
3596
4040
|
for (const file of [...tracked, ...untracked]) {
|
|
3597
|
-
const fullPath =
|
|
4041
|
+
const fullPath = path6.join(dir, file);
|
|
3598
4042
|
if (!snapshotFiles.has(file) && fs4.existsSync(fullPath)) {
|
|
3599
4043
|
fs4.unlinkSync(fullPath);
|
|
3600
4044
|
}
|
|
@@ -3608,7 +4052,7 @@ function applyUndo(hash, cwd) {
|
|
|
3608
4052
|
// src/cli.ts
|
|
3609
4053
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
3610
4054
|
var { version } = JSON.parse(
|
|
3611
|
-
fs5.readFileSync(
|
|
4055
|
+
fs5.readFileSync(path7.join(__dirname, "../package.json"), "utf-8")
|
|
3612
4056
|
);
|
|
3613
4057
|
function parseDuration(str) {
|
|
3614
4058
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -3801,9 +4245,9 @@ async function runProxy(targetCommand) {
|
|
|
3801
4245
|
}
|
|
3802
4246
|
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) => {
|
|
3803
4247
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
3804
|
-
const credPath =
|
|
3805
|
-
if (!fs5.existsSync(
|
|
3806
|
-
fs5.mkdirSync(
|
|
4248
|
+
const credPath = path7.join(os5.homedir(), ".node9", "credentials.json");
|
|
4249
|
+
if (!fs5.existsSync(path7.dirname(credPath)))
|
|
4250
|
+
fs5.mkdirSync(path7.dirname(credPath), { recursive: true });
|
|
3807
4251
|
const profileName = options.profile || "default";
|
|
3808
4252
|
let existingCreds = {};
|
|
3809
4253
|
try {
|
|
@@ -3822,7 +4266,7 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3822
4266
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
3823
4267
|
fs5.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
3824
4268
|
if (profileName === "default") {
|
|
3825
|
-
const configPath =
|
|
4269
|
+
const configPath = path7.join(os5.homedir(), ".node9", "config.json");
|
|
3826
4270
|
let config = {};
|
|
3827
4271
|
try {
|
|
3828
4272
|
if (fs5.existsSync(configPath))
|
|
@@ -3837,10 +4281,12 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3837
4281
|
cloud: true,
|
|
3838
4282
|
terminal: true
|
|
3839
4283
|
};
|
|
3840
|
-
|
|
4284
|
+
if (options.local) {
|
|
4285
|
+
approvers.cloud = false;
|
|
4286
|
+
}
|
|
3841
4287
|
s.approvers = approvers;
|
|
3842
|
-
if (!fs5.existsSync(
|
|
3843
|
-
fs5.mkdirSync(
|
|
4288
|
+
if (!fs5.existsSync(path7.dirname(configPath)))
|
|
4289
|
+
fs5.mkdirSync(path7.dirname(configPath), { recursive: true });
|
|
3844
4290
|
fs5.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3845
4291
|
}
|
|
3846
4292
|
if (options.profile && profileName !== "default") {
|
|
@@ -3926,7 +4372,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3926
4372
|
);
|
|
3927
4373
|
}
|
|
3928
4374
|
section("Configuration");
|
|
3929
|
-
const globalConfigPath =
|
|
4375
|
+
const globalConfigPath = path7.join(homeDir2, ".node9", "config.json");
|
|
3930
4376
|
if (fs5.existsSync(globalConfigPath)) {
|
|
3931
4377
|
try {
|
|
3932
4378
|
JSON.parse(fs5.readFileSync(globalConfigPath, "utf-8"));
|
|
@@ -3937,7 +4383,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3937
4383
|
} else {
|
|
3938
4384
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
3939
4385
|
}
|
|
3940
|
-
const projectConfigPath =
|
|
4386
|
+
const projectConfigPath = path7.join(process.cwd(), "node9.config.json");
|
|
3941
4387
|
if (fs5.existsSync(projectConfigPath)) {
|
|
3942
4388
|
try {
|
|
3943
4389
|
JSON.parse(fs5.readFileSync(projectConfigPath, "utf-8"));
|
|
@@ -3946,7 +4392,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3946
4392
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
3947
4393
|
}
|
|
3948
4394
|
}
|
|
3949
|
-
const credsPath =
|
|
4395
|
+
const credsPath = path7.join(homeDir2, ".node9", "credentials.json");
|
|
3950
4396
|
if (fs5.existsSync(credsPath)) {
|
|
3951
4397
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
3952
4398
|
} else {
|
|
@@ -3956,7 +4402,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3956
4402
|
);
|
|
3957
4403
|
}
|
|
3958
4404
|
section("Agent Hooks");
|
|
3959
|
-
const claudeSettingsPath =
|
|
4405
|
+
const claudeSettingsPath = path7.join(homeDir2, ".claude", "settings.json");
|
|
3960
4406
|
if (fs5.existsSync(claudeSettingsPath)) {
|
|
3961
4407
|
try {
|
|
3962
4408
|
const cs = JSON.parse(fs5.readFileSync(claudeSettingsPath, "utf-8"));
|
|
@@ -3972,7 +4418,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3972
4418
|
} else {
|
|
3973
4419
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
3974
4420
|
}
|
|
3975
|
-
const geminiSettingsPath =
|
|
4421
|
+
const geminiSettingsPath = path7.join(homeDir2, ".gemini", "settings.json");
|
|
3976
4422
|
if (fs5.existsSync(geminiSettingsPath)) {
|
|
3977
4423
|
try {
|
|
3978
4424
|
const gs = JSON.parse(fs5.readFileSync(geminiSettingsPath, "utf-8"));
|
|
@@ -3988,7 +4434,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3988
4434
|
} else {
|
|
3989
4435
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
3990
4436
|
}
|
|
3991
|
-
const cursorHooksPath =
|
|
4437
|
+
const cursorHooksPath = path7.join(homeDir2, ".cursor", "hooks.json");
|
|
3992
4438
|
if (fs5.existsSync(cursorHooksPath)) {
|
|
3993
4439
|
try {
|
|
3994
4440
|
const cur = JSON.parse(fs5.readFileSync(cursorHooksPath, "utf-8"));
|
|
@@ -4093,7 +4539,7 @@ program.command("explain").description(
|
|
|
4093
4539
|
console.log("");
|
|
4094
4540
|
});
|
|
4095
4541
|
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) => {
|
|
4096
|
-
const configPath =
|
|
4542
|
+
const configPath = path7.join(os5.homedir(), ".node9", "config.json");
|
|
4097
4543
|
if (fs5.existsSync(configPath) && !options.force) {
|
|
4098
4544
|
console.log(chalk5.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
4099
4545
|
console.log(chalk5.gray(` Run with --force to overwrite.`));
|
|
@@ -4108,7 +4554,7 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
4108
4554
|
mode: safeMode
|
|
4109
4555
|
}
|
|
4110
4556
|
};
|
|
4111
|
-
const dir =
|
|
4557
|
+
const dir = path7.dirname(configPath);
|
|
4112
4558
|
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
4113
4559
|
fs5.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
4114
4560
|
console.log(chalk5.green(`\u2705 Global config created: ${configPath}`));
|
|
@@ -4128,7 +4574,7 @@ function formatRelativeTime(timestamp) {
|
|
|
4128
4574
|
return new Date(timestamp).toLocaleDateString();
|
|
4129
4575
|
}
|
|
4130
4576
|
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) => {
|
|
4131
|
-
const logPath =
|
|
4577
|
+
const logPath = path7.join(os5.homedir(), ".node9", "audit.log");
|
|
4132
4578
|
if (!fs5.existsSync(logPath)) {
|
|
4133
4579
|
console.log(
|
|
4134
4580
|
chalk5.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
@@ -4218,8 +4664,8 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4218
4664
|
console.log("");
|
|
4219
4665
|
const modeLabel = settings.mode === "audit" ? chalk5.blue("audit") : settings.mode === "strict" ? chalk5.red("strict") : chalk5.white("standard");
|
|
4220
4666
|
console.log(` Mode: ${modeLabel}`);
|
|
4221
|
-
const projectConfig =
|
|
4222
|
-
const globalConfig =
|
|
4667
|
+
const projectConfig = path7.join(process.cwd(), "node9.config.json");
|
|
4668
|
+
const globalConfig = path7.join(os5.homedir(), ".node9", "config.json");
|
|
4223
4669
|
console.log(
|
|
4224
4670
|
` Local: ${fs5.existsSync(projectConfig) ? chalk5.green("Active (node9.config.json)") : chalk5.gray("Not present")}`
|
|
4225
4671
|
);
|
|
@@ -4287,7 +4733,7 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
4287
4733
|
} catch (err) {
|
|
4288
4734
|
const tempConfig = getConfig();
|
|
4289
4735
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
4290
|
-
const logPath =
|
|
4736
|
+
const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
|
|
4291
4737
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4292
4738
|
fs5.appendFileSync(
|
|
4293
4739
|
logPath,
|
|
@@ -4307,9 +4753,9 @@ RAW: ${raw}
|
|
|
4307
4753
|
}
|
|
4308
4754
|
const config = getConfig();
|
|
4309
4755
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
4310
|
-
const logPath =
|
|
4311
|
-
if (!fs5.existsSync(
|
|
4312
|
-
fs5.mkdirSync(
|
|
4756
|
+
const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
|
|
4757
|
+
if (!fs5.existsSync(path7.dirname(logPath)))
|
|
4758
|
+
fs5.mkdirSync(path7.dirname(logPath), { recursive: true });
|
|
4313
4759
|
fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
4314
4760
|
`);
|
|
4315
4761
|
}
|
|
@@ -4381,7 +4827,7 @@ RAW: ${raw}
|
|
|
4381
4827
|
});
|
|
4382
4828
|
} catch (err) {
|
|
4383
4829
|
if (process.env.NODE9_DEBUG === "1") {
|
|
4384
|
-
const logPath =
|
|
4830
|
+
const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
|
|
4385
4831
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4386
4832
|
fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
4387
4833
|
`);
|
|
@@ -4428,9 +4874,9 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
4428
4874
|
decision: "allowed",
|
|
4429
4875
|
source: "post-hook"
|
|
4430
4876
|
};
|
|
4431
|
-
const logPath =
|
|
4432
|
-
if (!fs5.existsSync(
|
|
4433
|
-
fs5.mkdirSync(
|
|
4877
|
+
const logPath = path7.join(os5.homedir(), ".node9", "audit.log");
|
|
4878
|
+
if (!fs5.existsSync(path7.dirname(logPath)))
|
|
4879
|
+
fs5.mkdirSync(path7.dirname(logPath), { recursive: true });
|
|
4434
4880
|
fs5.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
4435
4881
|
const config = getConfig();
|
|
4436
4882
|
if (shouldSnapshot(tool, {}, config)) {
|
|
@@ -4605,7 +5051,7 @@ process.on("unhandledRejection", (reason) => {
|
|
|
4605
5051
|
const isCheckHook = process.argv[2] === "check";
|
|
4606
5052
|
if (isCheckHook) {
|
|
4607
5053
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
4608
|
-
const logPath =
|
|
5054
|
+
const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
|
|
4609
5055
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
4610
5056
|
fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
4611
5057
|
`);
|