@node9/proxy 2.14.2 → 2.15.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
@@ -1,7 +1,12 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __esm = (fn, res) => function __init() {
4
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
5
10
  };
6
11
  var __export = (target, all) => {
7
12
  for (var name in all)
@@ -377,8 +382,8 @@ var ConfigFileSchema = z.object({
377
382
  }).strict();
378
383
  function formatIssues(issues) {
379
384
  const lines = issues.map((issue) => {
380
- const path15 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
381
- return ` \u2022 ${path15}: ${issue.message}`;
385
+ const path16 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
386
+ return ` \u2022 ${path16}: ${issue.message}`;
382
387
  });
383
388
  return `Invalid config:
384
389
  ${lines.join("\n")}`;
@@ -391,9 +396,9 @@ function prunePaths(root, paths) {
391
396
  const yi = y[y.length - 1];
392
397
  return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
393
398
  });
394
- for (const path15 of ordered) {
399
+ for (const path16 of ordered) {
395
400
  let cur = root;
396
- for (const key of path15.slice(0, -1)) {
401
+ for (const key of path16.slice(0, -1)) {
397
402
  if (cur === null || typeof cur !== "object") {
398
403
  cur = void 0;
399
404
  break;
@@ -401,7 +406,7 @@ function prunePaths(root, paths) {
401
406
  cur = cur[key];
402
407
  }
403
408
  if (cur === null || typeof cur !== "object") continue;
404
- const last = path15[path15.length - 1];
409
+ const last = path16[path16.length - 1];
405
410
  if (Array.isArray(cur)) {
406
411
  const i = Number(last);
407
412
  if (Number.isInteger(i) && i >= 0 && i < cur.length) {
@@ -437,7 +442,7 @@ function sanitizeConfig(raw) {
437
442
  return issue.keys.map((k) => [...at, k]);
438
443
  }
439
444
  return [at.slice(0, -level || void 0)];
440
- }).filter((path15) => path15.length > 0);
445
+ }).filter((path16) => path16.length > 0);
441
446
  if (paths.length === 0 || !prunePaths(working, paths)) break;
442
447
  }
443
448
  if (ConfigFileSchema.safeParse(working).success) break;
@@ -1349,9 +1354,9 @@ function matchesPattern(text, patterns) {
1349
1354
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1350
1355
  }
1351
1356
  var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1352
- function getNestedValue(obj, path15) {
1357
+ function getNestedValue(obj, path16) {
1353
1358
  if (!obj || typeof obj !== "object") return null;
1354
- const segments2 = path15.split(".");
1359
+ const segments2 = path16.split(".");
1355
1360
  for (const seg of segments2) {
1356
1361
  if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
1357
1362
  }
@@ -4016,9 +4021,9 @@ function bareToolName(toolName) {
4016
4021
  const parts = toolName.split("__");
4017
4022
  return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
4018
4023
  }
4019
- function valuesAt(args, path15) {
4024
+ function valuesAt(args, path16) {
4020
4025
  let cursors = [args];
4021
- for (const rawSegment of path15.split(".")) {
4026
+ for (const rawSegment of path16.split(".")) {
4022
4027
  const isArray = rawSegment.endsWith("[]");
4023
4028
  const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
4024
4029
  const next = [];
@@ -4050,15 +4055,15 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
4050
4055
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
4051
4056
  if (!paths) return null;
4052
4057
  const exempt = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
4053
- for (const path15 of paths) {
4054
- for (const value of valuesAt(args, path15)) {
4058
+ for (const path16 of paths) {
4059
+ for (const value of valuesAt(args, path16)) {
4055
4060
  const host = hostOf(value);
4056
4061
  if (!host) continue;
4057
4062
  const m = classifySsrf(host);
4058
4063
  if (!m) continue;
4059
4064
  if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
4060
4065
  if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
4061
- return { ...m, argPath: path15, host, reason: ssrfReason(m, host) };
4066
+ return { ...m, argPath: path16, host, reason: ssrfReason(m, host) };
4062
4067
  }
4063
4068
  }
4064
4069
  return null;
@@ -4181,7 +4186,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4181
4186
  reason: dest.reason,
4182
4187
  ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
4183
4188
  ruleDescription: dest.reason,
4184
- tier: 3
4189
+ tier: 3,
4190
+ overridable: dest.overridable
4185
4191
  };
4186
4192
  }
4187
4193
  }
@@ -4330,7 +4336,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4330
4336
  reason: ssrf.reason,
4331
4337
  ruleName: `ssrf:${ssrf.tier}:${ssrf.binary}:${ssrf.host}`,
4332
4338
  ruleDescription: ssrf.reason,
4333
- tier: 3
4339
+ tier: 3,
4340
+ overridable: ssrf.overridable
4334
4341
  };
4335
4342
  }
4336
4343
  }
@@ -5836,6 +5843,45 @@ function isTrustedHost(host) {
5836
5843
  );
5837
5844
  }
5838
5845
 
5846
+ // src/auth/api-url.ts
5847
+ var DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
5848
+ var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]", "0.0.0.0"]);
5849
+ var HOST_ALLOW_ENV = "NODE9_API_HOST_ALLOW";
5850
+ function defaultHostSuffixes() {
5851
+ const host = new URL(DEFAULT_API_URL).hostname.toLowerCase();
5852
+ const parent = host.split(".").slice(1).join(".");
5853
+ return parent.includes(".") ? [host, parent] : [host];
5854
+ }
5855
+ function allowedSuffixes() {
5856
+ const extra = (process.env[HOST_ALLOW_ENV] ?? "").split(",").map(
5857
+ (s) => s.trim().toLowerCase().replace(/^\*?\./, "")
5858
+ ).filter(Boolean);
5859
+ return [...defaultHostSuffixes(), ...extra];
5860
+ }
5861
+ function validateApiUrl(raw) {
5862
+ if (typeof raw !== "string" || raw.length === 0 || raw.length > 2048) return null;
5863
+ let u;
5864
+ try {
5865
+ u = new URL(raw);
5866
+ } catch {
5867
+ return null;
5868
+ }
5869
+ if (u.username || u.password) return null;
5870
+ if (u.protocol !== "https:" && u.protocol !== "http:") return null;
5871
+ const host = u.hostname.toLowerCase();
5872
+ if (LOOPBACK.has(host)) return u;
5873
+ if (u.protocol !== "https:") return null;
5874
+ const suffixes = allowedSuffixes();
5875
+ const ok = suffixes.some((s) => host === s || host.endsWith("." + s));
5876
+ return ok ? u : null;
5877
+ }
5878
+ function safeApiUrl(raw, onReject) {
5879
+ const ok = validateApiUrl(raw);
5880
+ if (ok) return typeof raw === "string" ? raw : ok.toString();
5881
+ onReject?.(raw);
5882
+ return DEFAULT_API_URL;
5883
+ }
5884
+
5839
5885
  // src/config/index.ts
5840
5886
  function sanitizeSsrfAllow(entries, source) {
5841
5887
  const kept = [];
@@ -6124,12 +6170,27 @@ var ADVISORY_SMART_RULES = [
6124
6170
  }
6125
6171
  ];
6126
6172
  var cachedConfig = null;
6173
+ var rejectedApiUrlsSeen = /* @__PURE__ */ new Set();
6174
+ function noteRejectedApiUrl(raw) {
6175
+ const key = String(raw).slice(0, 200);
6176
+ if (rejectedApiUrlsSeen.has(key)) return;
6177
+ rejectedApiUrlsSeen.add(key);
6178
+ try {
6179
+ fs4.appendFileSync(
6180
+ path4.join(os4.homedir(), ".node9", "hook-debug.log"),
6181
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] REFUSED apiUrl, using the default instead: ${String(raw).slice(0, 200).replace(/[\r\n]+/g, " ")}
6182
+ `
6183
+ );
6184
+ } catch {
6185
+ }
6186
+ }
6127
6187
  function getCredentials() {
6128
- const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
6129
6188
  if (process.env.NODE9_API_KEY) {
6130
6189
  return {
6131
6190
  apiKey: process.env.NODE9_API_KEY,
6132
- apiUrl: process.env.NODE9_API_URL || DEFAULT_API_URL
6191
+ // NODE9_API_URL is env, and a hook inherits the agent's env, so it is
6192
+ // no more trusted than the file below.
6193
+ apiUrl: safeApiUrl(process.env.NODE9_API_URL || DEFAULT_API_URL, noteRejectedApiUrl)
6133
6194
  };
6134
6195
  }
6135
6196
  try {
@@ -6141,14 +6202,14 @@ function getCredentials() {
6141
6202
  if (profile?.apiKey) {
6142
6203
  return {
6143
6204
  apiKey: profile.apiKey,
6144
- apiUrl: profile.apiUrl || DEFAULT_API_URL,
6205
+ apiUrl: safeApiUrl(profile.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
6145
6206
  localOnly: profile.localOnly === true || profileName !== "default"
6146
6207
  };
6147
6208
  }
6148
6209
  if (creds.apiKey) {
6149
6210
  return {
6150
6211
  apiKey: creds.apiKey,
6151
- apiUrl: creds.apiUrl || DEFAULT_API_URL,
6212
+ apiUrl: safeApiUrl(creds.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
6152
6213
  localOnly: creds.localOnly === true
6153
6214
  };
6154
6215
  }
@@ -6581,13 +6642,13 @@ function getConfig(cwd) {
6581
6642
  }
6582
6643
  if (Array.isArray(mc.jailPaths)) {
6583
6644
  for (const jp of mc.jailPaths) {
6584
- const path15 = typeof jp?.path === "string" ? jp.path.trim() : "";
6585
- if (!path15) continue;
6645
+ const path16 = typeof jp?.path === "string" ? jp.path.trim() : "";
6646
+ if (!path16) continue;
6586
6647
  const verdict = jp?.verdict === "review" ? "review" : "block";
6587
- for (const r of pathRules(path15, verdict, "org-managed jail")) {
6648
+ for (const r of pathRules(path16, verdict, "org-managed jail")) {
6588
6649
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
6589
6650
  }
6590
- mergedPolicy.managedJailPaths.push({ path: path15, verdict });
6651
+ mergedPolicy.managedJailPaths.push({ path: path16, verdict });
6591
6652
  }
6592
6653
  }
6593
6654
  if (Array.isArray(mc.trustedHosts)) {
@@ -6924,11 +6985,41 @@ function isIgnoredTool2(toolName) {
6924
6985
  }
6925
6986
 
6926
6987
  // src/auth/state.ts
6988
+ import fs8 from "fs";
6989
+ import path8 from "path";
6990
+ import os6 from "os";
6991
+
6992
+ // src/utils/atomic-write.ts
6927
6993
  import fs7 from "fs";
6928
6994
  import path7 from "path";
6929
- import os6 from "os";
6930
- var PAUSED_FILE = path7.join(os6.homedir(), ".node9", "PAUSED");
6931
- var TRUST_FILE = path7.join(os6.homedir(), ".node9", "trust.json");
6995
+ import { randomUUID } from "crypto";
6996
+ function atomicWriteSync(filePath, data, options) {
6997
+ const dir = path7.dirname(filePath);
6998
+ if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
6999
+ const tmpPath = `${filePath}.${randomUUID()}.tmp`;
7000
+ try {
7001
+ fs7.writeFileSync(tmpPath, data, options);
7002
+ } catch (err) {
7003
+ try {
7004
+ fs7.unlinkSync(tmpPath);
7005
+ } catch {
7006
+ }
7007
+ throw err;
7008
+ }
7009
+ try {
7010
+ fs7.renameSync(tmpPath, filePath);
7011
+ } catch (err) {
7012
+ try {
7013
+ fs7.unlinkSync(tmpPath);
7014
+ } catch {
7015
+ }
7016
+ throw err;
7017
+ }
7018
+ }
7019
+
7020
+ // src/auth/state.ts
7021
+ var PAUSED_FILE = path8.join(os6.homedir(), ".node9", "PAUSED");
7022
+ var TRUST_FILE = path8.join(os6.homedir(), ".node9", "trust.json");
6932
7023
  function extractCommandPattern(toolName, args) {
6933
7024
  const lower = toolName.toLowerCase();
6934
7025
  if (lower !== "bash" && lower !== "execute_bash" && lower !== "shell") return void 0;
@@ -6940,11 +7031,11 @@ function extractCommandPattern(toolName, args) {
6940
7031
  }
6941
7032
  function checkPause() {
6942
7033
  try {
6943
- if (!fs7.existsSync(PAUSED_FILE)) return { paused: false };
6944
- const state = JSON.parse(fs7.readFileSync(PAUSED_FILE, "utf-8"));
7034
+ if (!fs8.existsSync(PAUSED_FILE)) return { paused: false };
7035
+ const state = JSON.parse(fs8.readFileSync(PAUSED_FILE, "utf-8"));
6945
7036
  if (state.expiry > 0 && Date.now() >= state.expiry) {
6946
7037
  try {
6947
- fs7.unlinkSync(PAUSED_FILE);
7038
+ fs8.unlinkSync(PAUSED_FILE);
6948
7039
  } catch {
6949
7040
  }
6950
7041
  return { paused: false };
@@ -6954,21 +7045,14 @@ function checkPause() {
6954
7045
  return { paused: false };
6955
7046
  }
6956
7047
  }
6957
- function atomicWriteSync(filePath, data, options) {
6958
- const dir = path7.dirname(filePath);
6959
- if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
6960
- const tmpPath = `${filePath}.${os6.hostname()}.${process.pid}.tmp`;
6961
- fs7.writeFileSync(tmpPath, data, options);
6962
- fs7.renameSync(tmpPath, filePath);
6963
- }
6964
7048
  function getActiveTrustSession(toolName, args) {
6965
7049
  try {
6966
- if (!fs7.existsSync(TRUST_FILE)) return false;
6967
- const trust = JSON.parse(fs7.readFileSync(TRUST_FILE, "utf-8"));
7050
+ if (!fs8.existsSync(TRUST_FILE)) return false;
7051
+ const trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
6968
7052
  const now = Date.now();
6969
7053
  const active = trust.entries.filter((e) => e.expiry > now);
6970
7054
  if (active.length !== trust.entries.length) {
6971
- fs7.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
7055
+ atomicWriteSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
6972
7056
  }
6973
7057
  return active.some((e) => {
6974
7058
  if (!(e.tool === toolName || matchesPattern(toolName, e.tool))) return false;
@@ -6987,8 +7071,8 @@ function writeTrustSession(toolName, durationMs, args) {
6987
7071
  try {
6988
7072
  let trust = { entries: [] };
6989
7073
  try {
6990
- if (fs7.existsSync(TRUST_FILE)) {
6991
- trust = JSON.parse(fs7.readFileSync(TRUST_FILE, "utf-8"));
7074
+ if (fs8.existsSync(TRUST_FILE)) {
7075
+ trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
6992
7076
  }
6993
7077
  } catch {
6994
7078
  }
@@ -7010,9 +7094,9 @@ function writeTrustSession(toolName, durationMs, args) {
7010
7094
  }
7011
7095
  function getPersistentDecision(toolName) {
7012
7096
  try {
7013
- const file = path7.join(os6.homedir(), ".node9", "decisions.json");
7014
- if (!fs7.existsSync(file)) return null;
7015
- const decisions = JSON.parse(fs7.readFileSync(file, "utf-8"));
7097
+ const file = path8.join(os6.homedir(), ".node9", "decisions.json");
7098
+ if (!fs8.existsSync(file)) return null;
7099
+ const decisions = JSON.parse(fs8.readFileSync(file, "utf-8"));
7016
7100
  const d = decisions[toolName];
7017
7101
  if (d === "allow" || d === "deny") return d;
7018
7102
  } catch {
@@ -7021,11 +7105,11 @@ function getPersistentDecision(toolName) {
7021
7105
  }
7022
7106
 
7023
7107
  // src/auth/daemon.ts
7024
- import fs8 from "fs";
7108
+ import fs9 from "fs";
7025
7109
  import net from "net";
7026
- import path8 from "path";
7110
+ import path9 from "path";
7027
7111
  import os7 from "os";
7028
- var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path8.join(os7.tmpdir(), "node9-activity.sock");
7112
+ var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path9.join(os7.tmpdir(), "node9-activity.sock");
7029
7113
  function notifyActivitySocket(data) {
7030
7114
  return new Promise((resolve) => {
7031
7115
  try {
@@ -7058,9 +7142,9 @@ var DAEMON_PORT = 7391;
7058
7142
  var DAEMON_HOST = "127.0.0.1";
7059
7143
  function getInternalToken() {
7060
7144
  try {
7061
- const pidFile = path8.join(os7.homedir(), ".node9", "daemon.pid");
7062
- if (!fs8.existsSync(pidFile)) return null;
7063
- const data = JSON.parse(fs8.readFileSync(pidFile, "utf-8"));
7145
+ const pidFile = path9.join(os7.homedir(), ".node9", "daemon.pid");
7146
+ if (!fs9.existsSync(pidFile)) return null;
7147
+ const data = JSON.parse(fs9.readFileSync(pidFile, "utf-8"));
7064
7148
  process.kill(data.pid, 0);
7065
7149
  return data.internalToken ?? null;
7066
7150
  } catch {
@@ -7068,12 +7152,12 @@ function getInternalToken() {
7068
7152
  }
7069
7153
  }
7070
7154
  function isDaemonRunning() {
7071
- const pidFile = path8.join(os7.homedir(), ".node9", "daemon.pid");
7072
- if (fs8.existsSync(pidFile)) {
7155
+ const pidFile = path9.join(os7.homedir(), ".node9", "daemon.pid");
7156
+ if (fs9.existsSync(pidFile)) {
7073
7157
  let pid;
7074
7158
  let port;
7075
7159
  try {
7076
- const data = JSON.parse(fs8.readFileSync(pidFile, "utf-8"));
7160
+ const data = JSON.parse(fs9.readFileSync(pidFile, "utf-8"));
7077
7161
  pid = data.pid;
7078
7162
  port = data.port;
7079
7163
  } catch {
@@ -7091,7 +7175,7 @@ function isDaemonRunning() {
7091
7175
  } catch (err) {
7092
7176
  if (err instanceof Error && "code" in err && err.code === "ESRCH") {
7093
7177
  try {
7094
- fs8.unlinkSync(pidFile);
7178
+ fs9.unlinkSync(pidFile);
7095
7179
  } catch {
7096
7180
  }
7097
7181
  }
@@ -7257,14 +7341,14 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
7257
7341
  }
7258
7342
 
7259
7343
  // src/auth/orchestrator.ts
7260
- import { randomUUID } from "crypto";
7344
+ import { randomUUID as randomUUID2 } from "crypto";
7261
7345
 
7262
7346
  // src/ui/native.ts
7263
7347
  import { spawn } from "child_process";
7264
- import path10 from "path";
7348
+ import path11 from "path";
7265
7349
 
7266
7350
  // src/context-sniper.ts
7267
- import path9 from "path";
7351
+ import path10 from "path";
7268
7352
  function smartTruncate(str, maxLen = 500) {
7269
7353
  if (str.length <= maxLen) return str;
7270
7354
  const edge = Math.floor(maxLen / 2) - 3;
@@ -7332,7 +7416,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
7332
7416
  intent = "EDIT";
7333
7417
  if (obj.file_path) {
7334
7418
  editFilePath = String(obj.file_path);
7335
- editFileName = path9.basename(editFilePath);
7419
+ editFileName = path10.basename(editFilePath);
7336
7420
  }
7337
7421
  const result = extractContext(String(obj.new_string), matchedWord);
7338
7422
  contextSnippet = result.snippet;
@@ -7396,7 +7480,7 @@ function formatArgs(args, matchedField, matchedWord) {
7396
7480
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
7397
7481
  const obj = parsed;
7398
7482
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
7399
- const file = obj.file_path ? path10.basename(String(obj.file_path)) : "file";
7483
+ const file = obj.file_path ? path11.basename(String(obj.file_path)) : "file";
7400
7484
  const oldPreview = smartTruncate(String(obj.old_string), 120);
7401
7485
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
7402
7486
  return {
@@ -7594,22 +7678,22 @@ end run`;
7594
7678
  }
7595
7679
 
7596
7680
  // src/canary/registry.ts
7597
- import fs10 from "fs";
7681
+ import fs11 from "fs";
7598
7682
  import os9 from "os";
7599
- import path12 from "path";
7683
+ import path13 from "path";
7600
7684
 
7601
7685
  // src/shields/jail.ts
7602
- import fs9 from "fs";
7686
+ import fs10 from "fs";
7603
7687
  import os8 from "os";
7604
- import path11 from "path";
7688
+ import path12 from "path";
7605
7689
  var USER_JAIL_SHIELD = "user-jail";
7606
7690
  function jailStorePath() {
7607
- return path11.join(os8.homedir(), ".node9", "jail-paths.json");
7691
+ return path12.join(os8.homedir(), ".node9", "jail-paths.json");
7608
7692
  }
7609
7693
  function readJailPaths() {
7610
7694
  let text;
7611
7695
  try {
7612
- text = fs9.readFileSync(jailStorePath(), "utf8");
7696
+ text = fs10.readFileSync(jailStorePath(), "utf8");
7613
7697
  } catch (err) {
7614
7698
  if (err.code === "ENOENT") return [];
7615
7699
  throw err;
@@ -7638,7 +7722,7 @@ function findJailedPathIn(candidate, paths) {
7638
7722
 
7639
7723
  // src/canary/registry.ts
7640
7724
  function canaryStorePath() {
7641
- return path12.join(os9.homedir(), ".node9", "canaries.json");
7725
+ return path13.join(os9.homedir(), ".node9", "canaries.json");
7642
7726
  }
7643
7727
  function isRecord(x) {
7644
7728
  if (!x || typeof x !== "object") return false;
@@ -7649,7 +7733,7 @@ function loadCanaries(opts) {
7649
7733
  const p = canaryStorePath();
7650
7734
  let raw;
7651
7735
  try {
7652
- raw = fs10.readFileSync(p, "utf-8");
7736
+ raw = fs11.readFileSync(p, "utf-8");
7653
7737
  } catch (e) {
7654
7738
  if (e.code === "ENOENT") return [];
7655
7739
  throw e;
@@ -7676,9 +7760,50 @@ init_audit();
7676
7760
 
7677
7761
  // src/auth/cloud.ts
7678
7762
  init_audit();
7679
- import fs11 from "fs";
7763
+ import fs13 from "fs";
7680
7764
  import os10 from "os";
7681
- import path13 from "path";
7765
+ import path14 from "path";
7766
+
7767
+ // src/utils/read-capped.ts
7768
+ import fs12 from "fs";
7769
+ function readCapped(file, maxBytes) {
7770
+ if (maxBytes < 0) return null;
7771
+ let fd;
7772
+ try {
7773
+ fd = fs12.openSync(file, "r");
7774
+ } catch {
7775
+ return null;
7776
+ }
7777
+ try {
7778
+ const st = fs12.fstatSync(fd);
7779
+ if (!st.isFile()) return null;
7780
+ const want = Math.min(st.size, maxBytes + 1);
7781
+ const buf = Buffer.alloc(want);
7782
+ let read = 0;
7783
+ while (read < want) {
7784
+ const n = fs12.readSync(fd, buf, read, want - read, read);
7785
+ if (n <= 0) break;
7786
+ read += n;
7787
+ }
7788
+ return {
7789
+ bytes: buf.subarray(0, Math.min(read, maxBytes)),
7790
+ truncated: read > maxBytes
7791
+ };
7792
+ } catch {
7793
+ return null;
7794
+ } finally {
7795
+ try {
7796
+ fs12.closeSync(fd);
7797
+ } catch {
7798
+ }
7799
+ }
7800
+ }
7801
+ function readCappedText(file, maxBytes) {
7802
+ const r = readCapped(file, maxBytes);
7803
+ return r ? { text: r.bytes.toString("utf8"), truncated: r.truncated } : null;
7804
+ }
7805
+
7806
+ // src/auth/cloud.ts
7682
7807
  async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
7683
7808
  const controller = new AbortController();
7684
7809
  const timeout = setTimeout(() => controller.abort(), 1e4);
@@ -7686,10 +7811,11 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
7686
7811
  let ciContext;
7687
7812
  if (process.env.CI) {
7688
7813
  try {
7689
- const ciContextPath = path13.join(os10.homedir(), ".node9", "ci-context.json");
7690
- const stats = fs11.statSync(ciContextPath);
7691
- if (stats.size > 1e4) throw new Error("ci-context.json exceeds 10 KB");
7692
- const raw = fs11.readFileSync(ciContextPath, "utf8");
7814
+ const ciContextPath = path14.join(os10.homedir(), ".node9", "ci-context.json");
7815
+ const ci = readCappedText(ciContextPath, 1e4);
7816
+ if (!ci) throw new Error("ci-context.json unreadable");
7817
+ if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
7818
+ const raw = ci.text;
7693
7819
  const parsed = JSON.parse(raw);
7694
7820
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
7695
7821
  throw new Error("ci-context.json is not a plain object");
@@ -7788,14 +7914,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
7788
7914
  });
7789
7915
  clearTimeout(timer);
7790
7916
  if (!res.ok) {
7791
- fs11.appendFileSync(
7917
+ fs13.appendFileSync(
7792
7918
  HOOK_DEBUG_LOG,
7793
7919
  `[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
7794
7920
  `
7795
7921
  );
7796
7922
  }
7797
7923
  } catch (err) {
7798
- fs11.appendFileSync(
7924
+ fs13.appendFileSync(
7799
7925
  HOOK_DEBUG_LOG,
7800
7926
  `[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
7801
7927
  `
@@ -7804,16 +7930,16 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
7804
7930
  }
7805
7931
 
7806
7932
  // src/loop-detector.ts
7807
- import fs12 from "fs";
7808
- import path14 from "path";
7933
+ import fs14 from "fs";
7934
+ import path15 from "path";
7809
7935
  import os11 from "os";
7810
7936
  function loopStateFile() {
7811
- return path14.join(os11.homedir(), ".node9", "loop-state.json");
7937
+ return path15.join(os11.homedir(), ".node9", "loop-state.json");
7812
7938
  }
7813
7939
  function readState() {
7814
7940
  try {
7815
- if (!fs12.existsSync(loopStateFile())) return [];
7816
- const raw = fs12.readFileSync(loopStateFile(), "utf-8");
7941
+ if (!fs14.existsSync(loopStateFile())) return [];
7942
+ const raw = fs14.readFileSync(loopStateFile(), "utf-8");
7817
7943
  const parsed = JSON.parse(raw);
7818
7944
  if (!Array.isArray(parsed)) return [];
7819
7945
  return parsed;
@@ -7822,11 +7948,11 @@ function readState() {
7822
7948
  }
7823
7949
  }
7824
7950
  function writeState(records) {
7825
- const dir = path14.dirname(loopStateFile());
7826
- if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
7951
+ const dir = path15.dirname(loopStateFile());
7952
+ if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
7827
7953
  const tmpPath = `${loopStateFile()}.${os11.hostname()}.${process.pid}.tmp`;
7828
- fs12.writeFileSync(tmpPath, JSON.stringify(records));
7829
- fs12.renameSync(tmpPath, loopStateFile());
7954
+ fs14.writeFileSync(tmpPath, JSON.stringify(records));
7955
+ fs14.renameSync(tmpPath, loopStateFile());
7830
7956
  }
7831
7957
  function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
7832
7958
  try {
@@ -7894,9 +8020,13 @@ async function hasReachableHumanApprover(opts) {
7894
8020
  if (nativeReachable) return true;
7895
8021
  return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
7896
8022
  }
8023
+ function mayDowngradeHardBlock(opts) {
8024
+ if (opts.overridable === false) return false;
8025
+ return opts.daemonUp && !opts.isTestEnv && opts.humanApproverReachable;
8026
+ }
7897
8027
  async function authorizeHeadless(toolName, args, meta, options) {
7898
8028
  if (!options?.calledFromDaemon) {
7899
- const actId = randomUUID();
8029
+ const actId = randomUUID2();
7900
8030
  const actTs = Date.now();
7901
8031
  const sanitizedAgent = meta?.agent ? safeMessage(meta.agent, 80) : void 0;
7902
8032
  const sanitizedMcpServer = meta?.mcpServer ? safeMessage(meta.mcpServer, 40) : void 0;
@@ -8284,7 +8414,12 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
8284
8414
  calledFromDaemon: options?.calledFromDaemon
8285
8415
  });
8286
8416
  }
8287
- const mayDowngrade = daemonUp && !isTestEnv2 && humanApproverReachable;
8417
+ const mayDowngrade = mayDowngradeHardBlock({
8418
+ daemonUp,
8419
+ isTestEnv: isTestEnv2,
8420
+ humanApproverReachable,
8421
+ overridable: policyResult.overridable
8422
+ });
8288
8423
  const hardBlock = () => {
8289
8424
  if (!isManual)
8290
8425
  appendLocalAudit(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "2.14.2",
3
+ "version": "2.15.0",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -16,7 +16,7 @@
16
16
  "node9": "./dist/cli.js"
17
17
  },
18
18
  "engines": {
19
- "node": ">=20"
19
+ "node": ">=22"
20
20
  },
21
21
  "workspaces": [
22
22
  "packages/*"
@@ -100,7 +100,7 @@
100
100
  "mvdan-sh"
101
101
  ],
102
102
  "devDependencies": {
103
- "@anthropic-ai/sdk": "^0.124.0",
103
+ "@anthropic-ai/sdk": "^0.125.0",
104
104
  "@octokit/rest": "^22.0.1",
105
105
  "@semantic-release/commit-analyzer": "^13.0.1",
106
106
  "@semantic-release/git": "^10.0.1",
@@ -123,7 +123,8 @@
123
123
  },
124
124
  "overrides": {
125
125
  "undici": "^7.24.0",
126
- "ws": "^8.21.0"
126
+ "ws": "^8.21.0",
127
+ "esbuild": "^0.28.1"
127
128
  },
128
129
  "publishConfig": {
129
130
  "access": "public"