@node9/proxy 1.5.0 → 1.5.1

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 CHANGED
@@ -30,7 +30,52 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  mod
31
31
  ));
32
32
 
33
+ // src/audit/hasher.ts
34
+ function canonicalise(value) {
35
+ return _canonicalise(value, /* @__PURE__ */ new WeakSet());
36
+ }
37
+ function _canonicalise(value, seen) {
38
+ if (value === null || typeof value !== "object") return value;
39
+ if (value instanceof Date) return value.toISOString();
40
+ if (value instanceof RegExp) return value.toString();
41
+ if (Buffer.isBuffer(value)) return value.toString("base64");
42
+ if (seen.has(value)) return "[Circular]";
43
+ seen.add(value);
44
+ let result;
45
+ if (Array.isArray(value)) {
46
+ result = value.map((v) => _canonicalise(v, seen));
47
+ } else {
48
+ const obj = value;
49
+ result = Object.fromEntries(
50
+ Object.keys(obj).sort().map((k) => [k, _canonicalise(obj[k], seen)])
51
+ );
52
+ }
53
+ seen.delete(value);
54
+ return result;
55
+ }
56
+ function hashArgs(args) {
57
+ const canonical = JSON.stringify(canonicalise(args) ?? null);
58
+ return (0, import_crypto.createHash)("sha256").update(canonical).digest("hex").slice(0, 32);
59
+ }
60
+ var import_crypto;
61
+ var init_hasher = __esm({
62
+ "src/audit/hasher.ts"() {
63
+ "use strict";
64
+ import_crypto = require("crypto");
65
+ }
66
+ });
67
+
33
68
  // src/audit/index.ts
69
+ var audit_exports = {};
70
+ __export(audit_exports, {
71
+ HOOK_DEBUG_LOG: () => HOOK_DEBUG_LOG,
72
+ LOCAL_AUDIT_LOG: () => LOCAL_AUDIT_LOG,
73
+ appendConfigAudit: () => appendConfigAudit,
74
+ appendHookDebug: () => appendHookDebug,
75
+ appendLocalAudit: () => appendLocalAudit,
76
+ appendToLog: () => appendToLog,
77
+ redactSecrets: () => redactSecrets
78
+ });
34
79
  function redactSecrets(text) {
35
80
  if (!text) return text;
36
81
  let redacted = text;
@@ -52,24 +97,24 @@ function appendToLog(logPath, entry) {
52
97
  } catch {
53
98
  }
54
99
  }
55
- function appendHookDebug(toolName, args, meta) {
56
- const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
100
+ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
101
+ const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
57
102
  appendToLog(HOOK_DEBUG_LOG, {
58
103
  ts: (/* @__PURE__ */ new Date()).toISOString(),
59
104
  tool: toolName,
60
- args: safeArgs,
105
+ ...argsField,
61
106
  agent: meta?.agent,
62
107
  mcpServer: meta?.mcpServer,
63
108
  hostname: import_os.default.hostname(),
64
109
  cwd: process.cwd()
65
110
  });
66
111
  }
67
- function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
68
- const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
112
+ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
113
+ const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
69
114
  appendToLog(LOCAL_AUDIT_LOG, {
70
115
  ts: (/* @__PURE__ */ new Date()).toISOString(),
71
116
  tool: toolName,
72
- args: safeArgs,
117
+ ...argsField,
73
118
  decision,
74
119
  checkedBy,
75
120
  agent: meta?.agent,
@@ -91,6 +136,7 @@ var init_audit = __esm({
91
136
  import_fs = __toESM(require("fs"));
92
137
  import_path = __toESM(require("path"));
93
138
  import_os = __toESM(require("os"));
139
+ init_hasher();
94
140
  LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
95
141
  HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
96
142
  }
@@ -114,8 +160,8 @@ function sanitizeConfig(raw) {
114
160
  }
115
161
  }
116
162
  const lines = result.error.issues.map((issue) => {
117
- const path27 = issue.path.length > 0 ? issue.path.join(".") : "root";
118
- return ` \u2022 ${path27}: ${issue.message}`;
163
+ const path28 = issue.path.length > 0 ? issue.path.join(".") : "root";
164
+ return ` \u2022 ${path28}: ${issue.message}`;
119
165
  });
120
166
  return {
121
167
  sanitized,
@@ -188,7 +234,8 @@ var init_config_schema = __esm({
188
234
  environment: import_zod.z.string().optional(),
189
235
  slackEnabled: import_zod.z.boolean().optional(),
190
236
  enableTrustSessions: import_zod.z.boolean().optional(),
191
- allowGlobalPause: import_zod.z.boolean().optional()
237
+ allowGlobalPause: import_zod.z.boolean().optional(),
238
+ auditHashArgs: import_zod.z.boolean().optional()
192
239
  }).optional(),
193
240
  policy: import_zod.z.object({
194
241
  sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
@@ -269,7 +316,7 @@ function readShieldsFile() {
269
316
  }
270
317
  function writeShieldsFile(data) {
271
318
  import_fs2.default.mkdirSync(import_path2.default.dirname(SHIELDS_STATE_FILE), { recursive: true });
272
- const tmp = `${SHIELDS_STATE_FILE}.${import_crypto.default.randomBytes(6).toString("hex")}.tmp`;
319
+ const tmp = `${SHIELDS_STATE_FILE}.${import_crypto2.default.randomBytes(6).toString("hex")}.tmp`;
273
320
  const toWrite = { active: data.active };
274
321
  if (data.overrides && Object.keys(data.overrides).length > 0) toWrite.overrides = data.overrides;
275
322
  import_fs2.default.writeFileSync(tmp, JSON.stringify(toWrite, null, 2), { mode: 384 });
@@ -318,14 +365,14 @@ function resolveShieldRule(shieldName, identifier) {
318
365
  }
319
366
  return null;
320
367
  }
321
- var import_fs2, import_path2, import_os2, import_crypto, SHIELDS, SHIELDS_STATE_FILE;
368
+ var import_fs2, import_path2, import_os2, import_crypto2, SHIELDS, SHIELDS_STATE_FILE;
322
369
  var init_shields = __esm({
323
370
  "src/shields.ts"() {
324
371
  "use strict";
325
372
  import_fs2 = __toESM(require("fs"));
326
373
  import_path2 = __toESM(require("path"));
327
374
  import_os2 = __toESM(require("os"));
328
- import_crypto = __toESM(require("crypto"));
375
+ import_crypto2 = __toESM(require("crypto"));
329
376
  SHIELDS = {
330
377
  postgres: {
331
378
  name: "postgres",
@@ -746,6 +793,7 @@ var init_config = __esm({
746
793
  approvalTimeoutMs: 12e4,
747
794
  // 120-second auto-deny timeout
748
795
  flightRecorder: true,
796
+ auditHashArgs: true,
749
797
  approvers: { native: true, browser: true, cloud: false, terminal: true }
750
798
  },
751
799
  policy: {
@@ -1710,9 +1758,9 @@ function matchesPattern(text, patterns) {
1710
1758
  const withoutDotSlash = text.replace(/^\.\//, "");
1711
1759
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1712
1760
  }
1713
- function getNestedValue(obj, path27) {
1761
+ function getNestedValue(obj, path28) {
1714
1762
  if (!obj || typeof obj !== "object") return null;
1715
- return path27.split(".").reduce((prev, curr) => prev?.[curr], obj);
1763
+ return path28.split(".").reduce((prev, curr) => prev?.[curr], obj);
1716
1764
  }
1717
1765
  function shouldSnapshot(toolName, args, config) {
1718
1766
  if (!config.settings.enableUndo) return false;
@@ -2488,6 +2536,58 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
2488
2536
  const { id, allowCount } = await res.json();
2489
2537
  return { id, allowCount: allowCount ?? 1 };
2490
2538
  }
2539
+ async function notifyTaint(filePath, source) {
2540
+ if (!isDaemonRunning()) return;
2541
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2542
+ try {
2543
+ await fetch(`${base}/taint`, {
2544
+ method: "POST",
2545
+ headers: { "Content-Type": "application/json" },
2546
+ body: JSON.stringify({ path: filePath, source }),
2547
+ signal: AbortSignal.timeout(1e3)
2548
+ });
2549
+ } catch {
2550
+ }
2551
+ }
2552
+ async function notifyTaintPropagate(src, dest, clearSource = false) {
2553
+ if (!isDaemonRunning()) return;
2554
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2555
+ try {
2556
+ await fetch(`${base}/taint/propagate`, {
2557
+ method: "POST",
2558
+ headers: { "Content-Type": "application/json" },
2559
+ body: JSON.stringify({ src, dest, clearSource }),
2560
+ signal: AbortSignal.timeout(1e3)
2561
+ });
2562
+ } catch {
2563
+ }
2564
+ }
2565
+ async function checkTaint(paths) {
2566
+ if (paths.length === 0) return { tainted: false };
2567
+ if (!isDaemonRunning()) return { tainted: false, daemonUnavailable: true };
2568
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2569
+ try {
2570
+ const res = await fetch(`${base}/taint/check`, {
2571
+ method: "POST",
2572
+ headers: { "Content-Type": "application/json" },
2573
+ body: JSON.stringify({ paths }),
2574
+ signal: AbortSignal.timeout(2e3)
2575
+ });
2576
+ return await res.json();
2577
+ } catch (err) {
2578
+ try {
2579
+ const { appendToLog: appendToLog2, HOOK_DEBUG_LOG: HOOK_DEBUG_LOG2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
2580
+ appendToLog2(HOOK_DEBUG_LOG2, {
2581
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2582
+ event: "checkTaint-error",
2583
+ error: String(err),
2584
+ paths
2585
+ });
2586
+ } catch {
2587
+ }
2588
+ return { tainted: false, daemonUnavailable: true };
2589
+ }
2590
+ }
2491
2591
  async function resolveViaDaemon(id, decision, internalToken, source) {
2492
2592
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2493
2593
  await fetch(`${base}/resolve/${id}`, {
@@ -2959,6 +3059,40 @@ var init_cloud = __esm({
2959
3059
  });
2960
3060
 
2961
3061
  // src/auth/orchestrator.ts
3062
+ function isWriteTool(toolName) {
3063
+ const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
3064
+ return WRITE_TOOLS.has(t);
3065
+ }
3066
+ function extractFilePaths(toolName, args) {
3067
+ const paths = [];
3068
+ if (!args || typeof args !== "object" || Array.isArray(args)) return paths;
3069
+ const a = args;
3070
+ for (const key of ["file_path", "path", "filename", "source", "src", "input"]) {
3071
+ if (typeof a[key] === "string" && a[key]) paths.push(a[key]);
3072
+ }
3073
+ const cmd = typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : "";
3074
+ if (cmd) {
3075
+ for (const m of cmd.matchAll(/(?:-T|--upload-file|--data(?:-binary)?)\s+@?(\S+)/g)) {
3076
+ paths.push(m[1]);
3077
+ }
3078
+ for (const m of cmd.matchAll(/\b(?:scp|rsync)\s+(?:-\S+\s+)*(\S+)\s+\S+@/g)) {
3079
+ paths.push(m[1]);
3080
+ }
3081
+ for (const m of cmd.matchAll(/<\s*(\S+)/g)) {
3082
+ paths.push(m[1]);
3083
+ }
3084
+ }
3085
+ return paths.filter(Boolean);
3086
+ }
3087
+ function isNetworkTool(toolName, args) {
3088
+ const t = toolName.toLowerCase();
3089
+ if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal.execute") {
3090
+ const a = args;
3091
+ const cmd = typeof a?.command === "string" ? a.command : typeof a?.cmd === "string" ? a.cmd : "";
3092
+ return /\b(curl|wget|scp|rsync|nc|ncat|netcat|ssh)\b/.test(cmd);
3093
+ }
3094
+ return false;
3095
+ }
2962
3096
  function notifyActivity(data) {
2963
3097
  return new Promise((resolve) => {
2964
3098
  try {
@@ -2976,7 +3110,7 @@ function notifyActivity(data) {
2976
3110
  }
2977
3111
  async function authorizeHeadless(toolName, args, meta, options) {
2978
3112
  if (!options?.calledFromDaemon) {
2979
- const actId = (0, import_crypto2.randomUUID)();
3113
+ const actId = (0, import_crypto3.randomUUID)();
2980
3114
  const actTs = Date.now();
2981
3115
  await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
2982
3116
  const result = await _authorizeHeadlessCore(toolName, args, meta, {
@@ -2988,7 +3122,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
2988
3122
  id: actId,
2989
3123
  tool: toolName,
2990
3124
  ts: actTs,
2991
- status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
3125
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
2992
3126
  label: result.blockedByLabel
2993
3127
  });
2994
3128
  }
@@ -3002,6 +3136,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3002
3136
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
3003
3137
  const creds = getCredentials();
3004
3138
  const config = getConfig(options?.cwd);
3139
+ const hashAuditArgs = config.settings.auditHashArgs === true;
3005
3140
  const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
3006
3141
  const approvers = {
3007
3142
  ...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
@@ -3012,13 +3147,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3012
3147
  approvers.terminal = false;
3013
3148
  }
3014
3149
  if (config.settings.enableHookLogDebug && !isTestEnv2) {
3015
- appendHookDebug(toolName, args, meta);
3150
+ appendHookDebug(toolName, args, meta, hashAuditArgs);
3016
3151
  }
3017
3152
  const isManual = meta?.agent === "Terminal";
3018
3153
  let explainableLabel = "Local Config";
3019
3154
  let policyMatchedField;
3020
3155
  let policyMatchedWord;
3021
3156
  let riskMetadata;
3157
+ let taintWarning = null;
3158
+ if (isNetworkTool(toolName, args)) {
3159
+ const filePaths = extractFilePaths(toolName, args);
3160
+ if (filePaths.length > 0) {
3161
+ const taintResult = await checkTaint(filePaths);
3162
+ if (taintResult.tainted && taintResult.record) {
3163
+ const { path: taintedPath, source: taintSource } = taintResult.record;
3164
+ taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
3165
+ } else if (taintResult.daemonUnavailable) {
3166
+ taintWarning = `\u26A0\uFE0F Taint service unavailable \u2014 cannot verify if ${filePaths.join(", ")} is clean`;
3167
+ }
3168
+ }
3169
+ }
3022
3170
  if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
3023
3171
  const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
3024
3172
  const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
@@ -3026,7 +3174,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3026
3174
  if (dlpMatch) {
3027
3175
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
3028
3176
  if (dlpMatch.severity === "block") {
3029
- if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
3177
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta, true);
3178
+ if (isWriteTool(toolName) && filePath) {
3179
+ await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
3180
+ }
3030
3181
  return {
3031
3182
  approved: false,
3032
3183
  reason: dlpReason,
@@ -3034,7 +3185,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3034
3185
  blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
3035
3186
  };
3036
3187
  }
3037
- if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
3188
+ if (!isManual)
3189
+ appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta, hashAuditArgs);
3038
3190
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
3039
3191
  }
3040
3192
  }
@@ -3042,7 +3194,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3042
3194
  if (!isIgnoredTool(toolName)) {
3043
3195
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
3044
3196
  if (policyResult.decision === "review") {
3045
- appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
3197
+ appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
3046
3198
  if (approvers.cloud && creds?.apiKey) {
3047
3199
  await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
3048
3200
  }
@@ -3050,22 +3202,23 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3050
3202
  }
3051
3203
  return { approved: true, checkedBy: "audit" };
3052
3204
  }
3053
- if (!isIgnoredTool(toolName)) {
3205
+ if (!taintWarning && !isIgnoredTool(toolName)) {
3054
3206
  if (getActiveTrustSession(toolName)) {
3055
3207
  if (approvers.cloud && creds?.apiKey)
3056
3208
  await auditLocalAllow(toolName, args, "trust", creds, meta);
3057
- if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
3209
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
3058
3210
  return { approved: true, checkedBy: "trust" };
3059
3211
  }
3060
3212
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
3061
3213
  if (policyResult.decision === "allow") {
3062
3214
  if (approvers.cloud && creds?.apiKey)
3063
3215
  auditLocalAllow(toolName, args, "local-policy", creds, meta);
3064
- if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
3216
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
3065
3217
  return { approved: true, checkedBy: "local-policy" };
3066
3218
  }
3067
3219
  if (policyResult.decision === "block") {
3068
- if (!isManual) appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta);
3220
+ if (!isManual)
3221
+ appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
3069
3222
  return {
3070
3223
  approved: false,
3071
3224
  reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
@@ -3084,15 +3237,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3084
3237
  policyMatchedWord,
3085
3238
  policyResult.ruleName
3086
3239
  );
3087
- const persistent = getPersistentDecision(toolName);
3240
+ const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
3088
3241
  if (persistent === "allow") {
3089
3242
  if (approvers.cloud && creds?.apiKey)
3090
3243
  await auditLocalAllow(toolName, args, "persistent", creds, meta);
3091
- if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
3244
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
3092
3245
  return { approved: true, checkedBy: "persistent" };
3093
3246
  }
3094
3247
  if (persistent === "deny") {
3095
- if (!isManual) appendLocalAudit(toolName, args, "deny", "persistent-deny", meta);
3248
+ if (!isManual)
3249
+ appendLocalAudit(toolName, args, "deny", "persistent-deny", meta, hashAuditArgs);
3096
3250
  return {
3097
3251
  approved: false,
3098
3252
  reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
@@ -3100,10 +3254,21 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3100
3254
  blockedByLabel: "Persistent User Rule"
3101
3255
  };
3102
3256
  }
3103
- } else {
3104
- if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
3257
+ } else if (!taintWarning) {
3258
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
3105
3259
  return { approved: true };
3106
3260
  }
3261
+ if (taintWarning) {
3262
+ explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
3263
+ riskMetadata = computeRiskMetadata(
3264
+ args,
3265
+ 7,
3266
+ explainableLabel,
3267
+ void 0,
3268
+ void 0,
3269
+ taintWarning
3270
+ );
3271
+ }
3107
3272
  let cloudRequestId = null;
3108
3273
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
3109
3274
  if (cloudEnforced) {
@@ -3122,7 +3287,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3122
3287
  };
3123
3288
  }
3124
3289
  cloudRequestId = initResult.requestId || null;
3125
- explainableLabel = "Organization Policy (SaaS)";
3290
+ if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
3126
3291
  } catch {
3127
3292
  }
3128
3293
  }
@@ -3309,19 +3474,20 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3309
3474
  args,
3310
3475
  finalResult.approved ? "allow" : "deny",
3311
3476
  finalResult.checkedBy || finalResult.blockedBy || "unknown",
3312
- meta
3477
+ meta,
3478
+ hashAuditArgs
3313
3479
  );
3314
3480
  }
3315
3481
  return finalResult;
3316
3482
  }
3317
- var import_net, import_path13, import_os10, import_crypto2, ACTIVITY_SOCKET_PATH;
3483
+ var import_net, import_path13, import_os10, import_crypto3, WRITE_TOOLS, ACTIVITY_SOCKET_PATH;
3318
3484
  var init_orchestrator = __esm({
3319
3485
  "src/auth/orchestrator.ts"() {
3320
3486
  "use strict";
3321
3487
  import_net = __toESM(require("net"));
3322
3488
  import_path13 = __toESM(require("path"));
3323
3489
  import_os10 = __toESM(require("os"));
3324
- import_crypto2 = require("crypto");
3490
+ import_crypto3 = require("crypto");
3325
3491
  init_native();
3326
3492
  init_context_sniper();
3327
3493
  init_dlp();
@@ -3331,6 +3497,17 @@ var init_orchestrator = __esm({
3331
3497
  init_state();
3332
3498
  init_daemon();
3333
3499
  init_cloud();
3500
+ WRITE_TOOLS = /* @__PURE__ */ new Set([
3501
+ "write",
3502
+ "write_file",
3503
+ "create_file",
3504
+ "edit",
3505
+ "multiedit",
3506
+ "str_replace_based_edit_tool",
3507
+ "replace",
3508
+ "notebook_edit",
3509
+ "notebookedit"
3510
+ ]);
3334
3511
  ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path13.default.join(import_os10.default.tmpdir(), "node9-activity.sock");
3335
3512
  }
3336
3513
  });
@@ -4515,12 +4692,15 @@ var init_ui = __esm({
4515
4692
  const badgeClass = isEdit ? 'sniper-badge-edit' : 'sniper-badge-exec';
4516
4693
  const badgeLabel = isEdit ? '\u{1F4DD} Code Edit' : '\u{1F6D1} Execution';
4517
4694
  const tierLabel = \`Tier \${rm.tier} \xB7 \${esc(rm.blockedByLabel)}\`;
4695
+ const isTaint = rm.blockedByLabel?.includes('Taint');
4518
4696
  const fileLine =
4519
- isEdit && rm.editFilePath
4520
- ? \`<div class="sniper-filepath">\u{1F4C2} \${esc(rm.editFilePath)}</div>\`
4521
- : !isEdit && rm.matchedWord
4522
- ? \`<div class="sniper-match">Matched: <code>\${esc(rm.matchedWord)}</code>\${rm.matchedField ? \` in <code>\${esc(rm.matchedField)}</code>\` : ''}</div>\`
4523
- : '';
4697
+ isTaint && rm.ruleName
4698
+ ? \`<div class="sniper-match">\u26A0\uFE0F \${esc(rm.ruleName)}</div>\`
4699
+ : isEdit && rm.editFilePath
4700
+ ? \`<div class="sniper-filepath">\u{1F4C2} \${esc(rm.editFilePath)}</div>\`
4701
+ : !isEdit && rm.matchedWord
4702
+ ? \`<div class="sniper-match">Matched: <code>\${esc(rm.matchedWord)}</code>\${rm.matchedField ? \` in <code>\${esc(rm.matchedField)}</code>\` : ''}</div>\`
4703
+ : '';
4524
4704
  const snippetHtml = rm.contextSnippet ? \`<pre>\${esc(rm.contextSnippet)}</pre>\` : '';
4525
4705
  return \`
4526
4706
  <div class="sniper-header">
@@ -4991,11 +5171,11 @@ function commonPathPrefix(paths) {
4991
5171
  const prefix = common.join("/").replace(/\/?$/, "/");
4992
5172
  return prefix.length > 1 ? prefix : null;
4993
5173
  }
4994
- var import_crypto3, SuggestionTracker;
5174
+ var import_crypto4, SuggestionTracker;
4995
5175
  var init_suggestion_tracker = __esm({
4996
5176
  "src/daemon/suggestion-tracker.ts"() {
4997
5177
  "use strict";
4998
- import_crypto3 = require("crypto");
5178
+ import_crypto4 = require("crypto");
4999
5179
  SuggestionTracker = class {
5000
5180
  events = /* @__PURE__ */ new Map();
5001
5181
  threshold;
@@ -5041,7 +5221,7 @@ var init_suggestion_tracker = __esm({
5041
5221
  }
5042
5222
  } : { type: "ignoredTool", toolName };
5043
5223
  return {
5044
- id: (0, import_crypto3.randomUUID)(),
5224
+ id: (0, import_crypto4.randomUUID)(),
5045
5225
  toolName,
5046
5226
  allowCount: events.length,
5047
5227
  suggestedRule,
@@ -5054,11 +5234,91 @@ var init_suggestion_tracker = __esm({
5054
5234
  }
5055
5235
  });
5056
5236
 
5237
+ // src/daemon/taint-store.ts
5238
+ var import_fs12, import_path15, DEFAULT_TTL_MS, TaintStore;
5239
+ var init_taint_store = __esm({
5240
+ "src/daemon/taint-store.ts"() {
5241
+ "use strict";
5242
+ import_fs12 = __toESM(require("fs"));
5243
+ import_path15 = __toESM(require("path"));
5244
+ DEFAULT_TTL_MS = 60 * 60 * 1e3;
5245
+ TaintStore = class {
5246
+ records = /* @__PURE__ */ new Map();
5247
+ /** Add or refresh taint on an absolute path. */
5248
+ taint(filePath, source, ttlMs = DEFAULT_TTL_MS) {
5249
+ const resolved = this._resolve(filePath);
5250
+ const now = Date.now();
5251
+ this.records.set(resolved, {
5252
+ path: resolved,
5253
+ source,
5254
+ createdAt: now,
5255
+ expiresAt: now + ttlMs
5256
+ });
5257
+ }
5258
+ /**
5259
+ * Check whether a path is currently tainted.
5260
+ * Returns the TaintRecord if tainted (and not expired), null otherwise.
5261
+ * Expired records are pruned on access.
5262
+ */
5263
+ check(filePath) {
5264
+ const resolved = this._resolve(filePath);
5265
+ const record = this.records.get(resolved);
5266
+ if (!record) return null;
5267
+ if (Date.now() > record.expiresAt) {
5268
+ this.records.delete(resolved);
5269
+ return null;
5270
+ }
5271
+ return record;
5272
+ }
5273
+ /**
5274
+ * Propagate taint from sourcePath to destPath (e.g. cp, mv).
5275
+ * For mv semantics (clearSource=true) the source taint is removed.
5276
+ */
5277
+ propagate(sourcePath, destPath, clearSource = false) {
5278
+ const taintRecord = this.check(sourcePath);
5279
+ if (!taintRecord) return;
5280
+ const remainingMs = taintRecord.expiresAt - Date.now();
5281
+ if (remainingMs > 0) {
5282
+ const baseSource = taintRecord.source.replace(/^(propagated:)+/, "");
5283
+ this.taint(destPath, `propagated:${baseSource}`, remainingMs);
5284
+ }
5285
+ if (clearSource) {
5286
+ this.records.delete(this._resolve(sourcePath));
5287
+ }
5288
+ }
5289
+ /** Remove all expired records. Called periodically by the daemon. */
5290
+ prune() {
5291
+ const now = Date.now();
5292
+ for (const [key, record] of this.records) {
5293
+ if (now > record.expiresAt) this.records.delete(key);
5294
+ }
5295
+ }
5296
+ /** Return all non-expired taint records (for audit/debug). */
5297
+ list() {
5298
+ this.prune();
5299
+ return [...this.records.values()];
5300
+ }
5301
+ /** Remove all taint records atomically. Used by tests to reset state between runs. */
5302
+ clear() {
5303
+ this.records.clear();
5304
+ }
5305
+ /** Resolve to absolute path, falling back to path.resolve if file doesn't exist yet. */
5306
+ _resolve(filePath) {
5307
+ try {
5308
+ return import_fs12.default.realpathSync.native(import_path15.default.resolve(filePath));
5309
+ } catch {
5310
+ return import_path15.default.resolve(filePath);
5311
+ }
5312
+ }
5313
+ };
5314
+ }
5315
+ });
5316
+
5057
5317
  // src/daemon/state.ts
5058
5318
  function loadInsightCounts() {
5059
5319
  try {
5060
- if (!import_fs12.default.existsSync(INSIGHT_COUNTS_FILE)) return;
5061
- const data = JSON.parse(import_fs12.default.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
5320
+ if (!import_fs13.default.existsSync(INSIGHT_COUNTS_FILE)) return;
5321
+ const data = JSON.parse(import_fs13.default.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
5062
5322
  for (const [tool, count] of Object.entries(data)) {
5063
5323
  if (typeof count === "number" && count > 0) insightCounts.set(tool, count);
5064
5324
  }
@@ -5097,23 +5357,23 @@ function markRejectionHandlerRegistered() {
5097
5357
  daemonRejectionHandlerRegistered = true;
5098
5358
  }
5099
5359
  function atomicWriteSync2(filePath, data, options) {
5100
- const dir = import_path15.default.dirname(filePath);
5101
- if (!import_fs12.default.existsSync(dir)) import_fs12.default.mkdirSync(dir, { recursive: true });
5102
- const tmpPath = `${filePath}.${(0, import_crypto4.randomUUID)()}.tmp`;
5360
+ const dir = import_path16.default.dirname(filePath);
5361
+ if (!import_fs13.default.existsSync(dir)) import_fs13.default.mkdirSync(dir, { recursive: true });
5362
+ const tmpPath = `${filePath}.${(0, import_crypto5.randomUUID)()}.tmp`;
5103
5363
  try {
5104
- import_fs12.default.writeFileSync(tmpPath, data, options);
5364
+ import_fs13.default.writeFileSync(tmpPath, data, options);
5105
5365
  } catch (err) {
5106
5366
  try {
5107
- import_fs12.default.unlinkSync(tmpPath);
5367
+ import_fs13.default.unlinkSync(tmpPath);
5108
5368
  } catch {
5109
5369
  }
5110
5370
  throw err;
5111
5371
  }
5112
5372
  try {
5113
- import_fs12.default.renameSync(tmpPath, filePath);
5373
+ import_fs13.default.renameSync(tmpPath, filePath);
5114
5374
  } catch (err) {
5115
5375
  try {
5116
- import_fs12.default.unlinkSync(tmpPath);
5376
+ import_fs13.default.unlinkSync(tmpPath);
5117
5377
  } catch {
5118
5378
  }
5119
5379
  throw err;
@@ -5137,16 +5397,16 @@ function appendAuditLog(data) {
5137
5397
  decision: data.decision,
5138
5398
  source: "daemon"
5139
5399
  };
5140
- const dir = import_path15.default.dirname(AUDIT_LOG_FILE);
5141
- if (!import_fs12.default.existsSync(dir)) import_fs12.default.mkdirSync(dir, { recursive: true });
5142
- import_fs12.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
5400
+ const dir = import_path16.default.dirname(AUDIT_LOG_FILE);
5401
+ if (!import_fs13.default.existsSync(dir)) import_fs13.default.mkdirSync(dir, { recursive: true });
5402
+ import_fs13.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
5143
5403
  } catch {
5144
5404
  }
5145
5405
  }
5146
5406
  function getAuditHistory(limit = 20) {
5147
5407
  try {
5148
- if (!import_fs12.default.existsSync(AUDIT_LOG_FILE)) return [];
5149
- const lines = import_fs12.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
5408
+ if (!import_fs13.default.existsSync(AUDIT_LOG_FILE)) return [];
5409
+ const lines = import_fs13.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
5150
5410
  if (lines.length === 1 && lines[0] === "") return [];
5151
5411
  return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
5152
5412
  } catch {
@@ -5155,19 +5415,19 @@ function getAuditHistory(limit = 20) {
5155
5415
  }
5156
5416
  function getOrgName() {
5157
5417
  try {
5158
- if (import_fs12.default.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
5418
+ if (import_fs13.default.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
5159
5419
  } catch {
5160
5420
  }
5161
5421
  return null;
5162
5422
  }
5163
5423
  function hasStoredSlackKey() {
5164
- return import_fs12.default.existsSync(CREDENTIALS_FILE);
5424
+ return import_fs13.default.existsSync(CREDENTIALS_FILE);
5165
5425
  }
5166
5426
  function writeGlobalSetting(key, value) {
5167
5427
  let config = {};
5168
5428
  try {
5169
- if (import_fs12.default.existsSync(GLOBAL_CONFIG_FILE)) {
5170
- config = JSON.parse(import_fs12.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
5429
+ if (import_fs13.default.existsSync(GLOBAL_CONFIG_FILE)) {
5430
+ config = JSON.parse(import_fs13.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
5171
5431
  }
5172
5432
  } catch {
5173
5433
  }
@@ -5179,8 +5439,8 @@ function writeTrustEntry(toolName, durationMs) {
5179
5439
  try {
5180
5440
  let trust = { entries: [] };
5181
5441
  try {
5182
- if (import_fs12.default.existsSync(TRUST_FILE2))
5183
- trust = JSON.parse(import_fs12.default.readFileSync(TRUST_FILE2, "utf-8"));
5442
+ if (import_fs13.default.existsSync(TRUST_FILE2))
5443
+ trust = JSON.parse(import_fs13.default.readFileSync(TRUST_FILE2, "utf-8"));
5184
5444
  } catch {
5185
5445
  }
5186
5446
  trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
@@ -5191,8 +5451,8 @@ function writeTrustEntry(toolName, durationMs) {
5191
5451
  }
5192
5452
  function readPersistentDecisions() {
5193
5453
  try {
5194
- if (import_fs12.default.existsSync(DECISIONS_FILE)) {
5195
- return JSON.parse(import_fs12.default.readFileSync(DECISIONS_FILE, "utf-8"));
5454
+ if (import_fs13.default.existsSync(DECISIONS_FILE)) {
5455
+ return JSON.parse(import_fs13.default.readFileSync(DECISIONS_FILE, "utf-8"));
5196
5456
  }
5197
5457
  } catch {
5198
5458
  }
@@ -5257,7 +5517,7 @@ function abandonPending() {
5257
5517
  });
5258
5518
  if (autoStarted) {
5259
5519
  try {
5260
- import_fs12.default.unlinkSync(DAEMON_PID_FILE);
5520
+ import_fs13.default.unlinkSync(DAEMON_PID_FILE);
5261
5521
  } catch {
5262
5522
  }
5263
5523
  setTimeout(() => {
@@ -5268,7 +5528,7 @@ function abandonPending() {
5268
5528
  }
5269
5529
  function startActivitySocket() {
5270
5530
  try {
5271
- import_fs12.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
5531
+ import_fs13.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
5272
5532
  } catch {
5273
5533
  }
5274
5534
  const ACTIVITY_MAX_BYTES = 1024 * 1024;
@@ -5310,35 +5570,37 @@ function startActivitySocket() {
5310
5570
  unixServer.listen(ACTIVITY_SOCKET_PATH2);
5311
5571
  process.on("exit", () => {
5312
5572
  try {
5313
- import_fs12.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
5573
+ import_fs13.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
5314
5574
  } catch {
5315
5575
  }
5316
5576
  });
5317
5577
  }
5318
- var import_net2, import_fs12, import_path15, import_os12, import_child_process3, import_crypto4, 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;
5578
+ var import_net2, import_fs13, import_path16, import_os12, import_child_process3, import_crypto5, 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;
5319
5579
  var init_state2 = __esm({
5320
5580
  "src/daemon/state.ts"() {
5321
5581
  "use strict";
5322
5582
  import_net2 = __toESM(require("net"));
5323
- import_fs12 = __toESM(require("fs"));
5324
- import_path15 = __toESM(require("path"));
5583
+ import_fs13 = __toESM(require("fs"));
5584
+ import_path16 = __toESM(require("path"));
5325
5585
  import_os12 = __toESM(require("os"));
5326
5586
  import_child_process3 = require("child_process");
5327
- import_crypto4 = require("crypto");
5587
+ import_crypto5 = require("crypto");
5328
5588
  init_daemon();
5329
5589
  init_suggestion_tracker();
5590
+ init_taint_store();
5330
5591
  homeDir = import_os12.default.homedir();
5331
- DAEMON_PID_FILE = import_path15.default.join(homeDir, ".node9", "daemon.pid");
5332
- DECISIONS_FILE = import_path15.default.join(homeDir, ".node9", "decisions.json");
5333
- AUDIT_LOG_FILE = import_path15.default.join(homeDir, ".node9", "audit.log");
5334
- TRUST_FILE2 = import_path15.default.join(homeDir, ".node9", "trust.json");
5335
- GLOBAL_CONFIG_FILE = import_path15.default.join(homeDir, ".node9", "config.json");
5336
- CREDENTIALS_FILE = import_path15.default.join(homeDir, ".node9", "credentials.json");
5337
- INSIGHT_COUNTS_FILE = import_path15.default.join(homeDir, ".node9", "insight-counts.json");
5592
+ DAEMON_PID_FILE = import_path16.default.join(homeDir, ".node9", "daemon.pid");
5593
+ DECISIONS_FILE = import_path16.default.join(homeDir, ".node9", "decisions.json");
5594
+ AUDIT_LOG_FILE = import_path16.default.join(homeDir, ".node9", "audit.log");
5595
+ TRUST_FILE2 = import_path16.default.join(homeDir, ".node9", "trust.json");
5596
+ GLOBAL_CONFIG_FILE = import_path16.default.join(homeDir, ".node9", "config.json");
5597
+ CREDENTIALS_FILE = import_path16.default.join(homeDir, ".node9", "credentials.json");
5598
+ INSIGHT_COUNTS_FILE = import_path16.default.join(homeDir, ".node9", "insight-counts.json");
5338
5599
  pending = /* @__PURE__ */ new Map();
5339
5600
  sseClients = /* @__PURE__ */ new Set();
5340
5601
  suggestionTracker = new SuggestionTracker(3);
5341
5602
  suggestions = /* @__PURE__ */ new Map();
5603
+ taintStore = new TaintStore();
5342
5604
  insightCounts = /* @__PURE__ */ new Map();
5343
5605
  _abandonTimer = null;
5344
5606
  _hadBrowserClient = false;
@@ -5351,7 +5613,7 @@ var init_state2 = __esm({
5351
5613
  "2h": 2 * 60 * 6e4
5352
5614
  };
5353
5615
  autoStarted = process.env.NODE9_AUTO_STARTED === "1";
5354
- ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path15.default.join(import_os12.default.tmpdir(), "node9-activity.sock");
5616
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path16.default.join(import_os12.default.tmpdir(), "node9-activity.sock");
5355
5617
  ACTIVITY_RING_SIZE = 100;
5356
5618
  activityRing = [];
5357
5619
  SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
@@ -5362,8 +5624,8 @@ var init_state2 = __esm({
5362
5624
  function patchConfig(configPath, patch) {
5363
5625
  let config = {};
5364
5626
  try {
5365
- if (import_fs13.default.existsSync(configPath)) {
5366
- config = JSON.parse(import_fs13.default.readFileSync(configPath, "utf8"));
5627
+ if (import_fs14.default.existsSync(configPath)) {
5628
+ config = JSON.parse(import_fs14.default.readFileSync(configPath, "utf8"));
5367
5629
  }
5368
5630
  } catch {
5369
5631
  throw new Error(`Cannot read config at ${configPath} \u2014 file may be corrupted`);
@@ -5382,44 +5644,44 @@ function patchConfig(configPath, patch) {
5382
5644
  ignored.push(patch.toolName);
5383
5645
  }
5384
5646
  }
5385
- const dir = import_path16.default.dirname(configPath);
5386
- import_fs13.default.mkdirSync(dir, { recursive: true });
5647
+ const dir = import_path17.default.dirname(configPath);
5648
+ import_fs14.default.mkdirSync(dir, { recursive: true });
5387
5649
  const tmp = configPath + ".node9-tmp";
5388
5650
  try {
5389
- import_fs13.default.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
5651
+ import_fs14.default.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
5390
5652
  } catch (err) {
5391
5653
  try {
5392
- import_fs13.default.unlinkSync(tmp);
5654
+ import_fs14.default.unlinkSync(tmp);
5393
5655
  } catch {
5394
5656
  }
5395
5657
  throw err;
5396
5658
  }
5397
5659
  try {
5398
- import_fs13.default.renameSync(tmp, configPath);
5660
+ import_fs14.default.renameSync(tmp, configPath);
5399
5661
  } catch (err) {
5400
5662
  try {
5401
- import_fs13.default.unlinkSync(tmp);
5663
+ import_fs14.default.unlinkSync(tmp);
5402
5664
  } catch {
5403
5665
  }
5404
5666
  throw err;
5405
5667
  }
5406
5668
  }
5407
- var import_fs13, import_path16, import_os13, GLOBAL_CONFIG_PATH;
5669
+ var import_fs14, import_path17, import_os13, GLOBAL_CONFIG_PATH;
5408
5670
  var init_patch = __esm({
5409
5671
  "src/config/patch.ts"() {
5410
5672
  "use strict";
5411
- import_fs13 = __toESM(require("fs"));
5412
- import_path16 = __toESM(require("path"));
5673
+ import_fs14 = __toESM(require("fs"));
5674
+ import_path17 = __toESM(require("path"));
5413
5675
  import_os13 = __toESM(require("os"));
5414
- GLOBAL_CONFIG_PATH = import_path16.default.join(import_os13.default.homedir(), ".node9", "config.json");
5676
+ GLOBAL_CONFIG_PATH = import_path17.default.join(import_os13.default.homedir(), ".node9", "config.json");
5415
5677
  }
5416
5678
  });
5417
5679
 
5418
5680
  // src/daemon/server.ts
5419
5681
  function startDaemon() {
5420
5682
  loadInsightCounts();
5421
- const csrfToken = (0, import_crypto5.randomUUID)();
5422
- const internalToken = (0, import_crypto5.randomUUID)();
5683
+ const csrfToken = (0, import_crypto6.randomUUID)();
5684
+ const internalToken = (0, import_crypto6.randomUUID)();
5423
5685
  const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
5424
5686
  const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
5425
5687
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
@@ -5432,7 +5694,7 @@ function startDaemon() {
5432
5694
  idleTimer = setTimeout(() => {
5433
5695
  if (autoStarted) {
5434
5696
  try {
5435
- import_fs14.default.unlinkSync(DAEMON_PID_FILE);
5697
+ import_fs15.default.unlinkSync(DAEMON_PID_FILE);
5436
5698
  } catch {
5437
5699
  }
5438
5700
  }
@@ -5547,7 +5809,7 @@ data: ${JSON.stringify(item.data)}
5547
5809
  activityId,
5548
5810
  cwd
5549
5811
  } = JSON.parse(body);
5550
- const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto5.randomUUID)();
5812
+ const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto6.randomUUID)();
5551
5813
  const entry = {
5552
5814
  id,
5553
5815
  toolName,
@@ -5587,7 +5849,7 @@ data: ${JSON.stringify(item.data)}
5587
5849
  status: "pending"
5588
5850
  });
5589
5851
  }
5590
- const projectCwd = typeof cwd === "string" && import_path17.default.isAbsolute(cwd) ? cwd : void 0;
5852
+ const projectCwd = typeof cwd === "string" && import_path18.default.isAbsolute(cwd) ? cwd : void 0;
5591
5853
  const projectConfig = getConfig(projectCwd);
5592
5854
  const browserEnabled = projectConfig.settings.approvers?.browser !== false;
5593
5855
  const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
@@ -5938,8 +6200,8 @@ data: ${JSON.stringify(item.data)}
5938
6200
  const body = await readBody(req);
5939
6201
  const data = body ? JSON.parse(body) : {};
5940
6202
  const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
5941
- const node9Dir = import_path17.default.dirname(GLOBAL_CONFIG_PATH);
5942
- if (!import_path17.default.resolve(configPath).startsWith(node9Dir + import_path17.default.sep)) {
6203
+ const node9Dir = import_path18.default.dirname(GLOBAL_CONFIG_PATH);
6204
+ if (!import_path18.default.resolve(configPath).startsWith(node9Dir + import_path18.default.sep)) {
5943
6205
  res.writeHead(400, { "Content-Type": "application/json" });
5944
6206
  return res.end(
5945
6207
  JSON.stringify({ error: "configPath must be within the node9 config directory" })
@@ -5987,20 +6249,77 @@ data: ${JSON.stringify(item.data)}
5987
6249
  res.writeHead(400).end();
5988
6250
  }
5989
6251
  }
6252
+ if (req.method === "POST" && pathname === "/taint") {
6253
+ try {
6254
+ const body = JSON.parse(await readBody(req));
6255
+ if (typeof body.path !== "string" || typeof body.source !== "string") {
6256
+ res.writeHead(400, { "Content-Type": "application/json" });
6257
+ return res.end(JSON.stringify({ error: "path and source are required strings" }));
6258
+ }
6259
+ const ttlMs = typeof body.ttlMs === "number" ? body.ttlMs : void 0;
6260
+ taintStore.taint(body.path, body.source, ttlMs);
6261
+ res.writeHead(200, { "Content-Type": "application/json" });
6262
+ return res.end(JSON.stringify({ ok: true }));
6263
+ } catch {
6264
+ res.writeHead(400).end();
6265
+ return;
6266
+ }
6267
+ }
6268
+ if (req.method === "POST" && pathname === "/taint/check") {
6269
+ try {
6270
+ const body = JSON.parse(await readBody(req));
6271
+ if (!Array.isArray(body.paths)) {
6272
+ res.writeHead(400, { "Content-Type": "application/json" });
6273
+ return res.end(JSON.stringify({ error: "paths must be an array" }));
6274
+ }
6275
+ if (body.paths.some((p) => typeof p !== "string")) {
6276
+ res.writeHead(400, { "Content-Type": "application/json" });
6277
+ return res.end(JSON.stringify({ error: "all paths must be strings" }));
6278
+ }
6279
+ for (const p of body.paths) {
6280
+ const record = taintStore.check(p);
6281
+ if (record) {
6282
+ res.writeHead(200, { "Content-Type": "application/json" });
6283
+ return res.end(JSON.stringify({ tainted: true, record }));
6284
+ }
6285
+ }
6286
+ res.writeHead(200, { "Content-Type": "application/json" });
6287
+ return res.end(JSON.stringify({ tainted: false }));
6288
+ } catch {
6289
+ res.writeHead(400).end();
6290
+ return;
6291
+ }
6292
+ }
6293
+ if (req.method === "POST" && pathname === "/taint/propagate") {
6294
+ try {
6295
+ const body = JSON.parse(await readBody(req));
6296
+ if (typeof body.src !== "string" || typeof body.dest !== "string") {
6297
+ res.writeHead(400, { "Content-Type": "application/json" });
6298
+ return res.end(JSON.stringify({ error: "src and dest are required strings" }));
6299
+ }
6300
+ const clearSource = body.clearSource === true;
6301
+ taintStore.propagate(body.src, body.dest, clearSource);
6302
+ res.writeHead(200, { "Content-Type": "application/json" });
6303
+ return res.end(JSON.stringify({ ok: true }));
6304
+ } catch {
6305
+ res.writeHead(400).end();
6306
+ return;
6307
+ }
6308
+ }
5990
6309
  res.writeHead(404).end();
5991
6310
  });
5992
6311
  setDaemonServer(server);
5993
6312
  server.on("error", (e) => {
5994
6313
  if (e.code === "EADDRINUSE") {
5995
6314
  try {
5996
- if (import_fs14.default.existsSync(DAEMON_PID_FILE)) {
5997
- const { pid } = JSON.parse(import_fs14.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
6315
+ if (import_fs15.default.existsSync(DAEMON_PID_FILE)) {
6316
+ const { pid } = JSON.parse(import_fs15.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
5998
6317
  process.kill(pid, 0);
5999
6318
  return process.exit(0);
6000
6319
  }
6001
6320
  } catch {
6002
6321
  try {
6003
- import_fs14.default.unlinkSync(DAEMON_PID_FILE);
6322
+ import_fs15.default.unlinkSync(DAEMON_PID_FILE);
6004
6323
  } catch {
6005
6324
  }
6006
6325
  server.listen(DAEMON_PORT, DAEMON_HOST);
@@ -6059,14 +6378,14 @@ data: ${JSON.stringify(item.data)}
6059
6378
  }
6060
6379
  startActivitySocket();
6061
6380
  }
6062
- var import_http, import_fs14, import_path17, import_crypto5, import_child_process4, import_chalk2;
6381
+ var import_http, import_fs15, import_path18, import_crypto6, import_child_process4, import_chalk2;
6063
6382
  var init_server = __esm({
6064
6383
  "src/daemon/server.ts"() {
6065
6384
  "use strict";
6066
6385
  import_http = __toESM(require("http"));
6067
- import_fs14 = __toESM(require("fs"));
6068
- import_path17 = __toESM(require("path"));
6069
- import_crypto5 = require("crypto");
6386
+ import_fs15 = __toESM(require("fs"));
6387
+ import_path18 = __toESM(require("path"));
6388
+ import_crypto6 = require("crypto");
6070
6389
  import_child_process4 = require("child_process");
6071
6390
  import_chalk2 = __toESM(require("chalk"));
6072
6391
  init_core();
@@ -6080,24 +6399,24 @@ var init_server = __esm({
6080
6399
 
6081
6400
  // src/daemon/index.ts
6082
6401
  function stopDaemon() {
6083
- if (!import_fs15.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk3.default.yellow("Not running."));
6402
+ if (!import_fs16.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk3.default.yellow("Not running."));
6084
6403
  try {
6085
- const { pid } = JSON.parse(import_fs15.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
6404
+ const { pid } = JSON.parse(import_fs16.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
6086
6405
  process.kill(pid, "SIGTERM");
6087
6406
  console.log(import_chalk3.default.green("\u2705 Stopped."));
6088
6407
  } catch {
6089
6408
  console.log(import_chalk3.default.gray("Cleaned up stale PID file."));
6090
6409
  } finally {
6091
6410
  try {
6092
- import_fs15.default.unlinkSync(DAEMON_PID_FILE);
6411
+ import_fs16.default.unlinkSync(DAEMON_PID_FILE);
6093
6412
  } catch {
6094
6413
  }
6095
6414
  }
6096
6415
  }
6097
6416
  function daemonStatus() {
6098
- if (import_fs15.default.existsSync(DAEMON_PID_FILE)) {
6417
+ if (import_fs16.default.existsSync(DAEMON_PID_FILE)) {
6099
6418
  try {
6100
- const { pid } = JSON.parse(import_fs15.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
6419
+ const { pid } = JSON.parse(import_fs16.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
6101
6420
  process.kill(pid, 0);
6102
6421
  console.log(import_chalk3.default.green("Node9 daemon: running"));
6103
6422
  return;
@@ -6116,11 +6435,11 @@ function daemonStatus() {
6116
6435
  console.log(import_chalk3.default.yellow("Node9 daemon: not running"));
6117
6436
  }
6118
6437
  }
6119
- var import_fs15, import_chalk3, import_child_process5;
6438
+ var import_fs16, import_chalk3, import_child_process5;
6120
6439
  var init_daemon2 = __esm({
6121
6440
  "src/daemon/index.ts"() {
6122
6441
  "use strict";
6123
- import_fs15 = __toESM(require("fs"));
6442
+ import_fs16 = __toESM(require("fs"));
6124
6443
  import_chalk3 = __toESM(require("chalk"));
6125
6444
  import_child_process5 = require("child_process");
6126
6445
  init_server();
@@ -6171,9 +6490,9 @@ function renderPending(activity) {
6171
6490
  }
6172
6491
  async function ensureDaemon() {
6173
6492
  let pidPort = null;
6174
- if (import_fs23.default.existsSync(PID_FILE)) {
6493
+ if (import_fs24.default.existsSync(PID_FILE)) {
6175
6494
  try {
6176
- const { port } = JSON.parse(import_fs23.default.readFileSync(PID_FILE, "utf-8"));
6495
+ const { port } = JSON.parse(import_fs24.default.readFileSync(PID_FILE, "utf-8"));
6177
6496
  pidPort = port;
6178
6497
  } catch {
6179
6498
  console.error(import_chalk16.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
@@ -6244,9 +6563,12 @@ function buildCardLines(req, localCount = 0) {
6244
6563
  ``,
6245
6564
  `${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
6246
6565
  `${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
6247
- `${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`,
6248
- `${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`
6566
+ `${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`
6249
6567
  ];
6568
+ if (req.riskMetadata?.ruleName && blockedBy.includes("Taint")) {
6569
+ lines.push(`${CYAN}\u2551${RESET} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET}`);
6570
+ }
6571
+ lines.push(`${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`);
6250
6572
  if (localCount >= 2) {
6251
6573
  lines.push(
6252
6574
  `${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
@@ -6308,12 +6630,13 @@ async function startTail(options = {}) {
6308
6630
  if (canApprove) import_readline3.default.emitKeypressEvents(process.stdin);
6309
6631
  function clearCard() {
6310
6632
  if (cardLineCount > 0) {
6311
- process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
6633
+ import_readline3.default.moveCursor(process.stdout, 0, -cardLineCount);
6634
+ process.stdout.write(ERASE_DOWN);
6312
6635
  cardLineCount = 0;
6313
6636
  }
6314
6637
  }
6315
6638
  function printCard(req2) {
6316
- process.stdout.write(HIDE_CURSOR + SAVE_CURSOR);
6639
+ process.stdout.write(HIDE_CURSOR);
6317
6640
  const daemonPrior = req2.allowCount !== void 0 ? req2.allowCount - 1 : 0;
6318
6641
  const localPrior = localAllowCounts.get(req2.toolName) ?? 0;
6319
6642
  const priorCount = Math.max(daemonPrior, localPrior);
@@ -6349,7 +6672,7 @@ async function startTail(options = {}) {
6349
6672
  if (settled) return;
6350
6673
  settled = true;
6351
6674
  cleanup();
6352
- process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
6675
+ clearCard();
6353
6676
  const stampedLines = buildCardLines(
6354
6677
  req2,
6355
6678
  Math.max(
@@ -6380,8 +6703,8 @@ async function startTail(options = {}) {
6380
6703
  }
6381
6704
  postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
6382
6705
  try {
6383
- import_fs23.default.appendFileSync(
6384
- import_path25.default.join(import_os21.default.homedir(), ".node9", "hook-debug.log"),
6706
+ import_fs24.default.appendFileSync(
6707
+ import_path26.default.join(import_os21.default.homedir(), ".node9", "hook-debug.log"),
6385
6708
  `[tail] POST /decision failed: ${String(err)}
6386
6709
  `
6387
6710
  );
@@ -6396,7 +6719,7 @@ async function startTail(options = {}) {
6396
6719
  if (settled) return;
6397
6720
  settled = true;
6398
6721
  cleanup();
6399
- process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
6722
+ clearCard();
6400
6723
  const priorCount = Math.max(
6401
6724
  req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
6402
6725
  localAllowCounts.get(req2.toolName) ?? 0
@@ -6593,21 +6916,21 @@ async function startTail(options = {}) {
6593
6916
  process.exit(1);
6594
6917
  });
6595
6918
  }
6596
- var import_http2, import_chalk16, import_fs23, import_os21, import_path25, import_readline3, import_child_process13, PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, SAVE_CURSOR, RESTORE_CURSOR;
6919
+ var import_http2, import_chalk16, import_fs24, import_os21, import_path26, import_readline3, import_child_process13, PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN;
6597
6920
  var init_tail = __esm({
6598
6921
  "src/tui/tail.ts"() {
6599
6922
  "use strict";
6600
6923
  import_http2 = __toESM(require("http"));
6601
6924
  import_chalk16 = __toESM(require("chalk"));
6602
- import_fs23 = __toESM(require("fs"));
6925
+ import_fs24 = __toESM(require("fs"));
6603
6926
  import_os21 = __toESM(require("os"));
6604
- import_path25 = __toESM(require("path"));
6927
+ import_path26 = __toESM(require("path"));
6605
6928
  import_readline3 = __toESM(require("readline"));
6606
6929
  import_child_process13 = require("child_process");
6607
6930
  init_daemon2();
6608
6931
  init_daemon();
6609
6932
  init_core();
6610
- PID_FILE = import_path25.default.join(import_os21.default.homedir(), ".node9", "daemon.pid");
6933
+ PID_FILE = import_path26.default.join(import_os21.default.homedir(), ".node9", "daemon.pid");
6611
6934
  ICONS = {
6612
6935
  bash: "\u{1F4BB}",
6613
6936
  shell: "\u{1F4BB}",
@@ -6635,8 +6958,6 @@ var init_tail = __esm({
6635
6958
  HIDE_CURSOR = "\x1B[?25l";
6636
6959
  SHOW_CURSOR = "\x1B[?25h";
6637
6960
  ERASE_DOWN = "\x1B[J";
6638
- SAVE_CURSOR = "\x1B7";
6639
- RESTORE_CURSOR = "\x1B8";
6640
6961
  }
6641
6962
  });
6642
6963
 
@@ -7033,8 +7354,8 @@ async function setupCursor() {
7033
7354
  // src/cli.ts
7034
7355
  init_daemon2();
7035
7356
  var import_chalk17 = __toESM(require("chalk"));
7036
- var import_fs24 = __toESM(require("fs"));
7037
- var import_path26 = __toESM(require("path"));
7357
+ var import_fs25 = __toESM(require("fs"));
7358
+ var import_path27 = __toESM(require("path"));
7038
7359
  var import_os22 = __toESM(require("os"));
7039
7360
  var import_prompts3 = require("@inquirer/prompts");
7040
7361
 
@@ -7256,8 +7577,8 @@ async function autoStartDaemonAndWait() {
7256
7577
 
7257
7578
  // src/cli/commands/check.ts
7258
7579
  var import_chalk5 = __toESM(require("chalk"));
7259
- var import_fs17 = __toESM(require("fs"));
7260
- var import_path19 = __toESM(require("path"));
7580
+ var import_fs18 = __toESM(require("fs"));
7581
+ var import_path20 = __toESM(require("path"));
7261
7582
  var import_os15 = __toESM(require("os"));
7262
7583
  init_orchestrator();
7263
7584
  init_daemon();
@@ -7266,26 +7587,26 @@ init_policy();
7266
7587
 
7267
7588
  // src/undo.ts
7268
7589
  var import_child_process8 = require("child_process");
7269
- var import_crypto6 = __toESM(require("crypto"));
7270
- var import_fs16 = __toESM(require("fs"));
7271
- var import_path18 = __toESM(require("path"));
7590
+ var import_crypto7 = __toESM(require("crypto"));
7591
+ var import_fs17 = __toESM(require("fs"));
7592
+ var import_path19 = __toESM(require("path"));
7272
7593
  var import_os14 = __toESM(require("os"));
7273
- var SNAPSHOT_STACK_PATH = import_path18.default.join(import_os14.default.homedir(), ".node9", "snapshots.json");
7274
- var UNDO_LATEST_PATH = import_path18.default.join(import_os14.default.homedir(), ".node9", "undo_latest.txt");
7594
+ var SNAPSHOT_STACK_PATH = import_path19.default.join(import_os14.default.homedir(), ".node9", "snapshots.json");
7595
+ var UNDO_LATEST_PATH = import_path19.default.join(import_os14.default.homedir(), ".node9", "undo_latest.txt");
7275
7596
  var MAX_SNAPSHOTS = 10;
7276
7597
  var GIT_TIMEOUT = 15e3;
7277
7598
  function readStack() {
7278
7599
  try {
7279
- if (import_fs16.default.existsSync(SNAPSHOT_STACK_PATH))
7280
- return JSON.parse(import_fs16.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
7600
+ if (import_fs17.default.existsSync(SNAPSHOT_STACK_PATH))
7601
+ return JSON.parse(import_fs17.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
7281
7602
  } catch {
7282
7603
  }
7283
7604
  return [];
7284
7605
  }
7285
7606
  function writeStack(stack) {
7286
- const dir = import_path18.default.dirname(SNAPSHOT_STACK_PATH);
7287
- if (!import_fs16.default.existsSync(dir)) import_fs16.default.mkdirSync(dir, { recursive: true });
7288
- import_fs16.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
7607
+ const dir = import_path19.default.dirname(SNAPSHOT_STACK_PATH);
7608
+ if (!import_fs17.default.existsSync(dir)) import_fs17.default.mkdirSync(dir, { recursive: true });
7609
+ import_fs17.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
7289
7610
  }
7290
7611
  function buildArgsSummary(tool, args) {
7291
7612
  if (!args || typeof args !== "object") return "";
@@ -7301,7 +7622,7 @@ function buildArgsSummary(tool, args) {
7301
7622
  function normalizeCwdForHash(cwd) {
7302
7623
  let normalized;
7303
7624
  try {
7304
- normalized = import_fs16.default.realpathSync(cwd);
7625
+ normalized = import_fs17.default.realpathSync(cwd);
7305
7626
  } catch {
7306
7627
  normalized = cwd;
7307
7628
  }
@@ -7310,17 +7631,17 @@ function normalizeCwdForHash(cwd) {
7310
7631
  return normalized;
7311
7632
  }
7312
7633
  function getShadowRepoDir(cwd) {
7313
- const hash = import_crypto6.default.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
7314
- return import_path18.default.join(import_os14.default.homedir(), ".node9", "snapshots", hash);
7634
+ const hash = import_crypto7.default.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
7635
+ return import_path19.default.join(import_os14.default.homedir(), ".node9", "snapshots", hash);
7315
7636
  }
7316
7637
  function cleanOrphanedIndexFiles(shadowDir) {
7317
7638
  try {
7318
7639
  const cutoff = Date.now() - 6e4;
7319
- for (const f of import_fs16.default.readdirSync(shadowDir)) {
7640
+ for (const f of import_fs17.default.readdirSync(shadowDir)) {
7320
7641
  if (f.startsWith("index_")) {
7321
- const fp = import_path18.default.join(shadowDir, f);
7642
+ const fp = import_path19.default.join(shadowDir, f);
7322
7643
  try {
7323
- if (import_fs16.default.statSync(fp).mtimeMs < cutoff) import_fs16.default.unlinkSync(fp);
7644
+ if (import_fs17.default.statSync(fp).mtimeMs < cutoff) import_fs17.default.unlinkSync(fp);
7324
7645
  } catch {
7325
7646
  }
7326
7647
  }
@@ -7332,7 +7653,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
7332
7653
  const hardcoded = [".git", ".node9"];
7333
7654
  const lines = [...hardcoded, ...ignorePaths].join("\n");
7334
7655
  try {
7335
- import_fs16.default.writeFileSync(import_path18.default.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
7656
+ import_fs17.default.writeFileSync(import_path19.default.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
7336
7657
  } catch {
7337
7658
  }
7338
7659
  }
@@ -7345,25 +7666,25 @@ function ensureShadowRepo(shadowDir, cwd) {
7345
7666
  timeout: 3e3
7346
7667
  });
7347
7668
  if (check.status === 0) {
7348
- const ptPath = import_path18.default.join(shadowDir, "project-path.txt");
7669
+ const ptPath = import_path19.default.join(shadowDir, "project-path.txt");
7349
7670
  try {
7350
- const stored = import_fs16.default.readFileSync(ptPath, "utf8").trim();
7671
+ const stored = import_fs17.default.readFileSync(ptPath, "utf8").trim();
7351
7672
  if (stored === normalizedCwd) return true;
7352
7673
  if (process.env.NODE9_DEBUG === "1")
7353
7674
  console.error(
7354
7675
  `[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
7355
7676
  );
7356
- import_fs16.default.rmSync(shadowDir, { recursive: true, force: true });
7677
+ import_fs17.default.rmSync(shadowDir, { recursive: true, force: true });
7357
7678
  } catch {
7358
7679
  try {
7359
- import_fs16.default.writeFileSync(ptPath, normalizedCwd, "utf8");
7680
+ import_fs17.default.writeFileSync(ptPath, normalizedCwd, "utf8");
7360
7681
  } catch {
7361
7682
  }
7362
7683
  return true;
7363
7684
  }
7364
7685
  }
7365
7686
  try {
7366
- import_fs16.default.mkdirSync(shadowDir, { recursive: true });
7687
+ import_fs17.default.mkdirSync(shadowDir, { recursive: true });
7367
7688
  } catch {
7368
7689
  }
7369
7690
  const init = (0, import_child_process8.spawnSync)("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
@@ -7372,7 +7693,7 @@ function ensureShadowRepo(shadowDir, cwd) {
7372
7693
  console.error("[Node9] git init --bare failed:", init.stderr?.toString());
7373
7694
  return false;
7374
7695
  }
7375
- const configFile = import_path18.default.join(shadowDir, "config");
7696
+ const configFile = import_path19.default.join(shadowDir, "config");
7376
7697
  (0, import_child_process8.spawnSync)("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
7377
7698
  timeout: 3e3
7378
7699
  });
@@ -7380,7 +7701,7 @@ function ensureShadowRepo(shadowDir, cwd) {
7380
7701
  timeout: 3e3
7381
7702
  });
7382
7703
  try {
7383
- import_fs16.default.writeFileSync(import_path18.default.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
7704
+ import_fs17.default.writeFileSync(import_path19.default.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
7384
7705
  } catch {
7385
7706
  }
7386
7707
  return true;
@@ -7403,7 +7724,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
7403
7724
  const shadowDir = getShadowRepoDir(cwd);
7404
7725
  if (!ensureShadowRepo(shadowDir, cwd)) return null;
7405
7726
  writeShadowExcludes(shadowDir, ignorePaths);
7406
- indexFile = import_path18.default.join(shadowDir, `index_${process.pid}_${Date.now()}`);
7727
+ indexFile = import_path19.default.join(shadowDir, `index_${process.pid}_${Date.now()}`);
7407
7728
  const shadowEnv = {
7408
7729
  ...process.env,
7409
7730
  GIT_DIR: shadowDir,
@@ -7432,7 +7753,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
7432
7753
  const shouldGc = stack.length % 5 === 0;
7433
7754
  if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
7434
7755
  writeStack(stack);
7435
- import_fs16.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
7756
+ import_fs17.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
7436
7757
  if (shouldGc) {
7437
7758
  (0, import_child_process8.spawn)("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
7438
7759
  }
@@ -7443,7 +7764,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
7443
7764
  } finally {
7444
7765
  if (indexFile) {
7445
7766
  try {
7446
- import_fs16.default.unlinkSync(indexFile);
7767
+ import_fs17.default.unlinkSync(indexFile);
7447
7768
  } catch {
7448
7769
  }
7449
7770
  }
@@ -7512,9 +7833,9 @@ function applyUndo(hash, cwd) {
7512
7833
  timeout: GIT_TIMEOUT
7513
7834
  }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
7514
7835
  for (const file of [...tracked, ...untracked]) {
7515
- const fullPath = import_path18.default.join(dir, file);
7516
- if (!snapshotFiles.has(file) && import_fs16.default.existsSync(fullPath)) {
7517
- import_fs16.default.unlinkSync(fullPath);
7836
+ const fullPath = import_path19.default.join(dir, file);
7837
+ if (!snapshotFiles.has(file) && import_fs17.default.existsSync(fullPath)) {
7838
+ import_fs17.default.unlinkSync(fullPath);
7518
7839
  }
7519
7840
  }
7520
7841
  return true;
@@ -7538,9 +7859,9 @@ function registerCheckCommand(program2) {
7538
7859
  } catch (err) {
7539
7860
  const tempConfig = getConfig();
7540
7861
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
7541
- const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
7862
+ const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
7542
7863
  const errMsg = err instanceof Error ? err.message : String(err);
7543
- import_fs17.default.appendFileSync(
7864
+ import_fs18.default.appendFileSync(
7544
7865
  logPath,
7545
7866
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
7546
7867
  RAW: ${raw}
@@ -7551,10 +7872,10 @@ RAW: ${raw}
7551
7872
  }
7552
7873
  const config = getConfig(payload.cwd || void 0);
7553
7874
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
7554
- const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
7555
- if (!import_fs17.default.existsSync(import_path19.default.dirname(logPath)))
7556
- import_fs17.default.mkdirSync(import_path19.default.dirname(logPath), { recursive: true });
7557
- import_fs17.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
7875
+ const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
7876
+ if (!import_fs18.default.existsSync(import_path20.default.dirname(logPath)))
7877
+ import_fs18.default.mkdirSync(import_path20.default.dirname(logPath), { recursive: true });
7878
+ import_fs18.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
7558
7879
  `);
7559
7880
  }
7560
7881
  const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
@@ -7567,8 +7888,8 @@ RAW: ${raw}
7567
7888
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
7568
7889
  let ttyFd = null;
7569
7890
  try {
7570
- ttyFd = import_fs17.default.openSync("/dev/tty", "w");
7571
- const writeTty = (line) => import_fs17.default.writeSync(ttyFd, line + "\n");
7891
+ ttyFd = import_fs18.default.openSync("/dev/tty", "w");
7892
+ const writeTty = (line) => import_fs18.default.writeSync(ttyFd, line + "\n");
7572
7893
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
7573
7894
  writeTty(import_chalk5.default.bgRed.white.bold(`
7574
7895
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
@@ -7584,7 +7905,7 @@ RAW: ${raw}
7584
7905
  } finally {
7585
7906
  if (ttyFd !== null)
7586
7907
  try {
7587
- import_fs17.default.closeSync(ttyFd);
7908
+ import_fs18.default.closeSync(ttyFd);
7588
7909
  } catch {
7589
7910
  }
7590
7911
  }
@@ -7615,7 +7936,7 @@ RAW: ${raw}
7615
7936
  if (shouldSnapshot(toolName, toolInput, config)) {
7616
7937
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
7617
7938
  }
7618
- const safeCwdForAuth = typeof payload.cwd === "string" && import_path19.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7939
+ const safeCwdForAuth = typeof payload.cwd === "string" && import_path20.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7619
7940
  const result = await authorizeHeadless(toolName, toolInput, meta, {
7620
7941
  cwd: safeCwdForAuth
7621
7942
  });
@@ -7627,12 +7948,12 @@ RAW: ${raw}
7627
7948
  }
7628
7949
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
7629
7950
  try {
7630
- const tty = import_fs17.default.openSync("/dev/tty", "w");
7631
- import_fs17.default.writeSync(
7951
+ const tty = import_fs18.default.openSync("/dev/tty", "w");
7952
+ import_fs18.default.writeSync(
7632
7953
  tty,
7633
7954
  import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
7634
7955
  );
7635
- import_fs17.default.closeSync(tty);
7956
+ import_fs18.default.closeSync(tty);
7636
7957
  } catch {
7637
7958
  }
7638
7959
  const daemonReady = await autoStartDaemonAndWait();
@@ -7659,9 +7980,9 @@ RAW: ${raw}
7659
7980
  });
7660
7981
  } catch (err) {
7661
7982
  if (process.env.NODE9_DEBUG === "1") {
7662
- const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
7983
+ const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
7663
7984
  const errMsg = err instanceof Error ? err.message : String(err);
7664
- import_fs17.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
7985
+ import_fs18.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
7665
7986
  `);
7666
7987
  }
7667
7988
  process.exit(0);
@@ -7695,12 +8016,52 @@ RAW: ${raw}
7695
8016
  }
7696
8017
 
7697
8018
  // src/cli/commands/log.ts
7698
- var import_fs18 = __toESM(require("fs"));
7699
- var import_path20 = __toESM(require("path"));
8019
+ var import_fs19 = __toESM(require("fs"));
8020
+ var import_path21 = __toESM(require("path"));
7700
8021
  var import_os16 = __toESM(require("os"));
7701
8022
  init_audit();
7702
8023
  init_config();
7703
8024
  init_policy();
8025
+ init_daemon();
8026
+
8027
+ // src/utils/cp-mv-parser.ts
8028
+ function parseCpMvOp(command) {
8029
+ const trimmed = command.trim();
8030
+ const tokens = trimmed.split(/\s+/);
8031
+ if (tokens.length < 3) return null;
8032
+ const [cmd, ...rest] = tokens;
8033
+ const base = cmd.split("/").pop() ?? cmd;
8034
+ if (base !== "cp" && base !== "mv") return null;
8035
+ const args = [];
8036
+ for (const tok of rest) {
8037
+ if (tok === "--") {
8038
+ args.push(...rest.slice(rest.indexOf("--") + 1));
8039
+ break;
8040
+ }
8041
+ if (tok === "-t" || tok === "--target-directory") return null;
8042
+ if (tok.startsWith("--target-directory=")) return null;
8043
+ if (tok.startsWith("-") && !tok.startsWith("--")) {
8044
+ if (tok.includes("t")) return null;
8045
+ continue;
8046
+ }
8047
+ if (tok.startsWith("--")) {
8048
+ continue;
8049
+ }
8050
+ args.push(tok);
8051
+ }
8052
+ if (args.length !== 2) return null;
8053
+ const [src, dest] = args;
8054
+ if (!src || !dest) return null;
8055
+ if (containsShellMetachar(src) || containsShellMetachar(dest)) return null;
8056
+ return { src, dest, clearSource: base === "mv" };
8057
+ }
8058
+ function containsShellMetachar(token) {
8059
+ if (/[$`{;*?]/.test(token)) return true;
8060
+ if (token.includes("\0")) return true;
8061
+ return false;
8062
+ }
8063
+
8064
+ // src/cli/commands/log.ts
7704
8065
  function sanitize3(value) {
7705
8066
  return value.replace(/[\x00-\x1F\x7F]/g, "");
7706
8067
  }
@@ -7719,11 +8080,20 @@ function registerLogCommand(program2) {
7719
8080
  decision: "allowed",
7720
8081
  source: "post-hook"
7721
8082
  };
7722
- const logPath = import_path20.default.join(import_os16.default.homedir(), ".node9", "audit.log");
7723
- if (!import_fs18.default.existsSync(import_path20.default.dirname(logPath)))
7724
- import_fs18.default.mkdirSync(import_path20.default.dirname(logPath), { recursive: true });
7725
- import_fs18.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
7726
- const safeCwd = typeof payload.cwd === "string" && import_path20.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
8083
+ const logPath = import_path21.default.join(import_os16.default.homedir(), ".node9", "audit.log");
8084
+ if (!import_fs19.default.existsSync(import_path21.default.dirname(logPath)))
8085
+ import_fs19.default.mkdirSync(import_path21.default.dirname(logPath), { recursive: true });
8086
+ import_fs19.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
8087
+ if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
8088
+ const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
8089
+ if (command) {
8090
+ const op = parseCpMvOp(command);
8091
+ if (op) {
8092
+ await notifyTaintPropagate(op.src, op.dest, op.clearSource);
8093
+ }
8094
+ }
8095
+ }
8096
+ const safeCwd = typeof payload.cwd === "string" && import_path21.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7727
8097
  const config = getConfig(safeCwd);
7728
8098
  if (shouldSnapshot(tool, {}, config)) {
7729
8099
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
@@ -7732,9 +8102,9 @@ function registerLogCommand(program2) {
7732
8102
  const msg = err instanceof Error ? err.message : String(err);
7733
8103
  process.stderr.write(`[Node9] audit log error: ${msg}
7734
8104
  `);
7735
- const debugPath = import_path20.default.join(import_os16.default.homedir(), ".node9", "hook-debug.log");
8105
+ const debugPath = import_path21.default.join(import_os16.default.homedir(), ".node9", "hook-debug.log");
7736
8106
  try {
7737
- import_fs18.default.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
8107
+ import_fs19.default.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
7738
8108
  `);
7739
8109
  } catch {
7740
8110
  }
@@ -8038,8 +8408,8 @@ function registerConfigShowCommand(program2) {
8038
8408
 
8039
8409
  // src/cli/commands/doctor.ts
8040
8410
  var import_chalk7 = __toESM(require("chalk"));
8041
- var import_fs19 = __toESM(require("fs"));
8042
- var import_path21 = __toESM(require("path"));
8411
+ var import_fs20 = __toESM(require("fs"));
8412
+ var import_path22 = __toESM(require("path"));
8043
8413
  var import_os17 = __toESM(require("os"));
8044
8414
  var import_child_process9 = require("child_process");
8045
8415
  init_daemon();
@@ -8094,10 +8464,10 @@ function registerDoctorCommand(program2, version2) {
8094
8464
  );
8095
8465
  }
8096
8466
  section("Configuration");
8097
- const globalConfigPath = import_path21.default.join(homeDir2, ".node9", "config.json");
8098
- if (import_fs19.default.existsSync(globalConfigPath)) {
8467
+ const globalConfigPath = import_path22.default.join(homeDir2, ".node9", "config.json");
8468
+ if (import_fs20.default.existsSync(globalConfigPath)) {
8099
8469
  try {
8100
- JSON.parse(import_fs19.default.readFileSync(globalConfigPath, "utf-8"));
8470
+ JSON.parse(import_fs20.default.readFileSync(globalConfigPath, "utf-8"));
8101
8471
  pass("~/.node9/config.json found and valid");
8102
8472
  } catch {
8103
8473
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -8105,10 +8475,10 @@ function registerDoctorCommand(program2, version2) {
8105
8475
  } else {
8106
8476
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
8107
8477
  }
8108
- const projectConfigPath = import_path21.default.join(process.cwd(), "node9.config.json");
8109
- if (import_fs19.default.existsSync(projectConfigPath)) {
8478
+ const projectConfigPath = import_path22.default.join(process.cwd(), "node9.config.json");
8479
+ if (import_fs20.default.existsSync(projectConfigPath)) {
8110
8480
  try {
8111
- JSON.parse(import_fs19.default.readFileSync(projectConfigPath, "utf-8"));
8481
+ JSON.parse(import_fs20.default.readFileSync(projectConfigPath, "utf-8"));
8112
8482
  pass("node9.config.json found and valid (project)");
8113
8483
  } catch {
8114
8484
  fail(
@@ -8117,8 +8487,8 @@ function registerDoctorCommand(program2, version2) {
8117
8487
  );
8118
8488
  }
8119
8489
  }
8120
- const credsPath = import_path21.default.join(homeDir2, ".node9", "credentials.json");
8121
- if (import_fs19.default.existsSync(credsPath)) {
8490
+ const credsPath = import_path22.default.join(homeDir2, ".node9", "credentials.json");
8491
+ if (import_fs20.default.existsSync(credsPath)) {
8122
8492
  pass("Cloud credentials found (~/.node9/credentials.json)");
8123
8493
  } else {
8124
8494
  warn(
@@ -8127,10 +8497,10 @@ function registerDoctorCommand(program2, version2) {
8127
8497
  );
8128
8498
  }
8129
8499
  section("Agent Hooks");
8130
- const claudeSettingsPath = import_path21.default.join(homeDir2, ".claude", "settings.json");
8131
- if (import_fs19.default.existsSync(claudeSettingsPath)) {
8500
+ const claudeSettingsPath = import_path22.default.join(homeDir2, ".claude", "settings.json");
8501
+ if (import_fs20.default.existsSync(claudeSettingsPath)) {
8132
8502
  try {
8133
- const cs = JSON.parse(import_fs19.default.readFileSync(claudeSettingsPath, "utf-8"));
8503
+ const cs = JSON.parse(import_fs20.default.readFileSync(claudeSettingsPath, "utf-8"));
8134
8504
  const hasHook = cs.hooks?.PreToolUse?.some(
8135
8505
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
8136
8506
  );
@@ -8146,10 +8516,10 @@ function registerDoctorCommand(program2, version2) {
8146
8516
  } else {
8147
8517
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
8148
8518
  }
8149
- const geminiSettingsPath = import_path21.default.join(homeDir2, ".gemini", "settings.json");
8150
- if (import_fs19.default.existsSync(geminiSettingsPath)) {
8519
+ const geminiSettingsPath = import_path22.default.join(homeDir2, ".gemini", "settings.json");
8520
+ if (import_fs20.default.existsSync(geminiSettingsPath)) {
8151
8521
  try {
8152
- const gs = JSON.parse(import_fs19.default.readFileSync(geminiSettingsPath, "utf-8"));
8522
+ const gs = JSON.parse(import_fs20.default.readFileSync(geminiSettingsPath, "utf-8"));
8153
8523
  const hasHook = gs.hooks?.BeforeTool?.some(
8154
8524
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
8155
8525
  );
@@ -8165,10 +8535,10 @@ function registerDoctorCommand(program2, version2) {
8165
8535
  } else {
8166
8536
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
8167
8537
  }
8168
- const cursorHooksPath = import_path21.default.join(homeDir2, ".cursor", "hooks.json");
8169
- if (import_fs19.default.existsSync(cursorHooksPath)) {
8538
+ const cursorHooksPath = import_path22.default.join(homeDir2, ".cursor", "hooks.json");
8539
+ if (import_fs20.default.existsSync(cursorHooksPath)) {
8170
8540
  try {
8171
- const cur = JSON.parse(import_fs19.default.readFileSync(cursorHooksPath, "utf-8"));
8541
+ const cur = JSON.parse(import_fs20.default.readFileSync(cursorHooksPath, "utf-8"));
8172
8542
  const hasHook = cur.hooks?.preToolUse?.some(
8173
8543
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
8174
8544
  );
@@ -8206,8 +8576,8 @@ function registerDoctorCommand(program2, version2) {
8206
8576
 
8207
8577
  // src/cli/commands/audit.ts
8208
8578
  var import_chalk8 = __toESM(require("chalk"));
8209
- var import_fs20 = __toESM(require("fs"));
8210
- var import_path22 = __toESM(require("path"));
8579
+ var import_fs21 = __toESM(require("fs"));
8580
+ var import_path23 = __toESM(require("path"));
8211
8581
  var import_os18 = __toESM(require("os"));
8212
8582
  function formatRelativeTime(timestamp) {
8213
8583
  const diff = Date.now() - new Date(timestamp).getTime();
@@ -8221,14 +8591,14 @@ function formatRelativeTime(timestamp) {
8221
8591
  }
8222
8592
  function registerAuditCommand(program2) {
8223
8593
  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) => {
8224
- const logPath = import_path22.default.join(import_os18.default.homedir(), ".node9", "audit.log");
8225
- if (!import_fs20.default.existsSync(logPath)) {
8594
+ const logPath = import_path23.default.join(import_os18.default.homedir(), ".node9", "audit.log");
8595
+ if (!import_fs21.default.existsSync(logPath)) {
8226
8596
  console.log(
8227
8597
  import_chalk8.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
8228
8598
  );
8229
8599
  return;
8230
8600
  }
8231
- const raw = import_fs20.default.readFileSync(logPath, "utf-8");
8601
+ const raw = import_fs21.default.readFileSync(logPath, "utf-8");
8232
8602
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
8233
8603
  let entries = lines.flatMap((line) => {
8234
8604
  try {
@@ -8346,14 +8716,14 @@ function registerDaemonCommand(program2) {
8346
8716
 
8347
8717
  // src/cli/commands/status.ts
8348
8718
  var import_chalk10 = __toESM(require("chalk"));
8349
- var import_fs21 = __toESM(require("fs"));
8350
- var import_path23 = __toESM(require("path"));
8719
+ var import_fs22 = __toESM(require("fs"));
8720
+ var import_path24 = __toESM(require("path"));
8351
8721
  var import_os19 = __toESM(require("os"));
8352
8722
  init_core();
8353
8723
  init_daemon();
8354
8724
  function readJson2(filePath) {
8355
8725
  try {
8356
- if (import_fs21.default.existsSync(filePath)) return JSON.parse(import_fs21.default.readFileSync(filePath, "utf-8"));
8726
+ if (import_fs22.default.existsSync(filePath)) return JSON.parse(import_fs22.default.readFileSync(filePath, "utf-8"));
8357
8727
  } catch {
8358
8728
  }
8359
8729
  return null;
@@ -8418,13 +8788,13 @@ function registerStatusCommand(program2) {
8418
8788
  console.log("");
8419
8789
  const modeLabel = settings.mode === "audit" ? import_chalk10.default.blue("audit") : settings.mode === "strict" ? import_chalk10.default.red("strict") : import_chalk10.default.white("standard");
8420
8790
  console.log(` Mode: ${modeLabel}`);
8421
- const projectConfig = import_path23.default.join(process.cwd(), "node9.config.json");
8422
- const globalConfig = import_path23.default.join(import_os19.default.homedir(), ".node9", "config.json");
8791
+ const projectConfig = import_path24.default.join(process.cwd(), "node9.config.json");
8792
+ const globalConfig = import_path24.default.join(import_os19.default.homedir(), ".node9", "config.json");
8423
8793
  console.log(
8424
- ` Local: ${import_fs21.default.existsSync(projectConfig) ? import_chalk10.default.green("Active (node9.config.json)") : import_chalk10.default.gray("Not present")}`
8794
+ ` Local: ${import_fs22.default.existsSync(projectConfig) ? import_chalk10.default.green("Active (node9.config.json)") : import_chalk10.default.gray("Not present")}`
8425
8795
  );
8426
8796
  console.log(
8427
- ` Global: ${import_fs21.default.existsSync(globalConfig) ? import_chalk10.default.green("Active (~/.node9/config.json)") : import_chalk10.default.gray("Not present")}`
8797
+ ` Global: ${import_fs22.default.existsSync(globalConfig) ? import_chalk10.default.green("Active (~/.node9/config.json)") : import_chalk10.default.gray("Not present")}`
8428
8798
  );
8429
8799
  if (mergedConfig.policy.sandboxPaths.length > 0) {
8430
8800
  console.log(
@@ -8433,13 +8803,13 @@ function registerStatusCommand(program2) {
8433
8803
  }
8434
8804
  const homeDir2 = import_os19.default.homedir();
8435
8805
  const claudeSettings = readJson2(
8436
- import_path23.default.join(homeDir2, ".claude", "settings.json")
8806
+ import_path24.default.join(homeDir2, ".claude", "settings.json")
8437
8807
  );
8438
- const claudeConfig = readJson2(import_path23.default.join(homeDir2, ".claude.json"));
8808
+ const claudeConfig = readJson2(import_path24.default.join(homeDir2, ".claude.json"));
8439
8809
  const geminiSettings = readJson2(
8440
- import_path23.default.join(homeDir2, ".gemini", "settings.json")
8810
+ import_path24.default.join(homeDir2, ".gemini", "settings.json")
8441
8811
  );
8442
- const cursorConfig = readJson2(import_path23.default.join(homeDir2, ".cursor", "mcp.json"));
8812
+ const cursorConfig = readJson2(import_path24.default.join(homeDir2, ".cursor", "mcp.json"));
8443
8813
  const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
8444
8814
  if (agentFound) {
8445
8815
  console.log("");
@@ -8498,15 +8868,15 @@ function registerStatusCommand(program2) {
8498
8868
 
8499
8869
  // src/cli/commands/init.ts
8500
8870
  var import_chalk11 = __toESM(require("chalk"));
8501
- var import_fs22 = __toESM(require("fs"));
8502
- var import_path24 = __toESM(require("path"));
8871
+ var import_fs23 = __toESM(require("fs"));
8872
+ var import_path25 = __toESM(require("path"));
8503
8873
  var import_os20 = __toESM(require("os"));
8504
8874
  init_core();
8505
8875
  function registerInitCommand(program2) {
8506
8876
  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) => {
8507
8877
  console.log(import_chalk11.default.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
8508
- const configPath = import_path24.default.join(import_os20.default.homedir(), ".node9", "config.json");
8509
- if (import_fs22.default.existsSync(configPath) && !options.force) {
8878
+ const configPath = import_path25.default.join(import_os20.default.homedir(), ".node9", "config.json");
8879
+ if (import_fs23.default.existsSync(configPath) && !options.force) {
8510
8880
  console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
8511
8881
  } else {
8512
8882
  const requestedMode = options.mode.toLowerCase();
@@ -8515,9 +8885,9 @@ function registerInitCommand(program2) {
8515
8885
  ...DEFAULT_CONFIG,
8516
8886
  settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
8517
8887
  };
8518
- const dir = import_path24.default.dirname(configPath);
8519
- if (!import_fs22.default.existsSync(dir)) import_fs22.default.mkdirSync(dir, { recursive: true });
8520
- import_fs22.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
8888
+ const dir = import_path25.default.dirname(configPath);
8889
+ if (!import_fs23.default.existsSync(dir)) import_fs23.default.mkdirSync(dir, { recursive: true });
8890
+ import_fs23.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
8521
8891
  console.log(import_chalk11.default.green(`\u2705 Config created: ${configPath}`));
8522
8892
  console.log(import_chalk11.default.gray(` Mode: ${safeMode}`));
8523
8893
  }
@@ -8973,20 +9343,20 @@ function registerTrustCommand(program2) {
8973
9343
 
8974
9344
  // src/cli.ts
8975
9345
  var { version } = JSON.parse(
8976
- import_fs24.default.readFileSync(import_path26.default.join(__dirname, "../package.json"), "utf-8")
9346
+ import_fs25.default.readFileSync(import_path27.default.join(__dirname, "../package.json"), "utf-8")
8977
9347
  );
8978
9348
  var program = new import_commander.Command();
8979
9349
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
8980
9350
  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) => {
8981
9351
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
8982
- const credPath = import_path26.default.join(import_os22.default.homedir(), ".node9", "credentials.json");
8983
- if (!import_fs24.default.existsSync(import_path26.default.dirname(credPath)))
8984
- import_fs24.default.mkdirSync(import_path26.default.dirname(credPath), { recursive: true });
9352
+ const credPath = import_path27.default.join(import_os22.default.homedir(), ".node9", "credentials.json");
9353
+ if (!import_fs25.default.existsSync(import_path27.default.dirname(credPath)))
9354
+ import_fs25.default.mkdirSync(import_path27.default.dirname(credPath), { recursive: true });
8985
9355
  const profileName = options.profile || "default";
8986
9356
  let existingCreds = {};
8987
9357
  try {
8988
- if (import_fs24.default.existsSync(credPath)) {
8989
- const raw = JSON.parse(import_fs24.default.readFileSync(credPath, "utf-8"));
9358
+ if (import_fs25.default.existsSync(credPath)) {
9359
+ const raw = JSON.parse(import_fs25.default.readFileSync(credPath, "utf-8"));
8990
9360
  if (raw.apiKey) {
8991
9361
  existingCreds = {
8992
9362
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -8998,13 +9368,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
8998
9368
  } catch {
8999
9369
  }
9000
9370
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
9001
- import_fs24.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
9371
+ import_fs25.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
9002
9372
  if (profileName === "default") {
9003
- const configPath = import_path26.default.join(import_os22.default.homedir(), ".node9", "config.json");
9373
+ const configPath = import_path27.default.join(import_os22.default.homedir(), ".node9", "config.json");
9004
9374
  let config = {};
9005
9375
  try {
9006
- if (import_fs24.default.existsSync(configPath))
9007
- config = JSON.parse(import_fs24.default.readFileSync(configPath, "utf-8"));
9376
+ if (import_fs25.default.existsSync(configPath))
9377
+ config = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
9008
9378
  } catch {
9009
9379
  }
9010
9380
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -9019,9 +9389,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
9019
9389
  approvers.cloud = false;
9020
9390
  }
9021
9391
  s.approvers = approvers;
9022
- if (!import_fs24.default.existsSync(import_path26.default.dirname(configPath)))
9023
- import_fs24.default.mkdirSync(import_path26.default.dirname(configPath), { recursive: true });
9024
- import_fs24.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
9392
+ if (!import_fs25.default.existsSync(import_path27.default.dirname(configPath)))
9393
+ import_fs25.default.mkdirSync(import_path27.default.dirname(configPath), { recursive: true });
9394
+ import_fs25.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
9025
9395
  }
9026
9396
  if (options.profile && profileName !== "default") {
9027
9397
  console.log(import_chalk17.default.green(`\u2705 Profile "${profileName}" saved`));
@@ -9107,15 +9477,15 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
9107
9477
  }
9108
9478
  }
9109
9479
  if (options.purge) {
9110
- const node9Dir = import_path26.default.join(import_os22.default.homedir(), ".node9");
9111
- if (import_fs24.default.existsSync(node9Dir)) {
9480
+ const node9Dir = import_path27.default.join(import_os22.default.homedir(), ".node9");
9481
+ if (import_fs25.default.existsSync(node9Dir)) {
9112
9482
  const confirmed = await (0, import_prompts3.confirm)({
9113
9483
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
9114
9484
  default: false
9115
9485
  });
9116
9486
  if (confirmed) {
9117
- import_fs24.default.rmSync(node9Dir, { recursive: true });
9118
- if (import_fs24.default.existsSync(node9Dir)) {
9487
+ import_fs25.default.rmSync(node9Dir, { recursive: true });
9488
+ if (import_fs25.default.existsSync(node9Dir)) {
9119
9489
  console.error(
9120
9490
  import_chalk17.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
9121
9491
  );
@@ -9327,9 +9697,9 @@ if (process.argv[2] !== "daemon") {
9327
9697
  const isCheckHook = process.argv[2] === "check";
9328
9698
  if (isCheckHook) {
9329
9699
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
9330
- const logPath = import_path26.default.join(import_os22.default.homedir(), ".node9", "hook-debug.log");
9700
+ const logPath = import_path27.default.join(import_os22.default.homedir(), ".node9", "hook-debug.log");
9331
9701
  const msg = reason instanceof Error ? reason.message : String(reason);
9332
- import_fs24.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
9702
+ import_fs25.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
9333
9703
  `);
9334
9704
  }
9335
9705
  process.exit(0);