@node9/proxy 1.5.0 → 1.5.2
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 +616 -246
- package/dist/cli.mjs +596 -227
- package/dist/index.js +222 -35
- package/dist/index.mjs +218 -25
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -9,7 +9,51 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// src/audit/hasher.ts
|
|
13
|
+
import { createHash } from "crypto";
|
|
14
|
+
function canonicalise(value) {
|
|
15
|
+
return _canonicalise(value, /* @__PURE__ */ new WeakSet());
|
|
16
|
+
}
|
|
17
|
+
function _canonicalise(value, seen) {
|
|
18
|
+
if (value === null || typeof value !== "object") return value;
|
|
19
|
+
if (value instanceof Date) return value.toISOString();
|
|
20
|
+
if (value instanceof RegExp) return value.toString();
|
|
21
|
+
if (Buffer.isBuffer(value)) return value.toString("base64");
|
|
22
|
+
if (seen.has(value)) return "[Circular]";
|
|
23
|
+
seen.add(value);
|
|
24
|
+
let result;
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
result = value.map((v) => _canonicalise(v, seen));
|
|
27
|
+
} else {
|
|
28
|
+
const obj = value;
|
|
29
|
+
result = Object.fromEntries(
|
|
30
|
+
Object.keys(obj).sort().map((k) => [k, _canonicalise(obj[k], seen)])
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
seen.delete(value);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
function hashArgs(args) {
|
|
37
|
+
const canonical = JSON.stringify(canonicalise(args) ?? null);
|
|
38
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
|
|
39
|
+
}
|
|
40
|
+
var init_hasher = __esm({
|
|
41
|
+
"src/audit/hasher.ts"() {
|
|
42
|
+
"use strict";
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
12
46
|
// src/audit/index.ts
|
|
47
|
+
var audit_exports = {};
|
|
48
|
+
__export(audit_exports, {
|
|
49
|
+
HOOK_DEBUG_LOG: () => HOOK_DEBUG_LOG,
|
|
50
|
+
LOCAL_AUDIT_LOG: () => LOCAL_AUDIT_LOG,
|
|
51
|
+
appendConfigAudit: () => appendConfigAudit,
|
|
52
|
+
appendHookDebug: () => appendHookDebug,
|
|
53
|
+
appendLocalAudit: () => appendLocalAudit,
|
|
54
|
+
appendToLog: () => appendToLog,
|
|
55
|
+
redactSecrets: () => redactSecrets
|
|
56
|
+
});
|
|
13
57
|
import fs from "fs";
|
|
14
58
|
import path from "path";
|
|
15
59
|
import os from "os";
|
|
@@ -34,24 +78,24 @@ function appendToLog(logPath, entry) {
|
|
|
34
78
|
} catch {
|
|
35
79
|
}
|
|
36
80
|
}
|
|
37
|
-
function appendHookDebug(toolName, args, meta) {
|
|
38
|
-
const
|
|
81
|
+
function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
82
|
+
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
39
83
|
appendToLog(HOOK_DEBUG_LOG, {
|
|
40
84
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
41
85
|
tool: toolName,
|
|
42
|
-
|
|
86
|
+
...argsField,
|
|
43
87
|
agent: meta?.agent,
|
|
44
88
|
mcpServer: meta?.mcpServer,
|
|
45
89
|
hostname: os.hostname(),
|
|
46
90
|
cwd: process.cwd()
|
|
47
91
|
});
|
|
48
92
|
}
|
|
49
|
-
function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
50
|
-
const
|
|
93
|
+
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
94
|
+
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
51
95
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
52
96
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
53
97
|
tool: toolName,
|
|
54
|
-
|
|
98
|
+
...argsField,
|
|
55
99
|
decision,
|
|
56
100
|
checkedBy,
|
|
57
101
|
agent: meta?.agent,
|
|
@@ -70,6 +114,7 @@ var LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG;
|
|
|
70
114
|
var init_audit = __esm({
|
|
71
115
|
"src/audit/index.ts"() {
|
|
72
116
|
"use strict";
|
|
117
|
+
init_hasher();
|
|
73
118
|
LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
|
|
74
119
|
HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
|
|
75
120
|
}
|
|
@@ -94,8 +139,8 @@ function sanitizeConfig(raw) {
|
|
|
94
139
|
}
|
|
95
140
|
}
|
|
96
141
|
const lines = result.error.issues.map((issue) => {
|
|
97
|
-
const
|
|
98
|
-
return ` \u2022 ${
|
|
142
|
+
const path28 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
143
|
+
return ` \u2022 ${path28}: ${issue.message}`;
|
|
99
144
|
});
|
|
100
145
|
return {
|
|
101
146
|
sanitized,
|
|
@@ -167,7 +212,8 @@ var init_config_schema = __esm({
|
|
|
167
212
|
environment: z.string().optional(),
|
|
168
213
|
slackEnabled: z.boolean().optional(),
|
|
169
214
|
enableTrustSessions: z.boolean().optional(),
|
|
170
|
-
allowGlobalPause: z.boolean().optional()
|
|
215
|
+
allowGlobalPause: z.boolean().optional(),
|
|
216
|
+
auditHashArgs: z.boolean().optional()
|
|
171
217
|
}).optional(),
|
|
172
218
|
policy: z.object({
|
|
173
219
|
sandboxPaths: z.array(z.string()).optional(),
|
|
@@ -725,6 +771,7 @@ var init_config = __esm({
|
|
|
725
771
|
approvalTimeoutMs: 12e4,
|
|
726
772
|
// 120-second auto-deny timeout
|
|
727
773
|
flightRecorder: true,
|
|
774
|
+
auditHashArgs: true,
|
|
728
775
|
approvers: { native: true, browser: true, cloud: false, terminal: true }
|
|
729
776
|
},
|
|
730
777
|
policy: {
|
|
@@ -1694,9 +1741,9 @@ function matchesPattern(text, patterns) {
|
|
|
1694
1741
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1695
1742
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1696
1743
|
}
|
|
1697
|
-
function getNestedValue(obj,
|
|
1744
|
+
function getNestedValue(obj, path28) {
|
|
1698
1745
|
if (!obj || typeof obj !== "object") return null;
|
|
1699
|
-
return
|
|
1746
|
+
return path28.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1700
1747
|
}
|
|
1701
1748
|
function shouldSnapshot(toolName, args, config) {
|
|
1702
1749
|
if (!config.settings.enableUndo) return false;
|
|
@@ -2471,6 +2518,58 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
|
2471
2518
|
const { id, allowCount } = await res.json();
|
|
2472
2519
|
return { id, allowCount: allowCount ?? 1 };
|
|
2473
2520
|
}
|
|
2521
|
+
async function notifyTaint(filePath, source) {
|
|
2522
|
+
if (!isDaemonRunning()) return;
|
|
2523
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2524
|
+
try {
|
|
2525
|
+
await fetch(`${base}/taint`, {
|
|
2526
|
+
method: "POST",
|
|
2527
|
+
headers: { "Content-Type": "application/json" },
|
|
2528
|
+
body: JSON.stringify({ path: filePath, source }),
|
|
2529
|
+
signal: AbortSignal.timeout(1e3)
|
|
2530
|
+
});
|
|
2531
|
+
} catch {
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
async function notifyTaintPropagate(src, dest, clearSource = false) {
|
|
2535
|
+
if (!isDaemonRunning()) return;
|
|
2536
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2537
|
+
try {
|
|
2538
|
+
await fetch(`${base}/taint/propagate`, {
|
|
2539
|
+
method: "POST",
|
|
2540
|
+
headers: { "Content-Type": "application/json" },
|
|
2541
|
+
body: JSON.stringify({ src, dest, clearSource }),
|
|
2542
|
+
signal: AbortSignal.timeout(1e3)
|
|
2543
|
+
});
|
|
2544
|
+
} catch {
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
async function checkTaint(paths) {
|
|
2548
|
+
if (paths.length === 0) return { tainted: false };
|
|
2549
|
+
if (!isDaemonRunning()) return { tainted: false, daemonUnavailable: true };
|
|
2550
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2551
|
+
try {
|
|
2552
|
+
const res = await fetch(`${base}/taint/check`, {
|
|
2553
|
+
method: "POST",
|
|
2554
|
+
headers: { "Content-Type": "application/json" },
|
|
2555
|
+
body: JSON.stringify({ paths }),
|
|
2556
|
+
signal: AbortSignal.timeout(2e3)
|
|
2557
|
+
});
|
|
2558
|
+
return await res.json();
|
|
2559
|
+
} catch (err) {
|
|
2560
|
+
try {
|
|
2561
|
+
const { appendToLog: appendToLog2, HOOK_DEBUG_LOG: HOOK_DEBUG_LOG2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
|
|
2562
|
+
appendToLog2(HOOK_DEBUG_LOG2, {
|
|
2563
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2564
|
+
event: "checkTaint-error",
|
|
2565
|
+
error: String(err),
|
|
2566
|
+
paths
|
|
2567
|
+
});
|
|
2568
|
+
} catch {
|
|
2569
|
+
}
|
|
2570
|
+
return { tainted: false, daemonUnavailable: true };
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2474
2573
|
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
2475
2574
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2476
2575
|
await fetch(`${base}/resolve/${id}`, {
|
|
@@ -2941,6 +3040,40 @@ import net from "net";
|
|
|
2941
3040
|
import path13 from "path";
|
|
2942
3041
|
import os10 from "os";
|
|
2943
3042
|
import { randomUUID } from "crypto";
|
|
3043
|
+
function isWriteTool(toolName) {
|
|
3044
|
+
const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
3045
|
+
return WRITE_TOOLS.has(t);
|
|
3046
|
+
}
|
|
3047
|
+
function extractFilePaths(toolName, args) {
|
|
3048
|
+
const paths = [];
|
|
3049
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return paths;
|
|
3050
|
+
const a = args;
|
|
3051
|
+
for (const key of ["file_path", "path", "filename", "source", "src", "input"]) {
|
|
3052
|
+
if (typeof a[key] === "string" && a[key]) paths.push(a[key]);
|
|
3053
|
+
}
|
|
3054
|
+
const cmd = typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : "";
|
|
3055
|
+
if (cmd) {
|
|
3056
|
+
for (const m of cmd.matchAll(/(?:-T|--upload-file|--data(?:-binary)?)\s+@?(\S+)/g)) {
|
|
3057
|
+
paths.push(m[1]);
|
|
3058
|
+
}
|
|
3059
|
+
for (const m of cmd.matchAll(/\b(?:scp|rsync)\s+(?:-\S+\s+)*(\S+)\s+\S+@/g)) {
|
|
3060
|
+
paths.push(m[1]);
|
|
3061
|
+
}
|
|
3062
|
+
for (const m of cmd.matchAll(/<\s*(\S+)/g)) {
|
|
3063
|
+
paths.push(m[1]);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
return paths.filter(Boolean);
|
|
3067
|
+
}
|
|
3068
|
+
function isNetworkTool(toolName, args) {
|
|
3069
|
+
const t = toolName.toLowerCase();
|
|
3070
|
+
if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal.execute") {
|
|
3071
|
+
const a = args;
|
|
3072
|
+
const cmd = typeof a?.command === "string" ? a.command : typeof a?.cmd === "string" ? a.cmd : "";
|
|
3073
|
+
return /\b(curl|wget|scp|rsync|nc|ncat|netcat|ssh)\b/.test(cmd);
|
|
3074
|
+
}
|
|
3075
|
+
return false;
|
|
3076
|
+
}
|
|
2944
3077
|
function notifyActivity(data) {
|
|
2945
3078
|
return new Promise((resolve) => {
|
|
2946
3079
|
try {
|
|
@@ -2970,7 +3103,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
2970
3103
|
id: actId,
|
|
2971
3104
|
tool: toolName,
|
|
2972
3105
|
ts: actTs,
|
|
2973
|
-
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
3106
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
2974
3107
|
label: result.blockedByLabel
|
|
2975
3108
|
});
|
|
2976
3109
|
}
|
|
@@ -2984,6 +3117,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2984
3117
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
2985
3118
|
const creds = getCredentials();
|
|
2986
3119
|
const config = getConfig(options?.cwd);
|
|
3120
|
+
const hashAuditArgs = config.settings.auditHashArgs === true;
|
|
2987
3121
|
const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
|
|
2988
3122
|
const approvers = {
|
|
2989
3123
|
...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
|
|
@@ -2994,13 +3128,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2994
3128
|
approvers.terminal = false;
|
|
2995
3129
|
}
|
|
2996
3130
|
if (config.settings.enableHookLogDebug && !isTestEnv2) {
|
|
2997
|
-
appendHookDebug(toolName, args, meta);
|
|
3131
|
+
appendHookDebug(toolName, args, meta, hashAuditArgs);
|
|
2998
3132
|
}
|
|
2999
3133
|
const isManual = meta?.agent === "Terminal";
|
|
3000
3134
|
let explainableLabel = "Local Config";
|
|
3001
3135
|
let policyMatchedField;
|
|
3002
3136
|
let policyMatchedWord;
|
|
3003
3137
|
let riskMetadata;
|
|
3138
|
+
let taintWarning = null;
|
|
3139
|
+
if (isNetworkTool(toolName, args)) {
|
|
3140
|
+
const filePaths = extractFilePaths(toolName, args);
|
|
3141
|
+
if (filePaths.length > 0) {
|
|
3142
|
+
const taintResult = await checkTaint(filePaths);
|
|
3143
|
+
if (taintResult.tainted && taintResult.record) {
|
|
3144
|
+
const { path: taintedPath, source: taintSource } = taintResult.record;
|
|
3145
|
+
taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
|
|
3146
|
+
} else if (taintResult.daemonUnavailable) {
|
|
3147
|
+
taintWarning = `\u26A0\uFE0F Taint service unavailable \u2014 cannot verify if ${filePaths.join(", ")} is clean`;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3004
3151
|
if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
3005
3152
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
3006
3153
|
const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
|
|
@@ -3008,7 +3155,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3008
3155
|
if (dlpMatch) {
|
|
3009
3156
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
3010
3157
|
if (dlpMatch.severity === "block") {
|
|
3011
|
-
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
|
|
3158
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta, true);
|
|
3159
|
+
if (isWriteTool(toolName) && filePath) {
|
|
3160
|
+
await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
|
|
3161
|
+
}
|
|
3012
3162
|
return {
|
|
3013
3163
|
approved: false,
|
|
3014
3164
|
reason: dlpReason,
|
|
@@ -3016,7 +3166,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3016
3166
|
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
3017
3167
|
};
|
|
3018
3168
|
}
|
|
3019
|
-
if (!isManual)
|
|
3169
|
+
if (!isManual)
|
|
3170
|
+
appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta, hashAuditArgs);
|
|
3020
3171
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
3021
3172
|
}
|
|
3022
3173
|
}
|
|
@@ -3024,7 +3175,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3024
3175
|
if (!isIgnoredTool(toolName)) {
|
|
3025
3176
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
3026
3177
|
if (policyResult.decision === "review") {
|
|
3027
|
-
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
3178
|
+
appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
|
|
3028
3179
|
if (approvers.cloud && creds?.apiKey) {
|
|
3029
3180
|
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
3030
3181
|
}
|
|
@@ -3032,22 +3183,23 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3032
3183
|
}
|
|
3033
3184
|
return { approved: true, checkedBy: "audit" };
|
|
3034
3185
|
}
|
|
3035
|
-
if (!isIgnoredTool(toolName)) {
|
|
3186
|
+
if (!taintWarning && !isIgnoredTool(toolName)) {
|
|
3036
3187
|
if (getActiveTrustSession(toolName)) {
|
|
3037
3188
|
if (approvers.cloud && creds?.apiKey)
|
|
3038
3189
|
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
3039
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
3190
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
|
|
3040
3191
|
return { approved: true, checkedBy: "trust" };
|
|
3041
3192
|
}
|
|
3042
3193
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
3043
3194
|
if (policyResult.decision === "allow") {
|
|
3044
3195
|
if (approvers.cloud && creds?.apiKey)
|
|
3045
3196
|
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
3046
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
3197
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
|
|
3047
3198
|
return { approved: true, checkedBy: "local-policy" };
|
|
3048
3199
|
}
|
|
3049
3200
|
if (policyResult.decision === "block") {
|
|
3050
|
-
if (!isManual)
|
|
3201
|
+
if (!isManual)
|
|
3202
|
+
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
3051
3203
|
return {
|
|
3052
3204
|
approved: false,
|
|
3053
3205
|
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
@@ -3066,15 +3218,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3066
3218
|
policyMatchedWord,
|
|
3067
3219
|
policyResult.ruleName
|
|
3068
3220
|
);
|
|
3069
|
-
const persistent = getPersistentDecision(toolName);
|
|
3221
|
+
const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
|
|
3070
3222
|
if (persistent === "allow") {
|
|
3071
3223
|
if (approvers.cloud && creds?.apiKey)
|
|
3072
3224
|
await auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
3073
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
3225
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
|
|
3074
3226
|
return { approved: true, checkedBy: "persistent" };
|
|
3075
3227
|
}
|
|
3076
3228
|
if (persistent === "deny") {
|
|
3077
|
-
if (!isManual)
|
|
3229
|
+
if (!isManual)
|
|
3230
|
+
appendLocalAudit(toolName, args, "deny", "persistent-deny", meta, hashAuditArgs);
|
|
3078
3231
|
return {
|
|
3079
3232
|
approved: false,
|
|
3080
3233
|
reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
|
|
@@ -3082,10 +3235,21 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3082
3235
|
blockedByLabel: "Persistent User Rule"
|
|
3083
3236
|
};
|
|
3084
3237
|
}
|
|
3085
|
-
} else {
|
|
3086
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
3238
|
+
} else if (!taintWarning) {
|
|
3239
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
|
|
3087
3240
|
return { approved: true };
|
|
3088
3241
|
}
|
|
3242
|
+
if (taintWarning) {
|
|
3243
|
+
explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
|
|
3244
|
+
riskMetadata = computeRiskMetadata(
|
|
3245
|
+
args,
|
|
3246
|
+
7,
|
|
3247
|
+
explainableLabel,
|
|
3248
|
+
void 0,
|
|
3249
|
+
void 0,
|
|
3250
|
+
taintWarning
|
|
3251
|
+
);
|
|
3252
|
+
}
|
|
3089
3253
|
let cloudRequestId = null;
|
|
3090
3254
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
3091
3255
|
if (cloudEnforced) {
|
|
@@ -3104,7 +3268,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3104
3268
|
};
|
|
3105
3269
|
}
|
|
3106
3270
|
cloudRequestId = initResult.requestId || null;
|
|
3107
|
-
explainableLabel = "Organization Policy (SaaS)";
|
|
3271
|
+
if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
|
|
3108
3272
|
} catch {
|
|
3109
3273
|
}
|
|
3110
3274
|
}
|
|
@@ -3291,12 +3455,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3291
3455
|
args,
|
|
3292
3456
|
finalResult.approved ? "allow" : "deny",
|
|
3293
3457
|
finalResult.checkedBy || finalResult.blockedBy || "unknown",
|
|
3294
|
-
meta
|
|
3458
|
+
meta,
|
|
3459
|
+
hashAuditArgs
|
|
3295
3460
|
);
|
|
3296
3461
|
}
|
|
3297
3462
|
return finalResult;
|
|
3298
3463
|
}
|
|
3299
|
-
var ACTIVITY_SOCKET_PATH;
|
|
3464
|
+
var WRITE_TOOLS, ACTIVITY_SOCKET_PATH;
|
|
3300
3465
|
var init_orchestrator = __esm({
|
|
3301
3466
|
"src/auth/orchestrator.ts"() {
|
|
3302
3467
|
"use strict";
|
|
@@ -3309,6 +3474,17 @@ var init_orchestrator = __esm({
|
|
|
3309
3474
|
init_state();
|
|
3310
3475
|
init_daemon();
|
|
3311
3476
|
init_cloud();
|
|
3477
|
+
WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
3478
|
+
"write",
|
|
3479
|
+
"write_file",
|
|
3480
|
+
"create_file",
|
|
3481
|
+
"edit",
|
|
3482
|
+
"multiedit",
|
|
3483
|
+
"str_replace_based_edit_tool",
|
|
3484
|
+
"replace",
|
|
3485
|
+
"notebook_edit",
|
|
3486
|
+
"notebookedit"
|
|
3487
|
+
]);
|
|
3312
3488
|
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os10.tmpdir(), "node9-activity.sock");
|
|
3313
3489
|
}
|
|
3314
3490
|
});
|
|
@@ -4493,12 +4669,15 @@ var init_ui = __esm({
|
|
|
4493
4669
|
const badgeClass = isEdit ? 'sniper-badge-edit' : 'sniper-badge-exec';
|
|
4494
4670
|
const badgeLabel = isEdit ? '\u{1F4DD} Code Edit' : '\u{1F6D1} Execution';
|
|
4495
4671
|
const tierLabel = \`Tier \${rm.tier} \xB7 \${esc(rm.blockedByLabel)}\`;
|
|
4672
|
+
const isTaint = rm.blockedByLabel?.includes('Taint');
|
|
4496
4673
|
const fileLine =
|
|
4497
|
-
|
|
4498
|
-
? \`<div class="sniper-
|
|
4499
|
-
:
|
|
4500
|
-
? \`<div class="sniper-
|
|
4501
|
-
:
|
|
4674
|
+
isTaint && rm.ruleName
|
|
4675
|
+
? \`<div class="sniper-match">\u26A0\uFE0F \${esc(rm.ruleName)}</div>\`
|
|
4676
|
+
: isEdit && rm.editFilePath
|
|
4677
|
+
? \`<div class="sniper-filepath">\u{1F4C2} \${esc(rm.editFilePath)}</div>\`
|
|
4678
|
+
: !isEdit && rm.matchedWord
|
|
4679
|
+
? \`<div class="sniper-match">Matched: <code>\${esc(rm.matchedWord)}</code>\${rm.matchedField ? \` in <code>\${esc(rm.matchedField)}</code>\` : ''}</div>\`
|
|
4680
|
+
: '';
|
|
4502
4681
|
const snippetHtml = rm.contextSnippet ? \`<pre>\${esc(rm.contextSnippet)}</pre>\` : '';
|
|
4503
4682
|
return \`
|
|
4504
4683
|
<div class="sniper-header">
|
|
@@ -5032,17 +5211,97 @@ var init_suggestion_tracker = __esm({
|
|
|
5032
5211
|
}
|
|
5033
5212
|
});
|
|
5034
5213
|
|
|
5035
|
-
// src/daemon/
|
|
5036
|
-
import net2 from "net";
|
|
5214
|
+
// src/daemon/taint-store.ts
|
|
5037
5215
|
import fs12 from "fs";
|
|
5038
5216
|
import path15 from "path";
|
|
5217
|
+
var DEFAULT_TTL_MS, TaintStore;
|
|
5218
|
+
var init_taint_store = __esm({
|
|
5219
|
+
"src/daemon/taint-store.ts"() {
|
|
5220
|
+
"use strict";
|
|
5221
|
+
DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
5222
|
+
TaintStore = class {
|
|
5223
|
+
records = /* @__PURE__ */ new Map();
|
|
5224
|
+
/** Add or refresh taint on an absolute path. */
|
|
5225
|
+
taint(filePath, source, ttlMs = DEFAULT_TTL_MS) {
|
|
5226
|
+
const resolved = this._resolve(filePath);
|
|
5227
|
+
const now = Date.now();
|
|
5228
|
+
this.records.set(resolved, {
|
|
5229
|
+
path: resolved,
|
|
5230
|
+
source,
|
|
5231
|
+
createdAt: now,
|
|
5232
|
+
expiresAt: now + ttlMs
|
|
5233
|
+
});
|
|
5234
|
+
}
|
|
5235
|
+
/**
|
|
5236
|
+
* Check whether a path is currently tainted.
|
|
5237
|
+
* Returns the TaintRecord if tainted (and not expired), null otherwise.
|
|
5238
|
+
* Expired records are pruned on access.
|
|
5239
|
+
*/
|
|
5240
|
+
check(filePath) {
|
|
5241
|
+
const resolved = this._resolve(filePath);
|
|
5242
|
+
const record = this.records.get(resolved);
|
|
5243
|
+
if (!record) return null;
|
|
5244
|
+
if (Date.now() > record.expiresAt) {
|
|
5245
|
+
this.records.delete(resolved);
|
|
5246
|
+
return null;
|
|
5247
|
+
}
|
|
5248
|
+
return record;
|
|
5249
|
+
}
|
|
5250
|
+
/**
|
|
5251
|
+
* Propagate taint from sourcePath to destPath (e.g. cp, mv).
|
|
5252
|
+
* For mv semantics (clearSource=true) the source taint is removed.
|
|
5253
|
+
*/
|
|
5254
|
+
propagate(sourcePath, destPath, clearSource = false) {
|
|
5255
|
+
const taintRecord = this.check(sourcePath);
|
|
5256
|
+
if (!taintRecord) return;
|
|
5257
|
+
const remainingMs = taintRecord.expiresAt - Date.now();
|
|
5258
|
+
if (remainingMs > 0) {
|
|
5259
|
+
const baseSource = taintRecord.source.replace(/^(propagated:)+/, "");
|
|
5260
|
+
this.taint(destPath, `propagated:${baseSource}`, remainingMs);
|
|
5261
|
+
}
|
|
5262
|
+
if (clearSource) {
|
|
5263
|
+
this.records.delete(this._resolve(sourcePath));
|
|
5264
|
+
}
|
|
5265
|
+
}
|
|
5266
|
+
/** Remove all expired records. Called periodically by the daemon. */
|
|
5267
|
+
prune() {
|
|
5268
|
+
const now = Date.now();
|
|
5269
|
+
for (const [key, record] of this.records) {
|
|
5270
|
+
if (now > record.expiresAt) this.records.delete(key);
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
/** Return all non-expired taint records (for audit/debug). */
|
|
5274
|
+
list() {
|
|
5275
|
+
this.prune();
|
|
5276
|
+
return [...this.records.values()];
|
|
5277
|
+
}
|
|
5278
|
+
/** Remove all taint records atomically. Used by tests to reset state between runs. */
|
|
5279
|
+
clear() {
|
|
5280
|
+
this.records.clear();
|
|
5281
|
+
}
|
|
5282
|
+
/** Resolve to absolute path, falling back to path.resolve if file doesn't exist yet. */
|
|
5283
|
+
_resolve(filePath) {
|
|
5284
|
+
try {
|
|
5285
|
+
return fs12.realpathSync.native(path15.resolve(filePath));
|
|
5286
|
+
} catch {
|
|
5287
|
+
return path15.resolve(filePath);
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5290
|
+
};
|
|
5291
|
+
}
|
|
5292
|
+
});
|
|
5293
|
+
|
|
5294
|
+
// src/daemon/state.ts
|
|
5295
|
+
import net2 from "net";
|
|
5296
|
+
import fs13 from "fs";
|
|
5297
|
+
import path16 from "path";
|
|
5039
5298
|
import os12 from "os";
|
|
5040
5299
|
import { spawn as spawn2 } from "child_process";
|
|
5041
5300
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5042
5301
|
function loadInsightCounts() {
|
|
5043
5302
|
try {
|
|
5044
|
-
if (!
|
|
5045
|
-
const data = JSON.parse(
|
|
5303
|
+
if (!fs13.existsSync(INSIGHT_COUNTS_FILE)) return;
|
|
5304
|
+
const data = JSON.parse(fs13.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
|
|
5046
5305
|
for (const [tool, count] of Object.entries(data)) {
|
|
5047
5306
|
if (typeof count === "number" && count > 0) insightCounts.set(tool, count);
|
|
5048
5307
|
}
|
|
@@ -5081,23 +5340,23 @@ function markRejectionHandlerRegistered() {
|
|
|
5081
5340
|
daemonRejectionHandlerRegistered = true;
|
|
5082
5341
|
}
|
|
5083
5342
|
function atomicWriteSync2(filePath, data, options) {
|
|
5084
|
-
const dir =
|
|
5085
|
-
if (!
|
|
5343
|
+
const dir = path16.dirname(filePath);
|
|
5344
|
+
if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
|
|
5086
5345
|
const tmpPath = `${filePath}.${randomUUID3()}.tmp`;
|
|
5087
5346
|
try {
|
|
5088
|
-
|
|
5347
|
+
fs13.writeFileSync(tmpPath, data, options);
|
|
5089
5348
|
} catch (err) {
|
|
5090
5349
|
try {
|
|
5091
|
-
|
|
5350
|
+
fs13.unlinkSync(tmpPath);
|
|
5092
5351
|
} catch {
|
|
5093
5352
|
}
|
|
5094
5353
|
throw err;
|
|
5095
5354
|
}
|
|
5096
5355
|
try {
|
|
5097
|
-
|
|
5356
|
+
fs13.renameSync(tmpPath, filePath);
|
|
5098
5357
|
} catch (err) {
|
|
5099
5358
|
try {
|
|
5100
|
-
|
|
5359
|
+
fs13.unlinkSync(tmpPath);
|
|
5101
5360
|
} catch {
|
|
5102
5361
|
}
|
|
5103
5362
|
throw err;
|
|
@@ -5121,16 +5380,16 @@ function appendAuditLog(data) {
|
|
|
5121
5380
|
decision: data.decision,
|
|
5122
5381
|
source: "daemon"
|
|
5123
5382
|
};
|
|
5124
|
-
const dir =
|
|
5125
|
-
if (!
|
|
5126
|
-
|
|
5383
|
+
const dir = path16.dirname(AUDIT_LOG_FILE);
|
|
5384
|
+
if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
|
|
5385
|
+
fs13.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
5127
5386
|
} catch {
|
|
5128
5387
|
}
|
|
5129
5388
|
}
|
|
5130
5389
|
function getAuditHistory(limit = 20) {
|
|
5131
5390
|
try {
|
|
5132
|
-
if (!
|
|
5133
|
-
const lines =
|
|
5391
|
+
if (!fs13.existsSync(AUDIT_LOG_FILE)) return [];
|
|
5392
|
+
const lines = fs13.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
|
|
5134
5393
|
if (lines.length === 1 && lines[0] === "") return [];
|
|
5135
5394
|
return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
|
|
5136
5395
|
} catch {
|
|
@@ -5139,19 +5398,19 @@ function getAuditHistory(limit = 20) {
|
|
|
5139
5398
|
}
|
|
5140
5399
|
function getOrgName() {
|
|
5141
5400
|
try {
|
|
5142
|
-
if (
|
|
5401
|
+
if (fs13.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
|
|
5143
5402
|
} catch {
|
|
5144
5403
|
}
|
|
5145
5404
|
return null;
|
|
5146
5405
|
}
|
|
5147
5406
|
function hasStoredSlackKey() {
|
|
5148
|
-
return
|
|
5407
|
+
return fs13.existsSync(CREDENTIALS_FILE);
|
|
5149
5408
|
}
|
|
5150
5409
|
function writeGlobalSetting(key, value) {
|
|
5151
5410
|
let config = {};
|
|
5152
5411
|
try {
|
|
5153
|
-
if (
|
|
5154
|
-
config = JSON.parse(
|
|
5412
|
+
if (fs13.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
5413
|
+
config = JSON.parse(fs13.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
|
|
5155
5414
|
}
|
|
5156
5415
|
} catch {
|
|
5157
5416
|
}
|
|
@@ -5163,8 +5422,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
5163
5422
|
try {
|
|
5164
5423
|
let trust = { entries: [] };
|
|
5165
5424
|
try {
|
|
5166
|
-
if (
|
|
5167
|
-
trust = JSON.parse(
|
|
5425
|
+
if (fs13.existsSync(TRUST_FILE2))
|
|
5426
|
+
trust = JSON.parse(fs13.readFileSync(TRUST_FILE2, "utf-8"));
|
|
5168
5427
|
} catch {
|
|
5169
5428
|
}
|
|
5170
5429
|
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
|
|
@@ -5175,8 +5434,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
5175
5434
|
}
|
|
5176
5435
|
function readPersistentDecisions() {
|
|
5177
5436
|
try {
|
|
5178
|
-
if (
|
|
5179
|
-
return JSON.parse(
|
|
5437
|
+
if (fs13.existsSync(DECISIONS_FILE)) {
|
|
5438
|
+
return JSON.parse(fs13.readFileSync(DECISIONS_FILE, "utf-8"));
|
|
5180
5439
|
}
|
|
5181
5440
|
} catch {
|
|
5182
5441
|
}
|
|
@@ -5241,7 +5500,7 @@ function abandonPending() {
|
|
|
5241
5500
|
});
|
|
5242
5501
|
if (autoStarted) {
|
|
5243
5502
|
try {
|
|
5244
|
-
|
|
5503
|
+
fs13.unlinkSync(DAEMON_PID_FILE);
|
|
5245
5504
|
} catch {
|
|
5246
5505
|
}
|
|
5247
5506
|
setTimeout(() => {
|
|
@@ -5252,7 +5511,7 @@ function abandonPending() {
|
|
|
5252
5511
|
}
|
|
5253
5512
|
function startActivitySocket() {
|
|
5254
5513
|
try {
|
|
5255
|
-
|
|
5514
|
+
fs13.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
5256
5515
|
} catch {
|
|
5257
5516
|
}
|
|
5258
5517
|
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
@@ -5294,29 +5553,31 @@ function startActivitySocket() {
|
|
|
5294
5553
|
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
5295
5554
|
process.on("exit", () => {
|
|
5296
5555
|
try {
|
|
5297
|
-
|
|
5556
|
+
fs13.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
5298
5557
|
} catch {
|
|
5299
5558
|
}
|
|
5300
5559
|
});
|
|
5301
5560
|
}
|
|
5302
|
-
var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
|
|
5561
|
+
var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
|
|
5303
5562
|
var init_state2 = __esm({
|
|
5304
5563
|
"src/daemon/state.ts"() {
|
|
5305
5564
|
"use strict";
|
|
5306
5565
|
init_daemon();
|
|
5307
5566
|
init_suggestion_tracker();
|
|
5567
|
+
init_taint_store();
|
|
5308
5568
|
homeDir = os12.homedir();
|
|
5309
|
-
DAEMON_PID_FILE =
|
|
5310
|
-
DECISIONS_FILE =
|
|
5311
|
-
AUDIT_LOG_FILE =
|
|
5312
|
-
TRUST_FILE2 =
|
|
5313
|
-
GLOBAL_CONFIG_FILE =
|
|
5314
|
-
CREDENTIALS_FILE =
|
|
5315
|
-
INSIGHT_COUNTS_FILE =
|
|
5569
|
+
DAEMON_PID_FILE = path16.join(homeDir, ".node9", "daemon.pid");
|
|
5570
|
+
DECISIONS_FILE = path16.join(homeDir, ".node9", "decisions.json");
|
|
5571
|
+
AUDIT_LOG_FILE = path16.join(homeDir, ".node9", "audit.log");
|
|
5572
|
+
TRUST_FILE2 = path16.join(homeDir, ".node9", "trust.json");
|
|
5573
|
+
GLOBAL_CONFIG_FILE = path16.join(homeDir, ".node9", "config.json");
|
|
5574
|
+
CREDENTIALS_FILE = path16.join(homeDir, ".node9", "credentials.json");
|
|
5575
|
+
INSIGHT_COUNTS_FILE = path16.join(homeDir, ".node9", "insight-counts.json");
|
|
5316
5576
|
pending = /* @__PURE__ */ new Map();
|
|
5317
5577
|
sseClients = /* @__PURE__ */ new Set();
|
|
5318
5578
|
suggestionTracker = new SuggestionTracker(3);
|
|
5319
5579
|
suggestions = /* @__PURE__ */ new Map();
|
|
5580
|
+
taintStore = new TaintStore();
|
|
5320
5581
|
insightCounts = /* @__PURE__ */ new Map();
|
|
5321
5582
|
_abandonTimer = null;
|
|
5322
5583
|
_hadBrowserClient = false;
|
|
@@ -5329,7 +5590,7 @@ var init_state2 = __esm({
|
|
|
5329
5590
|
"2h": 2 * 60 * 6e4
|
|
5330
5591
|
};
|
|
5331
5592
|
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
5332
|
-
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
5593
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path16.join(os12.tmpdir(), "node9-activity.sock");
|
|
5333
5594
|
ACTIVITY_RING_SIZE = 100;
|
|
5334
5595
|
activityRing = [];
|
|
5335
5596
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
@@ -5337,14 +5598,14 @@ var init_state2 = __esm({
|
|
|
5337
5598
|
});
|
|
5338
5599
|
|
|
5339
5600
|
// src/config/patch.ts
|
|
5340
|
-
import
|
|
5341
|
-
import
|
|
5601
|
+
import fs14 from "fs";
|
|
5602
|
+
import path17 from "path";
|
|
5342
5603
|
import os13 from "os";
|
|
5343
5604
|
function patchConfig(configPath, patch) {
|
|
5344
5605
|
let config = {};
|
|
5345
5606
|
try {
|
|
5346
|
-
if (
|
|
5347
|
-
config = JSON.parse(
|
|
5607
|
+
if (fs14.existsSync(configPath)) {
|
|
5608
|
+
config = JSON.parse(fs14.readFileSync(configPath, "utf8"));
|
|
5348
5609
|
}
|
|
5349
5610
|
} catch {
|
|
5350
5611
|
throw new Error(`Cannot read config at ${configPath} \u2014 file may be corrupted`);
|
|
@@ -5363,23 +5624,23 @@ function patchConfig(configPath, patch) {
|
|
|
5363
5624
|
ignored.push(patch.toolName);
|
|
5364
5625
|
}
|
|
5365
5626
|
}
|
|
5366
|
-
const dir =
|
|
5367
|
-
|
|
5627
|
+
const dir = path17.dirname(configPath);
|
|
5628
|
+
fs14.mkdirSync(dir, { recursive: true });
|
|
5368
5629
|
const tmp = configPath + ".node9-tmp";
|
|
5369
5630
|
try {
|
|
5370
|
-
|
|
5631
|
+
fs14.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
|
|
5371
5632
|
} catch (err) {
|
|
5372
5633
|
try {
|
|
5373
|
-
|
|
5634
|
+
fs14.unlinkSync(tmp);
|
|
5374
5635
|
} catch {
|
|
5375
5636
|
}
|
|
5376
5637
|
throw err;
|
|
5377
5638
|
}
|
|
5378
5639
|
try {
|
|
5379
|
-
|
|
5640
|
+
fs14.renameSync(tmp, configPath);
|
|
5380
5641
|
} catch (err) {
|
|
5381
5642
|
try {
|
|
5382
|
-
|
|
5643
|
+
fs14.unlinkSync(tmp);
|
|
5383
5644
|
} catch {
|
|
5384
5645
|
}
|
|
5385
5646
|
throw err;
|
|
@@ -5389,14 +5650,14 @@ var GLOBAL_CONFIG_PATH;
|
|
|
5389
5650
|
var init_patch = __esm({
|
|
5390
5651
|
"src/config/patch.ts"() {
|
|
5391
5652
|
"use strict";
|
|
5392
|
-
GLOBAL_CONFIG_PATH =
|
|
5653
|
+
GLOBAL_CONFIG_PATH = path17.join(os13.homedir(), ".node9", "config.json");
|
|
5393
5654
|
}
|
|
5394
5655
|
});
|
|
5395
5656
|
|
|
5396
5657
|
// src/daemon/server.ts
|
|
5397
5658
|
import http from "http";
|
|
5398
|
-
import
|
|
5399
|
-
import
|
|
5659
|
+
import fs15 from "fs";
|
|
5660
|
+
import path18 from "path";
|
|
5400
5661
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
5401
5662
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5402
5663
|
import chalk2 from "chalk";
|
|
@@ -5416,7 +5677,7 @@ function startDaemon() {
|
|
|
5416
5677
|
idleTimer = setTimeout(() => {
|
|
5417
5678
|
if (autoStarted) {
|
|
5418
5679
|
try {
|
|
5419
|
-
|
|
5680
|
+
fs15.unlinkSync(DAEMON_PID_FILE);
|
|
5420
5681
|
} catch {
|
|
5421
5682
|
}
|
|
5422
5683
|
}
|
|
@@ -5571,7 +5832,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5571
5832
|
status: "pending"
|
|
5572
5833
|
});
|
|
5573
5834
|
}
|
|
5574
|
-
const projectCwd = typeof cwd === "string" &&
|
|
5835
|
+
const projectCwd = typeof cwd === "string" && path18.isAbsolute(cwd) ? cwd : void 0;
|
|
5575
5836
|
const projectConfig = getConfig(projectCwd);
|
|
5576
5837
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
5577
5838
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -5922,8 +6183,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
5922
6183
|
const body = await readBody(req);
|
|
5923
6184
|
const data = body ? JSON.parse(body) : {};
|
|
5924
6185
|
const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
|
|
5925
|
-
const node9Dir =
|
|
5926
|
-
if (!
|
|
6186
|
+
const node9Dir = path18.dirname(GLOBAL_CONFIG_PATH);
|
|
6187
|
+
if (!path18.resolve(configPath).startsWith(node9Dir + path18.sep)) {
|
|
5927
6188
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5928
6189
|
return res.end(
|
|
5929
6190
|
JSON.stringify({ error: "configPath must be within the node9 config directory" })
|
|
@@ -5971,20 +6232,77 @@ data: ${JSON.stringify(item.data)}
|
|
|
5971
6232
|
res.writeHead(400).end();
|
|
5972
6233
|
}
|
|
5973
6234
|
}
|
|
6235
|
+
if (req.method === "POST" && pathname === "/taint") {
|
|
6236
|
+
try {
|
|
6237
|
+
const body = JSON.parse(await readBody(req));
|
|
6238
|
+
if (typeof body.path !== "string" || typeof body.source !== "string") {
|
|
6239
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6240
|
+
return res.end(JSON.stringify({ error: "path and source are required strings" }));
|
|
6241
|
+
}
|
|
6242
|
+
const ttlMs = typeof body.ttlMs === "number" ? body.ttlMs : void 0;
|
|
6243
|
+
taintStore.taint(body.path, body.source, ttlMs);
|
|
6244
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6245
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
6246
|
+
} catch {
|
|
6247
|
+
res.writeHead(400).end();
|
|
6248
|
+
return;
|
|
6249
|
+
}
|
|
6250
|
+
}
|
|
6251
|
+
if (req.method === "POST" && pathname === "/taint/check") {
|
|
6252
|
+
try {
|
|
6253
|
+
const body = JSON.parse(await readBody(req));
|
|
6254
|
+
if (!Array.isArray(body.paths)) {
|
|
6255
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6256
|
+
return res.end(JSON.stringify({ error: "paths must be an array" }));
|
|
6257
|
+
}
|
|
6258
|
+
if (body.paths.some((p) => typeof p !== "string")) {
|
|
6259
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6260
|
+
return res.end(JSON.stringify({ error: "all paths must be strings" }));
|
|
6261
|
+
}
|
|
6262
|
+
for (const p of body.paths) {
|
|
6263
|
+
const record = taintStore.check(p);
|
|
6264
|
+
if (record) {
|
|
6265
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6266
|
+
return res.end(JSON.stringify({ tainted: true, record }));
|
|
6267
|
+
}
|
|
6268
|
+
}
|
|
6269
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6270
|
+
return res.end(JSON.stringify({ tainted: false }));
|
|
6271
|
+
} catch {
|
|
6272
|
+
res.writeHead(400).end();
|
|
6273
|
+
return;
|
|
6274
|
+
}
|
|
6275
|
+
}
|
|
6276
|
+
if (req.method === "POST" && pathname === "/taint/propagate") {
|
|
6277
|
+
try {
|
|
6278
|
+
const body = JSON.parse(await readBody(req));
|
|
6279
|
+
if (typeof body.src !== "string" || typeof body.dest !== "string") {
|
|
6280
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6281
|
+
return res.end(JSON.stringify({ error: "src and dest are required strings" }));
|
|
6282
|
+
}
|
|
6283
|
+
const clearSource = body.clearSource === true;
|
|
6284
|
+
taintStore.propagate(body.src, body.dest, clearSource);
|
|
6285
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6286
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
6287
|
+
} catch {
|
|
6288
|
+
res.writeHead(400).end();
|
|
6289
|
+
return;
|
|
6290
|
+
}
|
|
6291
|
+
}
|
|
5974
6292
|
res.writeHead(404).end();
|
|
5975
6293
|
});
|
|
5976
6294
|
setDaemonServer(server);
|
|
5977
6295
|
server.on("error", (e) => {
|
|
5978
6296
|
if (e.code === "EADDRINUSE") {
|
|
5979
6297
|
try {
|
|
5980
|
-
if (
|
|
5981
|
-
const { pid } = JSON.parse(
|
|
6298
|
+
if (fs15.existsSync(DAEMON_PID_FILE)) {
|
|
6299
|
+
const { pid } = JSON.parse(fs15.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5982
6300
|
process.kill(pid, 0);
|
|
5983
6301
|
return process.exit(0);
|
|
5984
6302
|
}
|
|
5985
6303
|
} catch {
|
|
5986
6304
|
try {
|
|
5987
|
-
|
|
6305
|
+
fs15.unlinkSync(DAEMON_PID_FILE);
|
|
5988
6306
|
} catch {
|
|
5989
6307
|
}
|
|
5990
6308
|
server.listen(DAEMON_PORT, DAEMON_HOST);
|
|
@@ -6056,28 +6374,28 @@ var init_server = __esm({
|
|
|
6056
6374
|
});
|
|
6057
6375
|
|
|
6058
6376
|
// src/daemon/index.ts
|
|
6059
|
-
import
|
|
6377
|
+
import fs16 from "fs";
|
|
6060
6378
|
import chalk3 from "chalk";
|
|
6061
6379
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
6062
6380
|
function stopDaemon() {
|
|
6063
|
-
if (!
|
|
6381
|
+
if (!fs16.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
|
|
6064
6382
|
try {
|
|
6065
|
-
const { pid } = JSON.parse(
|
|
6383
|
+
const { pid } = JSON.parse(fs16.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
6066
6384
|
process.kill(pid, "SIGTERM");
|
|
6067
6385
|
console.log(chalk3.green("\u2705 Stopped."));
|
|
6068
6386
|
} catch {
|
|
6069
6387
|
console.log(chalk3.gray("Cleaned up stale PID file."));
|
|
6070
6388
|
} finally {
|
|
6071
6389
|
try {
|
|
6072
|
-
|
|
6390
|
+
fs16.unlinkSync(DAEMON_PID_FILE);
|
|
6073
6391
|
} catch {
|
|
6074
6392
|
}
|
|
6075
6393
|
}
|
|
6076
6394
|
}
|
|
6077
6395
|
function daemonStatus() {
|
|
6078
|
-
if (
|
|
6396
|
+
if (fs16.existsSync(DAEMON_PID_FILE)) {
|
|
6079
6397
|
try {
|
|
6080
|
-
const { pid } = JSON.parse(
|
|
6398
|
+
const { pid } = JSON.parse(fs16.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
6081
6399
|
process.kill(pid, 0);
|
|
6082
6400
|
console.log(chalk3.green("Node9 daemon: running"));
|
|
6083
6401
|
return;
|
|
@@ -6112,9 +6430,9 @@ __export(tail_exports, {
|
|
|
6112
6430
|
});
|
|
6113
6431
|
import http2 from "http";
|
|
6114
6432
|
import chalk16 from "chalk";
|
|
6115
|
-
import
|
|
6433
|
+
import fs24 from "fs";
|
|
6116
6434
|
import os21 from "os";
|
|
6117
|
-
import
|
|
6435
|
+
import path26 from "path";
|
|
6118
6436
|
import readline3 from "readline";
|
|
6119
6437
|
import { spawn as spawn9, execSync as execSync3 } from "child_process";
|
|
6120
6438
|
function getIcon(tool) {
|
|
@@ -6154,9 +6472,9 @@ function renderPending(activity) {
|
|
|
6154
6472
|
}
|
|
6155
6473
|
async function ensureDaemon() {
|
|
6156
6474
|
let pidPort = null;
|
|
6157
|
-
if (
|
|
6475
|
+
if (fs24.existsSync(PID_FILE)) {
|
|
6158
6476
|
try {
|
|
6159
|
-
const { port } = JSON.parse(
|
|
6477
|
+
const { port } = JSON.parse(fs24.readFileSync(PID_FILE, "utf-8"));
|
|
6160
6478
|
pidPort = port;
|
|
6161
6479
|
} catch {
|
|
6162
6480
|
console.error(chalk16.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
@@ -6227,9 +6545,12 @@ function buildCardLines(req, localCount = 0) {
|
|
|
6227
6545
|
``,
|
|
6228
6546
|
`${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
|
|
6229
6547
|
`${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
|
|
6230
|
-
`${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}
|
|
6231
|
-
`${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`
|
|
6548
|
+
`${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`
|
|
6232
6549
|
];
|
|
6550
|
+
if (req.riskMetadata?.ruleName && blockedBy.includes("Taint")) {
|
|
6551
|
+
lines.push(`${CYAN}\u2551${RESET} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET}`);
|
|
6552
|
+
}
|
|
6553
|
+
lines.push(`${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`);
|
|
6233
6554
|
if (localCount >= 2) {
|
|
6234
6555
|
lines.push(
|
|
6235
6556
|
`${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
|
|
@@ -6291,12 +6612,13 @@ async function startTail(options = {}) {
|
|
|
6291
6612
|
if (canApprove) readline3.emitKeypressEvents(process.stdin);
|
|
6292
6613
|
function clearCard() {
|
|
6293
6614
|
if (cardLineCount > 0) {
|
|
6294
|
-
process.stdout
|
|
6615
|
+
readline3.moveCursor(process.stdout, 0, -cardLineCount);
|
|
6616
|
+
process.stdout.write(ERASE_DOWN);
|
|
6295
6617
|
cardLineCount = 0;
|
|
6296
6618
|
}
|
|
6297
6619
|
}
|
|
6298
6620
|
function printCard(req2) {
|
|
6299
|
-
process.stdout.write(HIDE_CURSOR
|
|
6621
|
+
process.stdout.write(HIDE_CURSOR);
|
|
6300
6622
|
const daemonPrior = req2.allowCount !== void 0 ? req2.allowCount - 1 : 0;
|
|
6301
6623
|
const localPrior = localAllowCounts.get(req2.toolName) ?? 0;
|
|
6302
6624
|
const priorCount = Math.max(daemonPrior, localPrior);
|
|
@@ -6332,7 +6654,7 @@ async function startTail(options = {}) {
|
|
|
6332
6654
|
if (settled) return;
|
|
6333
6655
|
settled = true;
|
|
6334
6656
|
cleanup();
|
|
6335
|
-
|
|
6657
|
+
clearCard();
|
|
6336
6658
|
const stampedLines = buildCardLines(
|
|
6337
6659
|
req2,
|
|
6338
6660
|
Math.max(
|
|
@@ -6363,8 +6685,8 @@ async function startTail(options = {}) {
|
|
|
6363
6685
|
}
|
|
6364
6686
|
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
|
|
6365
6687
|
try {
|
|
6366
|
-
|
|
6367
|
-
|
|
6688
|
+
fs24.appendFileSync(
|
|
6689
|
+
path26.join(os21.homedir(), ".node9", "hook-debug.log"),
|
|
6368
6690
|
`[tail] POST /decision failed: ${String(err)}
|
|
6369
6691
|
`
|
|
6370
6692
|
);
|
|
@@ -6379,7 +6701,7 @@ async function startTail(options = {}) {
|
|
|
6379
6701
|
if (settled) return;
|
|
6380
6702
|
settled = true;
|
|
6381
6703
|
cleanup();
|
|
6382
|
-
|
|
6704
|
+
clearCard();
|
|
6383
6705
|
const priorCount = Math.max(
|
|
6384
6706
|
req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
|
|
6385
6707
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
@@ -6576,14 +6898,14 @@ async function startTail(options = {}) {
|
|
|
6576
6898
|
process.exit(1);
|
|
6577
6899
|
});
|
|
6578
6900
|
}
|
|
6579
|
-
var PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN
|
|
6901
|
+
var PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN;
|
|
6580
6902
|
var init_tail = __esm({
|
|
6581
6903
|
"src/tui/tail.ts"() {
|
|
6582
6904
|
"use strict";
|
|
6583
6905
|
init_daemon2();
|
|
6584
6906
|
init_daemon();
|
|
6585
6907
|
init_core();
|
|
6586
|
-
PID_FILE =
|
|
6908
|
+
PID_FILE = path26.join(os21.homedir(), ".node9", "daemon.pid");
|
|
6587
6909
|
ICONS = {
|
|
6588
6910
|
bash: "\u{1F4BB}",
|
|
6589
6911
|
shell: "\u{1F4BB}",
|
|
@@ -6611,8 +6933,6 @@ var init_tail = __esm({
|
|
|
6611
6933
|
HIDE_CURSOR = "\x1B[?25l";
|
|
6612
6934
|
SHOW_CURSOR = "\x1B[?25h";
|
|
6613
6935
|
ERASE_DOWN = "\x1B[J";
|
|
6614
|
-
SAVE_CURSOR = "\x1B7";
|
|
6615
|
-
RESTORE_CURSOR = "\x1B8";
|
|
6616
6936
|
}
|
|
6617
6937
|
});
|
|
6618
6938
|
|
|
@@ -7009,8 +7329,8 @@ async function setupCursor() {
|
|
|
7009
7329
|
// src/cli.ts
|
|
7010
7330
|
init_daemon2();
|
|
7011
7331
|
import chalk17 from "chalk";
|
|
7012
|
-
import
|
|
7013
|
-
import
|
|
7332
|
+
import fs25 from "fs";
|
|
7333
|
+
import path27 from "path";
|
|
7014
7334
|
import os22 from "os";
|
|
7015
7335
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
7016
7336
|
|
|
@@ -7236,32 +7556,32 @@ init_daemon();
|
|
|
7236
7556
|
init_config();
|
|
7237
7557
|
init_policy();
|
|
7238
7558
|
import chalk5 from "chalk";
|
|
7239
|
-
import
|
|
7240
|
-
import
|
|
7559
|
+
import fs18 from "fs";
|
|
7560
|
+
import path20 from "path";
|
|
7241
7561
|
import os15 from "os";
|
|
7242
7562
|
|
|
7243
7563
|
// src/undo.ts
|
|
7244
7564
|
import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
|
|
7245
7565
|
import crypto2 from "crypto";
|
|
7246
|
-
import
|
|
7247
|
-
import
|
|
7566
|
+
import fs17 from "fs";
|
|
7567
|
+
import path19 from "path";
|
|
7248
7568
|
import os14 from "os";
|
|
7249
|
-
var SNAPSHOT_STACK_PATH =
|
|
7250
|
-
var UNDO_LATEST_PATH =
|
|
7569
|
+
var SNAPSHOT_STACK_PATH = path19.join(os14.homedir(), ".node9", "snapshots.json");
|
|
7570
|
+
var UNDO_LATEST_PATH = path19.join(os14.homedir(), ".node9", "undo_latest.txt");
|
|
7251
7571
|
var MAX_SNAPSHOTS = 10;
|
|
7252
7572
|
var GIT_TIMEOUT = 15e3;
|
|
7253
7573
|
function readStack() {
|
|
7254
7574
|
try {
|
|
7255
|
-
if (
|
|
7256
|
-
return JSON.parse(
|
|
7575
|
+
if (fs17.existsSync(SNAPSHOT_STACK_PATH))
|
|
7576
|
+
return JSON.parse(fs17.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
7257
7577
|
} catch {
|
|
7258
7578
|
}
|
|
7259
7579
|
return [];
|
|
7260
7580
|
}
|
|
7261
7581
|
function writeStack(stack) {
|
|
7262
|
-
const dir =
|
|
7263
|
-
if (!
|
|
7264
|
-
|
|
7582
|
+
const dir = path19.dirname(SNAPSHOT_STACK_PATH);
|
|
7583
|
+
if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
|
|
7584
|
+
fs17.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
7265
7585
|
}
|
|
7266
7586
|
function buildArgsSummary(tool, args) {
|
|
7267
7587
|
if (!args || typeof args !== "object") return "";
|
|
@@ -7277,7 +7597,7 @@ function buildArgsSummary(tool, args) {
|
|
|
7277
7597
|
function normalizeCwdForHash(cwd) {
|
|
7278
7598
|
let normalized;
|
|
7279
7599
|
try {
|
|
7280
|
-
normalized =
|
|
7600
|
+
normalized = fs17.realpathSync(cwd);
|
|
7281
7601
|
} catch {
|
|
7282
7602
|
normalized = cwd;
|
|
7283
7603
|
}
|
|
@@ -7287,16 +7607,16 @@ function normalizeCwdForHash(cwd) {
|
|
|
7287
7607
|
}
|
|
7288
7608
|
function getShadowRepoDir(cwd) {
|
|
7289
7609
|
const hash = crypto2.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
7290
|
-
return
|
|
7610
|
+
return path19.join(os14.homedir(), ".node9", "snapshots", hash);
|
|
7291
7611
|
}
|
|
7292
7612
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
7293
7613
|
try {
|
|
7294
7614
|
const cutoff = Date.now() - 6e4;
|
|
7295
|
-
for (const f of
|
|
7615
|
+
for (const f of fs17.readdirSync(shadowDir)) {
|
|
7296
7616
|
if (f.startsWith("index_")) {
|
|
7297
|
-
const fp =
|
|
7617
|
+
const fp = path19.join(shadowDir, f);
|
|
7298
7618
|
try {
|
|
7299
|
-
if (
|
|
7619
|
+
if (fs17.statSync(fp).mtimeMs < cutoff) fs17.unlinkSync(fp);
|
|
7300
7620
|
} catch {
|
|
7301
7621
|
}
|
|
7302
7622
|
}
|
|
@@ -7308,7 +7628,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
7308
7628
|
const hardcoded = [".git", ".node9"];
|
|
7309
7629
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
7310
7630
|
try {
|
|
7311
|
-
|
|
7631
|
+
fs17.writeFileSync(path19.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
7312
7632
|
} catch {
|
|
7313
7633
|
}
|
|
7314
7634
|
}
|
|
@@ -7321,25 +7641,25 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
7321
7641
|
timeout: 3e3
|
|
7322
7642
|
});
|
|
7323
7643
|
if (check.status === 0) {
|
|
7324
|
-
const ptPath =
|
|
7644
|
+
const ptPath = path19.join(shadowDir, "project-path.txt");
|
|
7325
7645
|
try {
|
|
7326
|
-
const stored =
|
|
7646
|
+
const stored = fs17.readFileSync(ptPath, "utf8").trim();
|
|
7327
7647
|
if (stored === normalizedCwd) return true;
|
|
7328
7648
|
if (process.env.NODE9_DEBUG === "1")
|
|
7329
7649
|
console.error(
|
|
7330
7650
|
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
7331
7651
|
);
|
|
7332
|
-
|
|
7652
|
+
fs17.rmSync(shadowDir, { recursive: true, force: true });
|
|
7333
7653
|
} catch {
|
|
7334
7654
|
try {
|
|
7335
|
-
|
|
7655
|
+
fs17.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
7336
7656
|
} catch {
|
|
7337
7657
|
}
|
|
7338
7658
|
return true;
|
|
7339
7659
|
}
|
|
7340
7660
|
}
|
|
7341
7661
|
try {
|
|
7342
|
-
|
|
7662
|
+
fs17.mkdirSync(shadowDir, { recursive: true });
|
|
7343
7663
|
} catch {
|
|
7344
7664
|
}
|
|
7345
7665
|
const init = spawnSync4("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
@@ -7348,7 +7668,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
7348
7668
|
console.error("[Node9] git init --bare failed:", init.stderr?.toString());
|
|
7349
7669
|
return false;
|
|
7350
7670
|
}
|
|
7351
|
-
const configFile =
|
|
7671
|
+
const configFile = path19.join(shadowDir, "config");
|
|
7352
7672
|
spawnSync4("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
7353
7673
|
timeout: 3e3
|
|
7354
7674
|
});
|
|
@@ -7356,7 +7676,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
7356
7676
|
timeout: 3e3
|
|
7357
7677
|
});
|
|
7358
7678
|
try {
|
|
7359
|
-
|
|
7679
|
+
fs17.writeFileSync(path19.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
7360
7680
|
} catch {
|
|
7361
7681
|
}
|
|
7362
7682
|
return true;
|
|
@@ -7379,7 +7699,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
7379
7699
|
const shadowDir = getShadowRepoDir(cwd);
|
|
7380
7700
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
7381
7701
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
7382
|
-
indexFile =
|
|
7702
|
+
indexFile = path19.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
7383
7703
|
const shadowEnv = {
|
|
7384
7704
|
...process.env,
|
|
7385
7705
|
GIT_DIR: shadowDir,
|
|
@@ -7408,7 +7728,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
7408
7728
|
const shouldGc = stack.length % 5 === 0;
|
|
7409
7729
|
if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
7410
7730
|
writeStack(stack);
|
|
7411
|
-
|
|
7731
|
+
fs17.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
7412
7732
|
if (shouldGc) {
|
|
7413
7733
|
spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
7414
7734
|
}
|
|
@@ -7419,7 +7739,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
7419
7739
|
} finally {
|
|
7420
7740
|
if (indexFile) {
|
|
7421
7741
|
try {
|
|
7422
|
-
|
|
7742
|
+
fs17.unlinkSync(indexFile);
|
|
7423
7743
|
} catch {
|
|
7424
7744
|
}
|
|
7425
7745
|
}
|
|
@@ -7488,9 +7808,9 @@ function applyUndo(hash, cwd) {
|
|
|
7488
7808
|
timeout: GIT_TIMEOUT
|
|
7489
7809
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
7490
7810
|
for (const file of [...tracked, ...untracked]) {
|
|
7491
|
-
const fullPath =
|
|
7492
|
-
if (!snapshotFiles.has(file) &&
|
|
7493
|
-
|
|
7811
|
+
const fullPath = path19.join(dir, file);
|
|
7812
|
+
if (!snapshotFiles.has(file) && fs17.existsSync(fullPath)) {
|
|
7813
|
+
fs17.unlinkSync(fullPath);
|
|
7494
7814
|
}
|
|
7495
7815
|
}
|
|
7496
7816
|
return true;
|
|
@@ -7514,9 +7834,9 @@ function registerCheckCommand(program2) {
|
|
|
7514
7834
|
} catch (err) {
|
|
7515
7835
|
const tempConfig = getConfig();
|
|
7516
7836
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
7517
|
-
const logPath =
|
|
7837
|
+
const logPath = path20.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
7518
7838
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
7519
|
-
|
|
7839
|
+
fs18.appendFileSync(
|
|
7520
7840
|
logPath,
|
|
7521
7841
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
7522
7842
|
RAW: ${raw}
|
|
@@ -7527,10 +7847,10 @@ RAW: ${raw}
|
|
|
7527
7847
|
}
|
|
7528
7848
|
const config = getConfig(payload.cwd || void 0);
|
|
7529
7849
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
7530
|
-
const logPath =
|
|
7531
|
-
if (!
|
|
7532
|
-
|
|
7533
|
-
|
|
7850
|
+
const logPath = path20.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
7851
|
+
if (!fs18.existsSync(path20.dirname(logPath)))
|
|
7852
|
+
fs18.mkdirSync(path20.dirname(logPath), { recursive: true });
|
|
7853
|
+
fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
7534
7854
|
`);
|
|
7535
7855
|
}
|
|
7536
7856
|
const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
|
|
@@ -7543,8 +7863,8 @@ RAW: ${raw}
|
|
|
7543
7863
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
7544
7864
|
let ttyFd = null;
|
|
7545
7865
|
try {
|
|
7546
|
-
ttyFd =
|
|
7547
|
-
const writeTty = (line) =>
|
|
7866
|
+
ttyFd = fs18.openSync("/dev/tty", "w");
|
|
7867
|
+
const writeTty = (line) => fs18.writeSync(ttyFd, line + "\n");
|
|
7548
7868
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
7549
7869
|
writeTty(chalk5.bgRed.white.bold(`
|
|
7550
7870
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
@@ -7560,7 +7880,7 @@ RAW: ${raw}
|
|
|
7560
7880
|
} finally {
|
|
7561
7881
|
if (ttyFd !== null)
|
|
7562
7882
|
try {
|
|
7563
|
-
|
|
7883
|
+
fs18.closeSync(ttyFd);
|
|
7564
7884
|
} catch {
|
|
7565
7885
|
}
|
|
7566
7886
|
}
|
|
@@ -7591,7 +7911,7 @@ RAW: ${raw}
|
|
|
7591
7911
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
7592
7912
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
7593
7913
|
}
|
|
7594
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
7914
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && path20.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
7595
7915
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
7596
7916
|
cwd: safeCwdForAuth
|
|
7597
7917
|
});
|
|
@@ -7603,12 +7923,12 @@ RAW: ${raw}
|
|
|
7603
7923
|
}
|
|
7604
7924
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
7605
7925
|
try {
|
|
7606
|
-
const tty =
|
|
7607
|
-
|
|
7926
|
+
const tty = fs18.openSync("/dev/tty", "w");
|
|
7927
|
+
fs18.writeSync(
|
|
7608
7928
|
tty,
|
|
7609
7929
|
chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
7610
7930
|
);
|
|
7611
|
-
|
|
7931
|
+
fs18.closeSync(tty);
|
|
7612
7932
|
} catch {
|
|
7613
7933
|
}
|
|
7614
7934
|
const daemonReady = await autoStartDaemonAndWait();
|
|
@@ -7635,9 +7955,9 @@ RAW: ${raw}
|
|
|
7635
7955
|
});
|
|
7636
7956
|
} catch (err) {
|
|
7637
7957
|
if (process.env.NODE9_DEBUG === "1") {
|
|
7638
|
-
const logPath =
|
|
7958
|
+
const logPath = path20.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
7639
7959
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
7640
|
-
|
|
7960
|
+
fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
7641
7961
|
`);
|
|
7642
7962
|
}
|
|
7643
7963
|
process.exit(0);
|
|
@@ -7674,9 +7994,49 @@ RAW: ${raw}
|
|
|
7674
7994
|
init_audit();
|
|
7675
7995
|
init_config();
|
|
7676
7996
|
init_policy();
|
|
7677
|
-
import
|
|
7678
|
-
import
|
|
7997
|
+
import fs19 from "fs";
|
|
7998
|
+
import path21 from "path";
|
|
7679
7999
|
import os16 from "os";
|
|
8000
|
+
init_daemon();
|
|
8001
|
+
|
|
8002
|
+
// src/utils/cp-mv-parser.ts
|
|
8003
|
+
function parseCpMvOp(command) {
|
|
8004
|
+
const trimmed = command.trim();
|
|
8005
|
+
const tokens = trimmed.split(/\s+/);
|
|
8006
|
+
if (tokens.length < 3) return null;
|
|
8007
|
+
const [cmd, ...rest] = tokens;
|
|
8008
|
+
const base = cmd.split("/").pop() ?? cmd;
|
|
8009
|
+
if (base !== "cp" && base !== "mv") return null;
|
|
8010
|
+
const args = [];
|
|
8011
|
+
for (const tok of rest) {
|
|
8012
|
+
if (tok === "--") {
|
|
8013
|
+
args.push(...rest.slice(rest.indexOf("--") + 1));
|
|
8014
|
+
break;
|
|
8015
|
+
}
|
|
8016
|
+
if (tok === "-t" || tok === "--target-directory") return null;
|
|
8017
|
+
if (tok.startsWith("--target-directory=")) return null;
|
|
8018
|
+
if (tok.startsWith("-") && !tok.startsWith("--")) {
|
|
8019
|
+
if (tok.includes("t")) return null;
|
|
8020
|
+
continue;
|
|
8021
|
+
}
|
|
8022
|
+
if (tok.startsWith("--")) {
|
|
8023
|
+
continue;
|
|
8024
|
+
}
|
|
8025
|
+
args.push(tok);
|
|
8026
|
+
}
|
|
8027
|
+
if (args.length !== 2) return null;
|
|
8028
|
+
const [src, dest] = args;
|
|
8029
|
+
if (!src || !dest) return null;
|
|
8030
|
+
if (containsShellMetachar(src) || containsShellMetachar(dest)) return null;
|
|
8031
|
+
return { src, dest, clearSource: base === "mv" };
|
|
8032
|
+
}
|
|
8033
|
+
function containsShellMetachar(token) {
|
|
8034
|
+
if (/[$`{;*?]/.test(token)) return true;
|
|
8035
|
+
if (token.includes("\0")) return true;
|
|
8036
|
+
return false;
|
|
8037
|
+
}
|
|
8038
|
+
|
|
8039
|
+
// src/cli/commands/log.ts
|
|
7680
8040
|
function sanitize3(value) {
|
|
7681
8041
|
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
7682
8042
|
}
|
|
@@ -7695,11 +8055,20 @@ function registerLogCommand(program2) {
|
|
|
7695
8055
|
decision: "allowed",
|
|
7696
8056
|
source: "post-hook"
|
|
7697
8057
|
};
|
|
7698
|
-
const logPath =
|
|
7699
|
-
if (!
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
8058
|
+
const logPath = path21.join(os16.homedir(), ".node9", "audit.log");
|
|
8059
|
+
if (!fs19.existsSync(path21.dirname(logPath)))
|
|
8060
|
+
fs19.mkdirSync(path21.dirname(logPath), { recursive: true });
|
|
8061
|
+
fs19.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
8062
|
+
if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
|
|
8063
|
+
const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
8064
|
+
if (command) {
|
|
8065
|
+
const op = parseCpMvOp(command);
|
|
8066
|
+
if (op) {
|
|
8067
|
+
await notifyTaintPropagate(op.src, op.dest, op.clearSource);
|
|
8068
|
+
}
|
|
8069
|
+
}
|
|
8070
|
+
}
|
|
8071
|
+
const safeCwd = typeof payload.cwd === "string" && path21.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
7703
8072
|
const config = getConfig(safeCwd);
|
|
7704
8073
|
if (shouldSnapshot(tool, {}, config)) {
|
|
7705
8074
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -7708,9 +8077,9 @@ function registerLogCommand(program2) {
|
|
|
7708
8077
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7709
8078
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
7710
8079
|
`);
|
|
7711
|
-
const debugPath =
|
|
8080
|
+
const debugPath = path21.join(os16.homedir(), ".node9", "hook-debug.log");
|
|
7712
8081
|
try {
|
|
7713
|
-
|
|
8082
|
+
fs19.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
7714
8083
|
`);
|
|
7715
8084
|
} catch {
|
|
7716
8085
|
}
|
|
@@ -8015,8 +8384,8 @@ function registerConfigShowCommand(program2) {
|
|
|
8015
8384
|
// src/cli/commands/doctor.ts
|
|
8016
8385
|
init_daemon();
|
|
8017
8386
|
import chalk7 from "chalk";
|
|
8018
|
-
import
|
|
8019
|
-
import
|
|
8387
|
+
import fs20 from "fs";
|
|
8388
|
+
import path22 from "path";
|
|
8020
8389
|
import os17 from "os";
|
|
8021
8390
|
import { execSync as execSync2 } from "child_process";
|
|
8022
8391
|
function registerDoctorCommand(program2, version2) {
|
|
@@ -8070,10 +8439,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
8070
8439
|
);
|
|
8071
8440
|
}
|
|
8072
8441
|
section("Configuration");
|
|
8073
|
-
const globalConfigPath =
|
|
8074
|
-
if (
|
|
8442
|
+
const globalConfigPath = path22.join(homeDir2, ".node9", "config.json");
|
|
8443
|
+
if (fs20.existsSync(globalConfigPath)) {
|
|
8075
8444
|
try {
|
|
8076
|
-
JSON.parse(
|
|
8445
|
+
JSON.parse(fs20.readFileSync(globalConfigPath, "utf-8"));
|
|
8077
8446
|
pass("~/.node9/config.json found and valid");
|
|
8078
8447
|
} catch {
|
|
8079
8448
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -8081,10 +8450,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
8081
8450
|
} else {
|
|
8082
8451
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
8083
8452
|
}
|
|
8084
|
-
const projectConfigPath =
|
|
8085
|
-
if (
|
|
8453
|
+
const projectConfigPath = path22.join(process.cwd(), "node9.config.json");
|
|
8454
|
+
if (fs20.existsSync(projectConfigPath)) {
|
|
8086
8455
|
try {
|
|
8087
|
-
JSON.parse(
|
|
8456
|
+
JSON.parse(fs20.readFileSync(projectConfigPath, "utf-8"));
|
|
8088
8457
|
pass("node9.config.json found and valid (project)");
|
|
8089
8458
|
} catch {
|
|
8090
8459
|
fail(
|
|
@@ -8093,8 +8462,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
8093
8462
|
);
|
|
8094
8463
|
}
|
|
8095
8464
|
}
|
|
8096
|
-
const credsPath =
|
|
8097
|
-
if (
|
|
8465
|
+
const credsPath = path22.join(homeDir2, ".node9", "credentials.json");
|
|
8466
|
+
if (fs20.existsSync(credsPath)) {
|
|
8098
8467
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
8099
8468
|
} else {
|
|
8100
8469
|
warn(
|
|
@@ -8103,10 +8472,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
8103
8472
|
);
|
|
8104
8473
|
}
|
|
8105
8474
|
section("Agent Hooks");
|
|
8106
|
-
const claudeSettingsPath =
|
|
8107
|
-
if (
|
|
8475
|
+
const claudeSettingsPath = path22.join(homeDir2, ".claude", "settings.json");
|
|
8476
|
+
if (fs20.existsSync(claudeSettingsPath)) {
|
|
8108
8477
|
try {
|
|
8109
|
-
const cs = JSON.parse(
|
|
8478
|
+
const cs = JSON.parse(fs20.readFileSync(claudeSettingsPath, "utf-8"));
|
|
8110
8479
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
8111
8480
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
8112
8481
|
);
|
|
@@ -8122,10 +8491,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
8122
8491
|
} else {
|
|
8123
8492
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
8124
8493
|
}
|
|
8125
|
-
const geminiSettingsPath =
|
|
8126
|
-
if (
|
|
8494
|
+
const geminiSettingsPath = path22.join(homeDir2, ".gemini", "settings.json");
|
|
8495
|
+
if (fs20.existsSync(geminiSettingsPath)) {
|
|
8127
8496
|
try {
|
|
8128
|
-
const gs = JSON.parse(
|
|
8497
|
+
const gs = JSON.parse(fs20.readFileSync(geminiSettingsPath, "utf-8"));
|
|
8129
8498
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
8130
8499
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
8131
8500
|
);
|
|
@@ -8141,10 +8510,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
8141
8510
|
} else {
|
|
8142
8511
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
8143
8512
|
}
|
|
8144
|
-
const cursorHooksPath =
|
|
8145
|
-
if (
|
|
8513
|
+
const cursorHooksPath = path22.join(homeDir2, ".cursor", "hooks.json");
|
|
8514
|
+
if (fs20.existsSync(cursorHooksPath)) {
|
|
8146
8515
|
try {
|
|
8147
|
-
const cur = JSON.parse(
|
|
8516
|
+
const cur = JSON.parse(fs20.readFileSync(cursorHooksPath, "utf-8"));
|
|
8148
8517
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
8149
8518
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
8150
8519
|
);
|
|
@@ -8182,8 +8551,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
8182
8551
|
|
|
8183
8552
|
// src/cli/commands/audit.ts
|
|
8184
8553
|
import chalk8 from "chalk";
|
|
8185
|
-
import
|
|
8186
|
-
import
|
|
8554
|
+
import fs21 from "fs";
|
|
8555
|
+
import path23 from "path";
|
|
8187
8556
|
import os18 from "os";
|
|
8188
8557
|
function formatRelativeTime(timestamp) {
|
|
8189
8558
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
@@ -8197,14 +8566,14 @@ function formatRelativeTime(timestamp) {
|
|
|
8197
8566
|
}
|
|
8198
8567
|
function registerAuditCommand(program2) {
|
|
8199
8568
|
program2.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) => {
|
|
8200
|
-
const logPath =
|
|
8201
|
-
if (!
|
|
8569
|
+
const logPath = path23.join(os18.homedir(), ".node9", "audit.log");
|
|
8570
|
+
if (!fs21.existsSync(logPath)) {
|
|
8202
8571
|
console.log(
|
|
8203
8572
|
chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
8204
8573
|
);
|
|
8205
8574
|
return;
|
|
8206
8575
|
}
|
|
8207
|
-
const raw =
|
|
8576
|
+
const raw = fs21.readFileSync(logPath, "utf-8");
|
|
8208
8577
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
8209
8578
|
let entries = lines.flatMap((line) => {
|
|
8210
8579
|
try {
|
|
@@ -8324,12 +8693,12 @@ function registerDaemonCommand(program2) {
|
|
|
8324
8693
|
init_core();
|
|
8325
8694
|
init_daemon();
|
|
8326
8695
|
import chalk10 from "chalk";
|
|
8327
|
-
import
|
|
8328
|
-
import
|
|
8696
|
+
import fs22 from "fs";
|
|
8697
|
+
import path24 from "path";
|
|
8329
8698
|
import os19 from "os";
|
|
8330
8699
|
function readJson2(filePath) {
|
|
8331
8700
|
try {
|
|
8332
|
-
if (
|
|
8701
|
+
if (fs22.existsSync(filePath)) return JSON.parse(fs22.readFileSync(filePath, "utf-8"));
|
|
8333
8702
|
} catch {
|
|
8334
8703
|
}
|
|
8335
8704
|
return null;
|
|
@@ -8394,13 +8763,13 @@ function registerStatusCommand(program2) {
|
|
|
8394
8763
|
console.log("");
|
|
8395
8764
|
const modeLabel = settings.mode === "audit" ? chalk10.blue("audit") : settings.mode === "strict" ? chalk10.red("strict") : chalk10.white("standard");
|
|
8396
8765
|
console.log(` Mode: ${modeLabel}`);
|
|
8397
|
-
const projectConfig =
|
|
8398
|
-
const globalConfig =
|
|
8766
|
+
const projectConfig = path24.join(process.cwd(), "node9.config.json");
|
|
8767
|
+
const globalConfig = path24.join(os19.homedir(), ".node9", "config.json");
|
|
8399
8768
|
console.log(
|
|
8400
|
-
` Local: ${
|
|
8769
|
+
` Local: ${fs22.existsSync(projectConfig) ? chalk10.green("Active (node9.config.json)") : chalk10.gray("Not present")}`
|
|
8401
8770
|
);
|
|
8402
8771
|
console.log(
|
|
8403
|
-
` Global: ${
|
|
8772
|
+
` Global: ${fs22.existsSync(globalConfig) ? chalk10.green("Active (~/.node9/config.json)") : chalk10.gray("Not present")}`
|
|
8404
8773
|
);
|
|
8405
8774
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
8406
8775
|
console.log(
|
|
@@ -8409,13 +8778,13 @@ function registerStatusCommand(program2) {
|
|
|
8409
8778
|
}
|
|
8410
8779
|
const homeDir2 = os19.homedir();
|
|
8411
8780
|
const claudeSettings = readJson2(
|
|
8412
|
-
|
|
8781
|
+
path24.join(homeDir2, ".claude", "settings.json")
|
|
8413
8782
|
);
|
|
8414
|
-
const claudeConfig = readJson2(
|
|
8783
|
+
const claudeConfig = readJson2(path24.join(homeDir2, ".claude.json"));
|
|
8415
8784
|
const geminiSettings = readJson2(
|
|
8416
|
-
|
|
8785
|
+
path24.join(homeDir2, ".gemini", "settings.json")
|
|
8417
8786
|
);
|
|
8418
|
-
const cursorConfig = readJson2(
|
|
8787
|
+
const cursorConfig = readJson2(path24.join(homeDir2, ".cursor", "mcp.json"));
|
|
8419
8788
|
const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
|
|
8420
8789
|
if (agentFound) {
|
|
8421
8790
|
console.log("");
|
|
@@ -8475,14 +8844,14 @@ function registerStatusCommand(program2) {
|
|
|
8475
8844
|
// src/cli/commands/init.ts
|
|
8476
8845
|
init_core();
|
|
8477
8846
|
import chalk11 from "chalk";
|
|
8478
|
-
import
|
|
8479
|
-
import
|
|
8847
|
+
import fs23 from "fs";
|
|
8848
|
+
import path25 from "path";
|
|
8480
8849
|
import os20 from "os";
|
|
8481
8850
|
function registerInitCommand(program2) {
|
|
8482
8851
|
program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").option("--skip-setup", "Only create config \u2014 do not wire AI agents").action(async (options) => {
|
|
8483
8852
|
console.log(chalk11.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
8484
|
-
const configPath =
|
|
8485
|
-
if (
|
|
8853
|
+
const configPath = path25.join(os20.homedir(), ".node9", "config.json");
|
|
8854
|
+
if (fs23.existsSync(configPath) && !options.force) {
|
|
8486
8855
|
console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
8487
8856
|
} else {
|
|
8488
8857
|
const requestedMode = options.mode.toLowerCase();
|
|
@@ -8491,9 +8860,9 @@ function registerInitCommand(program2) {
|
|
|
8491
8860
|
...DEFAULT_CONFIG,
|
|
8492
8861
|
settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
|
|
8493
8862
|
};
|
|
8494
|
-
const dir =
|
|
8495
|
-
if (!
|
|
8496
|
-
|
|
8863
|
+
const dir = path25.dirname(configPath);
|
|
8864
|
+
if (!fs23.existsSync(dir)) fs23.mkdirSync(dir, { recursive: true });
|
|
8865
|
+
fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
8497
8866
|
console.log(chalk11.green(`\u2705 Config created: ${configPath}`));
|
|
8498
8867
|
console.log(chalk11.gray(` Mode: ${safeMode}`));
|
|
8499
8868
|
}
|
|
@@ -8949,20 +9318,20 @@ function registerTrustCommand(program2) {
|
|
|
8949
9318
|
|
|
8950
9319
|
// src/cli.ts
|
|
8951
9320
|
var { version } = JSON.parse(
|
|
8952
|
-
|
|
9321
|
+
fs25.readFileSync(path27.join(__dirname, "../package.json"), "utf-8")
|
|
8953
9322
|
);
|
|
8954
9323
|
var program = new Command();
|
|
8955
9324
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
8956
9325
|
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) => {
|
|
8957
9326
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
8958
|
-
const credPath =
|
|
8959
|
-
if (!
|
|
8960
|
-
|
|
9327
|
+
const credPath = path27.join(os22.homedir(), ".node9", "credentials.json");
|
|
9328
|
+
if (!fs25.existsSync(path27.dirname(credPath)))
|
|
9329
|
+
fs25.mkdirSync(path27.dirname(credPath), { recursive: true });
|
|
8961
9330
|
const profileName = options.profile || "default";
|
|
8962
9331
|
let existingCreds = {};
|
|
8963
9332
|
try {
|
|
8964
|
-
if (
|
|
8965
|
-
const raw = JSON.parse(
|
|
9333
|
+
if (fs25.existsSync(credPath)) {
|
|
9334
|
+
const raw = JSON.parse(fs25.readFileSync(credPath, "utf-8"));
|
|
8966
9335
|
if (raw.apiKey) {
|
|
8967
9336
|
existingCreds = {
|
|
8968
9337
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -8974,13 +9343,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
8974
9343
|
} catch {
|
|
8975
9344
|
}
|
|
8976
9345
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
8977
|
-
|
|
9346
|
+
fs25.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
8978
9347
|
if (profileName === "default") {
|
|
8979
|
-
const configPath =
|
|
9348
|
+
const configPath = path27.join(os22.homedir(), ".node9", "config.json");
|
|
8980
9349
|
let config = {};
|
|
8981
9350
|
try {
|
|
8982
|
-
if (
|
|
8983
|
-
config = JSON.parse(
|
|
9351
|
+
if (fs25.existsSync(configPath))
|
|
9352
|
+
config = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
|
|
8984
9353
|
} catch {
|
|
8985
9354
|
}
|
|
8986
9355
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -8995,9 +9364,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
8995
9364
|
approvers.cloud = false;
|
|
8996
9365
|
}
|
|
8997
9366
|
s.approvers = approvers;
|
|
8998
|
-
if (!
|
|
8999
|
-
|
|
9000
|
-
|
|
9367
|
+
if (!fs25.existsSync(path27.dirname(configPath)))
|
|
9368
|
+
fs25.mkdirSync(path27.dirname(configPath), { recursive: true });
|
|
9369
|
+
fs25.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
9001
9370
|
}
|
|
9002
9371
|
if (options.profile && profileName !== "default") {
|
|
9003
9372
|
console.log(chalk17.green(`\u2705 Profile "${profileName}" saved`));
|
|
@@ -9083,15 +9452,15 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
9083
9452
|
}
|
|
9084
9453
|
}
|
|
9085
9454
|
if (options.purge) {
|
|
9086
|
-
const node9Dir =
|
|
9087
|
-
if (
|
|
9455
|
+
const node9Dir = path27.join(os22.homedir(), ".node9");
|
|
9456
|
+
if (fs25.existsSync(node9Dir)) {
|
|
9088
9457
|
const confirmed = await confirm3({
|
|
9089
9458
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
9090
9459
|
default: false
|
|
9091
9460
|
});
|
|
9092
9461
|
if (confirmed) {
|
|
9093
|
-
|
|
9094
|
-
if (
|
|
9462
|
+
fs25.rmSync(node9Dir, { recursive: true });
|
|
9463
|
+
if (fs25.existsSync(node9Dir)) {
|
|
9095
9464
|
console.error(
|
|
9096
9465
|
chalk17.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
9097
9466
|
);
|
|
@@ -9303,9 +9672,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
9303
9672
|
const isCheckHook = process.argv[2] === "check";
|
|
9304
9673
|
if (isCheckHook) {
|
|
9305
9674
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
9306
|
-
const logPath =
|
|
9675
|
+
const logPath = path27.join(os22.homedir(), ".node9", "hook-debug.log");
|
|
9307
9676
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
9308
|
-
|
|
9677
|
+
fs25.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
9309
9678
|
`);
|
|
9310
9679
|
}
|
|
9311
9680
|
process.exit(0);
|