@node9/proxy 2.14.2 → 2.14.3

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
@@ -377,8 +377,8 @@ var ConfigFileSchema = z.object({
377
377
  }).strict();
378
378
  function formatIssues(issues) {
379
379
  const lines = issues.map((issue) => {
380
- const path15 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
381
- return ` \u2022 ${path15}: ${issue.message}`;
380
+ const path16 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
381
+ return ` \u2022 ${path16}: ${issue.message}`;
382
382
  });
383
383
  return `Invalid config:
384
384
  ${lines.join("\n")}`;
@@ -391,9 +391,9 @@ function prunePaths(root, paths) {
391
391
  const yi = y[y.length - 1];
392
392
  return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
393
393
  });
394
- for (const path15 of ordered) {
394
+ for (const path16 of ordered) {
395
395
  let cur = root;
396
- for (const key of path15.slice(0, -1)) {
396
+ for (const key of path16.slice(0, -1)) {
397
397
  if (cur === null || typeof cur !== "object") {
398
398
  cur = void 0;
399
399
  break;
@@ -401,7 +401,7 @@ function prunePaths(root, paths) {
401
401
  cur = cur[key];
402
402
  }
403
403
  if (cur === null || typeof cur !== "object") continue;
404
- const last = path15[path15.length - 1];
404
+ const last = path16[path16.length - 1];
405
405
  if (Array.isArray(cur)) {
406
406
  const i = Number(last);
407
407
  if (Number.isInteger(i) && i >= 0 && i < cur.length) {
@@ -437,7 +437,7 @@ function sanitizeConfig(raw) {
437
437
  return issue.keys.map((k) => [...at, k]);
438
438
  }
439
439
  return [at.slice(0, -level || void 0)];
440
- }).filter((path15) => path15.length > 0);
440
+ }).filter((path16) => path16.length > 0);
441
441
  if (paths.length === 0 || !prunePaths(working, paths)) break;
442
442
  }
443
443
  if (ConfigFileSchema.safeParse(working).success) break;
@@ -1349,9 +1349,9 @@ function matchesPattern(text, patterns) {
1349
1349
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1350
1350
  }
1351
1351
  var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1352
- function getNestedValue(obj, path15) {
1352
+ function getNestedValue(obj, path16) {
1353
1353
  if (!obj || typeof obj !== "object") return null;
1354
- const segments2 = path15.split(".");
1354
+ const segments2 = path16.split(".");
1355
1355
  for (const seg of segments2) {
1356
1356
  if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
1357
1357
  }
@@ -4016,9 +4016,9 @@ function bareToolName(toolName) {
4016
4016
  const parts = toolName.split("__");
4017
4017
  return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
4018
4018
  }
4019
- function valuesAt(args, path15) {
4019
+ function valuesAt(args, path16) {
4020
4020
  let cursors = [args];
4021
- for (const rawSegment of path15.split(".")) {
4021
+ for (const rawSegment of path16.split(".")) {
4022
4022
  const isArray = rawSegment.endsWith("[]");
4023
4023
  const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
4024
4024
  const next = [];
@@ -4050,15 +4050,15 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
4050
4050
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
4051
4051
  if (!paths) return null;
4052
4052
  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)) {
4053
+ for (const path16 of paths) {
4054
+ for (const value of valuesAt(args, path16)) {
4055
4055
  const host = hostOf(value);
4056
4056
  if (!host) continue;
4057
4057
  const m = classifySsrf(host);
4058
4058
  if (!m) continue;
4059
4059
  if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
4060
4060
  if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
4061
- return { ...m, argPath: path15, host, reason: ssrfReason(m, host) };
4061
+ return { ...m, argPath: path16, host, reason: ssrfReason(m, host) };
4062
4062
  }
4063
4063
  }
4064
4064
  return null;
@@ -4181,7 +4181,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4181
4181
  reason: dest.reason,
4182
4182
  ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
4183
4183
  ruleDescription: dest.reason,
4184
- tier: 3
4184
+ tier: 3,
4185
+ overridable: dest.overridable
4185
4186
  };
4186
4187
  }
4187
4188
  }
@@ -4330,7 +4331,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4330
4331
  reason: ssrf.reason,
4331
4332
  ruleName: `ssrf:${ssrf.tier}:${ssrf.binary}:${ssrf.host}`,
4332
4333
  ruleDescription: ssrf.reason,
4333
- tier: 3
4334
+ tier: 3,
4335
+ overridable: ssrf.overridable
4334
4336
  };
4335
4337
  }
4336
4338
  }
@@ -5836,6 +5838,45 @@ function isTrustedHost(host) {
5836
5838
  );
5837
5839
  }
5838
5840
 
5841
+ // src/auth/api-url.ts
5842
+ var DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
5843
+ var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]", "0.0.0.0"]);
5844
+ var HOST_ALLOW_ENV = "NODE9_API_HOST_ALLOW";
5845
+ function defaultHostSuffixes() {
5846
+ const host = new URL(DEFAULT_API_URL).hostname.toLowerCase();
5847
+ const parent = host.split(".").slice(1).join(".");
5848
+ return parent.includes(".") ? [host, parent] : [host];
5849
+ }
5850
+ function allowedSuffixes() {
5851
+ const extra = (process.env[HOST_ALLOW_ENV] ?? "").split(",").map(
5852
+ (s) => s.trim().toLowerCase().replace(/^\*?\./, "")
5853
+ ).filter(Boolean);
5854
+ return [...defaultHostSuffixes(), ...extra];
5855
+ }
5856
+ function validateApiUrl(raw) {
5857
+ if (typeof raw !== "string" || raw.length === 0 || raw.length > 2048) return null;
5858
+ let u;
5859
+ try {
5860
+ u = new URL(raw);
5861
+ } catch {
5862
+ return null;
5863
+ }
5864
+ if (u.username || u.password) return null;
5865
+ if (u.protocol !== "https:" && u.protocol !== "http:") return null;
5866
+ const host = u.hostname.toLowerCase();
5867
+ if (LOOPBACK.has(host)) return u;
5868
+ if (u.protocol !== "https:") return null;
5869
+ const suffixes = allowedSuffixes();
5870
+ const ok = suffixes.some((s) => host === s || host.endsWith("." + s));
5871
+ return ok ? u : null;
5872
+ }
5873
+ function safeApiUrl(raw, onReject) {
5874
+ const ok = validateApiUrl(raw);
5875
+ if (ok) return typeof raw === "string" ? raw : ok.toString();
5876
+ onReject?.(raw);
5877
+ return DEFAULT_API_URL;
5878
+ }
5879
+
5839
5880
  // src/config/index.ts
5840
5881
  function sanitizeSsrfAllow(entries, source) {
5841
5882
  const kept = [];
@@ -6124,12 +6165,27 @@ var ADVISORY_SMART_RULES = [
6124
6165
  }
6125
6166
  ];
6126
6167
  var cachedConfig = null;
6168
+ var rejectedApiUrlsSeen = /* @__PURE__ */ new Set();
6169
+ function noteRejectedApiUrl(raw) {
6170
+ const key = String(raw).slice(0, 200);
6171
+ if (rejectedApiUrlsSeen.has(key)) return;
6172
+ rejectedApiUrlsSeen.add(key);
6173
+ try {
6174
+ fs4.appendFileSync(
6175
+ path4.join(os4.homedir(), ".node9", "hook-debug.log"),
6176
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] REFUSED apiUrl, using the default instead: ${String(raw).slice(0, 200).replace(/[\r\n]+/g, " ")}
6177
+ `
6178
+ );
6179
+ } catch {
6180
+ }
6181
+ }
6127
6182
  function getCredentials() {
6128
- const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
6129
6183
  if (process.env.NODE9_API_KEY) {
6130
6184
  return {
6131
6185
  apiKey: process.env.NODE9_API_KEY,
6132
- apiUrl: process.env.NODE9_API_URL || DEFAULT_API_URL
6186
+ // NODE9_API_URL is env, and a hook inherits the agent's env, so it is
6187
+ // no more trusted than the file below.
6188
+ apiUrl: safeApiUrl(process.env.NODE9_API_URL || DEFAULT_API_URL, noteRejectedApiUrl)
6133
6189
  };
6134
6190
  }
6135
6191
  try {
@@ -6141,14 +6197,14 @@ function getCredentials() {
6141
6197
  if (profile?.apiKey) {
6142
6198
  return {
6143
6199
  apiKey: profile.apiKey,
6144
- apiUrl: profile.apiUrl || DEFAULT_API_URL,
6200
+ apiUrl: safeApiUrl(profile.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
6145
6201
  localOnly: profile.localOnly === true || profileName !== "default"
6146
6202
  };
6147
6203
  }
6148
6204
  if (creds.apiKey) {
6149
6205
  return {
6150
6206
  apiKey: creds.apiKey,
6151
- apiUrl: creds.apiUrl || DEFAULT_API_URL,
6207
+ apiUrl: safeApiUrl(creds.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
6152
6208
  localOnly: creds.localOnly === true
6153
6209
  };
6154
6210
  }
@@ -6581,13 +6637,13 @@ function getConfig(cwd) {
6581
6637
  }
6582
6638
  if (Array.isArray(mc.jailPaths)) {
6583
6639
  for (const jp of mc.jailPaths) {
6584
- const path15 = typeof jp?.path === "string" ? jp.path.trim() : "";
6585
- if (!path15) continue;
6640
+ const path16 = typeof jp?.path === "string" ? jp.path.trim() : "";
6641
+ if (!path16) continue;
6586
6642
  const verdict = jp?.verdict === "review" ? "review" : "block";
6587
- for (const r of pathRules(path15, verdict, "org-managed jail")) {
6643
+ for (const r of pathRules(path16, verdict, "org-managed jail")) {
6588
6644
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
6589
6645
  }
6590
- mergedPolicy.managedJailPaths.push({ path: path15, verdict });
6646
+ mergedPolicy.managedJailPaths.push({ path: path16, verdict });
6591
6647
  }
6592
6648
  }
6593
6649
  if (Array.isArray(mc.trustedHosts)) {
@@ -6924,11 +6980,41 @@ function isIgnoredTool2(toolName) {
6924
6980
  }
6925
6981
 
6926
6982
  // src/auth/state.ts
6983
+ import fs8 from "fs";
6984
+ import path8 from "path";
6985
+ import os6 from "os";
6986
+
6987
+ // src/utils/atomic-write.ts
6927
6988
  import fs7 from "fs";
6928
6989
  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");
6990
+ import { randomUUID } from "crypto";
6991
+ function atomicWriteSync(filePath, data, options) {
6992
+ const dir = path7.dirname(filePath);
6993
+ if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
6994
+ const tmpPath = `${filePath}.${randomUUID()}.tmp`;
6995
+ try {
6996
+ fs7.writeFileSync(tmpPath, data, options);
6997
+ } catch (err) {
6998
+ try {
6999
+ fs7.unlinkSync(tmpPath);
7000
+ } catch {
7001
+ }
7002
+ throw err;
7003
+ }
7004
+ try {
7005
+ fs7.renameSync(tmpPath, filePath);
7006
+ } catch (err) {
7007
+ try {
7008
+ fs7.unlinkSync(tmpPath);
7009
+ } catch {
7010
+ }
7011
+ throw err;
7012
+ }
7013
+ }
7014
+
7015
+ // src/auth/state.ts
7016
+ var PAUSED_FILE = path8.join(os6.homedir(), ".node9", "PAUSED");
7017
+ var TRUST_FILE = path8.join(os6.homedir(), ".node9", "trust.json");
6932
7018
  function extractCommandPattern(toolName, args) {
6933
7019
  const lower = toolName.toLowerCase();
6934
7020
  if (lower !== "bash" && lower !== "execute_bash" && lower !== "shell") return void 0;
@@ -6940,11 +7026,11 @@ function extractCommandPattern(toolName, args) {
6940
7026
  }
6941
7027
  function checkPause() {
6942
7028
  try {
6943
- if (!fs7.existsSync(PAUSED_FILE)) return { paused: false };
6944
- const state = JSON.parse(fs7.readFileSync(PAUSED_FILE, "utf-8"));
7029
+ if (!fs8.existsSync(PAUSED_FILE)) return { paused: false };
7030
+ const state = JSON.parse(fs8.readFileSync(PAUSED_FILE, "utf-8"));
6945
7031
  if (state.expiry > 0 && Date.now() >= state.expiry) {
6946
7032
  try {
6947
- fs7.unlinkSync(PAUSED_FILE);
7033
+ fs8.unlinkSync(PAUSED_FILE);
6948
7034
  } catch {
6949
7035
  }
6950
7036
  return { paused: false };
@@ -6954,21 +7040,14 @@ function checkPause() {
6954
7040
  return { paused: false };
6955
7041
  }
6956
7042
  }
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
7043
  function getActiveTrustSession(toolName, args) {
6965
7044
  try {
6966
- if (!fs7.existsSync(TRUST_FILE)) return false;
6967
- const trust = JSON.parse(fs7.readFileSync(TRUST_FILE, "utf-8"));
7045
+ if (!fs8.existsSync(TRUST_FILE)) return false;
7046
+ const trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
6968
7047
  const now = Date.now();
6969
7048
  const active = trust.entries.filter((e) => e.expiry > now);
6970
7049
  if (active.length !== trust.entries.length) {
6971
- fs7.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
7050
+ atomicWriteSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
6972
7051
  }
6973
7052
  return active.some((e) => {
6974
7053
  if (!(e.tool === toolName || matchesPattern(toolName, e.tool))) return false;
@@ -6987,8 +7066,8 @@ function writeTrustSession(toolName, durationMs, args) {
6987
7066
  try {
6988
7067
  let trust = { entries: [] };
6989
7068
  try {
6990
- if (fs7.existsSync(TRUST_FILE)) {
6991
- trust = JSON.parse(fs7.readFileSync(TRUST_FILE, "utf-8"));
7069
+ if (fs8.existsSync(TRUST_FILE)) {
7070
+ trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
6992
7071
  }
6993
7072
  } catch {
6994
7073
  }
@@ -7010,9 +7089,9 @@ function writeTrustSession(toolName, durationMs, args) {
7010
7089
  }
7011
7090
  function getPersistentDecision(toolName) {
7012
7091
  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"));
7092
+ const file = path8.join(os6.homedir(), ".node9", "decisions.json");
7093
+ if (!fs8.existsSync(file)) return null;
7094
+ const decisions = JSON.parse(fs8.readFileSync(file, "utf-8"));
7016
7095
  const d = decisions[toolName];
7017
7096
  if (d === "allow" || d === "deny") return d;
7018
7097
  } catch {
@@ -7021,11 +7100,11 @@ function getPersistentDecision(toolName) {
7021
7100
  }
7022
7101
 
7023
7102
  // src/auth/daemon.ts
7024
- import fs8 from "fs";
7103
+ import fs9 from "fs";
7025
7104
  import net from "net";
7026
- import path8 from "path";
7105
+ import path9 from "path";
7027
7106
  import os7 from "os";
7028
- var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path8.join(os7.tmpdir(), "node9-activity.sock");
7107
+ var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path9.join(os7.tmpdir(), "node9-activity.sock");
7029
7108
  function notifyActivitySocket(data) {
7030
7109
  return new Promise((resolve) => {
7031
7110
  try {
@@ -7058,9 +7137,9 @@ var DAEMON_PORT = 7391;
7058
7137
  var DAEMON_HOST = "127.0.0.1";
7059
7138
  function getInternalToken() {
7060
7139
  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"));
7140
+ const pidFile = path9.join(os7.homedir(), ".node9", "daemon.pid");
7141
+ if (!fs9.existsSync(pidFile)) return null;
7142
+ const data = JSON.parse(fs9.readFileSync(pidFile, "utf-8"));
7064
7143
  process.kill(data.pid, 0);
7065
7144
  return data.internalToken ?? null;
7066
7145
  } catch {
@@ -7068,12 +7147,12 @@ function getInternalToken() {
7068
7147
  }
7069
7148
  }
7070
7149
  function isDaemonRunning() {
7071
- const pidFile = path8.join(os7.homedir(), ".node9", "daemon.pid");
7072
- if (fs8.existsSync(pidFile)) {
7150
+ const pidFile = path9.join(os7.homedir(), ".node9", "daemon.pid");
7151
+ if (fs9.existsSync(pidFile)) {
7073
7152
  let pid;
7074
7153
  let port;
7075
7154
  try {
7076
- const data = JSON.parse(fs8.readFileSync(pidFile, "utf-8"));
7155
+ const data = JSON.parse(fs9.readFileSync(pidFile, "utf-8"));
7077
7156
  pid = data.pid;
7078
7157
  port = data.port;
7079
7158
  } catch {
@@ -7091,7 +7170,7 @@ function isDaemonRunning() {
7091
7170
  } catch (err) {
7092
7171
  if (err instanceof Error && "code" in err && err.code === "ESRCH") {
7093
7172
  try {
7094
- fs8.unlinkSync(pidFile);
7173
+ fs9.unlinkSync(pidFile);
7095
7174
  } catch {
7096
7175
  }
7097
7176
  }
@@ -7257,14 +7336,14 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
7257
7336
  }
7258
7337
 
7259
7338
  // src/auth/orchestrator.ts
7260
- import { randomUUID } from "crypto";
7339
+ import { randomUUID as randomUUID2 } from "crypto";
7261
7340
 
7262
7341
  // src/ui/native.ts
7263
7342
  import { spawn } from "child_process";
7264
- import path10 from "path";
7343
+ import path11 from "path";
7265
7344
 
7266
7345
  // src/context-sniper.ts
7267
- import path9 from "path";
7346
+ import path10 from "path";
7268
7347
  function smartTruncate(str, maxLen = 500) {
7269
7348
  if (str.length <= maxLen) return str;
7270
7349
  const edge = Math.floor(maxLen / 2) - 3;
@@ -7332,7 +7411,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
7332
7411
  intent = "EDIT";
7333
7412
  if (obj.file_path) {
7334
7413
  editFilePath = String(obj.file_path);
7335
- editFileName = path9.basename(editFilePath);
7414
+ editFileName = path10.basename(editFilePath);
7336
7415
  }
7337
7416
  const result = extractContext(String(obj.new_string), matchedWord);
7338
7417
  contextSnippet = result.snippet;
@@ -7396,7 +7475,7 @@ function formatArgs(args, matchedField, matchedWord) {
7396
7475
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
7397
7476
  const obj = parsed;
7398
7477
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
7399
- const file = obj.file_path ? path10.basename(String(obj.file_path)) : "file";
7478
+ const file = obj.file_path ? path11.basename(String(obj.file_path)) : "file";
7400
7479
  const oldPreview = smartTruncate(String(obj.old_string), 120);
7401
7480
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
7402
7481
  return {
@@ -7594,22 +7673,22 @@ end run`;
7594
7673
  }
7595
7674
 
7596
7675
  // src/canary/registry.ts
7597
- import fs10 from "fs";
7676
+ import fs11 from "fs";
7598
7677
  import os9 from "os";
7599
- import path12 from "path";
7678
+ import path13 from "path";
7600
7679
 
7601
7680
  // src/shields/jail.ts
7602
- import fs9 from "fs";
7681
+ import fs10 from "fs";
7603
7682
  import os8 from "os";
7604
- import path11 from "path";
7683
+ import path12 from "path";
7605
7684
  var USER_JAIL_SHIELD = "user-jail";
7606
7685
  function jailStorePath() {
7607
- return path11.join(os8.homedir(), ".node9", "jail-paths.json");
7686
+ return path12.join(os8.homedir(), ".node9", "jail-paths.json");
7608
7687
  }
7609
7688
  function readJailPaths() {
7610
7689
  let text;
7611
7690
  try {
7612
- text = fs9.readFileSync(jailStorePath(), "utf8");
7691
+ text = fs10.readFileSync(jailStorePath(), "utf8");
7613
7692
  } catch (err) {
7614
7693
  if (err.code === "ENOENT") return [];
7615
7694
  throw err;
@@ -7638,7 +7717,7 @@ function findJailedPathIn(candidate, paths) {
7638
7717
 
7639
7718
  // src/canary/registry.ts
7640
7719
  function canaryStorePath() {
7641
- return path12.join(os9.homedir(), ".node9", "canaries.json");
7720
+ return path13.join(os9.homedir(), ".node9", "canaries.json");
7642
7721
  }
7643
7722
  function isRecord(x) {
7644
7723
  if (!x || typeof x !== "object") return false;
@@ -7649,7 +7728,7 @@ function loadCanaries(opts) {
7649
7728
  const p = canaryStorePath();
7650
7729
  let raw;
7651
7730
  try {
7652
- raw = fs10.readFileSync(p, "utf-8");
7731
+ raw = fs11.readFileSync(p, "utf-8");
7653
7732
  } catch (e) {
7654
7733
  if (e.code === "ENOENT") return [];
7655
7734
  throw e;
@@ -7676,9 +7755,50 @@ init_audit();
7676
7755
 
7677
7756
  // src/auth/cloud.ts
7678
7757
  init_audit();
7679
- import fs11 from "fs";
7758
+ import fs13 from "fs";
7680
7759
  import os10 from "os";
7681
- import path13 from "path";
7760
+ import path14 from "path";
7761
+
7762
+ // src/utils/read-capped.ts
7763
+ import fs12 from "fs";
7764
+ function readCapped(file, maxBytes) {
7765
+ if (maxBytes < 0) return null;
7766
+ let fd;
7767
+ try {
7768
+ fd = fs12.openSync(file, "r");
7769
+ } catch {
7770
+ return null;
7771
+ }
7772
+ try {
7773
+ const st = fs12.fstatSync(fd);
7774
+ if (!st.isFile()) return null;
7775
+ const want = Math.min(st.size, maxBytes + 1);
7776
+ const buf = Buffer.alloc(want);
7777
+ let read = 0;
7778
+ while (read < want) {
7779
+ const n = fs12.readSync(fd, buf, read, want - read, read);
7780
+ if (n <= 0) break;
7781
+ read += n;
7782
+ }
7783
+ return {
7784
+ bytes: buf.subarray(0, Math.min(read, maxBytes)),
7785
+ truncated: read > maxBytes
7786
+ };
7787
+ } catch {
7788
+ return null;
7789
+ } finally {
7790
+ try {
7791
+ fs12.closeSync(fd);
7792
+ } catch {
7793
+ }
7794
+ }
7795
+ }
7796
+ function readCappedText(file, maxBytes) {
7797
+ const r = readCapped(file, maxBytes);
7798
+ return r ? { text: r.bytes.toString("utf8"), truncated: r.truncated } : null;
7799
+ }
7800
+
7801
+ // src/auth/cloud.ts
7682
7802
  async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
7683
7803
  const controller = new AbortController();
7684
7804
  const timeout = setTimeout(() => controller.abort(), 1e4);
@@ -7686,10 +7806,11 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
7686
7806
  let ciContext;
7687
7807
  if (process.env.CI) {
7688
7808
  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");
7809
+ const ciContextPath = path14.join(os10.homedir(), ".node9", "ci-context.json");
7810
+ const ci = readCappedText(ciContextPath, 1e4);
7811
+ if (!ci) throw new Error("ci-context.json unreadable");
7812
+ if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
7813
+ const raw = ci.text;
7693
7814
  const parsed = JSON.parse(raw);
7694
7815
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
7695
7816
  throw new Error("ci-context.json is not a plain object");
@@ -7788,14 +7909,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
7788
7909
  });
7789
7910
  clearTimeout(timer);
7790
7911
  if (!res.ok) {
7791
- fs11.appendFileSync(
7912
+ fs13.appendFileSync(
7792
7913
  HOOK_DEBUG_LOG,
7793
7914
  `[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
7794
7915
  `
7795
7916
  );
7796
7917
  }
7797
7918
  } catch (err) {
7798
- fs11.appendFileSync(
7919
+ fs13.appendFileSync(
7799
7920
  HOOK_DEBUG_LOG,
7800
7921
  `[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
7801
7922
  `
@@ -7804,16 +7925,16 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
7804
7925
  }
7805
7926
 
7806
7927
  // src/loop-detector.ts
7807
- import fs12 from "fs";
7808
- import path14 from "path";
7928
+ import fs14 from "fs";
7929
+ import path15 from "path";
7809
7930
  import os11 from "os";
7810
7931
  function loopStateFile() {
7811
- return path14.join(os11.homedir(), ".node9", "loop-state.json");
7932
+ return path15.join(os11.homedir(), ".node9", "loop-state.json");
7812
7933
  }
7813
7934
  function readState() {
7814
7935
  try {
7815
- if (!fs12.existsSync(loopStateFile())) return [];
7816
- const raw = fs12.readFileSync(loopStateFile(), "utf-8");
7936
+ if (!fs14.existsSync(loopStateFile())) return [];
7937
+ const raw = fs14.readFileSync(loopStateFile(), "utf-8");
7817
7938
  const parsed = JSON.parse(raw);
7818
7939
  if (!Array.isArray(parsed)) return [];
7819
7940
  return parsed;
@@ -7822,11 +7943,11 @@ function readState() {
7822
7943
  }
7823
7944
  }
7824
7945
  function writeState(records) {
7825
- const dir = path14.dirname(loopStateFile());
7826
- if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
7946
+ const dir = path15.dirname(loopStateFile());
7947
+ if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
7827
7948
  const tmpPath = `${loopStateFile()}.${os11.hostname()}.${process.pid}.tmp`;
7828
- fs12.writeFileSync(tmpPath, JSON.stringify(records));
7829
- fs12.renameSync(tmpPath, loopStateFile());
7949
+ fs14.writeFileSync(tmpPath, JSON.stringify(records));
7950
+ fs14.renameSync(tmpPath, loopStateFile());
7830
7951
  }
7831
7952
  function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
7832
7953
  try {
@@ -7894,9 +8015,13 @@ async function hasReachableHumanApprover(opts) {
7894
8015
  if (nativeReachable) return true;
7895
8016
  return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
7896
8017
  }
8018
+ function mayDowngradeHardBlock(opts) {
8019
+ if (opts.overridable === false) return false;
8020
+ return opts.daemonUp && !opts.isTestEnv && opts.humanApproverReachable;
8021
+ }
7897
8022
  async function authorizeHeadless(toolName, args, meta, options) {
7898
8023
  if (!options?.calledFromDaemon) {
7899
- const actId = randomUUID();
8024
+ const actId = randomUUID2();
7900
8025
  const actTs = Date.now();
7901
8026
  const sanitizedAgent = meta?.agent ? safeMessage(meta.agent, 80) : void 0;
7902
8027
  const sanitizedMcpServer = meta?.mcpServer ? safeMessage(meta.mcpServer, 40) : void 0;
@@ -8284,7 +8409,12 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
8284
8409
  calledFromDaemon: options?.calledFromDaemon
8285
8410
  });
8286
8411
  }
8287
- const mayDowngrade = daemonUp && !isTestEnv2 && humanApproverReachable;
8412
+ const mayDowngrade = mayDowngradeHardBlock({
8413
+ daemonUp,
8414
+ isTestEnv: isTestEnv2,
8415
+ humanApproverReachable,
8416
+ overridable: policyResult.overridable
8417
+ });
8288
8418
  const hardBlock = () => {
8289
8419
  if (!isManual)
8290
8420
  appendLocalAudit(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "2.14.2",
3
+ "version": "2.14.3",
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",