@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.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 };
|
|
70
73
|
}
|
|
74
|
+
var CODE_KEYS = [
|
|
75
|
+
"command",
|
|
76
|
+
"cmd",
|
|
77
|
+
"shell_command",
|
|
78
|
+
"bash_command",
|
|
79
|
+
"script",
|
|
80
|
+
"code",
|
|
81
|
+
"input",
|
|
82
|
+
"sql",
|
|
83
|
+
"query",
|
|
84
|
+
"arguments",
|
|
85
|
+
"args",
|
|
86
|
+
"param",
|
|
87
|
+
"params",
|
|
88
|
+
"text"
|
|
89
|
+
];
|
|
90
|
+
function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
|
|
91
|
+
let intent = "EXEC";
|
|
92
|
+
let contextSnippet;
|
|
93
|
+
let contextLineIndex;
|
|
94
|
+
let editFileName;
|
|
95
|
+
let editFilePath;
|
|
96
|
+
let parsed = args;
|
|
97
|
+
if (typeof args === "string") {
|
|
98
|
+
const trimmed = args.trim();
|
|
99
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(trimmed);
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
107
|
+
const obj = parsed;
|
|
108
|
+
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
109
|
+
intent = "EDIT";
|
|
110
|
+
if (obj.file_path) {
|
|
111
|
+
editFilePath = String(obj.file_path);
|
|
112
|
+
editFileName = import_path.default.basename(editFilePath);
|
|
113
|
+
}
|
|
114
|
+
const result = extractContext(String(obj.new_string), matchedWord);
|
|
115
|
+
contextSnippet = result.snippet;
|
|
116
|
+
if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
|
|
117
|
+
} else if (matchedField && obj[matchedField] !== void 0) {
|
|
118
|
+
const result = extractContext(String(obj[matchedField]), matchedWord);
|
|
119
|
+
contextSnippet = result.snippet;
|
|
120
|
+
if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
|
|
121
|
+
} else {
|
|
122
|
+
const foundKey = Object.keys(obj).find((k) => CODE_KEYS.includes(k.toLowerCase()));
|
|
123
|
+
if (foundKey) {
|
|
124
|
+
const val = obj[foundKey];
|
|
125
|
+
contextSnippet = smartTruncate(typeof val === "string" ? val : JSON.stringify(val), 500);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else if (typeof parsed === "string") {
|
|
129
|
+
contextSnippet = smartTruncate(parsed, 500);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
intent,
|
|
133
|
+
tier,
|
|
134
|
+
blockedByLabel,
|
|
135
|
+
...matchedWord && { matchedWord },
|
|
136
|
+
...matchedField && { matchedField },
|
|
137
|
+
...contextSnippet !== void 0 && { contextSnippet },
|
|
138
|
+
...contextLineIndex !== void 0 && { contextLineIndex },
|
|
139
|
+
...editFileName && { editFileName },
|
|
140
|
+
...editFilePath && { editFilePath },
|
|
141
|
+
...ruleName && { ruleName }
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/ui/native.ts
|
|
146
|
+
var isTestEnv = () => {
|
|
147
|
+
return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
|
|
148
|
+
};
|
|
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\\s+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 = {
|
|
@@ -1554,6 +1855,7 @@ function getConfig() {
|
|
|
1554
1855
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
1555
1856
|
}
|
|
1556
1857
|
};
|
|
1858
|
+
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
1557
1859
|
const applyLayer = (source) => {
|
|
1558
1860
|
if (!source) return;
|
|
1559
1861
|
const s = source.settings || {};
|
|
@@ -1579,6 +1881,17 @@ function getConfig() {
|
|
|
1579
1881
|
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
1580
1882
|
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
1581
1883
|
}
|
|
1884
|
+
const envs = source.environments || {};
|
|
1885
|
+
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
1886
|
+
if (envConfig && typeof envConfig === "object") {
|
|
1887
|
+
const ec = envConfig;
|
|
1888
|
+
mergedEnvironments[envName] = {
|
|
1889
|
+
...mergedEnvironments[envName],
|
|
1890
|
+
// Validate field types before merging — do not blindly spread user input
|
|
1891
|
+
...typeof ec.requireApproval === "boolean" ? { requireApproval: ec.requireApproval } : {}
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1582
1895
|
};
|
|
1583
1896
|
applyLayer(globalConfig);
|
|
1584
1897
|
applyLayer(projectConfig);
|
|
@@ -1592,17 +1905,62 @@ function getConfig() {
|
|
|
1592
1905
|
cachedConfig = {
|
|
1593
1906
|
settings: mergedSettings,
|
|
1594
1907
|
policy: mergedPolicy,
|
|
1595
|
-
environments:
|
|
1908
|
+
environments: mergedEnvironments
|
|
1596
1909
|
};
|
|
1597
1910
|
return cachedConfig;
|
|
1598
1911
|
}
|
|
1599
1912
|
function tryLoadConfig(filePath) {
|
|
1600
1913
|
if (!import_fs.default.existsSync(filePath)) return null;
|
|
1914
|
+
let raw;
|
|
1601
1915
|
try {
|
|
1602
|
-
|
|
1603
|
-
} catch {
|
|
1916
|
+
raw = JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
|
|
1917
|
+
} catch (err) {
|
|
1918
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1919
|
+
process.stderr.write(
|
|
1920
|
+
`
|
|
1921
|
+
\u26A0\uFE0F Node9: Failed to parse ${filePath}
|
|
1922
|
+
${msg}
|
|
1923
|
+
\u2192 Using default config
|
|
1924
|
+
|
|
1925
|
+
`
|
|
1926
|
+
);
|
|
1604
1927
|
return null;
|
|
1605
1928
|
}
|
|
1929
|
+
const SUPPORTED_VERSION = "1.0";
|
|
1930
|
+
const SUPPORTED_MAJOR = SUPPORTED_VERSION.split(".")[0];
|
|
1931
|
+
const fileVersion = raw?.version;
|
|
1932
|
+
if (fileVersion !== void 0) {
|
|
1933
|
+
const vStr = String(fileVersion);
|
|
1934
|
+
const fileMajor = vStr.split(".")[0];
|
|
1935
|
+
if (fileMajor !== SUPPORTED_MAJOR) {
|
|
1936
|
+
process.stderr.write(
|
|
1937
|
+
`
|
|
1938
|
+
\u274C Node9: Config at ${filePath} has version "${vStr}" \u2014 major version is incompatible with this release (expected "${SUPPORTED_VERSION}"). Config will not be loaded.
|
|
1939
|
+
|
|
1940
|
+
`
|
|
1941
|
+
);
|
|
1942
|
+
return null;
|
|
1943
|
+
} else if (vStr !== SUPPORTED_VERSION) {
|
|
1944
|
+
process.stderr.write(
|
|
1945
|
+
`
|
|
1946
|
+
\u26A0\uFE0F Node9: Config at ${filePath} declares version "${vStr}" \u2014 expected "${SUPPORTED_VERSION}". Continuing with best-effort parsing.
|
|
1947
|
+
|
|
1948
|
+
`
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
const { sanitized, error } = sanitizeConfig(raw);
|
|
1953
|
+
if (error) {
|
|
1954
|
+
process.stderr.write(
|
|
1955
|
+
`
|
|
1956
|
+
\u26A0\uFE0F Node9: Invalid config at ${filePath}:
|
|
1957
|
+
${error.replace("Invalid config:\n", "")}
|
|
1958
|
+
\u2192 Invalid fields ignored, using defaults for those keys
|
|
1959
|
+
|
|
1960
|
+
`
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
return sanitized;
|
|
1606
1964
|
}
|
|
1607
1965
|
function getActiveEnvironment(config) {
|
|
1608
1966
|
const env = config.settings.environment || process.env.NODE_ENV || "development";
|
|
@@ -1617,7 +1975,7 @@ function getCredentials() {
|
|
|
1617
1975
|
};
|
|
1618
1976
|
}
|
|
1619
1977
|
try {
|
|
1620
|
-
const credPath =
|
|
1978
|
+
const credPath = import_path3.default.join(import_os.default.homedir(), ".node9", "credentials.json");
|
|
1621
1979
|
if (import_fs.default.existsSync(credPath)) {
|
|
1622
1980
|
const creds = JSON.parse(import_fs.default.readFileSync(credPath, "utf-8"));
|
|
1623
1981
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
@@ -1640,9 +1998,7 @@ function getCredentials() {
|
|
|
1640
1998
|
return null;
|
|
1641
1999
|
}
|
|
1642
2000
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
1643
|
-
|
|
1644
|
-
setTimeout(() => controller.abort(), 5e3);
|
|
1645
|
-
fetch(`${creds.apiUrl}/audit`, {
|
|
2001
|
+
return fetch(`${creds.apiUrl}/audit`, {
|
|
1646
2002
|
method: "POST",
|
|
1647
2003
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
1648
2004
|
body: JSON.stringify({
|
|
@@ -1657,11 +2013,12 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1657
2013
|
platform: import_os.default.platform()
|
|
1658
2014
|
}
|
|
1659
2015
|
}),
|
|
1660
|
-
signal:
|
|
2016
|
+
signal: AbortSignal.timeout(5e3)
|
|
2017
|
+
}).then(() => {
|
|
1661
2018
|
}).catch(() => {
|
|
1662
2019
|
});
|
|
1663
2020
|
}
|
|
1664
|
-
async function initNode9SaaS(toolName, args, creds, meta) {
|
|
2021
|
+
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
1665
2022
|
const controller = new AbortController();
|
|
1666
2023
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
1667
2024
|
try {
|
|
@@ -1677,7 +2034,8 @@ async function initNode9SaaS(toolName, args, creds, meta) {
|
|
|
1677
2034
|
hostname: import_os.default.hostname(),
|
|
1678
2035
|
cwd: process.cwd(),
|
|
1679
2036
|
platform: import_os.default.platform()
|
|
1680
|
-
}
|
|
2037
|
+
},
|
|
2038
|
+
...riskMetadata && { riskMetadata }
|
|
1681
2039
|
}),
|
|
1682
2040
|
signal: controller.signal
|
|
1683
2041
|
});
|
|
@@ -1735,7 +2093,7 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
1735
2093
|
|
|
1736
2094
|
// src/setup.ts
|
|
1737
2095
|
var import_fs2 = __toESM(require("fs"));
|
|
1738
|
-
var
|
|
2096
|
+
var import_path4 = __toESM(require("path"));
|
|
1739
2097
|
var import_os2 = __toESM(require("os"));
|
|
1740
2098
|
var import_chalk3 = __toESM(require("chalk"));
|
|
1741
2099
|
var import_prompts2 = require("@inquirer/prompts");
|
|
@@ -1760,14 +2118,14 @@ function readJson(filePath) {
|
|
|
1760
2118
|
return null;
|
|
1761
2119
|
}
|
|
1762
2120
|
function writeJson(filePath, data) {
|
|
1763
|
-
const dir =
|
|
2121
|
+
const dir = import_path4.default.dirname(filePath);
|
|
1764
2122
|
if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
|
|
1765
2123
|
import_fs2.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
1766
2124
|
}
|
|
1767
2125
|
async function setupClaude() {
|
|
1768
2126
|
const homeDir2 = import_os2.default.homedir();
|
|
1769
|
-
const mcpPath =
|
|
1770
|
-
const hooksPath =
|
|
2127
|
+
const mcpPath = import_path4.default.join(homeDir2, ".claude.json");
|
|
2128
|
+
const hooksPath = import_path4.default.join(homeDir2, ".claude", "settings.json");
|
|
1771
2129
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
1772
2130
|
const settings = readJson(hooksPath) ?? {};
|
|
1773
2131
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -1842,7 +2200,7 @@ async function setupClaude() {
|
|
|
1842
2200
|
}
|
|
1843
2201
|
async function setupGemini() {
|
|
1844
2202
|
const homeDir2 = import_os2.default.homedir();
|
|
1845
|
-
const settingsPath =
|
|
2203
|
+
const settingsPath = import_path4.default.join(homeDir2, ".gemini", "settings.json");
|
|
1846
2204
|
const settings = readJson(settingsPath) ?? {};
|
|
1847
2205
|
const servers = settings.mcpServers ?? {};
|
|
1848
2206
|
let anythingChanged = false;
|
|
@@ -1925,8 +2283,8 @@ async function setupGemini() {
|
|
|
1925
2283
|
}
|
|
1926
2284
|
async function setupCursor() {
|
|
1927
2285
|
const homeDir2 = import_os2.default.homedir();
|
|
1928
|
-
const mcpPath =
|
|
1929
|
-
const hooksPath =
|
|
2286
|
+
const mcpPath = import_path4.default.join(homeDir2, ".cursor", "mcp.json");
|
|
2287
|
+
const hooksPath = import_path4.default.join(homeDir2, ".cursor", "hooks.json");
|
|
1930
2288
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
1931
2289
|
const hooksFile = readJson(hooksPath) ?? { version: 1 };
|
|
1932
2290
|
const servers = mcpConfig.mcpServers ?? {};
|
|
@@ -2222,6 +2580,55 @@ var ui_default = `<!doctype html>
|
|
|
2222
2580
|
white-space: pre-wrap;
|
|
2223
2581
|
word-break: break-all;
|
|
2224
2582
|
}
|
|
2583
|
+
/* \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 */
|
|
2584
|
+
.sniper-header {
|
|
2585
|
+
display: flex;
|
|
2586
|
+
align-items: center;
|
|
2587
|
+
gap: 8px;
|
|
2588
|
+
flex-wrap: wrap;
|
|
2589
|
+
margin-bottom: 8px;
|
|
2590
|
+
}
|
|
2591
|
+
.sniper-badge {
|
|
2592
|
+
font-size: 11px;
|
|
2593
|
+
font-weight: 600;
|
|
2594
|
+
padding: 3px 8px;
|
|
2595
|
+
border-radius: 5px;
|
|
2596
|
+
letter-spacing: 0.02em;
|
|
2597
|
+
}
|
|
2598
|
+
.sniper-badge-edit {
|
|
2599
|
+
background: rgba(59, 130, 246, 0.15);
|
|
2600
|
+
color: #60a5fa;
|
|
2601
|
+
border: 1px solid rgba(59, 130, 246, 0.3);
|
|
2602
|
+
}
|
|
2603
|
+
.sniper-badge-exec {
|
|
2604
|
+
background: rgba(239, 68, 68, 0.12);
|
|
2605
|
+
color: #f87171;
|
|
2606
|
+
border: 1px solid rgba(239, 68, 68, 0.25);
|
|
2607
|
+
}
|
|
2608
|
+
.sniper-tier {
|
|
2609
|
+
font-size: 10px;
|
|
2610
|
+
color: var(--muted);
|
|
2611
|
+
font-family: 'Fira Code', monospace;
|
|
2612
|
+
}
|
|
2613
|
+
.sniper-filepath {
|
|
2614
|
+
font-size: 11px;
|
|
2615
|
+
color: #a8b3c4;
|
|
2616
|
+
font-family: 'Fira Code', monospace;
|
|
2617
|
+
margin-bottom: 6px;
|
|
2618
|
+
word-break: break-all;
|
|
2619
|
+
}
|
|
2620
|
+
.sniper-match {
|
|
2621
|
+
font-size: 11px;
|
|
2622
|
+
color: #a8b3c4;
|
|
2623
|
+
margin-bottom: 6px;
|
|
2624
|
+
}
|
|
2625
|
+
.sniper-match code {
|
|
2626
|
+
background: rgba(239, 68, 68, 0.15);
|
|
2627
|
+
color: #f87171;
|
|
2628
|
+
padding: 1px 5px;
|
|
2629
|
+
border-radius: 3px;
|
|
2630
|
+
font-family: 'Fira Code', monospace;
|
|
2631
|
+
}
|
|
2225
2632
|
.actions {
|
|
2226
2633
|
display: grid;
|
|
2227
2634
|
grid-template-columns: 1fr 1fr;
|
|
@@ -2728,20 +3135,47 @@ var ui_default = `<!doctype html>
|
|
|
2728
3135
|
}, 200);
|
|
2729
3136
|
}
|
|
2730
3137
|
|
|
3138
|
+
function renderPayload(req) {
|
|
3139
|
+
const rm = req.riskMetadata;
|
|
3140
|
+
if (!rm) {
|
|
3141
|
+
// Fallback: raw args for requests without context sniper data
|
|
3142
|
+
const cmd = esc(
|
|
3143
|
+
String(
|
|
3144
|
+
req.args &&
|
|
3145
|
+
(req.args.command ||
|
|
3146
|
+
req.args.cmd ||
|
|
3147
|
+
req.args.script ||
|
|
3148
|
+
JSON.stringify(req.args, null, 2))
|
|
3149
|
+
)
|
|
3150
|
+
);
|
|
3151
|
+
return \`<span class="label">Input Payload</span><pre>\${cmd}</pre>\`;
|
|
3152
|
+
}
|
|
3153
|
+
const isEdit = rm.intent === 'EDIT';
|
|
3154
|
+
const badgeClass = isEdit ? 'sniper-badge-edit' : 'sniper-badge-exec';
|
|
3155
|
+
const badgeLabel = isEdit ? '\u{1F4DD} Code Edit' : '\u{1F6D1} Execution';
|
|
3156
|
+
const tierLabel = \`Tier \${rm.tier} \xB7 \${esc(rm.blockedByLabel)}\`;
|
|
3157
|
+
const fileLine =
|
|
3158
|
+
isEdit && rm.editFilePath
|
|
3159
|
+
? \`<div class="sniper-filepath">\u{1F4C2} \${esc(rm.editFilePath)}</div>\`
|
|
3160
|
+
: !isEdit && rm.matchedWord
|
|
3161
|
+
? \`<div class="sniper-match">Matched: <code>\${esc(rm.matchedWord)}</code>\${rm.matchedField ? \` in <code>\${esc(rm.matchedField)}</code>\` : ''}</div>\`
|
|
3162
|
+
: '';
|
|
3163
|
+
const snippetHtml = rm.contextSnippet ? \`<pre>\${esc(rm.contextSnippet)}</pre>\` : '';
|
|
3164
|
+
return \`
|
|
3165
|
+
<div class="sniper-header">
|
|
3166
|
+
<span class="sniper-badge \${badgeClass}">\${badgeLabel}</span>
|
|
3167
|
+
<span class="sniper-tier">\${tierLabel}</span>
|
|
3168
|
+
</div>
|
|
3169
|
+
\${fileLine}
|
|
3170
|
+
\${snippetHtml}
|
|
3171
|
+
\`;
|
|
3172
|
+
}
|
|
3173
|
+
|
|
2731
3174
|
function addCard(req) {
|
|
2732
3175
|
if (requests.has(req.id)) return;
|
|
2733
3176
|
requests.add(req.id);
|
|
2734
3177
|
refresh();
|
|
2735
3178
|
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
3179
|
const card = document.createElement('div');
|
|
2746
3180
|
card.className = 'card' + (isSlack ? ' slack-viewer' : '');
|
|
2747
3181
|
card.id = 'c-' + req.id;
|
|
@@ -2755,8 +3189,7 @@ var ui_default = `<!doctype html>
|
|
|
2755
3189
|
</div>
|
|
2756
3190
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
2757
3191
|
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Slack approval \u2014 view only</div>' : ''}
|
|
2758
|
-
|
|
2759
|
-
<pre>\${cmd}</pre>
|
|
3192
|
+
\${renderPayload(req)}
|
|
2760
3193
|
<div class="actions" id="act-\${req.id}">
|
|
2761
3194
|
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>Approve Execution</button>
|
|
2762
3195
|
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>Block Action</button>
|
|
@@ -2964,7 +3397,7 @@ var UI_HTML_TEMPLATE = ui_default;
|
|
|
2964
3397
|
// src/daemon/index.ts
|
|
2965
3398
|
var import_http = __toESM(require("http"));
|
|
2966
3399
|
var import_fs3 = __toESM(require("fs"));
|
|
2967
|
-
var
|
|
3400
|
+
var import_path5 = __toESM(require("path"));
|
|
2968
3401
|
var import_os3 = __toESM(require("os"));
|
|
2969
3402
|
var import_child_process2 = require("child_process");
|
|
2970
3403
|
var import_crypto = require("crypto");
|
|
@@ -2972,14 +3405,14 @@ var import_chalk4 = __toESM(require("chalk"));
|
|
|
2972
3405
|
var DAEMON_PORT2 = 7391;
|
|
2973
3406
|
var DAEMON_HOST2 = "127.0.0.1";
|
|
2974
3407
|
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 =
|
|
3408
|
+
var DAEMON_PID_FILE = import_path5.default.join(homeDir, ".node9", "daemon.pid");
|
|
3409
|
+
var DECISIONS_FILE = import_path5.default.join(homeDir, ".node9", "decisions.json");
|
|
3410
|
+
var GLOBAL_CONFIG_FILE = import_path5.default.join(homeDir, ".node9", "config.json");
|
|
3411
|
+
var CREDENTIALS_FILE = import_path5.default.join(homeDir, ".node9", "credentials.json");
|
|
3412
|
+
var AUDIT_LOG_FILE = import_path5.default.join(homeDir, ".node9", "audit.log");
|
|
3413
|
+
var TRUST_FILE2 = import_path5.default.join(homeDir, ".node9", "trust.json");
|
|
2981
3414
|
function atomicWriteSync2(filePath, data, options) {
|
|
2982
|
-
const dir =
|
|
3415
|
+
const dir = import_path5.default.dirname(filePath);
|
|
2983
3416
|
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
2984
3417
|
const tmpPath = `${filePath}.${(0, import_crypto.randomUUID)()}.tmp`;
|
|
2985
3418
|
import_fs3.default.writeFileSync(tmpPath, data, options);
|
|
@@ -3023,7 +3456,7 @@ function appendAuditLog(data) {
|
|
|
3023
3456
|
decision: data.decision,
|
|
3024
3457
|
source: "daemon"
|
|
3025
3458
|
};
|
|
3026
|
-
const dir =
|
|
3459
|
+
const dir = import_path5.default.dirname(AUDIT_LOG_FILE);
|
|
3027
3460
|
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
3028
3461
|
import_fs3.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
3029
3462
|
} catch {
|
|
@@ -3181,6 +3614,7 @@ data: ${JSON.stringify({
|
|
|
3181
3614
|
id: e.id,
|
|
3182
3615
|
toolName: e.toolName,
|
|
3183
3616
|
args: e.args,
|
|
3617
|
+
riskMetadata: e.riskMetadata,
|
|
3184
3618
|
slackDelegated: e.slackDelegated,
|
|
3185
3619
|
timestamp: e.timestamp,
|
|
3186
3620
|
agent: e.agent,
|
|
@@ -3206,14 +3640,23 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3206
3640
|
if (req.method === "POST" && pathname === "/check") {
|
|
3207
3641
|
try {
|
|
3208
3642
|
resetIdleTimer();
|
|
3643
|
+
_resetConfigCache();
|
|
3209
3644
|
const body = await readBody(req);
|
|
3210
3645
|
if (body.length > 65536) return res.writeHead(413).end();
|
|
3211
|
-
const {
|
|
3646
|
+
const {
|
|
3647
|
+
toolName,
|
|
3648
|
+
args,
|
|
3649
|
+
slackDelegated = false,
|
|
3650
|
+
agent,
|
|
3651
|
+
mcpServer,
|
|
3652
|
+
riskMetadata
|
|
3653
|
+
} = JSON.parse(body);
|
|
3212
3654
|
const id = (0, import_crypto.randomUUID)();
|
|
3213
3655
|
const entry = {
|
|
3214
3656
|
id,
|
|
3215
3657
|
toolName,
|
|
3216
3658
|
args,
|
|
3659
|
+
riskMetadata: riskMetadata ?? void 0,
|
|
3217
3660
|
agent: typeof agent === "string" ? agent : void 0,
|
|
3218
3661
|
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0,
|
|
3219
3662
|
slackDelegated: !!slackDelegated,
|
|
@@ -3245,6 +3688,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3245
3688
|
id,
|
|
3246
3689
|
toolName,
|
|
3247
3690
|
args,
|
|
3691
|
+
riskMetadata: entry.riskMetadata,
|
|
3248
3692
|
slackDelegated: entry.slackDelegated,
|
|
3249
3693
|
agent: entry.agent,
|
|
3250
3694
|
mcpServer: entry.mcpServer
|
|
@@ -3514,16 +3958,16 @@ var import_execa2 = require("execa");
|
|
|
3514
3958
|
var import_chalk5 = __toESM(require("chalk"));
|
|
3515
3959
|
var import_readline = __toESM(require("readline"));
|
|
3516
3960
|
var import_fs5 = __toESM(require("fs"));
|
|
3517
|
-
var
|
|
3961
|
+
var import_path7 = __toESM(require("path"));
|
|
3518
3962
|
var import_os5 = __toESM(require("os"));
|
|
3519
3963
|
|
|
3520
3964
|
// src/undo.ts
|
|
3521
3965
|
var import_child_process3 = require("child_process");
|
|
3522
3966
|
var import_fs4 = __toESM(require("fs"));
|
|
3523
|
-
var
|
|
3967
|
+
var import_path6 = __toESM(require("path"));
|
|
3524
3968
|
var import_os4 = __toESM(require("os"));
|
|
3525
|
-
var SNAPSHOT_STACK_PATH =
|
|
3526
|
-
var UNDO_LATEST_PATH =
|
|
3969
|
+
var SNAPSHOT_STACK_PATH = import_path6.default.join(import_os4.default.homedir(), ".node9", "snapshots.json");
|
|
3970
|
+
var UNDO_LATEST_PATH = import_path6.default.join(import_os4.default.homedir(), ".node9", "undo_latest.txt");
|
|
3527
3971
|
var MAX_SNAPSHOTS = 10;
|
|
3528
3972
|
function readStack() {
|
|
3529
3973
|
try {
|
|
@@ -3534,7 +3978,7 @@ function readStack() {
|
|
|
3534
3978
|
return [];
|
|
3535
3979
|
}
|
|
3536
3980
|
function writeStack(stack) {
|
|
3537
|
-
const dir =
|
|
3981
|
+
const dir = import_path6.default.dirname(SNAPSHOT_STACK_PATH);
|
|
3538
3982
|
if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
|
|
3539
3983
|
import_fs4.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
3540
3984
|
}
|
|
@@ -3552,8 +3996,8 @@ function buildArgsSummary(tool, args) {
|
|
|
3552
3996
|
async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
3553
3997
|
try {
|
|
3554
3998
|
const cwd = process.cwd();
|
|
3555
|
-
if (!import_fs4.default.existsSync(
|
|
3556
|
-
const tempIndex =
|
|
3999
|
+
if (!import_fs4.default.existsSync(import_path6.default.join(cwd, ".git"))) return null;
|
|
4000
|
+
const tempIndex = import_path6.default.join(cwd, ".git", `node9_index_${Date.now()}`);
|
|
3557
4001
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
3558
4002
|
(0, import_child_process3.spawnSync)("git", ["add", "-A"], { env });
|
|
3559
4003
|
const treeRes = (0, import_child_process3.spawnSync)("git", ["write-tree"], { env });
|
|
@@ -3617,7 +4061,7 @@ function applyUndo(hash, cwd) {
|
|
|
3617
4061
|
const tracked = (0, import_child_process3.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
3618
4062
|
const untracked = (0, import_child_process3.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
3619
4063
|
for (const file of [...tracked, ...untracked]) {
|
|
3620
|
-
const fullPath =
|
|
4064
|
+
const fullPath = import_path6.default.join(dir, file);
|
|
3621
4065
|
if (!snapshotFiles.has(file) && import_fs4.default.existsSync(fullPath)) {
|
|
3622
4066
|
import_fs4.default.unlinkSync(fullPath);
|
|
3623
4067
|
}
|
|
@@ -3631,7 +4075,7 @@ function applyUndo(hash, cwd) {
|
|
|
3631
4075
|
// src/cli.ts
|
|
3632
4076
|
var import_prompts3 = require("@inquirer/prompts");
|
|
3633
4077
|
var { version } = JSON.parse(
|
|
3634
|
-
import_fs5.default.readFileSync(
|
|
4078
|
+
import_fs5.default.readFileSync(import_path7.default.join(__dirname, "../package.json"), "utf-8")
|
|
3635
4079
|
);
|
|
3636
4080
|
function parseDuration(str) {
|
|
3637
4081
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -3824,9 +4268,9 @@ async function runProxy(targetCommand) {
|
|
|
3824
4268
|
}
|
|
3825
4269
|
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
4270
|
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(
|
|
4271
|
+
const credPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "credentials.json");
|
|
4272
|
+
if (!import_fs5.default.existsSync(import_path7.default.dirname(credPath)))
|
|
4273
|
+
import_fs5.default.mkdirSync(import_path7.default.dirname(credPath), { recursive: true });
|
|
3830
4274
|
const profileName = options.profile || "default";
|
|
3831
4275
|
let existingCreds = {};
|
|
3832
4276
|
try {
|
|
@@ -3845,7 +4289,7 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3845
4289
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
3846
4290
|
import_fs5.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
3847
4291
|
if (profileName === "default") {
|
|
3848
|
-
const configPath =
|
|
4292
|
+
const configPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
3849
4293
|
let config = {};
|
|
3850
4294
|
try {
|
|
3851
4295
|
if (import_fs5.default.existsSync(configPath))
|
|
@@ -3860,10 +4304,12 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3860
4304
|
cloud: true,
|
|
3861
4305
|
terminal: true
|
|
3862
4306
|
};
|
|
3863
|
-
|
|
4307
|
+
if (options.local) {
|
|
4308
|
+
approvers.cloud = false;
|
|
4309
|
+
}
|
|
3864
4310
|
s.approvers = approvers;
|
|
3865
|
-
if (!import_fs5.default.existsSync(
|
|
3866
|
-
import_fs5.default.mkdirSync(
|
|
4311
|
+
if (!import_fs5.default.existsSync(import_path7.default.dirname(configPath)))
|
|
4312
|
+
import_fs5.default.mkdirSync(import_path7.default.dirname(configPath), { recursive: true });
|
|
3867
4313
|
import_fs5.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3868
4314
|
}
|
|
3869
4315
|
if (options.profile && profileName !== "default") {
|
|
@@ -3949,7 +4395,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3949
4395
|
);
|
|
3950
4396
|
}
|
|
3951
4397
|
section("Configuration");
|
|
3952
|
-
const globalConfigPath =
|
|
4398
|
+
const globalConfigPath = import_path7.default.join(homeDir2, ".node9", "config.json");
|
|
3953
4399
|
if (import_fs5.default.existsSync(globalConfigPath)) {
|
|
3954
4400
|
try {
|
|
3955
4401
|
JSON.parse(import_fs5.default.readFileSync(globalConfigPath, "utf-8"));
|
|
@@ -3960,7 +4406,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3960
4406
|
} else {
|
|
3961
4407
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
3962
4408
|
}
|
|
3963
|
-
const projectConfigPath =
|
|
4409
|
+
const projectConfigPath = import_path7.default.join(process.cwd(), "node9.config.json");
|
|
3964
4410
|
if (import_fs5.default.existsSync(projectConfigPath)) {
|
|
3965
4411
|
try {
|
|
3966
4412
|
JSON.parse(import_fs5.default.readFileSync(projectConfigPath, "utf-8"));
|
|
@@ -3969,7 +4415,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3969
4415
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
3970
4416
|
}
|
|
3971
4417
|
}
|
|
3972
|
-
const credsPath =
|
|
4418
|
+
const credsPath = import_path7.default.join(homeDir2, ".node9", "credentials.json");
|
|
3973
4419
|
if (import_fs5.default.existsSync(credsPath)) {
|
|
3974
4420
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
3975
4421
|
} else {
|
|
@@ -3979,7 +4425,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3979
4425
|
);
|
|
3980
4426
|
}
|
|
3981
4427
|
section("Agent Hooks");
|
|
3982
|
-
const claudeSettingsPath =
|
|
4428
|
+
const claudeSettingsPath = import_path7.default.join(homeDir2, ".claude", "settings.json");
|
|
3983
4429
|
if (import_fs5.default.existsSync(claudeSettingsPath)) {
|
|
3984
4430
|
try {
|
|
3985
4431
|
const cs = JSON.parse(import_fs5.default.readFileSync(claudeSettingsPath, "utf-8"));
|
|
@@ -3995,7 +4441,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3995
4441
|
} else {
|
|
3996
4442
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
3997
4443
|
}
|
|
3998
|
-
const geminiSettingsPath =
|
|
4444
|
+
const geminiSettingsPath = import_path7.default.join(homeDir2, ".gemini", "settings.json");
|
|
3999
4445
|
if (import_fs5.default.existsSync(geminiSettingsPath)) {
|
|
4000
4446
|
try {
|
|
4001
4447
|
const gs = JSON.parse(import_fs5.default.readFileSync(geminiSettingsPath, "utf-8"));
|
|
@@ -4011,7 +4457,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4011
4457
|
} else {
|
|
4012
4458
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
4013
4459
|
}
|
|
4014
|
-
const cursorHooksPath =
|
|
4460
|
+
const cursorHooksPath = import_path7.default.join(homeDir2, ".cursor", "hooks.json");
|
|
4015
4461
|
if (import_fs5.default.existsSync(cursorHooksPath)) {
|
|
4016
4462
|
try {
|
|
4017
4463
|
const cur = JSON.parse(import_fs5.default.readFileSync(cursorHooksPath, "utf-8"));
|
|
@@ -4116,7 +4562,7 @@ program.command("explain").description(
|
|
|
4116
4562
|
console.log("");
|
|
4117
4563
|
});
|
|
4118
4564
|
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 =
|
|
4565
|
+
const configPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
4120
4566
|
if (import_fs5.default.existsSync(configPath) && !options.force) {
|
|
4121
4567
|
console.log(import_chalk5.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
4122
4568
|
console.log(import_chalk5.default.gray(` Run with --force to overwrite.`));
|
|
@@ -4131,7 +4577,7 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
4131
4577
|
mode: safeMode
|
|
4132
4578
|
}
|
|
4133
4579
|
};
|
|
4134
|
-
const dir =
|
|
4580
|
+
const dir = import_path7.default.dirname(configPath);
|
|
4135
4581
|
if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
|
|
4136
4582
|
import_fs5.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
4137
4583
|
console.log(import_chalk5.default.green(`\u2705 Global config created: ${configPath}`));
|
|
@@ -4151,7 +4597,7 @@ function formatRelativeTime(timestamp) {
|
|
|
4151
4597
|
return new Date(timestamp).toLocaleDateString();
|
|
4152
4598
|
}
|
|
4153
4599
|
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 =
|
|
4600
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "audit.log");
|
|
4155
4601
|
if (!import_fs5.default.existsSync(logPath)) {
|
|
4156
4602
|
console.log(
|
|
4157
4603
|
import_chalk5.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
@@ -4241,8 +4687,8 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4241
4687
|
console.log("");
|
|
4242
4688
|
const modeLabel = settings.mode === "audit" ? import_chalk5.default.blue("audit") : settings.mode === "strict" ? import_chalk5.default.red("strict") : import_chalk5.default.white("standard");
|
|
4243
4689
|
console.log(` Mode: ${modeLabel}`);
|
|
4244
|
-
const projectConfig =
|
|
4245
|
-
const globalConfig =
|
|
4690
|
+
const projectConfig = import_path7.default.join(process.cwd(), "node9.config.json");
|
|
4691
|
+
const globalConfig = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
4246
4692
|
console.log(
|
|
4247
4693
|
` Local: ${import_fs5.default.existsSync(projectConfig) ? import_chalk5.default.green("Active (node9.config.json)") : import_chalk5.default.gray("Not present")}`
|
|
4248
4694
|
);
|
|
@@ -4310,7 +4756,7 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
4310
4756
|
} catch (err) {
|
|
4311
4757
|
const tempConfig = getConfig();
|
|
4312
4758
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
4313
|
-
const logPath =
|
|
4759
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4314
4760
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4315
4761
|
import_fs5.default.appendFileSync(
|
|
4316
4762
|
logPath,
|
|
@@ -4330,9 +4776,9 @@ RAW: ${raw}
|
|
|
4330
4776
|
}
|
|
4331
4777
|
const config = getConfig();
|
|
4332
4778
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
4333
|
-
const logPath =
|
|
4334
|
-
if (!import_fs5.default.existsSync(
|
|
4335
|
-
import_fs5.default.mkdirSync(
|
|
4779
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4780
|
+
if (!import_fs5.default.existsSync(import_path7.default.dirname(logPath)))
|
|
4781
|
+
import_fs5.default.mkdirSync(import_path7.default.dirname(logPath), { recursive: true });
|
|
4336
4782
|
import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
4337
4783
|
`);
|
|
4338
4784
|
}
|
|
@@ -4404,7 +4850,7 @@ RAW: ${raw}
|
|
|
4404
4850
|
});
|
|
4405
4851
|
} catch (err) {
|
|
4406
4852
|
if (process.env.NODE9_DEBUG === "1") {
|
|
4407
|
-
const logPath =
|
|
4853
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4408
4854
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4409
4855
|
import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
4410
4856
|
`);
|
|
@@ -4451,9 +4897,9 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
4451
4897
|
decision: "allowed",
|
|
4452
4898
|
source: "post-hook"
|
|
4453
4899
|
};
|
|
4454
|
-
const logPath =
|
|
4455
|
-
if (!import_fs5.default.existsSync(
|
|
4456
|
-
import_fs5.default.mkdirSync(
|
|
4900
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "audit.log");
|
|
4901
|
+
if (!import_fs5.default.existsSync(import_path7.default.dirname(logPath)))
|
|
4902
|
+
import_fs5.default.mkdirSync(import_path7.default.dirname(logPath), { recursive: true });
|
|
4457
4903
|
import_fs5.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
4458
4904
|
const config = getConfig();
|
|
4459
4905
|
if (shouldSnapshot(tool, {}, config)) {
|
|
@@ -4628,7 +5074,7 @@ process.on("unhandledRejection", (reason) => {
|
|
|
4628
5074
|
const isCheckHook = process.argv[2] === "check";
|
|
4629
5075
|
if (isCheckHook) {
|
|
4630
5076
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
4631
|
-
const logPath =
|
|
5077
|
+
const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4632
5078
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
4633
5079
|
import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
4634
5080
|
`);
|