@node9/proxy 1.4.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/index.mjs CHANGED
@@ -1,9 +1,61 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/audit/hasher.ts
12
+ import { createHash } from "crypto";
13
+ function canonicalise(value) {
14
+ return _canonicalise(value, /* @__PURE__ */ new WeakSet());
15
+ }
16
+ function _canonicalise(value, seen) {
17
+ if (value === null || typeof value !== "object") return value;
18
+ if (value instanceof Date) return value.toISOString();
19
+ if (value instanceof RegExp) return value.toString();
20
+ if (Buffer.isBuffer(value)) return value.toString("base64");
21
+ if (seen.has(value)) return "[Circular]";
22
+ seen.add(value);
23
+ let result;
24
+ if (Array.isArray(value)) {
25
+ result = value.map((v) => _canonicalise(v, seen));
26
+ } else {
27
+ const obj = value;
28
+ result = Object.fromEntries(
29
+ Object.keys(obj).sort().map((k) => [k, _canonicalise(obj[k], seen)])
30
+ );
31
+ }
32
+ seen.delete(value);
33
+ return result;
34
+ }
35
+ function hashArgs(args) {
36
+ const canonical = JSON.stringify(canonicalise(args) ?? null);
37
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
38
+ }
39
+ var init_hasher = __esm({
40
+ "src/audit/hasher.ts"() {
41
+ "use strict";
42
+ }
43
+ });
44
+
1
45
  // src/audit/index.ts
46
+ var audit_exports = {};
47
+ __export(audit_exports, {
48
+ HOOK_DEBUG_LOG: () => HOOK_DEBUG_LOG,
49
+ LOCAL_AUDIT_LOG: () => LOCAL_AUDIT_LOG,
50
+ appendConfigAudit: () => appendConfigAudit,
51
+ appendHookDebug: () => appendHookDebug,
52
+ appendLocalAudit: () => appendLocalAudit,
53
+ appendToLog: () => appendToLog,
54
+ redactSecrets: () => redactSecrets
55
+ });
2
56
  import fs from "fs";
3
57
  import path from "path";
4
58
  import os from "os";
5
- var LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
6
- var HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
7
59
  function redactSecrets(text) {
8
60
  if (!text) return text;
9
61
  let redacted = text;
@@ -25,24 +77,24 @@ function appendToLog(logPath, entry) {
25
77
  } catch {
26
78
  }
27
79
  }
28
- function appendHookDebug(toolName, args, meta) {
29
- const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
80
+ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
81
+ const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
30
82
  appendToLog(HOOK_DEBUG_LOG, {
31
83
  ts: (/* @__PURE__ */ new Date()).toISOString(),
32
84
  tool: toolName,
33
- args: safeArgs,
85
+ ...argsField,
34
86
  agent: meta?.agent,
35
87
  mcpServer: meta?.mcpServer,
36
88
  hostname: os.hostname(),
37
89
  cwd: process.cwd()
38
90
  });
39
91
  }
40
- function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
41
- const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
92
+ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
93
+ const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
42
94
  appendToLog(LOCAL_AUDIT_LOG, {
43
95
  ts: (/* @__PURE__ */ new Date()).toISOString(),
44
96
  tool: toolName,
45
- args: safeArgs,
97
+ ...argsField,
46
98
  decision,
47
99
  checkedBy,
48
100
  agent: meta?.agent,
@@ -50,6 +102,25 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
50
102
  hostname: os.hostname()
51
103
  });
52
104
  }
105
+ function appendConfigAudit(entry) {
106
+ appendToLog(LOCAL_AUDIT_LOG, {
107
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
108
+ ...entry,
109
+ hostname: os.hostname()
110
+ });
111
+ }
112
+ var LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG;
113
+ var init_audit = __esm({
114
+ "src/audit/index.ts"() {
115
+ "use strict";
116
+ init_hasher();
117
+ LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
118
+ HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
119
+ }
120
+ });
121
+
122
+ // src/core.ts
123
+ init_audit();
53
124
 
54
125
  // src/config/index.ts
55
126
  import fs3 from "fs";
@@ -118,7 +189,8 @@ var ConfigFileSchema = z.object({
118
189
  environment: z.string().optional(),
119
190
  slackEnabled: z.boolean().optional(),
120
191
  enableTrustSessions: z.boolean().optional(),
121
- allowGlobalPause: z.boolean().optional()
192
+ allowGlobalPause: z.boolean().optional(),
193
+ auditHashArgs: z.boolean().optional()
122
194
  }).optional(),
123
195
  policy: z.object({
124
196
  sandboxPaths: z.array(z.string()).optional(),
@@ -414,6 +486,7 @@ var DEFAULT_CONFIG = {
414
486
  approvalTimeoutMs: 12e4,
415
487
  // 120-second auto-deny timeout
416
488
  flightRecorder: true,
489
+ auditHashArgs: true,
417
490
  approvers: { native: true, browser: true, cloud: false, terminal: true }
418
491
  },
419
492
  policy: {
@@ -1456,16 +1529,35 @@ function readTrustedHosts() {
1456
1529
  return [];
1457
1530
  }
1458
1531
  }
1532
+ var _cache = null;
1533
+ var CACHE_TTL_MS = 5e3;
1534
+ function getFileMtime() {
1535
+ try {
1536
+ return fs6.statSync(getTrustedHostsPath()).mtimeMs;
1537
+ } catch {
1538
+ return 0;
1539
+ }
1540
+ }
1541
+ function getCachedHosts() {
1542
+ const now = Date.now();
1543
+ if (_cache && now < _cache.expiry) {
1544
+ const mtime = getFileMtime();
1545
+ if (mtime === _cache.mtime) return _cache.hosts;
1546
+ }
1547
+ const hosts = readTrustedHosts();
1548
+ _cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
1549
+ return hosts;
1550
+ }
1459
1551
  function normalizeHost(raw) {
1460
1552
  return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
1461
1553
  }
1462
1554
  function isTrustedHost(host) {
1463
1555
  const normalized = normalizeHost(host);
1464
- return readTrustedHosts().some((entry) => {
1556
+ return getCachedHosts().some((entry) => {
1465
1557
  const entryHost = entry.host.toLowerCase();
1466
1558
  if (entryHost.startsWith("*.")) {
1467
1559
  const domain = entryHost.slice(2);
1468
- return normalized === domain || normalized.endsWith("." + domain);
1560
+ return normalized.endsWith("." + domain);
1469
1561
  }
1470
1562
  return normalized === entryHost;
1471
1563
  });
@@ -1662,7 +1754,12 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1662
1754
  };
1663
1755
  }
1664
1756
  if (allTrusted) {
1665
- return { decision: "allow" };
1757
+ return {
1758
+ decision: "allow",
1759
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1760
+ reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1761
+ tier: 3
1762
+ };
1666
1763
  }
1667
1764
  return {
1668
1765
  decision: "review",
@@ -1914,8 +2011,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
1914
2011
  signal: ctrl.signal
1915
2012
  });
1916
2013
  if (!res.ok) throw new Error("Daemon fail");
1917
- const { id } = await res.json();
1918
- return id;
2014
+ const { id, allowCount } = await res.json();
2015
+ return { id, allowCount: allowCount ?? 1 };
1919
2016
  } finally {
1920
2017
  clearTimeout(timer);
1921
2018
  }
@@ -1954,15 +2051,54 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
1954
2051
  signal: AbortSignal.timeout(3e3)
1955
2052
  });
1956
2053
  if (!res.ok) throw new Error("Daemon unreachable");
1957
- const { id } = await res.json();
1958
- return id;
2054
+ const { id, allowCount } = await res.json();
2055
+ return { id, allowCount: allowCount ?? 1 };
2056
+ }
2057
+ async function notifyTaint(filePath, source) {
2058
+ if (!isDaemonRunning()) return;
2059
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2060
+ try {
2061
+ await fetch(`${base}/taint`, {
2062
+ method: "POST",
2063
+ headers: { "Content-Type": "application/json" },
2064
+ body: JSON.stringify({ path: filePath, source }),
2065
+ signal: AbortSignal.timeout(1e3)
2066
+ });
2067
+ } catch {
2068
+ }
2069
+ }
2070
+ async function checkTaint(paths) {
2071
+ if (paths.length === 0) return { tainted: false };
2072
+ if (!isDaemonRunning()) return { tainted: false, daemonUnavailable: true };
2073
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2074
+ try {
2075
+ const res = await fetch(`${base}/taint/check`, {
2076
+ method: "POST",
2077
+ headers: { "Content-Type": "application/json" },
2078
+ body: JSON.stringify({ paths }),
2079
+ signal: AbortSignal.timeout(2e3)
2080
+ });
2081
+ return await res.json();
2082
+ } catch (err) {
2083
+ try {
2084
+ const { appendToLog: appendToLog2, HOOK_DEBUG_LOG: HOOK_DEBUG_LOG2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
2085
+ appendToLog2(HOOK_DEBUG_LOG2, {
2086
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2087
+ event: "checkTaint-error",
2088
+ error: String(err),
2089
+ paths
2090
+ });
2091
+ } catch {
2092
+ }
2093
+ return { tainted: false, daemonUnavailable: true };
2094
+ }
1959
2095
  }
1960
- async function resolveViaDaemon(id, decision, internalToken) {
2096
+ async function resolveViaDaemon(id, decision, internalToken, source) {
1961
2097
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1962
2098
  await fetch(`${base}/resolve/${id}`, {
1963
2099
  method: "POST",
1964
2100
  headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
1965
- body: JSON.stringify({ decision }),
2101
+ body: JSON.stringify({ decision, ...source && { source } }),
1966
2102
  signal: AbortSignal.timeout(3e3)
1967
2103
  });
1968
2104
  }
@@ -2164,20 +2300,24 @@ ${smartTruncate(str, 500)}`
2164
2300
  function escapePango(text) {
2165
2301
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2166
2302
  }
2167
- function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2303
+ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2168
2304
  const lines = [];
2169
2305
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
2170
2306
  lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
2171
2307
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
2172
2308
  lines.push("");
2173
2309
  lines.push(formattedArgs);
2310
+ if (allowCount >= 3) {
2311
+ lines.push("");
2312
+ lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
2313
+ }
2174
2314
  if (!locked) {
2175
2315
  lines.push("");
2176
2316
  lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
2177
2317
  }
2178
2318
  return lines.join("\n");
2179
2319
  }
2180
- function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2320
+ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2181
2321
  const lines = [];
2182
2322
  if (locked) {
2183
2323
  lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
@@ -2189,6 +2329,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2189
2329
  lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
2190
2330
  lines.push("");
2191
2331
  lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
2332
+ if (allowCount >= 3) {
2333
+ lines.push("");
2334
+ lines.push(
2335
+ `<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
2336
+ );
2337
+ }
2192
2338
  if (!locked) {
2193
2339
  lines.push("");
2194
2340
  lines.push(
@@ -2197,12 +2343,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2197
2343
  }
2198
2344
  return lines.join("\n");
2199
2345
  }
2200
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
2346
+ async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
2201
2347
  if (isTestEnv()) return "deny";
2202
2348
  const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
2203
2349
  const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
2204
2350
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
2205
- const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
2351
+ const message = buildPlainMessage(
2352
+ toolName,
2353
+ formattedArgs,
2354
+ agent,
2355
+ explainableLabel,
2356
+ locked,
2357
+ allowCount
2358
+ );
2206
2359
  return new Promise((resolve) => {
2207
2360
  let childProcess = null;
2208
2361
  const onAbort = () => {
@@ -2234,7 +2387,8 @@ end run`;
2234
2387
  formattedArgs,
2235
2388
  agent,
2236
2389
  explainableLabel,
2237
- locked
2390
+ locked,
2391
+ allowCount
2238
2392
  );
2239
2393
  const argsList = [
2240
2394
  locked ? "--info" : "--question",
@@ -2275,7 +2429,11 @@ end run`;
2275
2429
  });
2276
2430
  }
2277
2431
 
2432
+ // src/auth/orchestrator.ts
2433
+ init_audit();
2434
+
2278
2435
  // src/auth/cloud.ts
2436
+ init_audit();
2279
2437
  import fs9 from "fs";
2280
2438
  import os8 from "os";
2281
2439
  function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
@@ -2386,6 +2544,51 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
2386
2544
  }
2387
2545
 
2388
2546
  // src/auth/orchestrator.ts
2547
+ var WRITE_TOOLS = /* @__PURE__ */ new Set([
2548
+ "write",
2549
+ "write_file",
2550
+ "create_file",
2551
+ "edit",
2552
+ "multiedit",
2553
+ "str_replace_based_edit_tool",
2554
+ "replace",
2555
+ "notebook_edit",
2556
+ "notebookedit"
2557
+ ]);
2558
+ function isWriteTool(toolName) {
2559
+ const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
2560
+ return WRITE_TOOLS.has(t);
2561
+ }
2562
+ function extractFilePaths(toolName, args) {
2563
+ const paths = [];
2564
+ if (!args || typeof args !== "object" || Array.isArray(args)) return paths;
2565
+ const a = args;
2566
+ for (const key of ["file_path", "path", "filename", "source", "src", "input"]) {
2567
+ if (typeof a[key] === "string" && a[key]) paths.push(a[key]);
2568
+ }
2569
+ const cmd = typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : "";
2570
+ if (cmd) {
2571
+ for (const m of cmd.matchAll(/(?:-T|--upload-file|--data(?:-binary)?)\s+@?(\S+)/g)) {
2572
+ paths.push(m[1]);
2573
+ }
2574
+ for (const m of cmd.matchAll(/\b(?:scp|rsync)\s+(?:-\S+\s+)*(\S+)\s+\S+@/g)) {
2575
+ paths.push(m[1]);
2576
+ }
2577
+ for (const m of cmd.matchAll(/<\s*(\S+)/g)) {
2578
+ paths.push(m[1]);
2579
+ }
2580
+ }
2581
+ return paths.filter(Boolean);
2582
+ }
2583
+ function isNetworkTool(toolName, args) {
2584
+ const t = toolName.toLowerCase();
2585
+ if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal.execute") {
2586
+ const a = args;
2587
+ const cmd = typeof a?.command === "string" ? a.command : typeof a?.cmd === "string" ? a.cmd : "";
2588
+ return /\b(curl|wget|scp|rsync|nc|ncat|netcat|ssh)\b/.test(cmd);
2589
+ }
2590
+ return false;
2591
+ }
2389
2592
  var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os9.tmpdir(), "node9-activity.sock");
2390
2593
  function notifyActivity(data) {
2391
2594
  return new Promise((resolve) => {
@@ -2416,7 +2619,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
2416
2619
  id: actId,
2417
2620
  tool: toolName,
2418
2621
  ts: actTs,
2419
- status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
2622
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
2420
2623
  label: result.blockedByLabel
2421
2624
  });
2422
2625
  }
@@ -2430,6 +2633,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2430
2633
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
2431
2634
  const creds = getCredentials();
2432
2635
  const config = getConfig(options?.cwd);
2636
+ const hashAuditArgs = config.settings.auditHashArgs === true;
2433
2637
  const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
2434
2638
  const approvers = {
2435
2639
  ...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
@@ -2440,13 +2644,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2440
2644
  approvers.terminal = false;
2441
2645
  }
2442
2646
  if (config.settings.enableHookLogDebug && !isTestEnv2) {
2443
- appendHookDebug(toolName, args, meta);
2647
+ appendHookDebug(toolName, args, meta, hashAuditArgs);
2444
2648
  }
2445
2649
  const isManual = meta?.agent === "Terminal";
2446
2650
  let explainableLabel = "Local Config";
2447
2651
  let policyMatchedField;
2448
2652
  let policyMatchedWord;
2449
2653
  let riskMetadata;
2654
+ let taintWarning = null;
2655
+ if (isNetworkTool(toolName, args)) {
2656
+ const filePaths = extractFilePaths(toolName, args);
2657
+ if (filePaths.length > 0) {
2658
+ const taintResult = await checkTaint(filePaths);
2659
+ if (taintResult.tainted && taintResult.record) {
2660
+ const { path: taintedPath, source: taintSource } = taintResult.record;
2661
+ taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
2662
+ } else if (taintResult.daemonUnavailable) {
2663
+ taintWarning = `\u26A0\uFE0F Taint service unavailable \u2014 cannot verify if ${filePaths.join(", ")} is clean`;
2664
+ }
2665
+ }
2666
+ }
2450
2667
  if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
2451
2668
  const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
2452
2669
  const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
@@ -2454,7 +2671,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2454
2671
  if (dlpMatch) {
2455
2672
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
2456
2673
  if (dlpMatch.severity === "block") {
2457
- if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
2674
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta, true);
2675
+ if (isWriteTool(toolName) && filePath) {
2676
+ await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
2677
+ }
2458
2678
  return {
2459
2679
  approved: false,
2460
2680
  reason: dlpReason,
@@ -2462,7 +2682,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2462
2682
  blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
2463
2683
  };
2464
2684
  }
2465
- if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
2685
+ if (!isManual)
2686
+ appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta, hashAuditArgs);
2466
2687
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
2467
2688
  }
2468
2689
  }
@@ -2470,7 +2691,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2470
2691
  if (!isIgnoredTool(toolName)) {
2471
2692
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
2472
2693
  if (policyResult.decision === "review") {
2473
- appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
2694
+ appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
2474
2695
  if (approvers.cloud && creds?.apiKey) {
2475
2696
  await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
2476
2697
  }
@@ -2478,22 +2699,23 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2478
2699
  }
2479
2700
  return { approved: true, checkedBy: "audit" };
2480
2701
  }
2481
- if (!isIgnoredTool(toolName)) {
2702
+ if (!taintWarning && !isIgnoredTool(toolName)) {
2482
2703
  if (getActiveTrustSession(toolName)) {
2483
2704
  if (approvers.cloud && creds?.apiKey)
2484
2705
  await auditLocalAllow(toolName, args, "trust", creds, meta);
2485
- if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
2706
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
2486
2707
  return { approved: true, checkedBy: "trust" };
2487
2708
  }
2488
2709
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
2489
2710
  if (policyResult.decision === "allow") {
2490
2711
  if (approvers.cloud && creds?.apiKey)
2491
2712
  auditLocalAllow(toolName, args, "local-policy", creds, meta);
2492
- if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
2713
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
2493
2714
  return { approved: true, checkedBy: "local-policy" };
2494
2715
  }
2495
2716
  if (policyResult.decision === "block") {
2496
- if (!isManual) appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta);
2717
+ if (!isManual)
2718
+ appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
2497
2719
  return {
2498
2720
  approved: false,
2499
2721
  reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
@@ -2512,15 +2734,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2512
2734
  policyMatchedWord,
2513
2735
  policyResult.ruleName
2514
2736
  );
2515
- const persistent = getPersistentDecision(toolName);
2737
+ const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
2516
2738
  if (persistent === "allow") {
2517
2739
  if (approvers.cloud && creds?.apiKey)
2518
2740
  await auditLocalAllow(toolName, args, "persistent", creds, meta);
2519
- if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
2741
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
2520
2742
  return { approved: true, checkedBy: "persistent" };
2521
2743
  }
2522
2744
  if (persistent === "deny") {
2523
- if (!isManual) appendLocalAudit(toolName, args, "deny", "persistent-deny", meta);
2745
+ if (!isManual)
2746
+ appendLocalAudit(toolName, args, "deny", "persistent-deny", meta, hashAuditArgs);
2524
2747
  return {
2525
2748
  approved: false,
2526
2749
  reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
@@ -2528,10 +2751,21 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2528
2751
  blockedByLabel: "Persistent User Rule"
2529
2752
  };
2530
2753
  }
2531
- } else {
2532
- if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
2754
+ } else if (!taintWarning) {
2755
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
2533
2756
  return { approved: true };
2534
2757
  }
2758
+ if (taintWarning) {
2759
+ explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
2760
+ riskMetadata = computeRiskMetadata(
2761
+ args,
2762
+ 7,
2763
+ explainableLabel,
2764
+ void 0,
2765
+ void 0,
2766
+ taintWarning
2767
+ );
2768
+ }
2535
2769
  let cloudRequestId = null;
2536
2770
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
2537
2771
  if (cloudEnforced) {
@@ -2550,7 +2784,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2550
2784
  };
2551
2785
  }
2552
2786
  cloudRequestId = initResult.requestId || null;
2553
- explainableLabel = "Organization Policy (SaaS)";
2787
+ if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
2554
2788
  } catch {
2555
2789
  }
2556
2790
  }
@@ -2579,13 +2813,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2579
2813
  let viewerId = null;
2580
2814
  const internalToken = getInternalToken();
2581
2815
  let daemonEntryId = null;
2816
+ let daemonAllowCount = 1;
2582
2817
  if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
2583
2818
  if (cloudEnforced && cloudRequestId) {
2584
- viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
2819
+ const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
2820
+ viewerId = viewer?.id ?? null;
2585
2821
  daemonEntryId = viewerId;
2822
+ if (viewer) daemonAllowCount = viewer.allowCount;
2586
2823
  } else {
2587
2824
  try {
2588
- daemonEntryId = await registerDaemonEntry(
2825
+ const entry = await registerDaemonEntry(
2589
2826
  toolName,
2590
2827
  args,
2591
2828
  meta,
@@ -2593,6 +2830,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2593
2830
  options?.activityId,
2594
2831
  options?.cwd
2595
2832
  );
2833
+ daemonEntryId = entry.id;
2834
+ daemonAllowCount = entry.allowCount;
2596
2835
  } catch {
2597
2836
  }
2598
2837
  }
@@ -2628,7 +2867,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2628
2867
  false,
2629
2868
  signal,
2630
2869
  policyMatchedField,
2631
- policyMatchedWord
2870
+ policyMatchedWord,
2871
+ daemonAllowCount
2632
2872
  );
2633
2873
  if (decision === "always_allow") {
2634
2874
  writeTrustSession(toolName, 36e5);
@@ -2686,10 +2926,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
2686
2926
  if (!resolved) {
2687
2927
  resolved = true;
2688
2928
  abortController.abort();
2689
- if (viewerId && internalToken) {
2690
- resolveViaDaemon(viewerId, res.approved ? "allow" : "deny", internalToken).catch(
2691
- () => null
2692
- );
2929
+ if (daemonEntryId && internalToken) {
2930
+ resolveViaDaemon(
2931
+ daemonEntryId,
2932
+ res.approved ? "allow" : "deny",
2933
+ internalToken,
2934
+ res.decisionSource
2935
+ ).catch(() => null);
2693
2936
  }
2694
2937
  resolve(res);
2695
2938
  }
@@ -2728,7 +2971,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
2728
2971
  args,
2729
2972
  finalResult.approved ? "allow" : "deny",
2730
2973
  finalResult.checkedBy || finalResult.blockedBy || "unknown",
2731
- meta
2974
+ meta,
2975
+ hashAuditArgs
2732
2976
  );
2733
2977
  }
2734
2978
  return finalResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",