@node9/proxy 1.30.0 → 1.31.0

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.
@@ -2241,7 +2241,15 @@ var init_config_schema = __esm({
2241
2241
  }).optional(),
2242
2242
  dlp: z.object({
2243
2243
  enabled: z.boolean().optional(),
2244
- scanIgnoredTools: z.boolean().optional()
2244
+ scanIgnoredTools: z.boolean().optional(),
2245
+ pii: z.enum(["off", "block"]).optional()
2246
+ }).optional(),
2247
+ egress: z.object({
2248
+ enabled: z.boolean().optional(),
2249
+ mode: z.enum(["off", "review", "block"]).optional(),
2250
+ allow: z.array(z.string()).optional(),
2251
+ deny: z.array(z.string()).optional(),
2252
+ allowPrivate: z.boolean().optional()
2245
2253
  }).optional(),
2246
2254
  loopDetection: z.object({
2247
2255
  enabled: z.boolean().optional(),
@@ -2546,7 +2554,8 @@ var init_config = __esm({
2546
2554
  description: "The AI wants to download a script from the internet and run it immediately, without you seeing what it contains. This is one of the most common ways malware gets installed."
2547
2555
  }
2548
2556
  ],
2549
- dlp: { enabled: true, scanIgnoredTools: true },
2557
+ dlp: { enabled: true, scanIgnoredTools: true, pii: "off" },
2558
+ egress: { enabled: false, mode: "review", allow: [], deny: [], allowPrivate: true },
2550
2559
  loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
2551
2560
  skillPinning: { enabled: false, mode: "warn", roots: [] }
2552
2561
  },
@@ -2642,6 +2651,14 @@ var init_litellm = __esm({
2642
2651
  }
2643
2652
  });
2644
2653
 
2654
+ // src/cost-codex.ts
2655
+ var init_cost_codex = __esm({
2656
+ "src/cost-codex.ts"() {
2657
+ "use strict";
2658
+ init_litellm();
2659
+ }
2660
+ });
2661
+
2645
2662
  // src/costSync.ts
2646
2663
  function decodeProjectDirName(dirName) {
2647
2664
  return dirName.replace(/-/g, "/");
@@ -2653,6 +2670,7 @@ var init_costSync = __esm({
2653
2670
  init_config();
2654
2671
  init_audit();
2655
2672
  init_litellm();
2673
+ init_cost_codex();
2656
2674
  SYNC_INTERVAL_MS = 10 * 60 * 1e3;
2657
2675
  }
2658
2676
  });
package/dist/index.js CHANGED
@@ -293,7 +293,15 @@ var ConfigFileSchema = import_zod.z.object({
293
293
  }).optional(),
294
294
  dlp: import_zod.z.object({
295
295
  enabled: import_zod.z.boolean().optional(),
296
- scanIgnoredTools: import_zod.z.boolean().optional()
296
+ scanIgnoredTools: import_zod.z.boolean().optional(),
297
+ pii: import_zod.z.enum(["off", "block"]).optional()
298
+ }).optional(),
299
+ egress: import_zod.z.object({
300
+ enabled: import_zod.z.boolean().optional(),
301
+ mode: import_zod.z.enum(["off", "review", "block"]).optional(),
302
+ allow: import_zod.z.array(import_zod.z.string()).optional(),
303
+ deny: import_zod.z.array(import_zod.z.string()).optional(),
304
+ allowPrivate: import_zod.z.boolean().optional()
297
305
  }).optional(),
298
306
  loopDetection: import_zod.z.object({
299
307
  enabled: import_zod.z.boolean().optional(),
@@ -1275,6 +1283,201 @@ function extractLiteralArgs(callExpr) {
1275
1283
  }
1276
1284
  return { name, flags, paths };
1277
1285
  }
1286
+ var NET_BINARIES = /* @__PURE__ */ new Set(["curl", "wget", "scp", "ssh", "nc", "ncat", "netcat"]);
1287
+ var VALUE_FLAGS = {
1288
+ curl: /* @__PURE__ */ new Set([
1289
+ "-d",
1290
+ "--data",
1291
+ "--data-ascii",
1292
+ "--data-binary",
1293
+ "--data-raw",
1294
+ "--data-urlencode",
1295
+ "-F",
1296
+ "--form",
1297
+ "-H",
1298
+ "--header",
1299
+ "-X",
1300
+ "--request",
1301
+ "-o",
1302
+ "--output",
1303
+ "-T",
1304
+ "--upload-file",
1305
+ "-u",
1306
+ "--user",
1307
+ "-e",
1308
+ "--referer",
1309
+ "-A",
1310
+ "--user-agent",
1311
+ "-b",
1312
+ "--cookie",
1313
+ "-c",
1314
+ "--cookie-jar",
1315
+ "--connect-to",
1316
+ "--resolve",
1317
+ "--cacert",
1318
+ "--cert",
1319
+ "--key",
1320
+ "-x",
1321
+ "--proxy",
1322
+ "-m",
1323
+ "--max-time",
1324
+ "--retry"
1325
+ ]),
1326
+ wget: /* @__PURE__ */ new Set([
1327
+ "-O",
1328
+ "--output-document",
1329
+ "--post-data",
1330
+ "--post-file",
1331
+ "--header",
1332
+ "-U",
1333
+ "--user-agent",
1334
+ "--user",
1335
+ "--password",
1336
+ "-o",
1337
+ "--output-file",
1338
+ "-P",
1339
+ "--directory-prefix",
1340
+ "-t",
1341
+ "--tries",
1342
+ "-T",
1343
+ "--timeout"
1344
+ ]),
1345
+ scp: /* @__PURE__ */ new Set(["-i", "-F", "-l", "-o", "-c", "-S", "-P", "-J", "-D", "-W"]),
1346
+ ssh: /* @__PURE__ */ new Set([
1347
+ "-i",
1348
+ "-p",
1349
+ "-o",
1350
+ "-l",
1351
+ "-F",
1352
+ "-c",
1353
+ "-L",
1354
+ "-R",
1355
+ "-D",
1356
+ "-W",
1357
+ "-b",
1358
+ "-e",
1359
+ "-m",
1360
+ "-O",
1361
+ "-Q",
1362
+ "-S",
1363
+ "-J",
1364
+ "-w",
1365
+ "-B",
1366
+ "-I",
1367
+ "-E"
1368
+ ]),
1369
+ nc: /* @__PURE__ */ new Set(["-p", "-s", "-w", "-X", "-x", "-e", "-g", "-G", "-i", "-O", "-T", "-q", "-m"])
1370
+ };
1371
+ function resolveWordLiteral(w) {
1372
+ const parts = w?.Parts || [];
1373
+ let s = "";
1374
+ for (const p of parts) {
1375
+ const t = syntax.NodeType(p);
1376
+ if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
1377
+ else if (t === "SglQuoted") s += p.Value ?? "";
1378
+ else if (t === "DblQuoted") {
1379
+ const inner = p.Parts || [];
1380
+ if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
1381
+ s += inner.map((ip) => ip.Value ?? "").join("");
1382
+ } else {
1383
+ return null;
1384
+ }
1385
+ }
1386
+ return s;
1387
+ }
1388
+ function parseDestHost(token) {
1389
+ if (!token) return null;
1390
+ let t = token.trim();
1391
+ if (!t || t.startsWith("-")) return null;
1392
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(t)) {
1393
+ try {
1394
+ const h = new URL(t).hostname.toLowerCase();
1395
+ return h || null;
1396
+ } catch {
1397
+ return null;
1398
+ }
1399
+ }
1400
+ const at = t.lastIndexOf("@");
1401
+ if (at >= 0) t = t.slice(at + 1);
1402
+ t = t.split("/")[0];
1403
+ t = t.replace(/:\d+$/, "");
1404
+ t = t.split(":")[0];
1405
+ t = t.toLowerCase();
1406
+ if (t.length > 253) return null;
1407
+ if (t === "localhost") return t;
1408
+ if (/^[a-z0-9.-]+\.[a-z0-9.-]+$/.test(t)) return t;
1409
+ return null;
1410
+ }
1411
+ function destTokensForBinary(binary, args) {
1412
+ const valueFlags = VALUE_FLAGS[binary] ?? /* @__PURE__ */ new Set();
1413
+ const positionals = [];
1414
+ const urlFlagValues = [];
1415
+ for (let i = 0; i < args.length; i++) {
1416
+ const tok = args[i];
1417
+ if (tok === null) continue;
1418
+ if (tok.startsWith("-")) {
1419
+ if (tok.startsWith("--url=")) {
1420
+ urlFlagValues.push(tok.slice("--url=".length));
1421
+ continue;
1422
+ }
1423
+ if (tok === "--url") {
1424
+ const next = args[i + 1];
1425
+ if (typeof next === "string") urlFlagValues.push(next);
1426
+ i++;
1427
+ continue;
1428
+ }
1429
+ if (tok.includes("=")) continue;
1430
+ if (valueFlags.has(tok)) i++;
1431
+ continue;
1432
+ }
1433
+ positionals.push(tok);
1434
+ }
1435
+ switch (binary) {
1436
+ case "curl":
1437
+ case "wget":
1438
+ return [...urlFlagValues, ...positionals];
1439
+ case "ssh":
1440
+ return positionals.slice(0, 1);
1441
+ case "scp":
1442
+ return positionals.filter((p) => p.includes(":") || p.includes("@"));
1443
+ case "nc":
1444
+ case "ncat":
1445
+ case "netcat":
1446
+ return positionals.slice(0, 1);
1447
+ default:
1448
+ return [];
1449
+ }
1450
+ }
1451
+ function extractShellDestinations(command) {
1452
+ const f = parseShared(command);
1453
+ if (f === PARSE_FAIL) return [];
1454
+ const out = [];
1455
+ const seen = /* @__PURE__ */ new Set();
1456
+ try {
1457
+ syntax.Walk(f, (node) => {
1458
+ if (!node) return false;
1459
+ const n = node;
1460
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1461
+ const callArgs = n.Args || [];
1462
+ if (callArgs.length === 0) return true;
1463
+ const name = (resolveWordLiteral(callArgs[0]) || "").toLowerCase();
1464
+ if (!NET_BINARIES.has(name)) return true;
1465
+ const rest = callArgs.slice(1).map((a) => resolveWordLiteral(a));
1466
+ for (const raw of destTokensForBinary(name, rest)) {
1467
+ const host = parseDestHost(raw);
1468
+ if (!host) continue;
1469
+ const key = `${name}:${host}`;
1470
+ if (seen.has(key)) continue;
1471
+ seen.add(key);
1472
+ out.push({ host, binary: name, raw });
1473
+ }
1474
+ return true;
1475
+ });
1476
+ } catch {
1477
+ return out;
1478
+ }
1479
+ return out;
1480
+ }
1278
1481
  var FS_OP_CACHE_MAX = 5e3;
1279
1482
  var fsOpCache = /* @__PURE__ */ new Map();
1280
1483
  function analyzeFsOperation(command) {
@@ -1404,6 +1607,83 @@ function analyzeShellCommand(command) {
1404
1607
  }
1405
1608
  return { actions, paths, allTokens };
1406
1609
  }
1610
+ var DEFAULT_EGRESS_ALLOWLIST = [
1611
+ "*.github.com",
1612
+ "*.githubusercontent.com",
1613
+ "*.npmjs.org",
1614
+ "pypi.org",
1615
+ "*.pythonhosted.org",
1616
+ "crates.io",
1617
+ "*.crates.io",
1618
+ "rubygems.org",
1619
+ "proxy.golang.org",
1620
+ "sum.golang.org",
1621
+ "*.anthropic.com",
1622
+ "*.openai.com",
1623
+ "*.googleapis.com",
1624
+ "*.docker.io",
1625
+ "*.docker.com",
1626
+ "deb.debian.org",
1627
+ "*.ubuntu.com"
1628
+ ];
1629
+ function hostMatches(host, pattern) {
1630
+ const h = host.toLowerCase();
1631
+ const p = pattern.toLowerCase().trim();
1632
+ if (!p) return false;
1633
+ if (p === "*") return true;
1634
+ if (p.startsWith("*.")) {
1635
+ const suffix = p.slice(2);
1636
+ return h === suffix || h.endsWith("." + suffix);
1637
+ }
1638
+ return h === p;
1639
+ }
1640
+ function matchesAny(host, patterns) {
1641
+ for (const p of patterns) if (hostMatches(host, p)) return true;
1642
+ return false;
1643
+ }
1644
+ function isPrivateHost(host) {
1645
+ const h = host.toLowerCase();
1646
+ if (h === "localhost" || h === "0.0.0.0") return true;
1647
+ if (h.endsWith(".local") || h.endsWith(".internal") || h.endsWith(".localhost")) return true;
1648
+ if (/^127\./.test(h)) return true;
1649
+ if (/^10\./.test(h)) return true;
1650
+ if (/^192\.168\./.test(h)) return true;
1651
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
1652
+ return false;
1653
+ }
1654
+ function evaluateEgress(dests, policy) {
1655
+ if (!policy.enabled) return null;
1656
+ let review = null;
1657
+ for (const d of dests) {
1658
+ if (matchesAny(d.host, policy.deny)) {
1659
+ return {
1660
+ verdict: "block",
1661
+ host: d.host,
1662
+ binary: d.binary,
1663
+ reason: `Egress to ${d.host} is on the deny list.`
1664
+ };
1665
+ }
1666
+ if (policy.allowPrivate && isPrivateHost(d.host)) continue;
1667
+ if (matchesAny(d.host, policy.allow) || matchesAny(d.host, DEFAULT_EGRESS_ALLOWLIST)) continue;
1668
+ if (policy.mode === "block") {
1669
+ return {
1670
+ verdict: "block",
1671
+ host: d.host,
1672
+ binary: d.binary,
1673
+ reason: `Egress to unknown host ${d.host} is blocked (egress policy: block).`
1674
+ };
1675
+ }
1676
+ if (policy.mode === "review" && !review) {
1677
+ review = {
1678
+ verdict: "review",
1679
+ host: d.host,
1680
+ binary: d.binary,
1681
+ reason: `${d.binary} is sending data to an unrecognized host (${d.host}). Approve this destination?`
1682
+ };
1683
+ }
1684
+ }
1685
+ return review;
1686
+ }
1407
1687
  var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
1408
1688
  "cat",
1409
1689
  "head",
@@ -1969,6 +2249,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1969
2249
  }
1970
2250
  const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
1971
2251
  if (ptVerdict) return ptVerdict;
2252
+ if (config.policy.egress?.enabled) {
2253
+ const dests = extractShellDestinations(shellCommand);
2254
+ if (dests.length > 0) {
2255
+ const eg = evaluateEgress(dests, config.policy.egress);
2256
+ if (eg) {
2257
+ return {
2258
+ decision: eg.verdict,
2259
+ blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
2260
+ reason: eg.reason,
2261
+ ruleName: `egress:${eg.binary}:${eg.host}`,
2262
+ ruleDescription: eg.reason,
2263
+ tier: eg.verdict === "block" ? 3 : 4
2264
+ };
2265
+ }
2266
+ }
2267
+ }
1972
2268
  const firstToken = analyzed.actions[0] ?? "";
1973
2269
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
1974
2270
  const rawTokens = shellCommand.trim().split(/\s+/);
@@ -2890,6 +3186,32 @@ function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
2890
3186
  const nextRecords = fresh.slice(-LOOP_MAX_RECORDS);
2891
3187
  return { nextRecords, count, looping: count >= threshold };
2892
3188
  }
3189
+ var PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
3190
+ var PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
3191
+ var PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
3192
+ var PII_CC_RE = /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6\d{3})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/;
3193
+ function detectPii(text) {
3194
+ const found = /* @__PURE__ */ new Set();
3195
+ if (/@/.test(text) && PII_EMAIL_RE.test(text)) found.add("Email");
3196
+ if (/-/.test(text) && PII_SSN_RE.test(text)) found.add("SSN");
3197
+ if (PII_PHONE_RE.test(text)) found.add("Phone");
3198
+ if (PII_CC_RE.test(text)) found.add("Credit Card");
3199
+ return [...found];
3200
+ }
3201
+ var REALTIME_PII_PATTERNS = ["SSN", "Credit Card"];
3202
+ var MAX_PII_SCAN_BYTES = 1e5;
3203
+ function detectArgsPii(args) {
3204
+ if (args === null || args === void 0) return [];
3205
+ let text;
3206
+ try {
3207
+ text = typeof args === "string" ? args : JSON.stringify(args);
3208
+ } catch {
3209
+ return [];
3210
+ }
3211
+ if (typeof text !== "string") return [];
3212
+ if (text.length > MAX_PII_SCAN_BYTES) text = text.slice(0, MAX_PII_SCAN_BYTES);
3213
+ return detectPii(text).filter((p) => REALTIME_PII_PATTERNS.includes(p));
3214
+ }
2893
3215
  var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
2894
3216
 
2895
3217
  // src/shields.ts
@@ -3177,7 +3499,8 @@ var DEFAULT_CONFIG = {
3177
3499
  description: "The AI wants to download a script from the internet and run it immediately, without you seeing what it contains. This is one of the most common ways malware gets installed."
3178
3500
  }
3179
3501
  ],
3180
- dlp: { enabled: true, scanIgnoredTools: true },
3502
+ dlp: { enabled: true, scanIgnoredTools: true, pii: "off" },
3503
+ egress: { enabled: false, mode: "review", allow: [], deny: [], allowPrivate: true },
3181
3504
  loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
3182
3505
  skillPinning: { enabled: false, mode: "warn", roots: [] }
3183
3506
  },
@@ -3303,6 +3626,11 @@ function getConfig(cwd) {
3303
3626
  ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
3304
3627
  },
3305
3628
  dlp: { ...DEFAULT_CONFIG.policy.dlp },
3629
+ egress: {
3630
+ ...DEFAULT_CONFIG.policy.egress,
3631
+ allow: [...DEFAULT_CONFIG.policy.egress.allow],
3632
+ deny: [...DEFAULT_CONFIG.policy.egress.deny]
3633
+ },
3306
3634
  loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
3307
3635
  skillPinning: {
3308
3636
  ...DEFAULT_CONFIG.policy.skillPinning,
@@ -3353,6 +3681,15 @@ function getConfig(cwd) {
3353
3681
  const d = p.dlp;
3354
3682
  if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
3355
3683
  if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
3684
+ if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
3685
+ }
3686
+ if (p.egress) {
3687
+ const e = p.egress;
3688
+ if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
3689
+ if (e.mode !== void 0) mergedPolicy.egress.mode = e.mode;
3690
+ if (Array.isArray(e.allow)) mergedPolicy.egress.allow.push(...e.allow);
3691
+ if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
3692
+ if (e.allowPrivate !== void 0) mergedPolicy.egress.allowPrivate = e.allowPrivate;
3356
3693
  }
3357
3694
  if (p.loopDetection) {
3358
3695
  const ld = p.loopDetection;
@@ -4095,6 +4432,15 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
4095
4432
  var isTestEnv = () => {
4096
4433
  return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
4097
4434
  };
4435
+ var MIN_INTERACTION_MS = 400;
4436
+ function resolveNativeDecision(opts) {
4437
+ const { code, output, elapsedMs, locked } = opts;
4438
+ if (locked) return "deny";
4439
+ const tooFast = elapsedMs < MIN_INTERACTION_MS;
4440
+ if (output.includes("Always Allow")) return tooFast ? "deny" : "always_allow";
4441
+ if (code === 0) return tooFast ? "deny" : "allow";
4442
+ return "deny";
4443
+ }
4098
4444
  function formatArgs(args, matchedField, matchedWord) {
4099
4445
  if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
4100
4446
  let parsed = args;
@@ -4238,6 +4584,7 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
4238
4584
  );
4239
4585
  return new Promise((resolve) => {
4240
4586
  let childProcess = null;
4587
+ const startedAt = Date.now();
4241
4588
  const onAbort = () => {
4242
4589
  if (childProcess && childProcess.pid) {
4243
4590
  try {
@@ -4295,13 +4642,14 @@ end run`;
4295
4642
  }
4296
4643
  let output = "";
4297
4644
  childProcess?.stdout?.on("data", (d) => output += d.toString());
4298
- childProcess?.on("close", (code) => {
4645
+ childProcess?.on("error", () => {
4299
4646
  if (signal) signal.removeEventListener("abort", onAbort);
4300
- if (locked) return resolve("deny");
4301
- if (output.includes("Always Allow")) return resolve("always_allow");
4302
- if (code === 0) return resolve("allow");
4303
4647
  resolve("deny");
4304
4648
  });
4649
+ childProcess?.on("close", (code) => {
4650
+ if (signal) signal.removeEventListener("abort", onAbort);
4651
+ resolve(resolveNativeDecision({ code, output, elapsedMs: Date.now() - startedAt, locked }));
4652
+ });
4305
4653
  } catch {
4306
4654
  resolve("deny");
4307
4655
  }
@@ -4603,6 +4951,37 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4603
4951
  if (taintResult.tainted && taintResult.record) {
4604
4952
  const { path: taintedPath, source: taintSource } = taintResult.record;
4605
4953
  taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
4954
+ if (config.policy.egress?.enabled) {
4955
+ const a = args && typeof args === "object" && !Array.isArray(args) ? args : {};
4956
+ const cmd = typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : "";
4957
+ const dests = cmd ? extractShellDestinations(cmd) : [];
4958
+ const eg = dests.length > 0 ? evaluateEgress(dests, config.policy.egress) : null;
4959
+ if (eg) {
4960
+ if (!isManual)
4961
+ appendLocalAudit(
4962
+ toolName,
4963
+ args,
4964
+ "deny",
4965
+ isObserveMode ? "observe-mode-taint-egress-would-block" : "taint-egress-block",
4966
+ { ...meta, ruleName: `taint-egress:${eg.host}` },
4967
+ hashAuditArgs
4968
+ );
4969
+ if (isObserveMode) {
4970
+ return {
4971
+ approved: true,
4972
+ checkedBy: "audit",
4973
+ observeWouldBlock: true,
4974
+ blockedByLabel: "\u{1F534} Node9 Taint+Egress (Exfiltration)"
4975
+ };
4976
+ }
4977
+ return {
4978
+ approved: false,
4979
+ reason: `\u{1F534} EXFILTRATION BLOCKED: the tainted file "${taintedPath}" is being sent to untrusted host "${eg.host}". A flagged file leaving to an unrecognized destination is blocked outright.`,
4980
+ blockedBy: "local-config",
4981
+ blockedByLabel: "\u{1F534} Node9 Taint+Egress (Exfiltration Blocked)"
4982
+ };
4983
+ }
4984
+ }
4606
4985
  } else if (taintResult.daemonUnavailable) {
4607
4986
  taintWarning = `\u26A0\uFE0F Taint service unavailable \u2014 cannot verify if ${filePaths.join(", ")} is clean`;
4608
4987
  }
@@ -4651,6 +5030,35 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4651
5030
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
4652
5031
  }
4653
5032
  }
5033
+ if (config.policy.dlp.pii === "block" && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
5034
+ const piiFound = detectArgsPii(args);
5035
+ if (piiFound.length > 0) {
5036
+ const piiReason = `\u{1F512} PII DETECTED: ${piiFound.join(", ")} found in tool arguments. Remove or tokenize personal data before passing it to a tool.`;
5037
+ if (!isManual)
5038
+ appendLocalAudit(
5039
+ toolName,
5040
+ args,
5041
+ "deny",
5042
+ isObserveMode ? "observe-mode-pii-would-block" : "pii-block",
5043
+ { ...meta, piiPatterns: piiFound.join(",") },
5044
+ true
5045
+ );
5046
+ if (isObserveMode) {
5047
+ return {
5048
+ approved: true,
5049
+ checkedBy: "audit",
5050
+ observeWouldBlock: true,
5051
+ blockedByLabel: "\u{1F512} Node9 PII (Detected)"
5052
+ };
5053
+ }
5054
+ return {
5055
+ approved: false,
5056
+ reason: piiReason,
5057
+ blockedBy: "local-config",
5058
+ blockedByLabel: "\u{1F512} Node9 PII (Detected)"
5059
+ };
5060
+ }
5061
+ }
4654
5062
  if (isObserveMode) {
4655
5063
  if (!isIgnoredTool2(toolName)) {
4656
5064
  const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);