@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.js CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,19 +30,52 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
30
- // src/index.ts
31
- var src_exports = {};
32
- __export(src_exports, {
33
- protect: () => protect
33
+ // src/audit/hasher.ts
34
+ function canonicalise(value) {
35
+ return _canonicalise(value, /* @__PURE__ */ new WeakSet());
36
+ }
37
+ function _canonicalise(value, seen) {
38
+ if (value === null || typeof value !== "object") return value;
39
+ if (value instanceof Date) return value.toISOString();
40
+ if (value instanceof RegExp) return value.toString();
41
+ if (Buffer.isBuffer(value)) return value.toString("base64");
42
+ if (seen.has(value)) return "[Circular]";
43
+ seen.add(value);
44
+ let result;
45
+ if (Array.isArray(value)) {
46
+ result = value.map((v) => _canonicalise(v, seen));
47
+ } else {
48
+ const obj = value;
49
+ result = Object.fromEntries(
50
+ Object.keys(obj).sort().map((k) => [k, _canonicalise(obj[k], seen)])
51
+ );
52
+ }
53
+ seen.delete(value);
54
+ return result;
55
+ }
56
+ function hashArgs(args) {
57
+ const canonical = JSON.stringify(canonicalise(args) ?? null);
58
+ return (0, import_crypto.createHash)("sha256").update(canonical).digest("hex").slice(0, 32);
59
+ }
60
+ var import_crypto;
61
+ var init_hasher = __esm({
62
+ "src/audit/hasher.ts"() {
63
+ "use strict";
64
+ import_crypto = require("crypto");
65
+ }
34
66
  });
35
- module.exports = __toCommonJS(src_exports);
36
67
 
37
68
  // src/audit/index.ts
38
- var import_fs = __toESM(require("fs"));
39
- var import_path = __toESM(require("path"));
40
- var import_os = __toESM(require("os"));
41
- var LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
42
- var HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
69
+ var audit_exports = {};
70
+ __export(audit_exports, {
71
+ HOOK_DEBUG_LOG: () => HOOK_DEBUG_LOG,
72
+ LOCAL_AUDIT_LOG: () => LOCAL_AUDIT_LOG,
73
+ appendConfigAudit: () => appendConfigAudit,
74
+ appendHookDebug: () => appendHookDebug,
75
+ appendLocalAudit: () => appendLocalAudit,
76
+ appendToLog: () => appendToLog,
77
+ redactSecrets: () => redactSecrets
78
+ });
43
79
  function redactSecrets(text) {
44
80
  if (!text) return text;
45
81
  let redacted = text;
@@ -61,24 +97,24 @@ function appendToLog(logPath, entry) {
61
97
  } catch {
62
98
  }
63
99
  }
64
- function appendHookDebug(toolName, args, meta) {
65
- const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
100
+ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
101
+ const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
66
102
  appendToLog(HOOK_DEBUG_LOG, {
67
103
  ts: (/* @__PURE__ */ new Date()).toISOString(),
68
104
  tool: toolName,
69
- args: safeArgs,
105
+ ...argsField,
70
106
  agent: meta?.agent,
71
107
  mcpServer: meta?.mcpServer,
72
108
  hostname: import_os.default.hostname(),
73
109
  cwd: process.cwd()
74
110
  });
75
111
  }
76
- function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
77
- const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
112
+ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
113
+ const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
78
114
  appendToLog(LOCAL_AUDIT_LOG, {
79
115
  ts: (/* @__PURE__ */ new Date()).toISOString(),
80
116
  tool: toolName,
81
- args: safeArgs,
117
+ ...argsField,
82
118
  decision,
83
119
  checkedBy,
84
120
  agent: meta?.agent,
@@ -86,6 +122,35 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
86
122
  hostname: import_os.default.hostname()
87
123
  });
88
124
  }
125
+ function appendConfigAudit(entry) {
126
+ appendToLog(LOCAL_AUDIT_LOG, {
127
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
128
+ ...entry,
129
+ hostname: import_os.default.hostname()
130
+ });
131
+ }
132
+ var import_fs, import_path, import_os, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG;
133
+ var init_audit = __esm({
134
+ "src/audit/index.ts"() {
135
+ "use strict";
136
+ import_fs = __toESM(require("fs"));
137
+ import_path = __toESM(require("path"));
138
+ import_os = __toESM(require("os"));
139
+ init_hasher();
140
+ LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
141
+ HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
142
+ }
143
+ });
144
+
145
+ // src/index.ts
146
+ var src_exports = {};
147
+ __export(src_exports, {
148
+ protect: () => protect
149
+ });
150
+ module.exports = __toCommonJS(src_exports);
151
+
152
+ // src/core.ts
153
+ init_audit();
89
154
 
90
155
  // src/config/index.ts
91
156
  var import_fs3 = __toESM(require("fs"));
@@ -154,7 +219,8 @@ var ConfigFileSchema = import_zod.z.object({
154
219
  environment: import_zod.z.string().optional(),
155
220
  slackEnabled: import_zod.z.boolean().optional(),
156
221
  enableTrustSessions: import_zod.z.boolean().optional(),
157
- allowGlobalPause: import_zod.z.boolean().optional()
222
+ allowGlobalPause: import_zod.z.boolean().optional(),
223
+ auditHashArgs: import_zod.z.boolean().optional()
158
224
  }).optional(),
159
225
  policy: import_zod.z.object({
160
226
  sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
@@ -450,6 +516,7 @@ var DEFAULT_CONFIG = {
450
516
  approvalTimeoutMs: 12e4,
451
517
  // 120-second auto-deny timeout
452
518
  flightRecorder: true,
519
+ auditHashArgs: true,
453
520
  approvers: { native: true, browser: true, cloud: false, terminal: true }
454
521
  },
455
522
  policy: {
@@ -1492,16 +1559,35 @@ function readTrustedHosts() {
1492
1559
  return [];
1493
1560
  }
1494
1561
  }
1562
+ var _cache = null;
1563
+ var CACHE_TTL_MS = 5e3;
1564
+ function getFileMtime() {
1565
+ try {
1566
+ return import_fs6.default.statSync(getTrustedHostsPath()).mtimeMs;
1567
+ } catch {
1568
+ return 0;
1569
+ }
1570
+ }
1571
+ function getCachedHosts() {
1572
+ const now = Date.now();
1573
+ if (_cache && now < _cache.expiry) {
1574
+ const mtime = getFileMtime();
1575
+ if (mtime === _cache.mtime) return _cache.hosts;
1576
+ }
1577
+ const hosts = readTrustedHosts();
1578
+ _cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
1579
+ return hosts;
1580
+ }
1495
1581
  function normalizeHost(raw) {
1496
1582
  return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
1497
1583
  }
1498
1584
  function isTrustedHost(host) {
1499
1585
  const normalized = normalizeHost(host);
1500
- return readTrustedHosts().some((entry) => {
1586
+ return getCachedHosts().some((entry) => {
1501
1587
  const entryHost = entry.host.toLowerCase();
1502
1588
  if (entryHost.startsWith("*.")) {
1503
1589
  const domain = entryHost.slice(2);
1504
- return normalized === domain || normalized.endsWith("." + domain);
1590
+ return normalized.endsWith("." + domain);
1505
1591
  }
1506
1592
  return normalized === entryHost;
1507
1593
  });
@@ -1698,7 +1784,12 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1698
1784
  };
1699
1785
  }
1700
1786
  if (allTrusted) {
1701
- return { decision: "allow" };
1787
+ return {
1788
+ decision: "allow",
1789
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1790
+ reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1791
+ tier: 3
1792
+ };
1702
1793
  }
1703
1794
  return {
1704
1795
  decision: "review",
@@ -1950,8 +2041,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
1950
2041
  signal: ctrl.signal
1951
2042
  });
1952
2043
  if (!res.ok) throw new Error("Daemon fail");
1953
- const { id } = await res.json();
1954
- return id;
2044
+ const { id, allowCount } = await res.json();
2045
+ return { id, allowCount: allowCount ?? 1 };
1955
2046
  } finally {
1956
2047
  clearTimeout(timer);
1957
2048
  }
@@ -1990,15 +2081,54 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
1990
2081
  signal: AbortSignal.timeout(3e3)
1991
2082
  });
1992
2083
  if (!res.ok) throw new Error("Daemon unreachable");
1993
- const { id } = await res.json();
1994
- return id;
2084
+ const { id, allowCount } = await res.json();
2085
+ return { id, allowCount: allowCount ?? 1 };
2086
+ }
2087
+ async function notifyTaint(filePath, source) {
2088
+ if (!isDaemonRunning()) return;
2089
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2090
+ try {
2091
+ await fetch(`${base}/taint`, {
2092
+ method: "POST",
2093
+ headers: { "Content-Type": "application/json" },
2094
+ body: JSON.stringify({ path: filePath, source }),
2095
+ signal: AbortSignal.timeout(1e3)
2096
+ });
2097
+ } catch {
2098
+ }
2099
+ }
2100
+ async function checkTaint(paths) {
2101
+ if (paths.length === 0) return { tainted: false };
2102
+ if (!isDaemonRunning()) return { tainted: false, daemonUnavailable: true };
2103
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2104
+ try {
2105
+ const res = await fetch(`${base}/taint/check`, {
2106
+ method: "POST",
2107
+ headers: { "Content-Type": "application/json" },
2108
+ body: JSON.stringify({ paths }),
2109
+ signal: AbortSignal.timeout(2e3)
2110
+ });
2111
+ return await res.json();
2112
+ } catch (err) {
2113
+ try {
2114
+ const { appendToLog: appendToLog2, HOOK_DEBUG_LOG: HOOK_DEBUG_LOG2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
2115
+ appendToLog2(HOOK_DEBUG_LOG2, {
2116
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2117
+ event: "checkTaint-error",
2118
+ error: String(err),
2119
+ paths
2120
+ });
2121
+ } catch {
2122
+ }
2123
+ return { tainted: false, daemonUnavailable: true };
2124
+ }
1995
2125
  }
1996
- async function resolveViaDaemon(id, decision, internalToken) {
2126
+ async function resolveViaDaemon(id, decision, internalToken, source) {
1997
2127
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1998
2128
  await fetch(`${base}/resolve/${id}`, {
1999
2129
  method: "POST",
2000
2130
  headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
2001
- body: JSON.stringify({ decision }),
2131
+ body: JSON.stringify({ decision, ...source && { source } }),
2002
2132
  signal: AbortSignal.timeout(3e3)
2003
2133
  });
2004
2134
  }
@@ -2007,7 +2137,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
2007
2137
  var import_net = __toESM(require("net"));
2008
2138
  var import_path13 = __toESM(require("path"));
2009
2139
  var import_os9 = __toESM(require("os"));
2010
- var import_crypto = require("crypto");
2140
+ var import_crypto2 = require("crypto");
2011
2141
 
2012
2142
  // src/ui/native.ts
2013
2143
  var import_child_process2 = require("child_process");
@@ -2200,20 +2330,24 @@ ${smartTruncate(str, 500)}`
2200
2330
  function escapePango(text) {
2201
2331
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2202
2332
  }
2203
- function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2333
+ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2204
2334
  const lines = [];
2205
2335
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
2206
2336
  lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
2207
2337
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
2208
2338
  lines.push("");
2209
2339
  lines.push(formattedArgs);
2340
+ if (allowCount >= 3) {
2341
+ lines.push("");
2342
+ lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
2343
+ }
2210
2344
  if (!locked) {
2211
2345
  lines.push("");
2212
2346
  lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
2213
2347
  }
2214
2348
  return lines.join("\n");
2215
2349
  }
2216
- function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2350
+ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2217
2351
  const lines = [];
2218
2352
  if (locked) {
2219
2353
  lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
@@ -2225,6 +2359,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2225
2359
  lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
2226
2360
  lines.push("");
2227
2361
  lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
2362
+ if (allowCount >= 3) {
2363
+ lines.push("");
2364
+ lines.push(
2365
+ `<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
2366
+ );
2367
+ }
2228
2368
  if (!locked) {
2229
2369
  lines.push("");
2230
2370
  lines.push(
@@ -2233,12 +2373,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2233
2373
  }
2234
2374
  return lines.join("\n");
2235
2375
  }
2236
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
2376
+ async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
2237
2377
  if (isTestEnv()) return "deny";
2238
2378
  const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
2239
2379
  const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
2240
2380
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
2241
- const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
2381
+ const message = buildPlainMessage(
2382
+ toolName,
2383
+ formattedArgs,
2384
+ agent,
2385
+ explainableLabel,
2386
+ locked,
2387
+ allowCount
2388
+ );
2242
2389
  return new Promise((resolve) => {
2243
2390
  let childProcess = null;
2244
2391
  const onAbort = () => {
@@ -2270,7 +2417,8 @@ end run`;
2270
2417
  formattedArgs,
2271
2418
  agent,
2272
2419
  explainableLabel,
2273
- locked
2420
+ locked,
2421
+ allowCount
2274
2422
  );
2275
2423
  const argsList = [
2276
2424
  locked ? "--info" : "--question",
@@ -2311,9 +2459,13 @@ end run`;
2311
2459
  });
2312
2460
  }
2313
2461
 
2462
+ // src/auth/orchestrator.ts
2463
+ init_audit();
2464
+
2314
2465
  // src/auth/cloud.ts
2315
2466
  var import_fs9 = __toESM(require("fs"));
2316
2467
  var import_os8 = __toESM(require("os"));
2468
+ init_audit();
2317
2469
  function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
2318
2470
  return fetch(`${creds.apiUrl}/audit`, {
2319
2471
  method: "POST",
@@ -2422,6 +2574,51 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
2422
2574
  }
2423
2575
 
2424
2576
  // src/auth/orchestrator.ts
2577
+ var WRITE_TOOLS = /* @__PURE__ */ new Set([
2578
+ "write",
2579
+ "write_file",
2580
+ "create_file",
2581
+ "edit",
2582
+ "multiedit",
2583
+ "str_replace_based_edit_tool",
2584
+ "replace",
2585
+ "notebook_edit",
2586
+ "notebookedit"
2587
+ ]);
2588
+ function isWriteTool(toolName) {
2589
+ const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
2590
+ return WRITE_TOOLS.has(t);
2591
+ }
2592
+ function extractFilePaths(toolName, args) {
2593
+ const paths = [];
2594
+ if (!args || typeof args !== "object" || Array.isArray(args)) return paths;
2595
+ const a = args;
2596
+ for (const key of ["file_path", "path", "filename", "source", "src", "input"]) {
2597
+ if (typeof a[key] === "string" && a[key]) paths.push(a[key]);
2598
+ }
2599
+ const cmd = typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : "";
2600
+ if (cmd) {
2601
+ for (const m of cmd.matchAll(/(?:-T|--upload-file|--data(?:-binary)?)\s+@?(\S+)/g)) {
2602
+ paths.push(m[1]);
2603
+ }
2604
+ for (const m of cmd.matchAll(/\b(?:scp|rsync)\s+(?:-\S+\s+)*(\S+)\s+\S+@/g)) {
2605
+ paths.push(m[1]);
2606
+ }
2607
+ for (const m of cmd.matchAll(/<\s*(\S+)/g)) {
2608
+ paths.push(m[1]);
2609
+ }
2610
+ }
2611
+ return paths.filter(Boolean);
2612
+ }
2613
+ function isNetworkTool(toolName, args) {
2614
+ const t = toolName.toLowerCase();
2615
+ if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal.execute") {
2616
+ const a = args;
2617
+ const cmd = typeof a?.command === "string" ? a.command : typeof a?.cmd === "string" ? a.cmd : "";
2618
+ return /\b(curl|wget|scp|rsync|nc|ncat|netcat|ssh)\b/.test(cmd);
2619
+ }
2620
+ return false;
2621
+ }
2425
2622
  var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path13.default.join(import_os9.default.tmpdir(), "node9-activity.sock");
2426
2623
  function notifyActivity(data) {
2427
2624
  return new Promise((resolve) => {
@@ -2440,7 +2637,7 @@ function notifyActivity(data) {
2440
2637
  }
2441
2638
  async function authorizeHeadless(toolName, args, meta, options) {
2442
2639
  if (!options?.calledFromDaemon) {
2443
- const actId = (0, import_crypto.randomUUID)();
2640
+ const actId = (0, import_crypto2.randomUUID)();
2444
2641
  const actTs = Date.now();
2445
2642
  await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
2446
2643
  const result = await _authorizeHeadlessCore(toolName, args, meta, {
@@ -2452,7 +2649,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
2452
2649
  id: actId,
2453
2650
  tool: toolName,
2454
2651
  ts: actTs,
2455
- status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
2652
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
2456
2653
  label: result.blockedByLabel
2457
2654
  });
2458
2655
  }
@@ -2466,6 +2663,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2466
2663
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
2467
2664
  const creds = getCredentials();
2468
2665
  const config = getConfig(options?.cwd);
2666
+ const hashAuditArgs = config.settings.auditHashArgs === true;
2469
2667
  const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
2470
2668
  const approvers = {
2471
2669
  ...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
@@ -2476,13 +2674,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2476
2674
  approvers.terminal = false;
2477
2675
  }
2478
2676
  if (config.settings.enableHookLogDebug && !isTestEnv2) {
2479
- appendHookDebug(toolName, args, meta);
2677
+ appendHookDebug(toolName, args, meta, hashAuditArgs);
2480
2678
  }
2481
2679
  const isManual = meta?.agent === "Terminal";
2482
2680
  let explainableLabel = "Local Config";
2483
2681
  let policyMatchedField;
2484
2682
  let policyMatchedWord;
2485
2683
  let riskMetadata;
2684
+ let taintWarning = null;
2685
+ if (isNetworkTool(toolName, args)) {
2686
+ const filePaths = extractFilePaths(toolName, args);
2687
+ if (filePaths.length > 0) {
2688
+ const taintResult = await checkTaint(filePaths);
2689
+ if (taintResult.tainted && taintResult.record) {
2690
+ const { path: taintedPath, source: taintSource } = taintResult.record;
2691
+ taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
2692
+ } else if (taintResult.daemonUnavailable) {
2693
+ taintWarning = `\u26A0\uFE0F Taint service unavailable \u2014 cannot verify if ${filePaths.join(", ")} is clean`;
2694
+ }
2695
+ }
2696
+ }
2486
2697
  if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
2487
2698
  const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
2488
2699
  const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
@@ -2490,7 +2701,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2490
2701
  if (dlpMatch) {
2491
2702
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
2492
2703
  if (dlpMatch.severity === "block") {
2493
- if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
2704
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta, true);
2705
+ if (isWriteTool(toolName) && filePath) {
2706
+ await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
2707
+ }
2494
2708
  return {
2495
2709
  approved: false,
2496
2710
  reason: dlpReason,
@@ -2498,7 +2712,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2498
2712
  blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
2499
2713
  };
2500
2714
  }
2501
- if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
2715
+ if (!isManual)
2716
+ appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta, hashAuditArgs);
2502
2717
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
2503
2718
  }
2504
2719
  }
@@ -2506,7 +2721,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2506
2721
  if (!isIgnoredTool(toolName)) {
2507
2722
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
2508
2723
  if (policyResult.decision === "review") {
2509
- appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
2724
+ appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
2510
2725
  if (approvers.cloud && creds?.apiKey) {
2511
2726
  await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
2512
2727
  }
@@ -2514,22 +2729,23 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2514
2729
  }
2515
2730
  return { approved: true, checkedBy: "audit" };
2516
2731
  }
2517
- if (!isIgnoredTool(toolName)) {
2732
+ if (!taintWarning && !isIgnoredTool(toolName)) {
2518
2733
  if (getActiveTrustSession(toolName)) {
2519
2734
  if (approvers.cloud && creds?.apiKey)
2520
2735
  await auditLocalAllow(toolName, args, "trust", creds, meta);
2521
- if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
2736
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
2522
2737
  return { approved: true, checkedBy: "trust" };
2523
2738
  }
2524
2739
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
2525
2740
  if (policyResult.decision === "allow") {
2526
2741
  if (approvers.cloud && creds?.apiKey)
2527
2742
  auditLocalAllow(toolName, args, "local-policy", creds, meta);
2528
- if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
2743
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
2529
2744
  return { approved: true, checkedBy: "local-policy" };
2530
2745
  }
2531
2746
  if (policyResult.decision === "block") {
2532
- if (!isManual) appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta);
2747
+ if (!isManual)
2748
+ appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
2533
2749
  return {
2534
2750
  approved: false,
2535
2751
  reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
@@ -2548,15 +2764,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2548
2764
  policyMatchedWord,
2549
2765
  policyResult.ruleName
2550
2766
  );
2551
- const persistent = getPersistentDecision(toolName);
2767
+ const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
2552
2768
  if (persistent === "allow") {
2553
2769
  if (approvers.cloud && creds?.apiKey)
2554
2770
  await auditLocalAllow(toolName, args, "persistent", creds, meta);
2555
- if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
2771
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
2556
2772
  return { approved: true, checkedBy: "persistent" };
2557
2773
  }
2558
2774
  if (persistent === "deny") {
2559
- if (!isManual) appendLocalAudit(toolName, args, "deny", "persistent-deny", meta);
2775
+ if (!isManual)
2776
+ appendLocalAudit(toolName, args, "deny", "persistent-deny", meta, hashAuditArgs);
2560
2777
  return {
2561
2778
  approved: false,
2562
2779
  reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
@@ -2564,10 +2781,21 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2564
2781
  blockedByLabel: "Persistent User Rule"
2565
2782
  };
2566
2783
  }
2567
- } else {
2568
- if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
2784
+ } else if (!taintWarning) {
2785
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
2569
2786
  return { approved: true };
2570
2787
  }
2788
+ if (taintWarning) {
2789
+ explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
2790
+ riskMetadata = computeRiskMetadata(
2791
+ args,
2792
+ 7,
2793
+ explainableLabel,
2794
+ void 0,
2795
+ void 0,
2796
+ taintWarning
2797
+ );
2798
+ }
2571
2799
  let cloudRequestId = null;
2572
2800
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
2573
2801
  if (cloudEnforced) {
@@ -2586,7 +2814,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2586
2814
  };
2587
2815
  }
2588
2816
  cloudRequestId = initResult.requestId || null;
2589
- explainableLabel = "Organization Policy (SaaS)";
2817
+ if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
2590
2818
  } catch {
2591
2819
  }
2592
2820
  }
@@ -2615,13 +2843,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2615
2843
  let viewerId = null;
2616
2844
  const internalToken = getInternalToken();
2617
2845
  let daemonEntryId = null;
2846
+ let daemonAllowCount = 1;
2618
2847
  if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
2619
2848
  if (cloudEnforced && cloudRequestId) {
2620
- viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
2849
+ const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
2850
+ viewerId = viewer?.id ?? null;
2621
2851
  daemonEntryId = viewerId;
2852
+ if (viewer) daemonAllowCount = viewer.allowCount;
2622
2853
  } else {
2623
2854
  try {
2624
- daemonEntryId = await registerDaemonEntry(
2855
+ const entry = await registerDaemonEntry(
2625
2856
  toolName,
2626
2857
  args,
2627
2858
  meta,
@@ -2629,6 +2860,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2629
2860
  options?.activityId,
2630
2861
  options?.cwd
2631
2862
  );
2863
+ daemonEntryId = entry.id;
2864
+ daemonAllowCount = entry.allowCount;
2632
2865
  } catch {
2633
2866
  }
2634
2867
  }
@@ -2664,7 +2897,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2664
2897
  false,
2665
2898
  signal,
2666
2899
  policyMatchedField,
2667
- policyMatchedWord
2900
+ policyMatchedWord,
2901
+ daemonAllowCount
2668
2902
  );
2669
2903
  if (decision === "always_allow") {
2670
2904
  writeTrustSession(toolName, 36e5);
@@ -2722,10 +2956,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
2722
2956
  if (!resolved) {
2723
2957
  resolved = true;
2724
2958
  abortController.abort();
2725
- if (viewerId && internalToken) {
2726
- resolveViaDaemon(viewerId, res.approved ? "allow" : "deny", internalToken).catch(
2727
- () => null
2728
- );
2959
+ if (daemonEntryId && internalToken) {
2960
+ resolveViaDaemon(
2961
+ daemonEntryId,
2962
+ res.approved ? "allow" : "deny",
2963
+ internalToken,
2964
+ res.decisionSource
2965
+ ).catch(() => null);
2729
2966
  }
2730
2967
  resolve(res);
2731
2968
  }
@@ -2764,7 +3001,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
2764
3001
  args,
2765
3002
  finalResult.approved ? "allow" : "deny",
2766
3003
  finalResult.checkedBy || finalResult.blockedBy || "unknown",
2767
- meta
3004
+ meta,
3005
+ hashAuditArgs
2768
3006
  );
2769
3007
  }
2770
3008
  return finalResult;