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