@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.mjs CHANGED
@@ -155,8 +155,8 @@ function sanitizeConfig(raw) {
155
155
  }
156
156
  }
157
157
  const lines = result.error.issues.map((issue) => {
158
- const path10 = issue.path.length > 0 ? issue.path.join(".") : "root";
159
- return ` \u2022 ${path10}: ${issue.message}`;
158
+ const path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
159
+ return ` \u2022 ${path14}: ${issue.message}`;
160
160
  });
161
161
  return {
162
162
  sanitized,
@@ -886,6 +886,7 @@ function getCompiledRegex(pattern, flags = "") {
886
886
  }
887
887
 
888
888
  // src/policy/index.ts
889
+ import path8 from "path";
889
890
  import pm from "picomatch";
890
891
  import { parse } from "sh-syntax";
891
892
 
@@ -1031,8 +1032,447 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
1031
1032
  return null;
1032
1033
  }
1033
1034
 
1035
+ // src/utils/provenance.ts
1036
+ import fs5 from "fs";
1037
+ import path5 from "path";
1038
+ import os4 from "os";
1039
+ var SYSTEM_PREFIXES = ["/usr/bin", "/usr/sbin", "/bin", "/sbin"];
1040
+ var MANAGED_PREFIXES = ["/usr/local/bin", "/opt/homebrew", "/home/linuxbrew", "/nix/store"];
1041
+ var USER_PREFIXES = [
1042
+ path5.join(os4.homedir(), "bin"),
1043
+ path5.join(os4.homedir(), ".local", "bin"),
1044
+ path5.join(os4.homedir(), ".cargo", "bin"),
1045
+ path5.join(os4.homedir(), ".npm-global", "bin"),
1046
+ path5.join(os4.homedir(), ".volta", "bin")
1047
+ ];
1048
+ var SUSPECT_PREFIXES = ["/tmp", "/var/tmp", "/dev/shm"];
1049
+ function findInPath(cmd) {
1050
+ if (path5.posix.isAbsolute(cmd)) return cmd;
1051
+ const pathEnv = process.env.PATH ?? "";
1052
+ for (const dir of pathEnv.split(path5.delimiter)) {
1053
+ if (!dir) continue;
1054
+ const full = path5.join(dir, cmd);
1055
+ try {
1056
+ fs5.accessSync(full, fs5.constants.X_OK);
1057
+ return full;
1058
+ } catch {
1059
+ }
1060
+ }
1061
+ return null;
1062
+ }
1063
+ function _classifyPath(resolved, cwd) {
1064
+ if (cwd && resolved.startsWith(cwd + "/")) {
1065
+ return { trustLevel: "user", reason: "binary in project directory" };
1066
+ }
1067
+ const osTmp = os4.tmpdir();
1068
+ const allSuspect = osTmp ? [...SUSPECT_PREFIXES, osTmp] : SUSPECT_PREFIXES;
1069
+ if (allSuspect.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
1070
+ return { trustLevel: "suspect", reason: `binary in temp directory: ${resolved}` };
1071
+ }
1072
+ if (SYSTEM_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
1073
+ return { trustLevel: "system", reason: "" };
1074
+ }
1075
+ if (MANAGED_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
1076
+ return { trustLevel: "managed", reason: "" };
1077
+ }
1078
+ if (USER_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
1079
+ return { trustLevel: "user", reason: "" };
1080
+ }
1081
+ return { trustLevel: "unknown", reason: "binary in unrecognized location" };
1082
+ }
1083
+ function checkProvenance(cmd, cwd) {
1084
+ const bare = cmd.startsWith("./") ? cmd.slice(2) : cmd;
1085
+ if (path5.posix.isAbsolute(bare)) {
1086
+ const early = _classifyPath(bare, cwd);
1087
+ if (early.trustLevel === "suspect") {
1088
+ return { resolvedPath: bare, ...early };
1089
+ }
1090
+ }
1091
+ let resolved;
1092
+ try {
1093
+ const found = findInPath(bare);
1094
+ if (!found) {
1095
+ return {
1096
+ resolvedPath: cmd,
1097
+ trustLevel: "unknown",
1098
+ reason: "binary not found in PATH"
1099
+ };
1100
+ }
1101
+ resolved = fs5.realpathSync(found);
1102
+ } catch {
1103
+ return {
1104
+ resolvedPath: cmd,
1105
+ trustLevel: "unknown",
1106
+ reason: "binary not found in PATH"
1107
+ };
1108
+ }
1109
+ try {
1110
+ const stat = fs5.statSync(resolved);
1111
+ if (stat.mode & 2) {
1112
+ return {
1113
+ resolvedPath: resolved,
1114
+ trustLevel: "suspect",
1115
+ reason: "binary is world-writable"
1116
+ };
1117
+ }
1118
+ } catch {
1119
+ return {
1120
+ resolvedPath: resolved,
1121
+ trustLevel: "unknown",
1122
+ reason: "could not stat binary"
1123
+ };
1124
+ }
1125
+ const classify = _classifyPath(resolved, cwd);
1126
+ return { resolvedPath: resolved, ...classify };
1127
+ }
1128
+
1129
+ // src/policy/pipe-chain.ts
1130
+ var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
1131
+ "cat",
1132
+ "head",
1133
+ "tail",
1134
+ "grep",
1135
+ "awk",
1136
+ "sed",
1137
+ "cut",
1138
+ "sort",
1139
+ "tee",
1140
+ "less",
1141
+ "more",
1142
+ "strings",
1143
+ "xxd"
1144
+ ]);
1145
+ var SINK_COMMANDS = /* @__PURE__ */ new Set([
1146
+ "curl",
1147
+ "wget",
1148
+ "nc",
1149
+ "ncat",
1150
+ "netcat",
1151
+ "ssh",
1152
+ "scp",
1153
+ "rsync",
1154
+ "socat",
1155
+ "ftp",
1156
+ "sftp",
1157
+ "telnet"
1158
+ ]);
1159
+ var OBFUSCATORS = /* @__PURE__ */ new Set([
1160
+ "base64",
1161
+ "gzip",
1162
+ "gunzip",
1163
+ "bzip2",
1164
+ "xz",
1165
+ "zstd",
1166
+ "openssl",
1167
+ "gpg",
1168
+ "python",
1169
+ "python3",
1170
+ "perl",
1171
+ "ruby",
1172
+ "node"
1173
+ ]);
1174
+ var SENSITIVE_PATTERNS = [
1175
+ /(?:^|\/)\.env(?:\.|$)/i,
1176
+ // .env, .env.local, .env.production
1177
+ /id_rsa|id_ed25519|id_ecdsa|id_dsa/i,
1178
+ // SSH private keys
1179
+ /\.pem$|\.key$|\.p12$|\.pfx$/i,
1180
+ // certificate files
1181
+ /(?:^|\/)\.ssh\//i,
1182
+ // ~/.ssh/ directory
1183
+ /(?:^|\/)\.aws\/credentials/i,
1184
+ // AWS credentials
1185
+ /(?:^|\/)\.netrc$/i,
1186
+ // netrc (stores HTTP credentials)
1187
+ /(?:^|\/)(passwd|shadow|sudoers)$/i,
1188
+ // /etc/passwd, /etc/shadow
1189
+ /(?:^|\/)credentials(?:\.json)?$/i
1190
+ // generic credentials files
1191
+ ];
1192
+ function isSensitivePath(p) {
1193
+ return SENSITIVE_PATTERNS.some((re) => re.test(p));
1194
+ }
1195
+ function splitOnPipe(cmd) {
1196
+ const segments = [];
1197
+ let current = "";
1198
+ let inSingle = false;
1199
+ let inDouble = false;
1200
+ for (let i = 0; i < cmd.length; i++) {
1201
+ const ch = cmd[i];
1202
+ if (ch === "'" && !inDouble) {
1203
+ inSingle = !inSingle;
1204
+ current += ch;
1205
+ } else if (ch === '"' && !inSingle) {
1206
+ inDouble = !inDouble;
1207
+ current += ch;
1208
+ } else if (ch === "|" && !inSingle && !inDouble && cmd[i + 1] !== "|" && (i === 0 || cmd[i - 1] !== "|")) {
1209
+ segments.push(current.trim());
1210
+ current = "";
1211
+ } else {
1212
+ current += ch;
1213
+ }
1214
+ }
1215
+ if (current.trim()) segments.push(current.trim());
1216
+ return segments.filter(Boolean);
1217
+ }
1218
+ function positionalTokens(segment) {
1219
+ return segment.split(/\s+/).slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
1220
+ }
1221
+ function analyzePipeChain(command) {
1222
+ const segments = splitOnPipe(command);
1223
+ if (segments.length < 2) {
1224
+ return {
1225
+ isPipeline: false,
1226
+ hasSensitiveSource: false,
1227
+ hasExternalSink: false,
1228
+ hasObfuscation: false,
1229
+ sourceFiles: [],
1230
+ sinkTargets: [],
1231
+ risk: "none"
1232
+ };
1233
+ }
1234
+ const sourceFiles = [];
1235
+ const sinkTargets = [];
1236
+ let hasSensitiveSource = false;
1237
+ let hasExternalSink = false;
1238
+ let hasObfuscation = false;
1239
+ for (const segment of segments) {
1240
+ const tokens = segment.split(/\s+/).filter(Boolean);
1241
+ if (tokens.length === 0) continue;
1242
+ const binary = tokens[0].toLowerCase();
1243
+ const args = positionalTokens(segment);
1244
+ if (SOURCE_COMMANDS.has(binary)) {
1245
+ sourceFiles.push(...args);
1246
+ if (args.some(isSensitivePath)) hasSensitiveSource = true;
1247
+ }
1248
+ if (OBFUSCATORS.has(binary)) hasObfuscation = true;
1249
+ if (SINK_COMMANDS.has(binary)) {
1250
+ const targets = args.filter(
1251
+ (a) => a.includes(".") || a.includes("://") || /^\d+\.\d+/.test(a)
1252
+ );
1253
+ sinkTargets.push(...targets);
1254
+ if (targets.length > 0) hasExternalSink = true;
1255
+ }
1256
+ }
1257
+ const fullCmd = command.toLowerCase();
1258
+ if (!hasSensitiveSource) {
1259
+ const redirMatch = fullCmd.match(/<\s*(\S+)/);
1260
+ if (redirMatch && isSensitivePath(redirMatch[1])) {
1261
+ hasSensitiveSource = true;
1262
+ sourceFiles.push(redirMatch[1]);
1263
+ }
1264
+ }
1265
+ const risk = hasSensitiveSource && hasExternalSink && hasObfuscation ? "critical" : hasSensitiveSource && hasExternalSink ? "high" : hasExternalSink ? "medium" : "none";
1266
+ return {
1267
+ isPipeline: true,
1268
+ hasSensitiveSource,
1269
+ hasExternalSink,
1270
+ hasObfuscation,
1271
+ sourceFiles,
1272
+ sinkTargets,
1273
+ risk
1274
+ };
1275
+ }
1276
+
1277
+ // src/policy/flag-tables.ts
1278
+ import path6 from "path";
1279
+ var FLAGS_WITH_VALUES = {
1280
+ curl: /* @__PURE__ */ new Set([
1281
+ "-H",
1282
+ "--header",
1283
+ "-A",
1284
+ "--user-agent",
1285
+ "-e",
1286
+ "--referer",
1287
+ "-x",
1288
+ "--proxy",
1289
+ "-u",
1290
+ "--user",
1291
+ "-d",
1292
+ "--data",
1293
+ "--data-raw",
1294
+ "--data-binary",
1295
+ "-o",
1296
+ "--output",
1297
+ "-F",
1298
+ "--form",
1299
+ "--connect-to",
1300
+ "--resolve",
1301
+ "--cacert",
1302
+ "--cert",
1303
+ "--key",
1304
+ "-m",
1305
+ "--max-time"
1306
+ ]),
1307
+ wget: /* @__PURE__ */ new Set([
1308
+ "-O",
1309
+ "--output-document",
1310
+ "-P",
1311
+ "--directory-prefix",
1312
+ "-U",
1313
+ "--user-agent",
1314
+ "-e",
1315
+ "--execute",
1316
+ "--proxy",
1317
+ "--ca-certificate"
1318
+ ]),
1319
+ nc: /* @__PURE__ */ new Set(["-x", "-p", "-s", "-w", "-W", "-I", "-O"]),
1320
+ ncat: /* @__PURE__ */ new Set(["-x", "-p", "-s", "--proxy", "--proxy-auth", "-w", "--wait"]),
1321
+ netcat: /* @__PURE__ */ new Set(["-x", "-p", "-s", "-w"]),
1322
+ ssh: /* @__PURE__ */ new Set([
1323
+ "-i",
1324
+ "-l",
1325
+ "-p",
1326
+ "-o",
1327
+ "-E",
1328
+ "-F",
1329
+ "-J",
1330
+ "-L",
1331
+ "-R",
1332
+ "-W",
1333
+ "-b",
1334
+ "-c",
1335
+ "-D",
1336
+ "-e",
1337
+ "-I",
1338
+ "-S"
1339
+ ]),
1340
+ scp: /* @__PURE__ */ new Set(["-i", "-o", "-P", "-S"]),
1341
+ rsync: /* @__PURE__ */ new Set(["-e", "--rsh", "--rsync-path", "--password-file", "--log-file"]),
1342
+ socat: /* @__PURE__ */ new Set([])
1343
+ // socat uses address syntax, not flags — no value-flags
1344
+ };
1345
+ function extractPositionalArgs(tokens, binary) {
1346
+ const binaryName = path6.basename(binary).replace(/\.exe$/i, "");
1347
+ const flagsWithValues = FLAGS_WITH_VALUES[binaryName] ?? /* @__PURE__ */ new Set();
1348
+ const positional = [];
1349
+ let skipNext = false;
1350
+ for (const token of tokens) {
1351
+ if (skipNext) {
1352
+ skipNext = false;
1353
+ continue;
1354
+ }
1355
+ if (token.startsWith("--") && token.includes("=")) continue;
1356
+ if (token.startsWith("-") && token.length === 2 && flagsWithValues.has(token)) {
1357
+ skipNext = true;
1358
+ continue;
1359
+ }
1360
+ if (token.startsWith("--") && flagsWithValues.has(token)) {
1361
+ skipNext = true;
1362
+ continue;
1363
+ }
1364
+ const shortFlag = token.slice(0, 2);
1365
+ if (token.startsWith("-") && token.length > 2 && flagsWithValues.has(shortFlag)) continue;
1366
+ if (token.startsWith("-")) continue;
1367
+ if (token.startsWith("@")) continue;
1368
+ positional.push(token);
1369
+ }
1370
+ return positional;
1371
+ }
1372
+ function extractNetworkTargets(tokens, binary) {
1373
+ return extractPositionalArgs(tokens, binary).map((t) => t.includes("@") ? t.split("@")[1] : t).map((t) => {
1374
+ const colonIdx = t.indexOf(":");
1375
+ if (colonIdx === -1) return t;
1376
+ const afterColon = t.slice(colonIdx + 1);
1377
+ if (/^\d+$/.test(afterColon)) return t.slice(0, colonIdx);
1378
+ return t;
1379
+ }).filter(Boolean);
1380
+ }
1381
+
1382
+ // src/policy/ssh-parser.ts
1383
+ function tokenize(cmd) {
1384
+ const tokens = [];
1385
+ let current = "";
1386
+ let inSingle = false;
1387
+ let inDouble = false;
1388
+ for (const ch of cmd) {
1389
+ if (ch === "'" && !inDouble) {
1390
+ inSingle = !inSingle;
1391
+ } else if (ch === '"' && !inSingle) {
1392
+ inDouble = !inDouble;
1393
+ } else if ((ch === " " || ch === " ") && !inSingle && !inDouble) {
1394
+ if (current) {
1395
+ tokens.push(current);
1396
+ current = "";
1397
+ }
1398
+ } else {
1399
+ current += ch;
1400
+ }
1401
+ }
1402
+ if (current) tokens.push(current);
1403
+ return tokens;
1404
+ }
1405
+ function parseHost(raw) {
1406
+ return raw.split("@").pop().split(":")[0];
1407
+ }
1408
+ function extractAllSshHosts(tokens) {
1409
+ const hosts = /* @__PURE__ */ new Set();
1410
+ for (let i = 0; i < tokens.length; i++) {
1411
+ const t = tokens[i];
1412
+ if (t === "-J" && tokens[i + 1]) {
1413
+ for (const hop of tokens[++i].split(",")) {
1414
+ const h = parseHost(hop);
1415
+ if (h) hosts.add(h);
1416
+ }
1417
+ continue;
1418
+ }
1419
+ if (t === "-o" && tokens[i + 1]?.toLowerCase().startsWith("proxyjump=")) {
1420
+ const val = tokens[++i].split("=").slice(1).join("=");
1421
+ for (const hop of val.split(",")) {
1422
+ const h = parseHost(hop);
1423
+ if (h) hosts.add(h);
1424
+ }
1425
+ continue;
1426
+ }
1427
+ if (t === "-o" && tokens[i + 1]?.toLowerCase().startsWith("proxycommand=")) {
1428
+ const raw = tokens[++i].split("=").slice(1).join("=").replace(/^['"]|['"]$/g, "");
1429
+ const subTokens = tokenize(raw);
1430
+ const binary = subTokens[0] ?? "";
1431
+ extractNetworkTargets(subTokens.slice(1), binary).forEach((h) => hosts.add(h));
1432
+ extractAllSshHosts(subTokens.slice(1)).forEach((h) => hosts.add(h));
1433
+ continue;
1434
+ }
1435
+ if (!t.startsWith("-")) {
1436
+ const h = parseHost(t);
1437
+ if (h) hosts.add(h);
1438
+ }
1439
+ }
1440
+ return [...hosts].filter(Boolean);
1441
+ }
1442
+
1443
+ // src/auth/trusted-hosts.ts
1444
+ import fs6 from "fs";
1445
+ import path7 from "path";
1446
+ import os5 from "os";
1447
+ function getTrustedHostsPath() {
1448
+ return path7.join(os5.homedir(), ".node9", "trusted-hosts.json");
1449
+ }
1450
+ function readTrustedHosts() {
1451
+ try {
1452
+ const raw = fs6.readFileSync(getTrustedHostsPath(), "utf8");
1453
+ const parsed = JSON.parse(raw);
1454
+ return Array.isArray(parsed.hosts) ? parsed.hosts : [];
1455
+ } catch {
1456
+ return [];
1457
+ }
1458
+ }
1459
+ function normalizeHost(raw) {
1460
+ return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
1461
+ }
1462
+ function isTrustedHost(host) {
1463
+ const normalized = normalizeHost(host);
1464
+ return readTrustedHosts().some((entry) => {
1465
+ const entryHost = entry.host.toLowerCase();
1466
+ if (entryHost.startsWith("*.")) {
1467
+ const domain = entryHost.slice(2);
1468
+ return normalized === domain || normalized.endsWith("." + domain);
1469
+ }
1470
+ return normalized === entryHost;
1471
+ });
1472
+ }
1473
+
1034
1474
  // src/policy/index.ts
1035
- function tokenize(toolName) {
1475
+ function tokenize2(toolName) {
1036
1476
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
1037
1477
  }
1038
1478
  function matchesPattern(text, patterns) {
@@ -1045,9 +1485,9 @@ function matchesPattern(text, patterns) {
1045
1485
  const withoutDotSlash = text.replace(/^\.\//, "");
1046
1486
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1047
1487
  }
1048
- function getNestedValue(obj, path10) {
1488
+ function getNestedValue(obj, path14) {
1049
1489
  if (!obj || typeof obj !== "object") return null;
1050
- return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
1490
+ return path14.split(".").reduce((prev, curr) => prev?.[curr], obj);
1051
1491
  }
1052
1492
  function evaluateSmartConditions(args, rule) {
1053
1493
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -1171,7 +1611,7 @@ async function analyzeShellCommand(command) {
1171
1611
  }
1172
1612
  return { actions, paths, allTokens };
1173
1613
  }
1174
- async function evaluatePolicy(toolName, args, agent) {
1614
+ async function evaluatePolicy(toolName, args, agent, cwd) {
1175
1615
  const config = getConfig();
1176
1616
  if (matchesPattern(toolName, config.policy.ignoredTools)) return { decision: "allow" };
1177
1617
  if (config.policy.smartRules.length > 0) {
@@ -1201,11 +1641,66 @@ async function evaluatePolicy(toolName, args, agent) {
1201
1641
  if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
1202
1642
  return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
1203
1643
  }
1644
+ const pipeAnalysis = analyzePipeChain(shellCommand);
1645
+ if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
1646
+ const sinks = pipeAnalysis.sinkTargets;
1647
+ const allTrusted = sinks.length > 0 && sinks.every(isTrustedHost);
1648
+ if (pipeAnalysis.risk === "critical") {
1649
+ if (allTrusted) {
1650
+ return {
1651
+ decision: "review",
1652
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
1653
+ reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
1654
+ tier: 3
1655
+ };
1656
+ }
1657
+ return {
1658
+ decision: "block",
1659
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1660
+ reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1661
+ tier: 3
1662
+ };
1663
+ }
1664
+ if (allTrusted) {
1665
+ return { decision: "allow" };
1666
+ }
1667
+ return {
1668
+ decision: "review",
1669
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1670
+ reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1671
+ tier: 3
1672
+ };
1673
+ }
1674
+ const firstToken = analyzed.actions[0] ?? "";
1675
+ if (["ssh", "scp", "rsync"].includes(firstToken)) {
1676
+ const rawTokens = shellCommand.trim().split(/\s+/);
1677
+ const sshHosts = extractAllSshHosts(rawTokens.slice(1));
1678
+ allTokens.push(...sshHosts);
1679
+ }
1680
+ if (firstToken && path8.posix.isAbsolute(firstToken)) {
1681
+ const prov = checkProvenance(firstToken, cwd);
1682
+ if (prov.trustLevel === "suspect") {
1683
+ return {
1684
+ decision: config.settings.mode === "strict" ? "block" : "review",
1685
+ blockedByLabel: "Node9: Suspect Binary",
1686
+ reason: `Binary "${firstToken}" resolved to ${prov.resolvedPath} \u2014 ${prov.reason}`,
1687
+ tier: 3
1688
+ };
1689
+ }
1690
+ if (prov.trustLevel === "unknown" && config.settings.mode === "strict") {
1691
+ return {
1692
+ decision: "review",
1693
+ blockedByLabel: "Node9: Unknown Binary (strict mode)",
1694
+ reason: `Binary "${firstToken}" \u2014 ${prov.reason}`,
1695
+ tier: 3
1696
+ };
1697
+ }
1698
+ }
1204
1699
  if (isSqlTool(toolName, config.policy.toolInspection)) {
1205
1700
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
1206
1701
  }
1207
1702
  } else {
1208
- allTokens = tokenize(toolName);
1703
+ allTokens = tokenize2(toolName);
1209
1704
  if (args && typeof args === "object") {
1210
1705
  const flattenedArgs = JSON.stringify(args).toLowerCase();
1211
1706
  const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
@@ -1283,18 +1778,18 @@ function isIgnoredTool(toolName) {
1283
1778
  }
1284
1779
 
1285
1780
  // src/auth/state.ts
1286
- import fs5 from "fs";
1287
- import path5 from "path";
1288
- import os4 from "os";
1289
- var PAUSED_FILE = path5.join(os4.homedir(), ".node9", "PAUSED");
1290
- var TRUST_FILE = path5.join(os4.homedir(), ".node9", "trust.json");
1781
+ import fs7 from "fs";
1782
+ import path9 from "path";
1783
+ import os6 from "os";
1784
+ var PAUSED_FILE = path9.join(os6.homedir(), ".node9", "PAUSED");
1785
+ var TRUST_FILE = path9.join(os6.homedir(), ".node9", "trust.json");
1291
1786
  function checkPause() {
1292
1787
  try {
1293
- if (!fs5.existsSync(PAUSED_FILE)) return { paused: false };
1294
- const state = JSON.parse(fs5.readFileSync(PAUSED_FILE, "utf-8"));
1788
+ if (!fs7.existsSync(PAUSED_FILE)) return { paused: false };
1789
+ const state = JSON.parse(fs7.readFileSync(PAUSED_FILE, "utf-8"));
1295
1790
  if (state.expiry > 0 && Date.now() >= state.expiry) {
1296
1791
  try {
1297
- fs5.unlinkSync(PAUSED_FILE);
1792
+ fs7.unlinkSync(PAUSED_FILE);
1298
1793
  } catch {
1299
1794
  }
1300
1795
  return { paused: false };
@@ -1305,20 +1800,20 @@ function checkPause() {
1305
1800
  }
1306
1801
  }
1307
1802
  function atomicWriteSync(filePath, data, options) {
1308
- const dir = path5.dirname(filePath);
1309
- if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
1310
- const tmpPath = `${filePath}.${os4.hostname()}.${process.pid}.tmp`;
1311
- fs5.writeFileSync(tmpPath, data, options);
1312
- fs5.renameSync(tmpPath, filePath);
1803
+ const dir = path9.dirname(filePath);
1804
+ if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
1805
+ const tmpPath = `${filePath}.${os6.hostname()}.${process.pid}.tmp`;
1806
+ fs7.writeFileSync(tmpPath, data, options);
1807
+ fs7.renameSync(tmpPath, filePath);
1313
1808
  }
1314
1809
  function getActiveTrustSession(toolName) {
1315
1810
  try {
1316
- if (!fs5.existsSync(TRUST_FILE)) return false;
1317
- const trust = JSON.parse(fs5.readFileSync(TRUST_FILE, "utf-8"));
1811
+ if (!fs7.existsSync(TRUST_FILE)) return false;
1812
+ const trust = JSON.parse(fs7.readFileSync(TRUST_FILE, "utf-8"));
1318
1813
  const now = Date.now();
1319
1814
  const active = trust.entries.filter((e) => e.expiry > now);
1320
1815
  if (active.length !== trust.entries.length) {
1321
- fs5.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
1816
+ fs7.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
1322
1817
  }
1323
1818
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
1324
1819
  } catch {
@@ -1329,8 +1824,8 @@ function writeTrustSession(toolName, durationMs) {
1329
1824
  try {
1330
1825
  let trust = { entries: [] };
1331
1826
  try {
1332
- if (fs5.existsSync(TRUST_FILE)) {
1333
- trust = JSON.parse(fs5.readFileSync(TRUST_FILE, "utf-8"));
1827
+ if (fs7.existsSync(TRUST_FILE)) {
1828
+ trust = JSON.parse(fs7.readFileSync(TRUST_FILE, "utf-8"));
1334
1829
  }
1335
1830
  } catch {
1336
1831
  }
@@ -1346,9 +1841,9 @@ function writeTrustSession(toolName, durationMs) {
1346
1841
  }
1347
1842
  function getPersistentDecision(toolName) {
1348
1843
  try {
1349
- const file = path5.join(os4.homedir(), ".node9", "decisions.json");
1350
- if (!fs5.existsSync(file)) return null;
1351
- const decisions = JSON.parse(fs5.readFileSync(file, "utf-8"));
1844
+ const file = path9.join(os6.homedir(), ".node9", "decisions.json");
1845
+ if (!fs7.existsSync(file)) return null;
1846
+ const decisions = JSON.parse(fs7.readFileSync(file, "utf-8"));
1352
1847
  const d = decisions[toolName];
1353
1848
  if (d === "allow" || d === "deny") return d;
1354
1849
  } catch {
@@ -1357,17 +1852,17 @@ function getPersistentDecision(toolName) {
1357
1852
  }
1358
1853
 
1359
1854
  // src/auth/daemon.ts
1360
- import fs6 from "fs";
1361
- import path6 from "path";
1362
- import os5 from "os";
1855
+ import fs8 from "fs";
1856
+ import path10 from "path";
1857
+ import os7 from "os";
1363
1858
  import { spawnSync } from "child_process";
1364
1859
  var DAEMON_PORT = 7391;
1365
1860
  var DAEMON_HOST = "127.0.0.1";
1366
1861
  function getInternalToken() {
1367
1862
  try {
1368
- const pidFile = path6.join(os5.homedir(), ".node9", "daemon.pid");
1369
- if (!fs6.existsSync(pidFile)) return null;
1370
- const data = JSON.parse(fs6.readFileSync(pidFile, "utf-8"));
1863
+ const pidFile = path10.join(os7.homedir(), ".node9", "daemon.pid");
1864
+ if (!fs8.existsSync(pidFile)) return null;
1865
+ const data = JSON.parse(fs8.readFileSync(pidFile, "utf-8"));
1371
1866
  process.kill(data.pid, 0);
1372
1867
  return data.internalToken ?? null;
1373
1868
  } catch {
@@ -1375,10 +1870,10 @@ function getInternalToken() {
1375
1870
  }
1376
1871
  }
1377
1872
  function isDaemonRunning() {
1378
- const pidFile = path6.join(os5.homedir(), ".node9", "daemon.pid");
1379
- if (fs6.existsSync(pidFile)) {
1873
+ const pidFile = path10.join(os7.homedir(), ".node9", "daemon.pid");
1874
+ if (fs8.existsSync(pidFile)) {
1380
1875
  try {
1381
- const { pid, port } = JSON.parse(fs6.readFileSync(pidFile, "utf-8"));
1876
+ const { pid, port } = JSON.parse(fs8.readFileSync(pidFile, "utf-8"));
1382
1877
  if (port !== DAEMON_PORT) return false;
1383
1878
  process.kill(pid, 0);
1384
1879
  return true;
@@ -1474,16 +1969,16 @@ async function resolveViaDaemon(id, decision, internalToken) {
1474
1969
 
1475
1970
  // src/auth/orchestrator.ts
1476
1971
  import net from "net";
1477
- import path9 from "path";
1478
- import os7 from "os";
1972
+ import path13 from "path";
1973
+ import os9 from "os";
1479
1974
  import { randomUUID } from "crypto";
1480
1975
 
1481
1976
  // src/ui/native.ts
1482
1977
  import { spawn } from "child_process";
1483
- import path8 from "path";
1978
+ import path12 from "path";
1484
1979
 
1485
1980
  // src/context-sniper.ts
1486
- import path7 from "path";
1981
+ import path11 from "path";
1487
1982
  function smartTruncate(str, maxLen = 500) {
1488
1983
  if (str.length <= maxLen) return str;
1489
1984
  const edge = Math.floor(maxLen / 2) - 3;
@@ -1551,7 +2046,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
1551
2046
  intent = "EDIT";
1552
2047
  if (obj.file_path) {
1553
2048
  editFilePath = String(obj.file_path);
1554
- editFileName = path7.basename(editFilePath);
2049
+ editFileName = path11.basename(editFilePath);
1555
2050
  }
1556
2051
  const result = extractContext(String(obj.new_string), matchedWord);
1557
2052
  contextSnippet = result.snippet;
@@ -1606,7 +2101,7 @@ function formatArgs(args, matchedField, matchedWord) {
1606
2101
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
1607
2102
  const obj = parsed;
1608
2103
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
1609
- const file = obj.file_path ? path8.basename(String(obj.file_path)) : "file";
2104
+ const file = obj.file_path ? path12.basename(String(obj.file_path)) : "file";
1610
2105
  const oldPreview = smartTruncate(String(obj.old_string), 120);
1611
2106
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
1612
2107
  return {
@@ -1781,8 +2276,8 @@ end run`;
1781
2276
  }
1782
2277
 
1783
2278
  // src/auth/cloud.ts
1784
- import fs7 from "fs";
1785
- import os6 from "os";
2279
+ import fs9 from "fs";
2280
+ import os8 from "os";
1786
2281
  function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1787
2282
  return fetch(`${creds.apiUrl}/audit`, {
1788
2283
  method: "POST",
@@ -1794,9 +2289,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1794
2289
  context: {
1795
2290
  agent: meta?.agent,
1796
2291
  mcpServer: meta?.mcpServer,
1797
- hostname: os6.hostname(),
2292
+ hostname: os8.hostname(),
1798
2293
  cwd: process.cwd(),
1799
- platform: os6.platform()
2294
+ platform: os8.platform()
1800
2295
  }
1801
2296
  }),
1802
2297
  signal: AbortSignal.timeout(5e3)
@@ -1817,9 +2312,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
1817
2312
  context: {
1818
2313
  agent: meta?.agent,
1819
2314
  mcpServer: meta?.mcpServer,
1820
- hostname: os6.hostname(),
2315
+ hostname: os8.hostname(),
1821
2316
  cwd: process.cwd(),
1822
- platform: os6.platform()
2317
+ platform: os8.platform()
1823
2318
  },
1824
2319
  ...riskMetadata && { riskMetadata }
1825
2320
  }),
@@ -1875,14 +2370,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
1875
2370
  });
1876
2371
  clearTimeout(timer);
1877
2372
  if (!res.ok) {
1878
- fs7.appendFileSync(
2373
+ fs9.appendFileSync(
1879
2374
  HOOK_DEBUG_LOG,
1880
2375
  `[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
1881
2376
  `
1882
2377
  );
1883
2378
  }
1884
2379
  } catch (err) {
1885
- fs7.appendFileSync(
2380
+ fs9.appendFileSync(
1886
2381
  HOOK_DEBUG_LOG,
1887
2382
  `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
1888
2383
  `
@@ -1891,7 +2386,7 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
1891
2386
  }
1892
2387
 
1893
2388
  // src/auth/orchestrator.ts
1894
- var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path9.join(os7.tmpdir(), "node9-activity.sock");
2389
+ var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os9.tmpdir(), "node9-activity.sock");
1895
2390
  function notifyActivity(data) {
1896
2391
  return new Promise((resolve) => {
1897
2392
  try {
@@ -1973,7 +2468,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
1973
2468
  }
1974
2469
  if (config.settings.mode === "audit") {
1975
2470
  if (!isIgnoredTool(toolName)) {
1976
- const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
2471
+ const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
1977
2472
  if (policyResult.decision === "review") {
1978
2473
  appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
1979
2474
  if (approvers.cloud && creds?.apiKey) {