@node9/proxy 1.3.1 → 1.4.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.
package/dist/index.js CHANGED
@@ -191,8 +191,8 @@ function sanitizeConfig(raw) {
191
191
  }
192
192
  }
193
193
  const lines = result.error.issues.map((issue) => {
194
- const path10 = issue.path.length > 0 ? issue.path.join(".") : "root";
195
- return ` \u2022 ${path10}: ${issue.message}`;
194
+ const path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
195
+ return ` \u2022 ${path14}: ${issue.message}`;
196
196
  });
197
197
  return {
198
198
  sanitized,
@@ -922,6 +922,7 @@ function getCompiledRegex(pattern, flags = "") {
922
922
  }
923
923
 
924
924
  // src/policy/index.ts
925
+ var import_path8 = __toESM(require("path"));
925
926
  var import_picomatch = __toESM(require("picomatch"));
926
927
  var import_sh_syntax = require("sh-syntax");
927
928
 
@@ -1067,8 +1068,447 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
1067
1068
  return null;
1068
1069
  }
1069
1070
 
1071
+ // src/utils/provenance.ts
1072
+ var import_fs5 = __toESM(require("fs"));
1073
+ var import_path5 = __toESM(require("path"));
1074
+ var import_os4 = __toESM(require("os"));
1075
+ var SYSTEM_PREFIXES = ["/usr/bin", "/usr/sbin", "/bin", "/sbin"];
1076
+ var MANAGED_PREFIXES = ["/usr/local/bin", "/opt/homebrew", "/home/linuxbrew", "/nix/store"];
1077
+ var USER_PREFIXES = [
1078
+ import_path5.default.join(import_os4.default.homedir(), "bin"),
1079
+ import_path5.default.join(import_os4.default.homedir(), ".local", "bin"),
1080
+ import_path5.default.join(import_os4.default.homedir(), ".cargo", "bin"),
1081
+ import_path5.default.join(import_os4.default.homedir(), ".npm-global", "bin"),
1082
+ import_path5.default.join(import_os4.default.homedir(), ".volta", "bin")
1083
+ ];
1084
+ var SUSPECT_PREFIXES = ["/tmp", "/var/tmp", "/dev/shm"];
1085
+ function findInPath(cmd) {
1086
+ if (import_path5.default.posix.isAbsolute(cmd)) return cmd;
1087
+ const pathEnv = process.env.PATH ?? "";
1088
+ for (const dir of pathEnv.split(import_path5.default.delimiter)) {
1089
+ if (!dir) continue;
1090
+ const full = import_path5.default.join(dir, cmd);
1091
+ try {
1092
+ import_fs5.default.accessSync(full, import_fs5.default.constants.X_OK);
1093
+ return full;
1094
+ } catch {
1095
+ }
1096
+ }
1097
+ return null;
1098
+ }
1099
+ function _classifyPath(resolved, cwd) {
1100
+ if (cwd && resolved.startsWith(cwd + "/")) {
1101
+ return { trustLevel: "user", reason: "binary in project directory" };
1102
+ }
1103
+ const osTmp = import_os4.default.tmpdir();
1104
+ const allSuspect = osTmp ? [...SUSPECT_PREFIXES, osTmp] : SUSPECT_PREFIXES;
1105
+ if (allSuspect.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
1106
+ return { trustLevel: "suspect", reason: `binary in temp directory: ${resolved}` };
1107
+ }
1108
+ if (SYSTEM_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
1109
+ return { trustLevel: "system", reason: "" };
1110
+ }
1111
+ if (MANAGED_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
1112
+ return { trustLevel: "managed", reason: "" };
1113
+ }
1114
+ if (USER_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
1115
+ return { trustLevel: "user", reason: "" };
1116
+ }
1117
+ return { trustLevel: "unknown", reason: "binary in unrecognized location" };
1118
+ }
1119
+ function checkProvenance(cmd, cwd) {
1120
+ const bare = cmd.startsWith("./") ? cmd.slice(2) : cmd;
1121
+ if (import_path5.default.posix.isAbsolute(bare)) {
1122
+ const early = _classifyPath(bare, cwd);
1123
+ if (early.trustLevel === "suspect") {
1124
+ return { resolvedPath: bare, ...early };
1125
+ }
1126
+ }
1127
+ let resolved;
1128
+ try {
1129
+ const found = findInPath(bare);
1130
+ if (!found) {
1131
+ return {
1132
+ resolvedPath: cmd,
1133
+ trustLevel: "unknown",
1134
+ reason: "binary not found in PATH"
1135
+ };
1136
+ }
1137
+ resolved = import_fs5.default.realpathSync(found);
1138
+ } catch {
1139
+ return {
1140
+ resolvedPath: cmd,
1141
+ trustLevel: "unknown",
1142
+ reason: "binary not found in PATH"
1143
+ };
1144
+ }
1145
+ try {
1146
+ const stat = import_fs5.default.statSync(resolved);
1147
+ if (stat.mode & 2) {
1148
+ return {
1149
+ resolvedPath: resolved,
1150
+ trustLevel: "suspect",
1151
+ reason: "binary is world-writable"
1152
+ };
1153
+ }
1154
+ } catch {
1155
+ return {
1156
+ resolvedPath: resolved,
1157
+ trustLevel: "unknown",
1158
+ reason: "could not stat binary"
1159
+ };
1160
+ }
1161
+ const classify = _classifyPath(resolved, cwd);
1162
+ return { resolvedPath: resolved, ...classify };
1163
+ }
1164
+
1165
+ // src/policy/pipe-chain.ts
1166
+ var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
1167
+ "cat",
1168
+ "head",
1169
+ "tail",
1170
+ "grep",
1171
+ "awk",
1172
+ "sed",
1173
+ "cut",
1174
+ "sort",
1175
+ "tee",
1176
+ "less",
1177
+ "more",
1178
+ "strings",
1179
+ "xxd"
1180
+ ]);
1181
+ var SINK_COMMANDS = /* @__PURE__ */ new Set([
1182
+ "curl",
1183
+ "wget",
1184
+ "nc",
1185
+ "ncat",
1186
+ "netcat",
1187
+ "ssh",
1188
+ "scp",
1189
+ "rsync",
1190
+ "socat",
1191
+ "ftp",
1192
+ "sftp",
1193
+ "telnet"
1194
+ ]);
1195
+ var OBFUSCATORS = /* @__PURE__ */ new Set([
1196
+ "base64",
1197
+ "gzip",
1198
+ "gunzip",
1199
+ "bzip2",
1200
+ "xz",
1201
+ "zstd",
1202
+ "openssl",
1203
+ "gpg",
1204
+ "python",
1205
+ "python3",
1206
+ "perl",
1207
+ "ruby",
1208
+ "node"
1209
+ ]);
1210
+ var SENSITIVE_PATTERNS = [
1211
+ /(?:^|\/)\.env(?:\.|$)/i,
1212
+ // .env, .env.local, .env.production
1213
+ /id_rsa|id_ed25519|id_ecdsa|id_dsa/i,
1214
+ // SSH private keys
1215
+ /\.pem$|\.key$|\.p12$|\.pfx$/i,
1216
+ // certificate files
1217
+ /(?:^|\/)\.ssh\//i,
1218
+ // ~/.ssh/ directory
1219
+ /(?:^|\/)\.aws\/credentials/i,
1220
+ // AWS credentials
1221
+ /(?:^|\/)\.netrc$/i,
1222
+ // netrc (stores HTTP credentials)
1223
+ /(?:^|\/)(passwd|shadow|sudoers)$/i,
1224
+ // /etc/passwd, /etc/shadow
1225
+ /(?:^|\/)credentials(?:\.json)?$/i
1226
+ // generic credentials files
1227
+ ];
1228
+ function isSensitivePath(p) {
1229
+ return SENSITIVE_PATTERNS.some((re) => re.test(p));
1230
+ }
1231
+ function splitOnPipe(cmd) {
1232
+ const segments = [];
1233
+ let current = "";
1234
+ let inSingle = false;
1235
+ let inDouble = false;
1236
+ for (let i = 0; i < cmd.length; i++) {
1237
+ const ch = cmd[i];
1238
+ if (ch === "'" && !inDouble) {
1239
+ inSingle = !inSingle;
1240
+ current += ch;
1241
+ } else if (ch === '"' && !inSingle) {
1242
+ inDouble = !inDouble;
1243
+ current += ch;
1244
+ } else if (ch === "|" && !inSingle && !inDouble && cmd[i + 1] !== "|" && (i === 0 || cmd[i - 1] !== "|")) {
1245
+ segments.push(current.trim());
1246
+ current = "";
1247
+ } else {
1248
+ current += ch;
1249
+ }
1250
+ }
1251
+ if (current.trim()) segments.push(current.trim());
1252
+ return segments.filter(Boolean);
1253
+ }
1254
+ function positionalTokens(segment) {
1255
+ return segment.split(/\s+/).slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
1256
+ }
1257
+ function analyzePipeChain(command) {
1258
+ const segments = splitOnPipe(command);
1259
+ if (segments.length < 2) {
1260
+ return {
1261
+ isPipeline: false,
1262
+ hasSensitiveSource: false,
1263
+ hasExternalSink: false,
1264
+ hasObfuscation: false,
1265
+ sourceFiles: [],
1266
+ sinkTargets: [],
1267
+ risk: "none"
1268
+ };
1269
+ }
1270
+ const sourceFiles = [];
1271
+ const sinkTargets = [];
1272
+ let hasSensitiveSource = false;
1273
+ let hasExternalSink = false;
1274
+ let hasObfuscation = false;
1275
+ for (const segment of segments) {
1276
+ const tokens = segment.split(/\s+/).filter(Boolean);
1277
+ if (tokens.length === 0) continue;
1278
+ const binary = tokens[0].toLowerCase();
1279
+ const args = positionalTokens(segment);
1280
+ if (SOURCE_COMMANDS.has(binary)) {
1281
+ sourceFiles.push(...args);
1282
+ if (args.some(isSensitivePath)) hasSensitiveSource = true;
1283
+ }
1284
+ if (OBFUSCATORS.has(binary)) hasObfuscation = true;
1285
+ if (SINK_COMMANDS.has(binary)) {
1286
+ const targets = args.filter(
1287
+ (a) => a.includes(".") || a.includes("://") || /^\d+\.\d+/.test(a)
1288
+ );
1289
+ sinkTargets.push(...targets);
1290
+ if (targets.length > 0) hasExternalSink = true;
1291
+ }
1292
+ }
1293
+ const fullCmd = command.toLowerCase();
1294
+ if (!hasSensitiveSource) {
1295
+ const redirMatch = fullCmd.match(/<\s*(\S+)/);
1296
+ if (redirMatch && isSensitivePath(redirMatch[1])) {
1297
+ hasSensitiveSource = true;
1298
+ sourceFiles.push(redirMatch[1]);
1299
+ }
1300
+ }
1301
+ const risk = hasSensitiveSource && hasExternalSink && hasObfuscation ? "critical" : hasSensitiveSource && hasExternalSink ? "high" : hasExternalSink ? "medium" : "none";
1302
+ return {
1303
+ isPipeline: true,
1304
+ hasSensitiveSource,
1305
+ hasExternalSink,
1306
+ hasObfuscation,
1307
+ sourceFiles,
1308
+ sinkTargets,
1309
+ risk
1310
+ };
1311
+ }
1312
+
1313
+ // src/policy/flag-tables.ts
1314
+ var import_path6 = __toESM(require("path"));
1315
+ var FLAGS_WITH_VALUES = {
1316
+ curl: /* @__PURE__ */ new Set([
1317
+ "-H",
1318
+ "--header",
1319
+ "-A",
1320
+ "--user-agent",
1321
+ "-e",
1322
+ "--referer",
1323
+ "-x",
1324
+ "--proxy",
1325
+ "-u",
1326
+ "--user",
1327
+ "-d",
1328
+ "--data",
1329
+ "--data-raw",
1330
+ "--data-binary",
1331
+ "-o",
1332
+ "--output",
1333
+ "-F",
1334
+ "--form",
1335
+ "--connect-to",
1336
+ "--resolve",
1337
+ "--cacert",
1338
+ "--cert",
1339
+ "--key",
1340
+ "-m",
1341
+ "--max-time"
1342
+ ]),
1343
+ wget: /* @__PURE__ */ new Set([
1344
+ "-O",
1345
+ "--output-document",
1346
+ "-P",
1347
+ "--directory-prefix",
1348
+ "-U",
1349
+ "--user-agent",
1350
+ "-e",
1351
+ "--execute",
1352
+ "--proxy",
1353
+ "--ca-certificate"
1354
+ ]),
1355
+ nc: /* @__PURE__ */ new Set(["-x", "-p", "-s", "-w", "-W", "-I", "-O"]),
1356
+ ncat: /* @__PURE__ */ new Set(["-x", "-p", "-s", "--proxy", "--proxy-auth", "-w", "--wait"]),
1357
+ netcat: /* @__PURE__ */ new Set(["-x", "-p", "-s", "-w"]),
1358
+ ssh: /* @__PURE__ */ new Set([
1359
+ "-i",
1360
+ "-l",
1361
+ "-p",
1362
+ "-o",
1363
+ "-E",
1364
+ "-F",
1365
+ "-J",
1366
+ "-L",
1367
+ "-R",
1368
+ "-W",
1369
+ "-b",
1370
+ "-c",
1371
+ "-D",
1372
+ "-e",
1373
+ "-I",
1374
+ "-S"
1375
+ ]),
1376
+ scp: /* @__PURE__ */ new Set(["-i", "-o", "-P", "-S"]),
1377
+ rsync: /* @__PURE__ */ new Set(["-e", "--rsh", "--rsync-path", "--password-file", "--log-file"]),
1378
+ socat: /* @__PURE__ */ new Set([])
1379
+ // socat uses address syntax, not flags — no value-flags
1380
+ };
1381
+ function extractPositionalArgs(tokens, binary) {
1382
+ const binaryName = import_path6.default.basename(binary).replace(/\.exe$/i, "");
1383
+ const flagsWithValues = FLAGS_WITH_VALUES[binaryName] ?? /* @__PURE__ */ new Set();
1384
+ const positional = [];
1385
+ let skipNext = false;
1386
+ for (const token of tokens) {
1387
+ if (skipNext) {
1388
+ skipNext = false;
1389
+ continue;
1390
+ }
1391
+ if (token.startsWith("--") && token.includes("=")) continue;
1392
+ if (token.startsWith("-") && token.length === 2 && flagsWithValues.has(token)) {
1393
+ skipNext = true;
1394
+ continue;
1395
+ }
1396
+ if (token.startsWith("--") && flagsWithValues.has(token)) {
1397
+ skipNext = true;
1398
+ continue;
1399
+ }
1400
+ const shortFlag = token.slice(0, 2);
1401
+ if (token.startsWith("-") && token.length > 2 && flagsWithValues.has(shortFlag)) continue;
1402
+ if (token.startsWith("-")) continue;
1403
+ if (token.startsWith("@")) continue;
1404
+ positional.push(token);
1405
+ }
1406
+ return positional;
1407
+ }
1408
+ function extractNetworkTargets(tokens, binary) {
1409
+ return extractPositionalArgs(tokens, binary).map((t) => t.includes("@") ? t.split("@")[1] : t).map((t) => {
1410
+ const colonIdx = t.indexOf(":");
1411
+ if (colonIdx === -1) return t;
1412
+ const afterColon = t.slice(colonIdx + 1);
1413
+ if (/^\d+$/.test(afterColon)) return t.slice(0, colonIdx);
1414
+ return t;
1415
+ }).filter(Boolean);
1416
+ }
1417
+
1418
+ // src/policy/ssh-parser.ts
1419
+ function tokenize(cmd) {
1420
+ const tokens = [];
1421
+ let current = "";
1422
+ let inSingle = false;
1423
+ let inDouble = false;
1424
+ for (const ch of cmd) {
1425
+ if (ch === "'" && !inDouble) {
1426
+ inSingle = !inSingle;
1427
+ } else if (ch === '"' && !inSingle) {
1428
+ inDouble = !inDouble;
1429
+ } else if ((ch === " " || ch === " ") && !inSingle && !inDouble) {
1430
+ if (current) {
1431
+ tokens.push(current);
1432
+ current = "";
1433
+ }
1434
+ } else {
1435
+ current += ch;
1436
+ }
1437
+ }
1438
+ if (current) tokens.push(current);
1439
+ return tokens;
1440
+ }
1441
+ function parseHost(raw) {
1442
+ return raw.split("@").pop().split(":")[0];
1443
+ }
1444
+ function extractAllSshHosts(tokens) {
1445
+ const hosts = /* @__PURE__ */ new Set();
1446
+ for (let i = 0; i < tokens.length; i++) {
1447
+ const t = tokens[i];
1448
+ if (t === "-J" && tokens[i + 1]) {
1449
+ for (const hop of tokens[++i].split(",")) {
1450
+ const h = parseHost(hop);
1451
+ if (h) hosts.add(h);
1452
+ }
1453
+ continue;
1454
+ }
1455
+ if (t === "-o" && tokens[i + 1]?.toLowerCase().startsWith("proxyjump=")) {
1456
+ const val = tokens[++i].split("=").slice(1).join("=");
1457
+ for (const hop of val.split(",")) {
1458
+ const h = parseHost(hop);
1459
+ if (h) hosts.add(h);
1460
+ }
1461
+ continue;
1462
+ }
1463
+ if (t === "-o" && tokens[i + 1]?.toLowerCase().startsWith("proxycommand=")) {
1464
+ const raw = tokens[++i].split("=").slice(1).join("=").replace(/^['"]|['"]$/g, "");
1465
+ const subTokens = tokenize(raw);
1466
+ const binary = subTokens[0] ?? "";
1467
+ extractNetworkTargets(subTokens.slice(1), binary).forEach((h) => hosts.add(h));
1468
+ extractAllSshHosts(subTokens.slice(1)).forEach((h) => hosts.add(h));
1469
+ continue;
1470
+ }
1471
+ if (!t.startsWith("-")) {
1472
+ const h = parseHost(t);
1473
+ if (h) hosts.add(h);
1474
+ }
1475
+ }
1476
+ return [...hosts].filter(Boolean);
1477
+ }
1478
+
1479
+ // src/auth/trusted-hosts.ts
1480
+ var import_fs6 = __toESM(require("fs"));
1481
+ var import_path7 = __toESM(require("path"));
1482
+ var import_os5 = __toESM(require("os"));
1483
+ function getTrustedHostsPath() {
1484
+ return import_path7.default.join(import_os5.default.homedir(), ".node9", "trusted-hosts.json");
1485
+ }
1486
+ function readTrustedHosts() {
1487
+ try {
1488
+ const raw = import_fs6.default.readFileSync(getTrustedHostsPath(), "utf8");
1489
+ const parsed = JSON.parse(raw);
1490
+ return Array.isArray(parsed.hosts) ? parsed.hosts : [];
1491
+ } catch {
1492
+ return [];
1493
+ }
1494
+ }
1495
+ function normalizeHost(raw) {
1496
+ return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
1497
+ }
1498
+ function isTrustedHost(host) {
1499
+ const normalized = normalizeHost(host);
1500
+ return readTrustedHosts().some((entry) => {
1501
+ const entryHost = entry.host.toLowerCase();
1502
+ if (entryHost.startsWith("*.")) {
1503
+ const domain = entryHost.slice(2);
1504
+ return normalized === domain || normalized.endsWith("." + domain);
1505
+ }
1506
+ return normalized === entryHost;
1507
+ });
1508
+ }
1509
+
1070
1510
  // src/policy/index.ts
1071
- function tokenize(toolName) {
1511
+ function tokenize2(toolName) {
1072
1512
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
1073
1513
  }
1074
1514
  function matchesPattern(text, patterns) {
@@ -1081,9 +1521,9 @@ function matchesPattern(text, patterns) {
1081
1521
  const withoutDotSlash = text.replace(/^\.\//, "");
1082
1522
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1083
1523
  }
1084
- function getNestedValue(obj, path10) {
1524
+ function getNestedValue(obj, path14) {
1085
1525
  if (!obj || typeof obj !== "object") return null;
1086
- return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
1526
+ return path14.split(".").reduce((prev, curr) => prev?.[curr], obj);
1087
1527
  }
1088
1528
  function evaluateSmartConditions(args, rule) {
1089
1529
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -1207,7 +1647,7 @@ async function analyzeShellCommand(command) {
1207
1647
  }
1208
1648
  return { actions, paths, allTokens };
1209
1649
  }
1210
- async function evaluatePolicy(toolName, args, agent) {
1650
+ async function evaluatePolicy(toolName, args, agent, cwd) {
1211
1651
  const config = getConfig();
1212
1652
  if (matchesPattern(toolName, config.policy.ignoredTools)) return { decision: "allow" };
1213
1653
  if (config.policy.smartRules.length > 0) {
@@ -1237,11 +1677,66 @@ async function evaluatePolicy(toolName, args, agent) {
1237
1677
  if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
1238
1678
  return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
1239
1679
  }
1680
+ const pipeAnalysis = analyzePipeChain(shellCommand);
1681
+ if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
1682
+ const sinks = pipeAnalysis.sinkTargets;
1683
+ const allTrusted = sinks.length > 0 && sinks.every(isTrustedHost);
1684
+ if (pipeAnalysis.risk === "critical") {
1685
+ if (allTrusted) {
1686
+ return {
1687
+ decision: "review",
1688
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
1689
+ reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
1690
+ tier: 3
1691
+ };
1692
+ }
1693
+ return {
1694
+ decision: "block",
1695
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1696
+ reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1697
+ tier: 3
1698
+ };
1699
+ }
1700
+ if (allTrusted) {
1701
+ return { decision: "allow" };
1702
+ }
1703
+ return {
1704
+ decision: "review",
1705
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1706
+ reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1707
+ tier: 3
1708
+ };
1709
+ }
1710
+ const firstToken = analyzed.actions[0] ?? "";
1711
+ if (["ssh", "scp", "rsync"].includes(firstToken)) {
1712
+ const rawTokens = shellCommand.trim().split(/\s+/);
1713
+ const sshHosts = extractAllSshHosts(rawTokens.slice(1));
1714
+ allTokens.push(...sshHosts);
1715
+ }
1716
+ if (firstToken && import_path8.default.posix.isAbsolute(firstToken)) {
1717
+ const prov = checkProvenance(firstToken, cwd);
1718
+ if (prov.trustLevel === "suspect") {
1719
+ return {
1720
+ decision: config.settings.mode === "strict" ? "block" : "review",
1721
+ blockedByLabel: "Node9: Suspect Binary",
1722
+ reason: `Binary "${firstToken}" resolved to ${prov.resolvedPath} \u2014 ${prov.reason}`,
1723
+ tier: 3
1724
+ };
1725
+ }
1726
+ if (prov.trustLevel === "unknown" && config.settings.mode === "strict") {
1727
+ return {
1728
+ decision: "review",
1729
+ blockedByLabel: "Node9: Unknown Binary (strict mode)",
1730
+ reason: `Binary "${firstToken}" \u2014 ${prov.reason}`,
1731
+ tier: 3
1732
+ };
1733
+ }
1734
+ }
1240
1735
  if (isSqlTool(toolName, config.policy.toolInspection)) {
1241
1736
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
1242
1737
  }
1243
1738
  } else {
1244
- allTokens = tokenize(toolName);
1739
+ allTokens = tokenize2(toolName);
1245
1740
  if (args && typeof args === "object") {
1246
1741
  const flattenedArgs = JSON.stringify(args).toLowerCase();
1247
1742
  const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
@@ -1319,18 +1814,18 @@ function isIgnoredTool(toolName) {
1319
1814
  }
1320
1815
 
1321
1816
  // src/auth/state.ts
1322
- var import_fs5 = __toESM(require("fs"));
1323
- var import_path5 = __toESM(require("path"));
1324
- var import_os4 = __toESM(require("os"));
1325
- var PAUSED_FILE = import_path5.default.join(import_os4.default.homedir(), ".node9", "PAUSED");
1326
- var TRUST_FILE = import_path5.default.join(import_os4.default.homedir(), ".node9", "trust.json");
1817
+ var import_fs7 = __toESM(require("fs"));
1818
+ var import_path9 = __toESM(require("path"));
1819
+ var import_os6 = __toESM(require("os"));
1820
+ var PAUSED_FILE = import_path9.default.join(import_os6.default.homedir(), ".node9", "PAUSED");
1821
+ var TRUST_FILE = import_path9.default.join(import_os6.default.homedir(), ".node9", "trust.json");
1327
1822
  function checkPause() {
1328
1823
  try {
1329
- if (!import_fs5.default.existsSync(PAUSED_FILE)) return { paused: false };
1330
- const state = JSON.parse(import_fs5.default.readFileSync(PAUSED_FILE, "utf-8"));
1824
+ if (!import_fs7.default.existsSync(PAUSED_FILE)) return { paused: false };
1825
+ const state = JSON.parse(import_fs7.default.readFileSync(PAUSED_FILE, "utf-8"));
1331
1826
  if (state.expiry > 0 && Date.now() >= state.expiry) {
1332
1827
  try {
1333
- import_fs5.default.unlinkSync(PAUSED_FILE);
1828
+ import_fs7.default.unlinkSync(PAUSED_FILE);
1334
1829
  } catch {
1335
1830
  }
1336
1831
  return { paused: false };
@@ -1341,20 +1836,20 @@ function checkPause() {
1341
1836
  }
1342
1837
  }
1343
1838
  function atomicWriteSync(filePath, data, options) {
1344
- const dir = import_path5.default.dirname(filePath);
1345
- if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
1346
- const tmpPath = `${filePath}.${import_os4.default.hostname()}.${process.pid}.tmp`;
1347
- import_fs5.default.writeFileSync(tmpPath, data, options);
1348
- import_fs5.default.renameSync(tmpPath, filePath);
1839
+ const dir = import_path9.default.dirname(filePath);
1840
+ if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
1841
+ const tmpPath = `${filePath}.${import_os6.default.hostname()}.${process.pid}.tmp`;
1842
+ import_fs7.default.writeFileSync(tmpPath, data, options);
1843
+ import_fs7.default.renameSync(tmpPath, filePath);
1349
1844
  }
1350
1845
  function getActiveTrustSession(toolName) {
1351
1846
  try {
1352
- if (!import_fs5.default.existsSync(TRUST_FILE)) return false;
1353
- const trust = JSON.parse(import_fs5.default.readFileSync(TRUST_FILE, "utf-8"));
1847
+ if (!import_fs7.default.existsSync(TRUST_FILE)) return false;
1848
+ const trust = JSON.parse(import_fs7.default.readFileSync(TRUST_FILE, "utf-8"));
1354
1849
  const now = Date.now();
1355
1850
  const active = trust.entries.filter((e) => e.expiry > now);
1356
1851
  if (active.length !== trust.entries.length) {
1357
- import_fs5.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
1852
+ import_fs7.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
1358
1853
  }
1359
1854
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
1360
1855
  } catch {
@@ -1365,8 +1860,8 @@ function writeTrustSession(toolName, durationMs) {
1365
1860
  try {
1366
1861
  let trust = { entries: [] };
1367
1862
  try {
1368
- if (import_fs5.default.existsSync(TRUST_FILE)) {
1369
- trust = JSON.parse(import_fs5.default.readFileSync(TRUST_FILE, "utf-8"));
1863
+ if (import_fs7.default.existsSync(TRUST_FILE)) {
1864
+ trust = JSON.parse(import_fs7.default.readFileSync(TRUST_FILE, "utf-8"));
1370
1865
  }
1371
1866
  } catch {
1372
1867
  }
@@ -1382,9 +1877,9 @@ function writeTrustSession(toolName, durationMs) {
1382
1877
  }
1383
1878
  function getPersistentDecision(toolName) {
1384
1879
  try {
1385
- const file = import_path5.default.join(import_os4.default.homedir(), ".node9", "decisions.json");
1386
- if (!import_fs5.default.existsSync(file)) return null;
1387
- const decisions = JSON.parse(import_fs5.default.readFileSync(file, "utf-8"));
1880
+ const file = import_path9.default.join(import_os6.default.homedir(), ".node9", "decisions.json");
1881
+ if (!import_fs7.default.existsSync(file)) return null;
1882
+ const decisions = JSON.parse(import_fs7.default.readFileSync(file, "utf-8"));
1388
1883
  const d = decisions[toolName];
1389
1884
  if (d === "allow" || d === "deny") return d;
1390
1885
  } catch {
@@ -1393,17 +1888,17 @@ function getPersistentDecision(toolName) {
1393
1888
  }
1394
1889
 
1395
1890
  // src/auth/daemon.ts
1396
- var import_fs6 = __toESM(require("fs"));
1397
- var import_path6 = __toESM(require("path"));
1398
- var import_os5 = __toESM(require("os"));
1891
+ var import_fs8 = __toESM(require("fs"));
1892
+ var import_path10 = __toESM(require("path"));
1893
+ var import_os7 = __toESM(require("os"));
1399
1894
  var import_child_process = require("child_process");
1400
1895
  var DAEMON_PORT = 7391;
1401
1896
  var DAEMON_HOST = "127.0.0.1";
1402
1897
  function getInternalToken() {
1403
1898
  try {
1404
- const pidFile = import_path6.default.join(import_os5.default.homedir(), ".node9", "daemon.pid");
1405
- if (!import_fs6.default.existsSync(pidFile)) return null;
1406
- const data = JSON.parse(import_fs6.default.readFileSync(pidFile, "utf-8"));
1899
+ const pidFile = import_path10.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
1900
+ if (!import_fs8.default.existsSync(pidFile)) return null;
1901
+ const data = JSON.parse(import_fs8.default.readFileSync(pidFile, "utf-8"));
1407
1902
  process.kill(data.pid, 0);
1408
1903
  return data.internalToken ?? null;
1409
1904
  } catch {
@@ -1411,10 +1906,10 @@ function getInternalToken() {
1411
1906
  }
1412
1907
  }
1413
1908
  function isDaemonRunning() {
1414
- const pidFile = import_path6.default.join(import_os5.default.homedir(), ".node9", "daemon.pid");
1415
- if (import_fs6.default.existsSync(pidFile)) {
1909
+ const pidFile = import_path10.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
1910
+ if (import_fs8.default.existsSync(pidFile)) {
1416
1911
  try {
1417
- const { pid, port } = JSON.parse(import_fs6.default.readFileSync(pidFile, "utf-8"));
1912
+ const { pid, port } = JSON.parse(import_fs8.default.readFileSync(pidFile, "utf-8"));
1418
1913
  if (port !== DAEMON_PORT) return false;
1419
1914
  process.kill(pid, 0);
1420
1915
  return true;
@@ -1510,16 +2005,16 @@ async function resolveViaDaemon(id, decision, internalToken) {
1510
2005
 
1511
2006
  // src/auth/orchestrator.ts
1512
2007
  var import_net = __toESM(require("net"));
1513
- var import_path9 = __toESM(require("path"));
1514
- var import_os7 = __toESM(require("os"));
2008
+ var import_path13 = __toESM(require("path"));
2009
+ var import_os9 = __toESM(require("os"));
1515
2010
  var import_crypto = require("crypto");
1516
2011
 
1517
2012
  // src/ui/native.ts
1518
2013
  var import_child_process2 = require("child_process");
1519
- var import_path8 = __toESM(require("path"));
2014
+ var import_path12 = __toESM(require("path"));
1520
2015
 
1521
2016
  // src/context-sniper.ts
1522
- var import_path7 = __toESM(require("path"));
2017
+ var import_path11 = __toESM(require("path"));
1523
2018
  function smartTruncate(str, maxLen = 500) {
1524
2019
  if (str.length <= maxLen) return str;
1525
2020
  const edge = Math.floor(maxLen / 2) - 3;
@@ -1587,7 +2082,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
1587
2082
  intent = "EDIT";
1588
2083
  if (obj.file_path) {
1589
2084
  editFilePath = String(obj.file_path);
1590
- editFileName = import_path7.default.basename(editFilePath);
2085
+ editFileName = import_path11.default.basename(editFilePath);
1591
2086
  }
1592
2087
  const result = extractContext(String(obj.new_string), matchedWord);
1593
2088
  contextSnippet = result.snippet;
@@ -1642,7 +2137,7 @@ function formatArgs(args, matchedField, matchedWord) {
1642
2137
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
1643
2138
  const obj = parsed;
1644
2139
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
1645
- const file = obj.file_path ? import_path8.default.basename(String(obj.file_path)) : "file";
2140
+ const file = obj.file_path ? import_path12.default.basename(String(obj.file_path)) : "file";
1646
2141
  const oldPreview = smartTruncate(String(obj.old_string), 120);
1647
2142
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
1648
2143
  return {
@@ -1817,8 +2312,8 @@ end run`;
1817
2312
  }
1818
2313
 
1819
2314
  // src/auth/cloud.ts
1820
- var import_fs7 = __toESM(require("fs"));
1821
- var import_os6 = __toESM(require("os"));
2315
+ var import_fs9 = __toESM(require("fs"));
2316
+ var import_os8 = __toESM(require("os"));
1822
2317
  function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1823
2318
  return fetch(`${creds.apiUrl}/audit`, {
1824
2319
  method: "POST",
@@ -1830,9 +2325,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1830
2325
  context: {
1831
2326
  agent: meta?.agent,
1832
2327
  mcpServer: meta?.mcpServer,
1833
- hostname: import_os6.default.hostname(),
2328
+ hostname: import_os8.default.hostname(),
1834
2329
  cwd: process.cwd(),
1835
- platform: import_os6.default.platform()
2330
+ platform: import_os8.default.platform()
1836
2331
  }
1837
2332
  }),
1838
2333
  signal: AbortSignal.timeout(5e3)
@@ -1853,9 +2348,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
1853
2348
  context: {
1854
2349
  agent: meta?.agent,
1855
2350
  mcpServer: meta?.mcpServer,
1856
- hostname: import_os6.default.hostname(),
2351
+ hostname: import_os8.default.hostname(),
1857
2352
  cwd: process.cwd(),
1858
- platform: import_os6.default.platform()
2353
+ platform: import_os8.default.platform()
1859
2354
  },
1860
2355
  ...riskMetadata && { riskMetadata }
1861
2356
  }),
@@ -1911,14 +2406,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
1911
2406
  });
1912
2407
  clearTimeout(timer);
1913
2408
  if (!res.ok) {
1914
- import_fs7.default.appendFileSync(
2409
+ import_fs9.default.appendFileSync(
1915
2410
  HOOK_DEBUG_LOG,
1916
2411
  `[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
1917
2412
  `
1918
2413
  );
1919
2414
  }
1920
2415
  } catch (err) {
1921
- import_fs7.default.appendFileSync(
2416
+ import_fs9.default.appendFileSync(
1922
2417
  HOOK_DEBUG_LOG,
1923
2418
  `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
1924
2419
  `
@@ -1927,7 +2422,7 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
1927
2422
  }
1928
2423
 
1929
2424
  // src/auth/orchestrator.ts
1930
- var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path9.default.join(import_os7.default.tmpdir(), "node9-activity.sock");
2425
+ var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path13.default.join(import_os9.default.tmpdir(), "node9-activity.sock");
1931
2426
  function notifyActivity(data) {
1932
2427
  return new Promise((resolve) => {
1933
2428
  try {
@@ -2009,7 +2504,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2009
2504
  }
2010
2505
  if (config.settings.mode === "audit") {
2011
2506
  if (!isIgnoredTool(toolName)) {
2012
- const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
2507
+ const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
2013
2508
  if (policyResult.decision === "review") {
2014
2509
  appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
2015
2510
  if (approvers.cloud && creds?.apiKey) {