@node9/proxy 1.3.0 → 1.3.2

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 path13 = issue.path.length > 0 ? issue.path.join(".") : "root";
195
+ return ` \u2022 ${path13}: ${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_path7 = __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,416 @@ 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
+
1070
1479
  // src/policy/index.ts
1071
- function tokenize(toolName) {
1480
+ function tokenize2(toolName) {
1072
1481
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
1073
1482
  }
1074
1483
  function matchesPattern(text, patterns) {
@@ -1081,9 +1490,9 @@ function matchesPattern(text, patterns) {
1081
1490
  const withoutDotSlash = text.replace(/^\.\//, "");
1082
1491
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1083
1492
  }
1084
- function getNestedValue(obj, path10) {
1493
+ function getNestedValue(obj, path13) {
1085
1494
  if (!obj || typeof obj !== "object") return null;
1086
- return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
1495
+ return path13.split(".").reduce((prev, curr) => prev?.[curr], obj);
1087
1496
  }
1088
1497
  function evaluateSmartConditions(args, rule) {
1089
1498
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -1207,7 +1616,7 @@ async function analyzeShellCommand(command) {
1207
1616
  }
1208
1617
  return { actions, paths, allTokens };
1209
1618
  }
1210
- async function evaluatePolicy(toolName, args, agent) {
1619
+ async function evaluatePolicy(toolName, args, agent, cwd) {
1211
1620
  const config = getConfig();
1212
1621
  if (matchesPattern(toolName, config.policy.ignoredTools)) return { decision: "allow" };
1213
1622
  if (config.policy.smartRules.length > 0) {
@@ -1237,11 +1646,55 @@ async function evaluatePolicy(toolName, args, agent) {
1237
1646
  if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
1238
1647
  return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
1239
1648
  }
1649
+ const pipeAnalysis = analyzePipeChain(shellCommand);
1650
+ if (pipeAnalysis.isPipeline) {
1651
+ if (pipeAnalysis.risk === "critical") {
1652
+ return {
1653
+ decision: "block",
1654
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1655
+ reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${pipeAnalysis.sinkTargets.join(", ")}`,
1656
+ tier: 3
1657
+ };
1658
+ }
1659
+ if (pipeAnalysis.risk === "high") {
1660
+ return {
1661
+ decision: "review",
1662
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1663
+ reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${pipeAnalysis.sinkTargets.join(", ")}`,
1664
+ tier: 3
1665
+ };
1666
+ }
1667
+ }
1668
+ const firstToken = analyzed.actions[0] ?? "";
1669
+ if (["ssh", "scp", "rsync"].includes(firstToken)) {
1670
+ const rawTokens = shellCommand.trim().split(/\s+/);
1671
+ const sshHosts = extractAllSshHosts(rawTokens.slice(1));
1672
+ allTokens.push(...sshHosts);
1673
+ }
1674
+ if (firstToken && import_path7.default.posix.isAbsolute(firstToken)) {
1675
+ const prov = checkProvenance(firstToken, cwd);
1676
+ if (prov.trustLevel === "suspect") {
1677
+ return {
1678
+ decision: config.settings.mode === "strict" ? "block" : "review",
1679
+ blockedByLabel: "Node9: Suspect Binary",
1680
+ reason: `Binary "${firstToken}" resolved to ${prov.resolvedPath} \u2014 ${prov.reason}`,
1681
+ tier: 3
1682
+ };
1683
+ }
1684
+ if (prov.trustLevel === "unknown" && config.settings.mode === "strict") {
1685
+ return {
1686
+ decision: "review",
1687
+ blockedByLabel: "Node9: Unknown Binary (strict mode)",
1688
+ reason: `Binary "${firstToken}" \u2014 ${prov.reason}`,
1689
+ tier: 3
1690
+ };
1691
+ }
1692
+ }
1240
1693
  if (isSqlTool(toolName, config.policy.toolInspection)) {
1241
1694
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
1242
1695
  }
1243
1696
  } else {
1244
- allTokens = tokenize(toolName);
1697
+ allTokens = tokenize2(toolName);
1245
1698
  if (args && typeof args === "object") {
1246
1699
  const flattenedArgs = JSON.stringify(args).toLowerCase();
1247
1700
  const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
@@ -1319,18 +1772,18 @@ function isIgnoredTool(toolName) {
1319
1772
  }
1320
1773
 
1321
1774
  // 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");
1775
+ var import_fs6 = __toESM(require("fs"));
1776
+ var import_path8 = __toESM(require("path"));
1777
+ var import_os5 = __toESM(require("os"));
1778
+ var PAUSED_FILE = import_path8.default.join(import_os5.default.homedir(), ".node9", "PAUSED");
1779
+ var TRUST_FILE = import_path8.default.join(import_os5.default.homedir(), ".node9", "trust.json");
1327
1780
  function checkPause() {
1328
1781
  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"));
1782
+ if (!import_fs6.default.existsSync(PAUSED_FILE)) return { paused: false };
1783
+ const state = JSON.parse(import_fs6.default.readFileSync(PAUSED_FILE, "utf-8"));
1331
1784
  if (state.expiry > 0 && Date.now() >= state.expiry) {
1332
1785
  try {
1333
- import_fs5.default.unlinkSync(PAUSED_FILE);
1786
+ import_fs6.default.unlinkSync(PAUSED_FILE);
1334
1787
  } catch {
1335
1788
  }
1336
1789
  return { paused: false };
@@ -1341,20 +1794,20 @@ function checkPause() {
1341
1794
  }
1342
1795
  }
1343
1796
  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);
1797
+ const dir = import_path8.default.dirname(filePath);
1798
+ if (!import_fs6.default.existsSync(dir)) import_fs6.default.mkdirSync(dir, { recursive: true });
1799
+ const tmpPath = `${filePath}.${import_os5.default.hostname()}.${process.pid}.tmp`;
1800
+ import_fs6.default.writeFileSync(tmpPath, data, options);
1801
+ import_fs6.default.renameSync(tmpPath, filePath);
1349
1802
  }
1350
1803
  function getActiveTrustSession(toolName) {
1351
1804
  try {
1352
- if (!import_fs5.default.existsSync(TRUST_FILE)) return false;
1353
- const trust = JSON.parse(import_fs5.default.readFileSync(TRUST_FILE, "utf-8"));
1805
+ if (!import_fs6.default.existsSync(TRUST_FILE)) return false;
1806
+ const trust = JSON.parse(import_fs6.default.readFileSync(TRUST_FILE, "utf-8"));
1354
1807
  const now = Date.now();
1355
1808
  const active = trust.entries.filter((e) => e.expiry > now);
1356
1809
  if (active.length !== trust.entries.length) {
1357
- import_fs5.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
1810
+ import_fs6.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
1358
1811
  }
1359
1812
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
1360
1813
  } catch {
@@ -1365,8 +1818,8 @@ function writeTrustSession(toolName, durationMs) {
1365
1818
  try {
1366
1819
  let trust = { entries: [] };
1367
1820
  try {
1368
- if (import_fs5.default.existsSync(TRUST_FILE)) {
1369
- trust = JSON.parse(import_fs5.default.readFileSync(TRUST_FILE, "utf-8"));
1821
+ if (import_fs6.default.existsSync(TRUST_FILE)) {
1822
+ trust = JSON.parse(import_fs6.default.readFileSync(TRUST_FILE, "utf-8"));
1370
1823
  }
1371
1824
  } catch {
1372
1825
  }
@@ -1382,9 +1835,9 @@ function writeTrustSession(toolName, durationMs) {
1382
1835
  }
1383
1836
  function getPersistentDecision(toolName) {
1384
1837
  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"));
1838
+ const file = import_path8.default.join(import_os5.default.homedir(), ".node9", "decisions.json");
1839
+ if (!import_fs6.default.existsSync(file)) return null;
1840
+ const decisions = JSON.parse(import_fs6.default.readFileSync(file, "utf-8"));
1388
1841
  const d = decisions[toolName];
1389
1842
  if (d === "allow" || d === "deny") return d;
1390
1843
  } catch {
@@ -1393,17 +1846,17 @@ function getPersistentDecision(toolName) {
1393
1846
  }
1394
1847
 
1395
1848
  // 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"));
1849
+ var import_fs7 = __toESM(require("fs"));
1850
+ var import_path9 = __toESM(require("path"));
1851
+ var import_os6 = __toESM(require("os"));
1399
1852
  var import_child_process = require("child_process");
1400
1853
  var DAEMON_PORT = 7391;
1401
1854
  var DAEMON_HOST = "127.0.0.1";
1402
1855
  function getInternalToken() {
1403
1856
  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"));
1857
+ const pidFile = import_path9.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
1858
+ if (!import_fs7.default.existsSync(pidFile)) return null;
1859
+ const data = JSON.parse(import_fs7.default.readFileSync(pidFile, "utf-8"));
1407
1860
  process.kill(data.pid, 0);
1408
1861
  return data.internalToken ?? null;
1409
1862
  } catch {
@@ -1411,10 +1864,10 @@ function getInternalToken() {
1411
1864
  }
1412
1865
  }
1413
1866
  function isDaemonRunning() {
1414
- const pidFile = import_path6.default.join(import_os5.default.homedir(), ".node9", "daemon.pid");
1415
- if (import_fs6.default.existsSync(pidFile)) {
1867
+ const pidFile = import_path9.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
1868
+ if (import_fs7.default.existsSync(pidFile)) {
1416
1869
  try {
1417
- const { pid, port } = JSON.parse(import_fs6.default.readFileSync(pidFile, "utf-8"));
1870
+ const { pid, port } = JSON.parse(import_fs7.default.readFileSync(pidFile, "utf-8"));
1418
1871
  if (port !== DAEMON_PORT) return false;
1419
1872
  process.kill(pid, 0);
1420
1873
  return true;
@@ -1510,16 +1963,16 @@ async function resolveViaDaemon(id, decision, internalToken) {
1510
1963
 
1511
1964
  // src/auth/orchestrator.ts
1512
1965
  var import_net = __toESM(require("net"));
1513
- var import_path9 = __toESM(require("path"));
1514
- var import_os7 = __toESM(require("os"));
1966
+ var import_path12 = __toESM(require("path"));
1967
+ var import_os8 = __toESM(require("os"));
1515
1968
  var import_crypto = require("crypto");
1516
1969
 
1517
1970
  // src/ui/native.ts
1518
1971
  var import_child_process2 = require("child_process");
1519
- var import_path8 = __toESM(require("path"));
1972
+ var import_path11 = __toESM(require("path"));
1520
1973
 
1521
1974
  // src/context-sniper.ts
1522
- var import_path7 = __toESM(require("path"));
1975
+ var import_path10 = __toESM(require("path"));
1523
1976
  function smartTruncate(str, maxLen = 500) {
1524
1977
  if (str.length <= maxLen) return str;
1525
1978
  const edge = Math.floor(maxLen / 2) - 3;
@@ -1587,7 +2040,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
1587
2040
  intent = "EDIT";
1588
2041
  if (obj.file_path) {
1589
2042
  editFilePath = String(obj.file_path);
1590
- editFileName = import_path7.default.basename(editFilePath);
2043
+ editFileName = import_path10.default.basename(editFilePath);
1591
2044
  }
1592
2045
  const result = extractContext(String(obj.new_string), matchedWord);
1593
2046
  contextSnippet = result.snippet;
@@ -1642,7 +2095,7 @@ function formatArgs(args, matchedField, matchedWord) {
1642
2095
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
1643
2096
  const obj = parsed;
1644
2097
  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";
2098
+ const file = obj.file_path ? import_path11.default.basename(String(obj.file_path)) : "file";
1646
2099
  const oldPreview = smartTruncate(String(obj.old_string), 120);
1647
2100
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
1648
2101
  return {
@@ -1817,8 +2270,8 @@ end run`;
1817
2270
  }
1818
2271
 
1819
2272
  // src/auth/cloud.ts
1820
- var import_fs7 = __toESM(require("fs"));
1821
- var import_os6 = __toESM(require("os"));
2273
+ var import_fs8 = __toESM(require("fs"));
2274
+ var import_os7 = __toESM(require("os"));
1822
2275
  function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1823
2276
  return fetch(`${creds.apiUrl}/audit`, {
1824
2277
  method: "POST",
@@ -1830,9 +2283,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1830
2283
  context: {
1831
2284
  agent: meta?.agent,
1832
2285
  mcpServer: meta?.mcpServer,
1833
- hostname: import_os6.default.hostname(),
2286
+ hostname: import_os7.default.hostname(),
1834
2287
  cwd: process.cwd(),
1835
- platform: import_os6.default.platform()
2288
+ platform: import_os7.default.platform()
1836
2289
  }
1837
2290
  }),
1838
2291
  signal: AbortSignal.timeout(5e3)
@@ -1853,9 +2306,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
1853
2306
  context: {
1854
2307
  agent: meta?.agent,
1855
2308
  mcpServer: meta?.mcpServer,
1856
- hostname: import_os6.default.hostname(),
2309
+ hostname: import_os7.default.hostname(),
1857
2310
  cwd: process.cwd(),
1858
- platform: import_os6.default.platform()
2311
+ platform: import_os7.default.platform()
1859
2312
  },
1860
2313
  ...riskMetadata && { riskMetadata }
1861
2314
  }),
@@ -1911,14 +2364,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
1911
2364
  });
1912
2365
  clearTimeout(timer);
1913
2366
  if (!res.ok) {
1914
- import_fs7.default.appendFileSync(
2367
+ import_fs8.default.appendFileSync(
1915
2368
  HOOK_DEBUG_LOG,
1916
2369
  `[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
1917
2370
  `
1918
2371
  );
1919
2372
  }
1920
2373
  } catch (err) {
1921
- import_fs7.default.appendFileSync(
2374
+ import_fs8.default.appendFileSync(
1922
2375
  HOOK_DEBUG_LOG,
1923
2376
  `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
1924
2377
  `
@@ -1927,7 +2380,7 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
1927
2380
  }
1928
2381
 
1929
2382
  // 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");
2383
+ var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path12.default.join(import_os8.default.tmpdir(), "node9-activity.sock");
1931
2384
  function notifyActivity(data) {
1932
2385
  return new Promise((resolve) => {
1933
2386
  try {
@@ -2009,7 +2462,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2009
2462
  }
2010
2463
  if (config.settings.mode === "audit") {
2011
2464
  if (!isIgnoredTool(toolName)) {
2012
- const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
2465
+ const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
2013
2466
  if (policyResult.decision === "review") {
2014
2467
  appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
2015
2468
  if (approvers.cloud && creds?.apiKey) {