@node9/proxy 1.0.8 → 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/dist/cli.js +551 -140
- package/dist/cli.mjs +551 -140
- package/dist/index.js +395 -72
- package/dist/index.mjs +395 -72
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -30,18 +30,18 @@ var import_commander = require("commander");
|
|
|
30
30
|
var import_chalk2 = __toESM(require("chalk"));
|
|
31
31
|
var import_prompts = require("@inquirer/prompts");
|
|
32
32
|
var import_fs = __toESM(require("fs"));
|
|
33
|
-
var
|
|
33
|
+
var import_path3 = __toESM(require("path"));
|
|
34
34
|
var import_os = __toESM(require("os"));
|
|
35
35
|
var import_picomatch = __toESM(require("picomatch"));
|
|
36
36
|
var import_sh_syntax = require("sh-syntax");
|
|
37
37
|
|
|
38
38
|
// src/ui/native.ts
|
|
39
39
|
var import_child_process = require("child_process");
|
|
40
|
-
var
|
|
40
|
+
var import_path2 = __toESM(require("path"));
|
|
41
41
|
var import_chalk = __toESM(require("chalk"));
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
|
|
43
|
+
// src/context-sniper.ts
|
|
44
|
+
var import_path = __toESM(require("path"));
|
|
45
45
|
function smartTruncate(str, maxLen = 500) {
|
|
46
46
|
if (str.length <= maxLen) return str;
|
|
47
47
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -49,11 +49,13 @@ function smartTruncate(str, maxLen = 500) {
|
|
|
49
49
|
}
|
|
50
50
|
function extractContext(text, matchedWord) {
|
|
51
51
|
const lines = text.split("\n");
|
|
52
|
-
if (lines.length <= 7 || !matchedWord)
|
|
52
|
+
if (lines.length <= 7 || !matchedWord) {
|
|
53
|
+
return { snippet: smartTruncate(text, 500), lineIndex: -1 };
|
|
54
|
+
}
|
|
53
55
|
const escaped = matchedWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
54
56
|
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
55
57
|
const allHits = lines.map((line, i) => ({ i, line })).filter(({ line }) => pattern.test(line));
|
|
56
|
-
if (allHits.length === 0) return smartTruncate(text, 500);
|
|
58
|
+
if (allHits.length === 0) return { snippet: smartTruncate(text, 500), lineIndex: -1 };
|
|
57
59
|
const nonComment = allHits.find(({ line }) => {
|
|
58
60
|
const trimmed = line.trim();
|
|
59
61
|
return !trimmed.startsWith("//") && !trimmed.startsWith("#");
|
|
@@ -61,13 +63,89 @@ function extractContext(text, matchedWord) {
|
|
|
61
63
|
const hitIndex = (nonComment ?? allHits[0]).i;
|
|
62
64
|
const start = Math.max(0, hitIndex - 3);
|
|
63
65
|
const end = Math.min(lines.length, hitIndex + 4);
|
|
66
|
+
const lineIndex = hitIndex - start;
|
|
64
67
|
const snippet = lines.slice(start, end).map((line, i) => `${start + i === hitIndex ? "\u{1F6D1} " : " "}${line}`).join("\n");
|
|
65
68
|
const head = start > 0 ? `... [${start} lines hidden] ...
|
|
66
69
|
` : "";
|
|
67
70
|
const tail = end < lines.length ? `
|
|
68
71
|
... [${lines.length - end} lines hidden] ...` : "";
|
|
69
|
-
return `${head}${snippet}${tail}
|
|
72
|
+
return { snippet: `${head}${snippet}${tail}`, lineIndex };
|
|
73
|
+
}
|
|
74
|
+
var CODE_KEYS = [
|
|
75
|
+
"command",
|
|
76
|
+
"cmd",
|
|
77
|
+
"shell_command",
|
|
78
|
+
"bash_command",
|
|
79
|
+
"script",
|
|
80
|
+
"code",
|
|
81
|
+
"input",
|
|
82
|
+
"sql",
|
|
83
|
+
"query",
|
|
84
|
+
"arguments",
|
|
85
|
+
"args",
|
|
86
|
+
"param",
|
|
87
|
+
"params",
|
|
88
|
+
"text"
|
|
89
|
+
];
|
|
90
|
+
function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
|
|
91
|
+
let intent = "EXEC";
|
|
92
|
+
let contextSnippet;
|
|
93
|
+
let contextLineIndex;
|
|
94
|
+
let editFileName;
|
|
95
|
+
let editFilePath;
|
|
96
|
+
let parsed = args;
|
|
97
|
+
if (typeof args === "string") {
|
|
98
|
+
const trimmed = args.trim();
|
|
99
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(trimmed);
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
107
|
+
const obj = parsed;
|
|
108
|
+
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
109
|
+
intent = "EDIT";
|
|
110
|
+
if (obj.file_path) {
|
|
111
|
+
editFilePath = String(obj.file_path);
|
|
112
|
+
editFileName = import_path.default.basename(editFilePath);
|
|
113
|
+
}
|
|
114
|
+
const result = extractContext(String(obj.new_string), matchedWord);
|
|
115
|
+
contextSnippet = result.snippet;
|
|
116
|
+
if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
|
|
117
|
+
} else if (matchedField && obj[matchedField] !== void 0) {
|
|
118
|
+
const result = extractContext(String(obj[matchedField]), matchedWord);
|
|
119
|
+
contextSnippet = result.snippet;
|
|
120
|
+
if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
|
|
121
|
+
} else {
|
|
122
|
+
const foundKey = Object.keys(obj).find((k) => CODE_KEYS.includes(k.toLowerCase()));
|
|
123
|
+
if (foundKey) {
|
|
124
|
+
const val = obj[foundKey];
|
|
125
|
+
contextSnippet = smartTruncate(typeof val === "string" ? val : JSON.stringify(val), 500);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else if (typeof parsed === "string") {
|
|
129
|
+
contextSnippet = smartTruncate(parsed, 500);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
intent,
|
|
133
|
+
tier,
|
|
134
|
+
blockedByLabel,
|
|
135
|
+
...matchedWord && { matchedWord },
|
|
136
|
+
...matchedField && { matchedField },
|
|
137
|
+
...contextSnippet !== void 0 && { contextSnippet },
|
|
138
|
+
...contextLineIndex !== void 0 && { contextLineIndex },
|
|
139
|
+
...editFileName && { editFileName },
|
|
140
|
+
...editFilePath && { editFilePath },
|
|
141
|
+
...ruleName && { ruleName }
|
|
142
|
+
};
|
|
70
143
|
}
|
|
144
|
+
|
|
145
|
+
// src/ui/native.ts
|
|
146
|
+
var isTestEnv = () => {
|
|
147
|
+
return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
|
|
148
|
+
};
|
|
71
149
|
function formatArgs(args, matchedField, matchedWord) {
|
|
72
150
|
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
73
151
|
let parsed = args;
|
|
@@ -86,9 +164,9 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
86
164
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
87
165
|
const obj = parsed;
|
|
88
166
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
89
|
-
const file = obj.file_path ?
|
|
167
|
+
const file = obj.file_path ? import_path2.default.basename(String(obj.file_path)) : "file";
|
|
90
168
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
91
|
-
const newPreview = extractContext(String(obj.new_string), matchedWord);
|
|
169
|
+
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
92
170
|
return {
|
|
93
171
|
intent: "EDIT",
|
|
94
172
|
message: `\u{1F4DD} EDITING: ${file}
|
|
@@ -106,7 +184,7 @@ ${newPreview}`
|
|
|
106
184
|
const context = otherKeys.length > 0 ? `\u2699\uFE0F Context: ${otherKeys.map((k) => `${k}=${smartTruncate(typeof obj[k] === "object" ? JSON.stringify(obj[k]) : String(obj[k]), 30)}`).join(", ")}
|
|
107
185
|
|
|
108
186
|
` : "";
|
|
109
|
-
const content = extractContext(String(obj[matchedField]), matchedWord);
|
|
187
|
+
const content = extractContext(String(obj[matchedField]), matchedWord).snippet;
|
|
110
188
|
return {
|
|
111
189
|
intent: "EXEC",
|
|
112
190
|
message: `${context}\u{1F6D1} [${matchedField.toUpperCase()}]:
|
|
@@ -278,11 +356,113 @@ end run`;
|
|
|
278
356
|
});
|
|
279
357
|
}
|
|
280
358
|
|
|
359
|
+
// src/config-schema.ts
|
|
360
|
+
var import_zod = require("zod");
|
|
361
|
+
var noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
362
|
+
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
363
|
+
});
|
|
364
|
+
var validRegex = noNewlines.refine(
|
|
365
|
+
(s) => {
|
|
366
|
+
try {
|
|
367
|
+
new RegExp(s);
|
|
368
|
+
return true;
|
|
369
|
+
} catch {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
{ message: "Value must be a valid regular expression" }
|
|
374
|
+
);
|
|
375
|
+
var SmartConditionSchema = import_zod.z.object({
|
|
376
|
+
field: import_zod.z.string().min(1, "Condition field must not be empty"),
|
|
377
|
+
op: import_zod.z.enum(["matches", "notMatches", "contains", "notContains", "exists", "notExists"], {
|
|
378
|
+
errorMap: () => ({
|
|
379
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists"
|
|
380
|
+
})
|
|
381
|
+
}),
|
|
382
|
+
value: validRegex.optional(),
|
|
383
|
+
flags: import_zod.z.string().optional()
|
|
384
|
+
});
|
|
385
|
+
var SmartRuleSchema = import_zod.z.object({
|
|
386
|
+
name: import_zod.z.string().optional(),
|
|
387
|
+
tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
|
|
388
|
+
conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
389
|
+
conditionMode: import_zod.z.enum(["all", "any"]).optional(),
|
|
390
|
+
verdict: import_zod.z.enum(["allow", "review", "block"], {
|
|
391
|
+
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
392
|
+
}),
|
|
393
|
+
reason: import_zod.z.string().optional()
|
|
394
|
+
});
|
|
395
|
+
var PolicyRuleSchema = import_zod.z.object({
|
|
396
|
+
action: import_zod.z.string().min(1),
|
|
397
|
+
allowPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
398
|
+
blockPaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
399
|
+
});
|
|
400
|
+
var ConfigFileSchema = import_zod.z.object({
|
|
401
|
+
version: import_zod.z.string().optional(),
|
|
402
|
+
settings: import_zod.z.object({
|
|
403
|
+
mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
|
|
404
|
+
autoStartDaemon: import_zod.z.boolean().optional(),
|
|
405
|
+
enableUndo: import_zod.z.boolean().optional(),
|
|
406
|
+
enableHookLogDebug: import_zod.z.boolean().optional(),
|
|
407
|
+
approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
|
|
408
|
+
approvers: import_zod.z.object({
|
|
409
|
+
native: import_zod.z.boolean().optional(),
|
|
410
|
+
browser: import_zod.z.boolean().optional(),
|
|
411
|
+
cloud: import_zod.z.boolean().optional(),
|
|
412
|
+
terminal: import_zod.z.boolean().optional()
|
|
413
|
+
}).optional(),
|
|
414
|
+
environment: import_zod.z.string().optional(),
|
|
415
|
+
slackEnabled: import_zod.z.boolean().optional(),
|
|
416
|
+
enableTrustSessions: import_zod.z.boolean().optional(),
|
|
417
|
+
allowGlobalPause: import_zod.z.boolean().optional()
|
|
418
|
+
}).optional(),
|
|
419
|
+
policy: import_zod.z.object({
|
|
420
|
+
sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
421
|
+
dangerousWords: import_zod.z.array(noNewlines).optional(),
|
|
422
|
+
ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
423
|
+
toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
|
|
424
|
+
rules: import_zod.z.array(PolicyRuleSchema).optional(),
|
|
425
|
+
smartRules: import_zod.z.array(SmartRuleSchema).optional(),
|
|
426
|
+
snapshot: import_zod.z.object({
|
|
427
|
+
tools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
428
|
+
onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
429
|
+
ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
430
|
+
}).optional()
|
|
431
|
+
}).optional(),
|
|
432
|
+
environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
|
|
433
|
+
}).strict({ message: "Config contains unknown top-level keys" });
|
|
434
|
+
function sanitizeConfig(raw) {
|
|
435
|
+
const result = ConfigFileSchema.safeParse(raw);
|
|
436
|
+
if (result.success) {
|
|
437
|
+
return { sanitized: result.data, error: null };
|
|
438
|
+
}
|
|
439
|
+
const invalidTopLevelKeys = new Set(
|
|
440
|
+
result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
|
|
441
|
+
);
|
|
442
|
+
const sanitized = {};
|
|
443
|
+
if (typeof raw === "object" && raw !== null) {
|
|
444
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
445
|
+
if (!invalidTopLevelKeys.has(key)) {
|
|
446
|
+
sanitized[key] = value;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const lines = result.error.issues.map((issue) => {
|
|
451
|
+
const path8 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
452
|
+
return ` \u2022 ${path8}: ${issue.message}`;
|
|
453
|
+
});
|
|
454
|
+
return {
|
|
455
|
+
sanitized,
|
|
456
|
+
error: `Invalid config:
|
|
457
|
+
${lines.join("\n")}`
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
281
461
|
// src/core.ts
|
|
282
|
-
var PAUSED_FILE =
|
|
283
|
-
var TRUST_FILE =
|
|
284
|
-
var LOCAL_AUDIT_LOG =
|
|
285
|
-
var HOOK_DEBUG_LOG =
|
|
462
|
+
var PAUSED_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "PAUSED");
|
|
463
|
+
var TRUST_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "trust.json");
|
|
464
|
+
var LOCAL_AUDIT_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
465
|
+
var HOOK_DEBUG_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
|
|
286
466
|
function checkPause() {
|
|
287
467
|
try {
|
|
288
468
|
if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -300,7 +480,7 @@ function checkPause() {
|
|
|
300
480
|
}
|
|
301
481
|
}
|
|
302
482
|
function atomicWriteSync(filePath, data, options) {
|
|
303
|
-
const dir =
|
|
483
|
+
const dir = import_path3.default.dirname(filePath);
|
|
304
484
|
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
305
485
|
const tmpPath = `${filePath}.${import_os.default.hostname()}.${process.pid}.tmp`;
|
|
306
486
|
import_fs.default.writeFileSync(tmpPath, data, options);
|
|
@@ -351,7 +531,7 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
351
531
|
}
|
|
352
532
|
function appendToLog(logPath, entry) {
|
|
353
533
|
try {
|
|
354
|
-
const dir =
|
|
534
|
+
const dir = import_path3.default.dirname(logPath);
|
|
355
535
|
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
356
536
|
import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
357
537
|
} catch {
|
|
@@ -395,9 +575,9 @@ function matchesPattern(text, patterns) {
|
|
|
395
575
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
396
576
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
397
577
|
}
|
|
398
|
-
function getNestedValue(obj,
|
|
578
|
+
function getNestedValue(obj, path8) {
|
|
399
579
|
if (!obj || typeof obj !== "object") return null;
|
|
400
|
-
return
|
|
580
|
+
return path8.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
401
581
|
}
|
|
402
582
|
function shouldSnapshot(toolName, args, config) {
|
|
403
583
|
if (!config.settings.enableUndo) return false;
|
|
@@ -546,15 +726,10 @@ function redactSecrets(text) {
|
|
|
546
726
|
return redacted;
|
|
547
727
|
}
|
|
548
728
|
var DANGEROUS_WORDS = [
|
|
549
|
-
"
|
|
550
|
-
|
|
551
|
-
"
|
|
552
|
-
|
|
553
|
-
"destroy",
|
|
554
|
-
"terminate",
|
|
555
|
-
"revoke",
|
|
556
|
-
"docker",
|
|
557
|
-
"psql"
|
|
729
|
+
"mkfs",
|
|
730
|
+
// formats/wipes a filesystem partition
|
|
731
|
+
"shred"
|
|
732
|
+
// permanently overwrites file contents (unrecoverable)
|
|
558
733
|
];
|
|
559
734
|
var DEFAULT_CONFIG = {
|
|
560
735
|
settings: {
|
|
@@ -611,6 +786,8 @@ var DEFAULT_CONFIG = {
|
|
|
611
786
|
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
612
787
|
},
|
|
613
788
|
rules: [
|
|
789
|
+
// Only use the legacy rules format for simple path-based rm control.
|
|
790
|
+
// All other command-level enforcement lives in smartRules below.
|
|
614
791
|
{
|
|
615
792
|
action: "rm",
|
|
616
793
|
allowPaths: [
|
|
@@ -627,6 +804,7 @@ var DEFAULT_CONFIG = {
|
|
|
627
804
|
}
|
|
628
805
|
],
|
|
629
806
|
smartRules: [
|
|
807
|
+
// ── SQL safety ────────────────────────────────────────────────────────
|
|
630
808
|
{
|
|
631
809
|
name: "no-delete-without-where",
|
|
632
810
|
tool: "*",
|
|
@@ -637,6 +815,84 @@ var DEFAULT_CONFIG = {
|
|
|
637
815
|
conditionMode: "all",
|
|
638
816
|
verdict: "review",
|
|
639
817
|
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
name: "review-drop-truncate-shell",
|
|
821
|
+
tool: "bash",
|
|
822
|
+
conditions: [
|
|
823
|
+
{
|
|
824
|
+
field: "command",
|
|
825
|
+
op: "matches",
|
|
826
|
+
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
827
|
+
flags: "i"
|
|
828
|
+
}
|
|
829
|
+
],
|
|
830
|
+
conditionMode: "all",
|
|
831
|
+
verdict: "review",
|
|
832
|
+
reason: "SQL DDL destructive statement inside a shell command"
|
|
833
|
+
},
|
|
834
|
+
// ── Git safety ────────────────────────────────────────────────────────
|
|
835
|
+
{
|
|
836
|
+
name: "block-force-push",
|
|
837
|
+
tool: "bash",
|
|
838
|
+
conditions: [
|
|
839
|
+
{
|
|
840
|
+
field: "command",
|
|
841
|
+
op: "matches",
|
|
842
|
+
value: "git push.*(--force|--force-with-lease|-f\\b)",
|
|
843
|
+
flags: "i"
|
|
844
|
+
}
|
|
845
|
+
],
|
|
846
|
+
conditionMode: "all",
|
|
847
|
+
verdict: "block",
|
|
848
|
+
reason: "Force push overwrites remote history and cannot be undone"
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
name: "review-git-push",
|
|
852
|
+
tool: "bash",
|
|
853
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*git push\\b", flags: "i" }],
|
|
854
|
+
conditionMode: "all",
|
|
855
|
+
verdict: "review",
|
|
856
|
+
reason: "git push sends changes to a shared remote"
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
name: "review-git-destructive",
|
|
860
|
+
tool: "bash",
|
|
861
|
+
conditions: [
|
|
862
|
+
{
|
|
863
|
+
field: "command",
|
|
864
|
+
op: "matches",
|
|
865
|
+
value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
|
|
866
|
+
flags: "i"
|
|
867
|
+
}
|
|
868
|
+
],
|
|
869
|
+
conditionMode: "all",
|
|
870
|
+
verdict: "review",
|
|
871
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
872
|
+
},
|
|
873
|
+
// ── Shell safety ──────────────────────────────────────────────────────
|
|
874
|
+
{
|
|
875
|
+
name: "review-sudo",
|
|
876
|
+
tool: "bash",
|
|
877
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
|
|
878
|
+
conditionMode: "all",
|
|
879
|
+
verdict: "review",
|
|
880
|
+
reason: "Command requires elevated privileges"
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
name: "review-curl-pipe-shell",
|
|
884
|
+
tool: "bash",
|
|
885
|
+
conditions: [
|
|
886
|
+
{
|
|
887
|
+
field: "command",
|
|
888
|
+
op: "matches",
|
|
889
|
+
value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
890
|
+
flags: "i"
|
|
891
|
+
}
|
|
892
|
+
],
|
|
893
|
+
conditionMode: "all",
|
|
894
|
+
verdict: "block",
|
|
895
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
640
896
|
}
|
|
641
897
|
]
|
|
642
898
|
},
|
|
@@ -648,7 +904,7 @@ function _resetConfigCache() {
|
|
|
648
904
|
}
|
|
649
905
|
function getGlobalSettings() {
|
|
650
906
|
try {
|
|
651
|
-
const globalConfigPath =
|
|
907
|
+
const globalConfigPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
|
|
652
908
|
if (import_fs.default.existsSync(globalConfigPath)) {
|
|
653
909
|
const parsed = JSON.parse(import_fs.default.readFileSync(globalConfigPath, "utf-8"));
|
|
654
910
|
const settings = parsed.settings || {};
|
|
@@ -672,7 +928,7 @@ function getGlobalSettings() {
|
|
|
672
928
|
}
|
|
673
929
|
function getInternalToken() {
|
|
674
930
|
try {
|
|
675
|
-
const pidFile =
|
|
931
|
+
const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
|
|
676
932
|
if (!import_fs.default.existsSync(pidFile)) return null;
|
|
677
933
|
const data = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
|
|
678
934
|
process.kill(data.pid, 0);
|
|
@@ -693,7 +949,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
693
949
|
return {
|
|
694
950
|
decision: matchedRule.verdict,
|
|
695
951
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
696
|
-
reason: matchedRule.reason
|
|
952
|
+
reason: matchedRule.reason,
|
|
953
|
+
tier: 2,
|
|
954
|
+
ruleName: matchedRule.name ?? matchedRule.tool
|
|
697
955
|
};
|
|
698
956
|
}
|
|
699
957
|
}
|
|
@@ -708,7 +966,7 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
708
966
|
pathTokens = analyzed.paths;
|
|
709
967
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
710
968
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
711
|
-
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)" };
|
|
969
|
+
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
712
970
|
}
|
|
713
971
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
714
972
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
@@ -731,7 +989,7 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
731
989
|
);
|
|
732
990
|
const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
|
|
733
991
|
if (hasSystemDisaster || isRootWipe) {
|
|
734
|
-
return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
|
|
992
|
+
return { decision: "review", blockedByLabel: "Manual Nuclear Protection", tier: 3 };
|
|
735
993
|
}
|
|
736
994
|
return { decision: "allow" };
|
|
737
995
|
}
|
|
@@ -749,14 +1007,16 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
749
1007
|
if (anyBlocked)
|
|
750
1008
|
return {
|
|
751
1009
|
decision: "review",
|
|
752
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)
|
|
1010
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
|
|
1011
|
+
tier: 5
|
|
753
1012
|
};
|
|
754
1013
|
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
755
1014
|
if (allAllowed) return { decision: "allow" };
|
|
756
1015
|
}
|
|
757
1016
|
return {
|
|
758
1017
|
decision: "review",
|
|
759
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)
|
|
1018
|
+
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
|
|
1019
|
+
tier: 5
|
|
760
1020
|
};
|
|
761
1021
|
}
|
|
762
1022
|
}
|
|
@@ -798,21 +1058,22 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
798
1058
|
decision: "review",
|
|
799
1059
|
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
|
|
800
1060
|
matchedWord: matchedDangerousWord,
|
|
801
|
-
matchedField
|
|
1061
|
+
matchedField,
|
|
1062
|
+
tier: 6
|
|
802
1063
|
};
|
|
803
1064
|
}
|
|
804
1065
|
if (config.settings.mode === "strict") {
|
|
805
1066
|
const envConfig = getActiveEnvironment(config);
|
|
806
1067
|
if (envConfig?.requireApproval === false) return { decision: "allow" };
|
|
807
|
-
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)" };
|
|
1068
|
+
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)", tier: 7 };
|
|
808
1069
|
}
|
|
809
1070
|
return { decision: "allow" };
|
|
810
1071
|
}
|
|
811
1072
|
async function explainPolicy(toolName, args) {
|
|
812
1073
|
const steps = [];
|
|
813
|
-
const globalPath =
|
|
814
|
-
const projectPath =
|
|
815
|
-
const credsPath =
|
|
1074
|
+
const globalPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
|
|
1075
|
+
const projectPath = import_path3.default.join(process.cwd(), "node9.config.json");
|
|
1076
|
+
const credsPath = import_path3.default.join(import_os.default.homedir(), ".node9", "credentials.json");
|
|
816
1077
|
const waterfall = [
|
|
817
1078
|
{
|
|
818
1079
|
tier: 1,
|
|
@@ -1116,7 +1377,7 @@ var DAEMON_PORT = 7391;
|
|
|
1116
1377
|
var DAEMON_HOST = "127.0.0.1";
|
|
1117
1378
|
function isDaemonRunning() {
|
|
1118
1379
|
try {
|
|
1119
|
-
const pidFile =
|
|
1380
|
+
const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
|
|
1120
1381
|
if (!import_fs.default.existsSync(pidFile)) return false;
|
|
1121
1382
|
const { pid, port } = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
|
|
1122
1383
|
if (port !== DAEMON_PORT) return false;
|
|
@@ -1128,7 +1389,7 @@ function isDaemonRunning() {
|
|
|
1128
1389
|
}
|
|
1129
1390
|
function getPersistentDecision(toolName) {
|
|
1130
1391
|
try {
|
|
1131
|
-
const file =
|
|
1392
|
+
const file = import_path3.default.join(import_os.default.homedir(), ".node9", "decisions.json");
|
|
1132
1393
|
if (!import_fs.default.existsSync(file)) return null;
|
|
1133
1394
|
const decisions = JSON.parse(import_fs.default.readFileSync(file, "utf-8"));
|
|
1134
1395
|
const d = decisions[toolName];
|
|
@@ -1137,7 +1398,7 @@ function getPersistentDecision(toolName) {
|
|
|
1137
1398
|
}
|
|
1138
1399
|
return null;
|
|
1139
1400
|
}
|
|
1140
|
-
async function askDaemon(toolName, args, meta, signal) {
|
|
1401
|
+
async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
1141
1402
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1142
1403
|
const checkCtrl = new AbortController();
|
|
1143
1404
|
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
@@ -1147,7 +1408,13 @@ async function askDaemon(toolName, args, meta, signal) {
|
|
|
1147
1408
|
const checkRes = await fetch(`${base}/check`, {
|
|
1148
1409
|
method: "POST",
|
|
1149
1410
|
headers: { "Content-Type": "application/json" },
|
|
1150
|
-
body: JSON.stringify({
|
|
1411
|
+
body: JSON.stringify({
|
|
1412
|
+
toolName,
|
|
1413
|
+
args,
|
|
1414
|
+
agent: meta?.agent,
|
|
1415
|
+
mcpServer: meta?.mcpServer,
|
|
1416
|
+
...riskMetadata && { riskMetadata }
|
|
1417
|
+
}),
|
|
1151
1418
|
signal: checkCtrl.signal
|
|
1152
1419
|
});
|
|
1153
1420
|
if (!checkRes.ok) throw new Error("Daemon fail");
|
|
@@ -1172,7 +1439,7 @@ async function askDaemon(toolName, args, meta, signal) {
|
|
|
1172
1439
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1173
1440
|
}
|
|
1174
1441
|
}
|
|
1175
|
-
async function notifyDaemonViewer(toolName, args, meta) {
|
|
1442
|
+
async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
1176
1443
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1177
1444
|
const res = await fetch(`${base}/check`, {
|
|
1178
1445
|
method: "POST",
|
|
@@ -1182,7 +1449,8 @@ async function notifyDaemonViewer(toolName, args, meta) {
|
|
|
1182
1449
|
args,
|
|
1183
1450
|
slackDelegated: true,
|
|
1184
1451
|
agent: meta?.agent,
|
|
1185
|
-
mcpServer: meta?.mcpServer
|
|
1452
|
+
mcpServer: meta?.mcpServer,
|
|
1453
|
+
...riskMetadata && { riskMetadata }
|
|
1186
1454
|
}),
|
|
1187
1455
|
signal: AbortSignal.timeout(3e3)
|
|
1188
1456
|
});
|
|
@@ -1221,11 +1489,15 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1221
1489
|
let explainableLabel = "Local Config";
|
|
1222
1490
|
let policyMatchedField;
|
|
1223
1491
|
let policyMatchedWord;
|
|
1492
|
+
let riskMetadata;
|
|
1224
1493
|
if (config.settings.mode === "audit") {
|
|
1225
1494
|
if (!isIgnoredTool(toolName)) {
|
|
1226
1495
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
1227
1496
|
if (policyResult.decision === "review") {
|
|
1228
1497
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
1498
|
+
if (approvers.cloud && creds?.apiKey) {
|
|
1499
|
+
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
1500
|
+
}
|
|
1229
1501
|
sendDesktopNotification(
|
|
1230
1502
|
"Node9 Audit Mode",
|
|
1231
1503
|
`Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
|
|
@@ -1236,13 +1508,15 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1236
1508
|
}
|
|
1237
1509
|
if (!isIgnoredTool(toolName)) {
|
|
1238
1510
|
if (getActiveTrustSession(toolName)) {
|
|
1239
|
-
if (creds?.apiKey)
|
|
1511
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1512
|
+
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
1240
1513
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
1241
1514
|
return { approved: true, checkedBy: "trust" };
|
|
1242
1515
|
}
|
|
1243
1516
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
1244
1517
|
if (policyResult.decision === "allow") {
|
|
1245
|
-
if (creds?.apiKey)
|
|
1518
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1519
|
+
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
1246
1520
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
1247
1521
|
return { approved: true, checkedBy: "local-policy" };
|
|
1248
1522
|
}
|
|
@@ -1258,9 +1532,18 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1258
1532
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
1259
1533
|
policyMatchedField = policyResult.matchedField;
|
|
1260
1534
|
policyMatchedWord = policyResult.matchedWord;
|
|
1535
|
+
riskMetadata = computeRiskMetadata(
|
|
1536
|
+
args,
|
|
1537
|
+
policyResult.tier ?? 6,
|
|
1538
|
+
explainableLabel,
|
|
1539
|
+
policyMatchedField,
|
|
1540
|
+
policyMatchedWord,
|
|
1541
|
+
policyResult.ruleName
|
|
1542
|
+
);
|
|
1261
1543
|
const persistent = getPersistentDecision(toolName);
|
|
1262
1544
|
if (persistent === "allow") {
|
|
1263
|
-
if (creds?.apiKey)
|
|
1545
|
+
if (approvers.cloud && creds?.apiKey)
|
|
1546
|
+
await auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
1264
1547
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
1265
1548
|
return { approved: true, checkedBy: "persistent" };
|
|
1266
1549
|
}
|
|
@@ -1274,7 +1557,6 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1274
1557
|
};
|
|
1275
1558
|
}
|
|
1276
1559
|
} else {
|
|
1277
|
-
if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
|
|
1278
1560
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
1279
1561
|
return { approved: true };
|
|
1280
1562
|
}
|
|
@@ -1283,8 +1565,21 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1283
1565
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
1284
1566
|
if (cloudEnforced) {
|
|
1285
1567
|
try {
|
|
1286
|
-
const initResult = await initNode9SaaS(toolName, args, creds, meta);
|
|
1568
|
+
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
1287
1569
|
if (!initResult.pending) {
|
|
1570
|
+
if (initResult.shadowMode) {
|
|
1571
|
+
console.error(
|
|
1572
|
+
import_chalk2.default.yellow(
|
|
1573
|
+
`
|
|
1574
|
+
\u26A0\uFE0F Node9 Shadow Mode: Action allowed, but would have been blocked by company policy.`
|
|
1575
|
+
)
|
|
1576
|
+
);
|
|
1577
|
+
if (initResult.shadowReason) {
|
|
1578
|
+
console.error(import_chalk2.default.dim(` Reason: ${initResult.shadowReason}
|
|
1579
|
+
`));
|
|
1580
|
+
}
|
|
1581
|
+
return { approved: true, checkedBy: "cloud" };
|
|
1582
|
+
}
|
|
1288
1583
|
return {
|
|
1289
1584
|
approved: !!initResult.approved,
|
|
1290
1585
|
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
@@ -1309,18 +1604,22 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1309
1604
|
);
|
|
1310
1605
|
}
|
|
1311
1606
|
}
|
|
1312
|
-
if (
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1607
|
+
if (!options?.calledFromDaemon) {
|
|
1608
|
+
if (cloudEnforced && cloudRequestId) {
|
|
1609
|
+
console.error(
|
|
1610
|
+
import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
|
|
1611
|
+
);
|
|
1612
|
+
console.error(
|
|
1613
|
+
import_chalk2.default.cyan(" Dashboard \u2192 ") + import_chalk2.default.bold("Mission Control > Activity Feed\n")
|
|
1614
|
+
);
|
|
1615
|
+
} else if (!cloudEnforced) {
|
|
1616
|
+
const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
|
|
1617
|
+
console.error(
|
|
1618
|
+
import_chalk2.default.dim(`
|
|
1321
1619
|
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
1322
1620
|
`)
|
|
1323
|
-
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1324
1623
|
}
|
|
1325
1624
|
const abortController = new AbortController();
|
|
1326
1625
|
const { signal } = abortController;
|
|
@@ -1351,7 +1650,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1351
1650
|
(async () => {
|
|
1352
1651
|
try {
|
|
1353
1652
|
if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
|
|
1354
|
-
viewerId = await notifyDaemonViewer(toolName, args, meta).catch(
|
|
1653
|
+
viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
|
|
1654
|
+
() => null
|
|
1655
|
+
);
|
|
1355
1656
|
}
|
|
1356
1657
|
const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
|
|
1357
1658
|
return {
|
|
@@ -1369,7 +1670,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1369
1670
|
})()
|
|
1370
1671
|
);
|
|
1371
1672
|
}
|
|
1372
|
-
if (approvers.native && !isManual) {
|
|
1673
|
+
if (approvers.native && !isManual && !options?.calledFromDaemon) {
|
|
1373
1674
|
racePromises.push(
|
|
1374
1675
|
(async () => {
|
|
1375
1676
|
const decision = await askNativePopup(
|
|
@@ -1397,7 +1698,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1397
1698
|
})()
|
|
1398
1699
|
);
|
|
1399
1700
|
}
|
|
1400
|
-
if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
1701
|
+
if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon && !cloudEnforced) {
|
|
1401
1702
|
racePromises.push(
|
|
1402
1703
|
(async () => {
|
|
1403
1704
|
try {
|
|
@@ -1408,7 +1709,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1408
1709
|
console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
1409
1710
|
`));
|
|
1410
1711
|
}
|
|
1411
|
-
const daemonDecision = await askDaemon(toolName, args, meta, signal);
|
|
1712
|
+
const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
|
|
1412
1713
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1413
1714
|
const isApproved = daemonDecision === "allow";
|
|
1414
1715
|
return {
|
|
@@ -1533,8 +1834,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1533
1834
|
}
|
|
1534
1835
|
function getConfig() {
|
|
1535
1836
|
if (cachedConfig) return cachedConfig;
|
|
1536
|
-
const globalPath =
|
|
1537
|
-
const projectPath =
|
|
1837
|
+
const globalPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
|
|
1838
|
+
const projectPath = import_path3.default.join(process.cwd(), "node9.config.json");
|
|
1538
1839
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1539
1840
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1540
1841
|
const mergedSettings = {
|
|
@@ -1598,11 +1899,33 @@ function getConfig() {
|
|
|
1598
1899
|
}
|
|
1599
1900
|
function tryLoadConfig(filePath) {
|
|
1600
1901
|
if (!import_fs.default.existsSync(filePath)) return null;
|
|
1902
|
+
let raw;
|
|
1601
1903
|
try {
|
|
1602
|
-
|
|
1603
|
-
} catch {
|
|
1904
|
+
raw = JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
|
|
1905
|
+
} catch (err) {
|
|
1906
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1907
|
+
process.stderr.write(
|
|
1908
|
+
`
|
|
1909
|
+
\u26A0\uFE0F Node9: Failed to parse ${filePath}
|
|
1910
|
+
${msg}
|
|
1911
|
+
\u2192 Using default config
|
|
1912
|
+
|
|
1913
|
+
`
|
|
1914
|
+
);
|
|
1604
1915
|
return null;
|
|
1605
1916
|
}
|
|
1917
|
+
const { sanitized, error } = sanitizeConfig(raw);
|
|
1918
|
+
if (error) {
|
|
1919
|
+
process.stderr.write(
|
|
1920
|
+
`
|
|
1921
|
+
\u26A0\uFE0F Node9: Invalid config at ${filePath}:
|
|
1922
|
+
${error.replace("Invalid config:\n", "")}
|
|
1923
|
+
\u2192 Invalid fields ignored, using defaults for those keys
|
|
1924
|
+
|
|
1925
|
+
`
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
return sanitized;
|
|
1606
1929
|
}
|
|
1607
1930
|
function getActiveEnvironment(config) {
|
|
1608
1931
|
const env = config.settings.environment || process.env.NODE_ENV || "development";
|
|
@@ -1617,7 +1940,7 @@ function getCredentials() {
|
|
|
1617
1940
|
};
|
|
1618
1941
|
}
|
|
1619
1942
|
try {
|
|
1620
|
-
const credPath =
|
|
1943
|
+
const credPath = import_path3.default.join(import_os.default.homedir(), ".node9", "credentials.json");
|
|
1621
1944
|
if (import_fs.default.existsSync(credPath)) {
|
|
1622
1945
|
const creds = JSON.parse(import_fs.default.readFileSync(credPath, "utf-8"));
|
|
1623
1946
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
@@ -1640,9 +1963,7 @@ function getCredentials() {
|
|
|
1640
1963
|
return null;
|
|
1641
1964
|
}
|
|
1642
1965
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
1643
|
-
|
|
1644
|
-
setTimeout(() => controller.abort(), 5e3);
|
|
1645
|
-
fetch(`${creds.apiUrl}/audit`, {
|
|
1966
|
+
return fetch(`${creds.apiUrl}/audit`, {
|
|
1646
1967
|
method: "POST",
|
|
1647
1968
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
1648
1969
|
body: JSON.stringify({
|
|
@@ -1657,11 +1978,12 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1657
1978
|
platform: import_os.default.platform()
|
|
1658
1979
|
}
|
|
1659
1980
|
}),
|
|
1660
|
-
signal:
|
|
1981
|
+
signal: AbortSignal.timeout(5e3)
|
|
1982
|
+
}).then(() => {
|
|
1661
1983
|
}).catch(() => {
|
|
1662
1984
|
});
|
|
1663
1985
|
}
|
|
1664
|
-
async function initNode9SaaS(toolName, args, creds, meta) {
|
|
1986
|
+
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
1665
1987
|
const controller = new AbortController();
|
|
1666
1988
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
1667
1989
|
try {
|
|
@@ -1677,7 +1999,8 @@ async function initNode9SaaS(toolName, args, creds, meta) {
|
|
|
1677
1999
|
hostname: import_os.default.hostname(),
|
|
1678
2000
|
cwd: process.cwd(),
|
|
1679
2001
|
platform: import_os.default.platform()
|
|
1680
|
-
}
|
|
2002
|
+
},
|
|
2003
|
+
...riskMetadata && { riskMetadata }
|
|
1681
2004
|
}),
|
|
1682
2005
|
signal: controller.signal
|
|
1683
2006
|
});
|
|
@@ -1735,7 +2058,7 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
1735
2058
|
|
|
1736
2059
|
// src/setup.ts
|
|
1737
2060
|
var import_fs2 = __toESM(require("fs"));
|
|
1738
|
-
var
|
|
2061
|
+
var import_path4 = __toESM(require("path"));
|
|
1739
2062
|
var import_os2 = __toESM(require("os"));
|
|
1740
2063
|
var import_chalk3 = __toESM(require("chalk"));
|
|
1741
2064
|
var import_prompts2 = require("@inquirer/prompts");
|
|
@@ -1760,14 +2083,14 @@ function readJson(filePath) {
|
|
|
1760
2083
|
return null;
|
|
1761
2084
|
}
|
|
1762
2085
|
function writeJson(filePath, data) {
|
|
1763
|
-
const dir =
|
|
2086
|
+
const dir = import_path4.default.dirname(filePath);
|
|
1764
2087
|
if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
|
|
1765
2088
|
import_fs2.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
1766
2089
|
}
|
|
1767
2090
|
async function setupClaude() {
|
|
1768
2091
|
const homeDir2 = import_os2.default.homedir();
|
|
1769
|
-
const mcpPath =
|
|
1770
|
-
const hooksPath =
|
|
2092
|
+
const mcpPath = import_path4.default.join(homeDir2, ".claude.json");
|
|
2093
|
+
const hooksPath = import_path4.default.join(homeDir2, ".claude", "settings.json");
|
|
1771
2094
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
1772
2095
|
const settings = readJson(hooksPath) ?? {};
|
|
1773
2096
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -1842,7 +2165,7 @@ async function setupClaude() {
|
|
|
1842
2165
|
}
|
|
1843
2166
|
async function setupGemini() {
|
|
1844
2167
|
const homeDir2 = import_os2.default.homedir();
|
|
1845
|
-
const settingsPath =
|
|
2168
|
+
const settingsPath = import_path4.default.join(homeDir2, ".gemini", "settings.json");
|
|
1846
2169
|
const settings = readJson(settingsPath) ?? {};
|
|
1847
2170
|
const servers = settings.mcpServers ?? {};
|
|
1848
2171
|
let anythingChanged = false;
|
|
@@ -1925,8 +2248,8 @@ async function setupGemini() {
|
|
|
1925
2248
|
}
|
|
1926
2249
|
async function setupCursor() {
|
|
1927
2250
|
const homeDir2 = import_os2.default.homedir();
|
|
1928
|
-
const mcpPath =
|
|
1929
|
-
const hooksPath =
|
|
2251
|
+
const mcpPath = import_path4.default.join(homeDir2, ".cursor", "mcp.json");
|
|
2252
|
+
const hooksPath = import_path4.default.join(homeDir2, ".cursor", "hooks.json");
|
|
1930
2253
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
1931
2254
|
const hooksFile = readJson(hooksPath) ?? { version: 1 };
|
|
1932
2255
|
const servers = mcpConfig.mcpServers ?? {};
|
|
@@ -2222,6 +2545,55 @@ var ui_default = `<!doctype html>
|
|
|
2222
2545
|
white-space: pre-wrap;
|
|
2223
2546
|
word-break: break-all;
|
|
2224
2547
|
}
|
|
2548
|
+
/* \u2500\u2500 Context Sniper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2549
|
+
.sniper-header {
|
|
2550
|
+
display: flex;
|
|
2551
|
+
align-items: center;
|
|
2552
|
+
gap: 8px;
|
|
2553
|
+
flex-wrap: wrap;
|
|
2554
|
+
margin-bottom: 8px;
|
|
2555
|
+
}
|
|
2556
|
+
.sniper-badge {
|
|
2557
|
+
font-size: 11px;
|
|
2558
|
+
font-weight: 600;
|
|
2559
|
+
padding: 3px 8px;
|
|
2560
|
+
border-radius: 5px;
|
|
2561
|
+
letter-spacing: 0.02em;
|
|
2562
|
+
}
|
|
2563
|
+
.sniper-badge-edit {
|
|
2564
|
+
background: rgba(59, 130, 246, 0.15);
|
|
2565
|
+
color: #60a5fa;
|
|
2566
|
+
border: 1px solid rgba(59, 130, 246, 0.3);
|
|
2567
|
+
}
|
|
2568
|
+
.sniper-badge-exec {
|
|
2569
|
+
background: rgba(239, 68, 68, 0.12);
|
|
2570
|
+
color: #f87171;
|
|
2571
|
+
border: 1px solid rgba(239, 68, 68, 0.25);
|
|
2572
|
+
}
|
|
2573
|
+
.sniper-tier {
|
|
2574
|
+
font-size: 10px;
|
|
2575
|
+
color: var(--muted);
|
|
2576
|
+
font-family: 'Fira Code', monospace;
|
|
2577
|
+
}
|
|
2578
|
+
.sniper-filepath {
|
|
2579
|
+
font-size: 11px;
|
|
2580
|
+
color: #a8b3c4;
|
|
2581
|
+
font-family: 'Fira Code', monospace;
|
|
2582
|
+
margin-bottom: 6px;
|
|
2583
|
+
word-break: break-all;
|
|
2584
|
+
}
|
|
2585
|
+
.sniper-match {
|
|
2586
|
+
font-size: 11px;
|
|
2587
|
+
color: #a8b3c4;
|
|
2588
|
+
margin-bottom: 6px;
|
|
2589
|
+
}
|
|
2590
|
+
.sniper-match code {
|
|
2591
|
+
background: rgba(239, 68, 68, 0.15);
|
|
2592
|
+
color: #f87171;
|
|
2593
|
+
padding: 1px 5px;
|
|
2594
|
+
border-radius: 3px;
|
|
2595
|
+
font-family: 'Fira Code', monospace;
|
|
2596
|
+
}
|
|
2225
2597
|
.actions {
|
|
2226
2598
|
display: grid;
|
|
2227
2599
|
grid-template-columns: 1fr 1fr;
|
|
@@ -2728,20 +3100,47 @@ var ui_default = `<!doctype html>
|
|
|
2728
3100
|
}, 200);
|
|
2729
3101
|
}
|
|
2730
3102
|
|
|
3103
|
+
function renderPayload(req) {
|
|
3104
|
+
const rm = req.riskMetadata;
|
|
3105
|
+
if (!rm) {
|
|
3106
|
+
// Fallback: raw args for requests without context sniper data
|
|
3107
|
+
const cmd = esc(
|
|
3108
|
+
String(
|
|
3109
|
+
req.args &&
|
|
3110
|
+
(req.args.command ||
|
|
3111
|
+
req.args.cmd ||
|
|
3112
|
+
req.args.script ||
|
|
3113
|
+
JSON.stringify(req.args, null, 2))
|
|
3114
|
+
)
|
|
3115
|
+
);
|
|
3116
|
+
return \`<span class="label">Input Payload</span><pre>\${cmd}</pre>\`;
|
|
3117
|
+
}
|
|
3118
|
+
const isEdit = rm.intent === 'EDIT';
|
|
3119
|
+
const badgeClass = isEdit ? 'sniper-badge-edit' : 'sniper-badge-exec';
|
|
3120
|
+
const badgeLabel = isEdit ? '\u{1F4DD} Code Edit' : '\u{1F6D1} Execution';
|
|
3121
|
+
const tierLabel = \`Tier \${rm.tier} \xB7 \${esc(rm.blockedByLabel)}\`;
|
|
3122
|
+
const fileLine =
|
|
3123
|
+
isEdit && rm.editFilePath
|
|
3124
|
+
? \`<div class="sniper-filepath">\u{1F4C2} \${esc(rm.editFilePath)}</div>\`
|
|
3125
|
+
: !isEdit && rm.matchedWord
|
|
3126
|
+
? \`<div class="sniper-match">Matched: <code>\${esc(rm.matchedWord)}</code>\${rm.matchedField ? \` in <code>\${esc(rm.matchedField)}</code>\` : ''}</div>\`
|
|
3127
|
+
: '';
|
|
3128
|
+
const snippetHtml = rm.contextSnippet ? \`<pre>\${esc(rm.contextSnippet)}</pre>\` : '';
|
|
3129
|
+
return \`
|
|
3130
|
+
<div class="sniper-header">
|
|
3131
|
+
<span class="sniper-badge \${badgeClass}">\${badgeLabel}</span>
|
|
3132
|
+
<span class="sniper-tier">\${tierLabel}</span>
|
|
3133
|
+
</div>
|
|
3134
|
+
\${fileLine}
|
|
3135
|
+
\${snippetHtml}
|
|
3136
|
+
\`;
|
|
3137
|
+
}
|
|
3138
|
+
|
|
2731
3139
|
function addCard(req) {
|
|
2732
3140
|
if (requests.has(req.id)) return;
|
|
2733
3141
|
requests.add(req.id);
|
|
2734
3142
|
refresh();
|
|
2735
3143
|
const isSlack = !!req.slackDelegated;
|
|
2736
|
-
const cmd = esc(
|
|
2737
|
-
String(
|
|
2738
|
-
req.args &&
|
|
2739
|
-
(req.args.command ||
|
|
2740
|
-
req.args.cmd ||
|
|
2741
|
-
req.args.script ||
|
|
2742
|
-
JSON.stringify(req.args, null, 2))
|
|
2743
|
-
)
|
|
2744
|
-
);
|
|
2745
3144
|
const card = document.createElement('div');
|
|
2746
3145
|
card.className = 'card' + (isSlack ? ' slack-viewer' : '');
|
|
2747
3146
|
card.id = 'c-' + req.id;
|
|
@@ -2755,8 +3154,7 @@ var ui_default = `<!doctype html>
|
|
|
2755
3154
|
</div>
|
|
2756
3155
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
2757
3156
|
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Slack approval \u2014 view only</div>' : ''}
|
|
2758
|
-
|
|
2759
|
-
<pre>\${cmd}</pre>
|
|
3157
|
+
\${renderPayload(req)}
|
|
2760
3158
|
<div class="actions" id="act-\${req.id}">
|
|
2761
3159
|
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>Approve Execution</button>
|
|
2762
3160
|
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>Block Action</button>
|
|
@@ -2964,7 +3362,7 @@ var UI_HTML_TEMPLATE = ui_default;
|
|
|
2964
3362
|
// src/daemon/index.ts
|
|
2965
3363
|
var import_http = __toESM(require("http"));
|
|
2966
3364
|
var import_fs3 = __toESM(require("fs"));
|
|
2967
|
-
var
|
|
3365
|
+
var import_path5 = __toESM(require("path"));
|
|
2968
3366
|
var import_os3 = __toESM(require("os"));
|
|
2969
3367
|
var import_child_process2 = require("child_process");
|
|
2970
3368
|
var import_crypto = require("crypto");
|
|
@@ -2972,14 +3370,14 @@ var import_chalk4 = __toESM(require("chalk"));
|
|
|
2972
3370
|
var DAEMON_PORT2 = 7391;
|
|
2973
3371
|
var DAEMON_HOST2 = "127.0.0.1";
|
|
2974
3372
|
var homeDir = import_os3.default.homedir();
|
|
2975
|
-
var DAEMON_PID_FILE =
|
|
2976
|
-
var DECISIONS_FILE =
|
|
2977
|
-
var GLOBAL_CONFIG_FILE =
|
|
2978
|
-
var CREDENTIALS_FILE =
|
|
2979
|
-
var AUDIT_LOG_FILE =
|
|
2980
|
-
var TRUST_FILE2 =
|
|
3373
|
+
var DAEMON_PID_FILE = import_path5.default.join(homeDir, ".node9", "daemon.pid");
|
|
3374
|
+
var DECISIONS_FILE = import_path5.default.join(homeDir, ".node9", "decisions.json");
|
|
3375
|
+
var GLOBAL_CONFIG_FILE = import_path5.default.join(homeDir, ".node9", "config.json");
|
|
3376
|
+
var CREDENTIALS_FILE = import_path5.default.join(homeDir, ".node9", "credentials.json");
|
|
3377
|
+
var AUDIT_LOG_FILE = import_path5.default.join(homeDir, ".node9", "audit.log");
|
|
3378
|
+
var TRUST_FILE2 = import_path5.default.join(homeDir, ".node9", "trust.json");
|
|
2981
3379
|
function atomicWriteSync2(filePath, data, options) {
|
|
2982
|
-
const dir =
|
|
3380
|
+
const dir = import_path5.default.dirname(filePath);
|
|
2983
3381
|
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
2984
3382
|
const tmpPath = `${filePath}.${(0, import_crypto.randomUUID)()}.tmp`;
|
|
2985
3383
|
import_fs3.default.writeFileSync(tmpPath, data, options);
|
|
@@ -3023,7 +3421,7 @@ function appendAuditLog(data) {
|
|
|
3023
3421
|
decision: data.decision,
|
|
3024
3422
|
source: "daemon"
|
|
3025
3423
|
};
|
|
3026
|
-
const dir =
|
|
3424
|
+
const dir = import_path5.default.dirname(AUDIT_LOG_FILE);
|
|
3027
3425
|
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
3028
3426
|
import_fs3.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
3029
3427
|
} catch {
|
|
@@ -3181,6 +3579,7 @@ data: ${JSON.stringify({
|
|
|
3181
3579
|
id: e.id,
|
|
3182
3580
|
toolName: e.toolName,
|
|
3183
3581
|
args: e.args,
|
|
3582
|
+
riskMetadata: e.riskMetadata,
|
|
3184
3583
|
slackDelegated: e.slackDelegated,
|
|
3185
3584
|
timestamp: e.timestamp,
|
|
3186
3585
|
agent: e.agent,
|
|
@@ -3206,14 +3605,23 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3206
3605
|
if (req.method === "POST" && pathname === "/check") {
|
|
3207
3606
|
try {
|
|
3208
3607
|
resetIdleTimer();
|
|
3608
|
+
_resetConfigCache();
|
|
3209
3609
|
const body = await readBody(req);
|
|
3210
3610
|
if (body.length > 65536) return res.writeHead(413).end();
|
|
3211
|
-
const {
|
|
3611
|
+
const {
|
|
3612
|
+
toolName,
|
|
3613
|
+
args,
|
|
3614
|
+
slackDelegated = false,
|
|
3615
|
+
agent,
|
|
3616
|
+
mcpServer,
|
|
3617
|
+
riskMetadata
|
|
3618
|
+
} = JSON.parse(body);
|
|
3212
3619
|
const id = (0, import_crypto.randomUUID)();
|
|
3213
3620
|
const entry = {
|
|
3214
3621
|
id,
|
|
3215
3622
|
toolName,
|
|
3216
3623
|
args,
|
|
3624
|
+
riskMetadata: riskMetadata ?? void 0,
|
|
3217
3625
|
agent: typeof agent === "string" ? agent : void 0,
|
|
3218
3626
|
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0,
|
|
3219
3627
|
slackDelegated: !!slackDelegated,
|
|
@@ -3245,6 +3653,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3245
3653
|
id,
|
|
3246
3654
|
toolName,
|
|
3247
3655
|
args,
|
|
3656
|
+
riskMetadata: entry.riskMetadata,
|
|
3248
3657
|
slackDelegated: entry.slackDelegated,
|
|
3249
3658
|
agent: entry.agent,
|
|
3250
3659
|
mcpServer: entry.mcpServer
|
|
@@ -3514,16 +3923,16 @@ var import_execa2 = require("execa");
|
|
|
3514
3923
|
var import_chalk5 = __toESM(require("chalk"));
|
|
3515
3924
|
var import_readline = __toESM(require("readline"));
|
|
3516
3925
|
var import_fs5 = __toESM(require("fs"));
|
|
3517
|
-
var
|
|
3926
|
+
var import_path7 = __toESM(require("path"));
|
|
3518
3927
|
var import_os5 = __toESM(require("os"));
|
|
3519
3928
|
|
|
3520
3929
|
// src/undo.ts
|
|
3521
3930
|
var import_child_process3 = require("child_process");
|
|
3522
3931
|
var import_fs4 = __toESM(require("fs"));
|
|
3523
|
-
var
|
|
3932
|
+
var import_path6 = __toESM(require("path"));
|
|
3524
3933
|
var import_os4 = __toESM(require("os"));
|
|
3525
|
-
var SNAPSHOT_STACK_PATH =
|
|
3526
|
-
var UNDO_LATEST_PATH =
|
|
3934
|
+
var SNAPSHOT_STACK_PATH = import_path6.default.join(import_os4.default.homedir(), ".node9", "snapshots.json");
|
|
3935
|
+
var UNDO_LATEST_PATH = import_path6.default.join(import_os4.default.homedir(), ".node9", "undo_latest.txt");
|
|
3527
3936
|
var MAX_SNAPSHOTS = 10;
|
|
3528
3937
|
function readStack() {
|
|
3529
3938
|
try {
|
|
@@ -3534,7 +3943,7 @@ function readStack() {
|
|
|
3534
3943
|
return [];
|
|
3535
3944
|
}
|
|
3536
3945
|
function writeStack(stack) {
|
|
3537
|
-
const dir =
|
|
3946
|
+
const dir = import_path6.default.dirname(SNAPSHOT_STACK_PATH);
|
|
3538
3947
|
if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
|
|
3539
3948
|
import_fs4.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
3540
3949
|
}
|
|
@@ -3552,8 +3961,8 @@ function buildArgsSummary(tool, args) {
|
|
|
3552
3961
|
async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
3553
3962
|
try {
|
|
3554
3963
|
const cwd = process.cwd();
|
|
3555
|
-
if (!import_fs4.default.existsSync(
|
|
3556
|
-
const tempIndex =
|
|
3964
|
+
if (!import_fs4.default.existsSync(import_path6.default.join(cwd, ".git"))) return null;
|
|
3965
|
+
const tempIndex = import_path6.default.join(cwd, ".git", `node9_index_${Date.now()}`);
|
|
3557
3966
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
3558
3967
|
(0, import_child_process3.spawnSync)("git", ["add", "-A"], { env });
|
|
3559
3968
|
const treeRes = (0, import_child_process3.spawnSync)("git", ["write-tree"], { env });
|
|
@@ -3617,7 +4026,7 @@ function applyUndo(hash, cwd) {
|
|
|
3617
4026
|
const tracked = (0, import_child_process3.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
3618
4027
|
const untracked = (0, import_child_process3.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
3619
4028
|
for (const file of [...tracked, ...untracked]) {
|
|
3620
|
-
const fullPath =
|
|
4029
|
+
const fullPath = import_path6.default.join(dir, file);
|
|
3621
4030
|
if (!snapshotFiles.has(file) && import_fs4.default.existsSync(fullPath)) {
|
|
3622
4031
|
import_fs4.default.unlinkSync(fullPath);
|
|
3623
4032
|
}
|
|
@@ -3631,7 +4040,7 @@ function applyUndo(hash, cwd) {
|
|
|
3631
4040
|
// src/cli.ts
|
|
3632
4041
|
var import_prompts3 = require("@inquirer/prompts");
|
|
3633
4042
|
var { version } = JSON.parse(
|
|
3634
|
-
import_fs5.default.readFileSync(
|
|
4043
|
+
import_fs5.default.readFileSync(import_path7.default.join(__dirname, "../package.json"), "utf-8")
|
|
3635
4044
|
);
|
|
3636
4045
|
function parseDuration(str) {
|
|
3637
4046
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -3824,9 +4233,9 @@ async function runProxy(targetCommand) {
|
|
|
3824
4233
|
}
|
|
3825
4234
|
program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
|
|
3826
4235
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
3827
|
-
const credPath =
|
|
3828
|
-
if (!import_fs5.default.existsSync(
|
|
3829
|
-
import_fs5.default.mkdirSync(
|
|
4236
|
+
const credPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "credentials.json");
|
|
4237
|
+
if (!import_fs5.default.existsSync(import_path7.default.dirname(credPath)))
|
|
4238
|
+
import_fs5.default.mkdirSync(import_path7.default.dirname(credPath), { recursive: true });
|
|
3830
4239
|
const profileName = options.profile || "default";
|
|
3831
4240
|
let existingCreds = {};
|
|
3832
4241
|
try {
|
|
@@ -3845,7 +4254,7 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3845
4254
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
3846
4255
|
import_fs5.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
3847
4256
|
if (profileName === "default") {
|
|
3848
|
-
const configPath =
|
|
4257
|
+
const configPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
3849
4258
|
let config = {};
|
|
3850
4259
|
try {
|
|
3851
4260
|
if (import_fs5.default.existsSync(configPath))
|
|
@@ -3860,10 +4269,12 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3860
4269
|
cloud: true,
|
|
3861
4270
|
terminal: true
|
|
3862
4271
|
};
|
|
3863
|
-
|
|
4272
|
+
if (options.local) {
|
|
4273
|
+
approvers.cloud = false;
|
|
4274
|
+
}
|
|
3864
4275
|
s.approvers = approvers;
|
|
3865
|
-
if (!import_fs5.default.existsSync(
|
|
3866
|
-
import_fs5.default.mkdirSync(
|
|
4276
|
+
if (!import_fs5.default.existsSync(import_path7.default.dirname(configPath)))
|
|
4277
|
+
import_fs5.default.mkdirSync(import_path7.default.dirname(configPath), { recursive: true });
|
|
3867
4278
|
import_fs5.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3868
4279
|
}
|
|
3869
4280
|
if (options.profile && profileName !== "default") {
|
|
@@ -3949,7 +4360,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3949
4360
|
);
|
|
3950
4361
|
}
|
|
3951
4362
|
section("Configuration");
|
|
3952
|
-
const globalConfigPath =
|
|
4363
|
+
const globalConfigPath = import_path7.default.join(homeDir2, ".node9", "config.json");
|
|
3953
4364
|
if (import_fs5.default.existsSync(globalConfigPath)) {
|
|
3954
4365
|
try {
|
|
3955
4366
|
JSON.parse(import_fs5.default.readFileSync(globalConfigPath, "utf-8"));
|
|
@@ -3960,7 +4371,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3960
4371
|
} else {
|
|
3961
4372
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
3962
4373
|
}
|
|
3963
|
-
const projectConfigPath =
|
|
4374
|
+
const projectConfigPath = import_path7.default.join(process.cwd(), "node9.config.json");
|
|
3964
4375
|
if (import_fs5.default.existsSync(projectConfigPath)) {
|
|
3965
4376
|
try {
|
|
3966
4377
|
JSON.parse(import_fs5.default.readFileSync(projectConfigPath, "utf-8"));
|
|
@@ -3969,7 +4380,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3969
4380
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
3970
4381
|
}
|
|
3971
4382
|
}
|
|
3972
|
-
const credsPath =
|
|
4383
|
+
const credsPath = import_path7.default.join(homeDir2, ".node9", "credentials.json");
|
|
3973
4384
|
if (import_fs5.default.existsSync(credsPath)) {
|
|
3974
4385
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
3975
4386
|
} else {
|
|
@@ -3979,7 +4390,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3979
4390
|
);
|
|
3980
4391
|
}
|
|
3981
4392
|
section("Agent Hooks");
|
|
3982
|
-
const claudeSettingsPath =
|
|
4393
|
+
const claudeSettingsPath = import_path7.default.join(homeDir2, ".claude", "settings.json");
|
|
3983
4394
|
if (import_fs5.default.existsSync(claudeSettingsPath)) {
|
|
3984
4395
|
try {
|
|
3985
4396
|
const cs = JSON.parse(import_fs5.default.readFileSync(claudeSettingsPath, "utf-8"));
|
|
@@ -3995,7 +4406,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3995
4406
|
} else {
|
|
3996
4407
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
3997
4408
|
}
|
|
3998
|
-
const geminiSettingsPath =
|
|
4409
|
+
const geminiSettingsPath = import_path7.default.join(homeDir2, ".gemini", "settings.json");
|
|
3999
4410
|
if (import_fs5.default.existsSync(geminiSettingsPath)) {
|
|
4000
4411
|
try {
|
|
4001
4412
|
const gs = JSON.parse(import_fs5.default.readFileSync(geminiSettingsPath, "utf-8"));
|
|
@@ -4011,7 +4422,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4011
4422
|
} else {
|
|
4012
4423
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
4013
4424
|
}
|
|
4014
|
-
const cursorHooksPath =
|
|
4425
|
+
const cursorHooksPath = import_path7.default.join(homeDir2, ".cursor", "hooks.json");
|
|
4015
4426
|
if (import_fs5.default.existsSync(cursorHooksPath)) {
|
|
4016
4427
|
try {
|
|
4017
4428
|
const cur = JSON.parse(import_fs5.default.readFileSync(cursorHooksPath, "utf-8"));
|
|
@@ -4116,7 +4527,7 @@ program.command("explain").description(
|
|
|
4116
4527
|
console.log("");
|
|
4117
4528
|
});
|
|
4118
4529
|
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
|
|
4119
|
-
const configPath =
|
|
4530
|
+
const configPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
4120
4531
|
if (import_fs5.default.existsSync(configPath) && !options.force) {
|
|
4121
4532
|
console.log(import_chalk5.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
4122
4533
|
console.log(import_chalk5.default.gray(` Run with --force to overwrite.`));
|
|
@@ -4131,7 +4542,7 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
4131
4542
|
mode: safeMode
|
|
4132
4543
|
}
|
|
4133
4544
|
};
|
|
4134
|
-
const dir =
|
|
4545
|
+
const dir = import_path7.default.dirname(configPath);
|
|
4135
4546
|
if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
|
|
4136
4547
|
import_fs5.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
4137
4548
|
console.log(import_chalk5.default.green(`\u2705 Global config created: ${configPath}`));
|
|
@@ -4151,7 +4562,7 @@ function formatRelativeTime(timestamp) {
|
|
|
4151
4562
|
return new Date(timestamp).toLocaleDateString();
|
|
4152
4563
|
}
|
|
4153
4564
|
program.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
|
|
4154
|
-
const logPath =
|
|
4565
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "audit.log");
|
|
4155
4566
|
if (!import_fs5.default.existsSync(logPath)) {
|
|
4156
4567
|
console.log(
|
|
4157
4568
|
import_chalk5.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
@@ -4241,8 +4652,8 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4241
4652
|
console.log("");
|
|
4242
4653
|
const modeLabel = settings.mode === "audit" ? import_chalk5.default.blue("audit") : settings.mode === "strict" ? import_chalk5.default.red("strict") : import_chalk5.default.white("standard");
|
|
4243
4654
|
console.log(` Mode: ${modeLabel}`);
|
|
4244
|
-
const projectConfig =
|
|
4245
|
-
const globalConfig =
|
|
4655
|
+
const projectConfig = import_path7.default.join(process.cwd(), "node9.config.json");
|
|
4656
|
+
const globalConfig = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
4246
4657
|
console.log(
|
|
4247
4658
|
` Local: ${import_fs5.default.existsSync(projectConfig) ? import_chalk5.default.green("Active (node9.config.json)") : import_chalk5.default.gray("Not present")}`
|
|
4248
4659
|
);
|
|
@@ -4310,7 +4721,7 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
4310
4721
|
} catch (err) {
|
|
4311
4722
|
const tempConfig = getConfig();
|
|
4312
4723
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
4313
|
-
const logPath =
|
|
4724
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4314
4725
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4315
4726
|
import_fs5.default.appendFileSync(
|
|
4316
4727
|
logPath,
|
|
@@ -4330,9 +4741,9 @@ RAW: ${raw}
|
|
|
4330
4741
|
}
|
|
4331
4742
|
const config = getConfig();
|
|
4332
4743
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
4333
|
-
const logPath =
|
|
4334
|
-
if (!import_fs5.default.existsSync(
|
|
4335
|
-
import_fs5.default.mkdirSync(
|
|
4744
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4745
|
+
if (!import_fs5.default.existsSync(import_path7.default.dirname(logPath)))
|
|
4746
|
+
import_fs5.default.mkdirSync(import_path7.default.dirname(logPath), { recursive: true });
|
|
4336
4747
|
import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
4337
4748
|
`);
|
|
4338
4749
|
}
|
|
@@ -4404,7 +4815,7 @@ RAW: ${raw}
|
|
|
4404
4815
|
});
|
|
4405
4816
|
} catch (err) {
|
|
4406
4817
|
if (process.env.NODE9_DEBUG === "1") {
|
|
4407
|
-
const logPath =
|
|
4818
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4408
4819
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4409
4820
|
import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
4410
4821
|
`);
|
|
@@ -4451,9 +4862,9 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
4451
4862
|
decision: "allowed",
|
|
4452
4863
|
source: "post-hook"
|
|
4453
4864
|
};
|
|
4454
|
-
const logPath =
|
|
4455
|
-
if (!import_fs5.default.existsSync(
|
|
4456
|
-
import_fs5.default.mkdirSync(
|
|
4865
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "audit.log");
|
|
4866
|
+
if (!import_fs5.default.existsSync(import_path7.default.dirname(logPath)))
|
|
4867
|
+
import_fs5.default.mkdirSync(import_path7.default.dirname(logPath), { recursive: true });
|
|
4457
4868
|
import_fs5.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
4458
4869
|
const config = getConfig();
|
|
4459
4870
|
if (shouldSnapshot(tool, {}, config)) {
|
|
@@ -4628,7 +5039,7 @@ process.on("unhandledRejection", (reason) => {
|
|
|
4628
5039
|
const isCheckHook = process.argv[2] === "check";
|
|
4629
5040
|
if (isCheckHook) {
|
|
4630
5041
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
4631
|
-
const logPath =
|
|
5042
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4632
5043
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
4633
5044
|
import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
4634
5045
|
`);
|