@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.js CHANGED
@@ -407,8 +407,8 @@ var ConfigFileSchema = import_zod.z.object({
407
407
  }).strict();
408
408
  function formatIssues(issues) {
409
409
  const lines = issues.map((issue) => {
410
- const path15 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
411
- return ` \u2022 ${path15}: ${issue.message}`;
410
+ const path16 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
411
+ return ` \u2022 ${path16}: ${issue.message}`;
412
412
  });
413
413
  return `Invalid config:
414
414
  ${lines.join("\n")}`;
@@ -421,9 +421,9 @@ function prunePaths(root, paths) {
421
421
  const yi = y[y.length - 1];
422
422
  return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
423
423
  });
424
- for (const path15 of ordered) {
424
+ for (const path16 of ordered) {
425
425
  let cur = root;
426
- for (const key of path15.slice(0, -1)) {
426
+ for (const key of path16.slice(0, -1)) {
427
427
  if (cur === null || typeof cur !== "object") {
428
428
  cur = void 0;
429
429
  break;
@@ -431,7 +431,7 @@ function prunePaths(root, paths) {
431
431
  cur = cur[key];
432
432
  }
433
433
  if (cur === null || typeof cur !== "object") continue;
434
- const last = path15[path15.length - 1];
434
+ const last = path16[path16.length - 1];
435
435
  if (Array.isArray(cur)) {
436
436
  const i = Number(last);
437
437
  if (Number.isInteger(i) && i >= 0 && i < cur.length) {
@@ -467,7 +467,7 @@ function sanitizeConfig(raw) {
467
467
  return issue.keys.map((k) => [...at, k]);
468
468
  }
469
469
  return [at.slice(0, -level || void 0)];
470
- }).filter((path15) => path15.length > 0);
470
+ }).filter((path16) => path16.length > 0);
471
471
  if (paths.length === 0 || !prunePaths(working, paths)) break;
472
472
  }
473
473
  if (ConfigFileSchema.safeParse(working).success) break;
@@ -1379,9 +1379,9 @@ function matchesPattern(text, patterns) {
1379
1379
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1380
1380
  }
1381
1381
  var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1382
- function getNestedValue(obj, path15) {
1382
+ function getNestedValue(obj, path16) {
1383
1383
  if (!obj || typeof obj !== "object") return null;
1384
- const segments2 = path15.split(".");
1384
+ const segments2 = path16.split(".");
1385
1385
  for (const seg of segments2) {
1386
1386
  if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
1387
1387
  }
@@ -4046,9 +4046,9 @@ function bareToolName(toolName) {
4046
4046
  const parts = toolName.split("__");
4047
4047
  return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
4048
4048
  }
4049
- function valuesAt(args, path15) {
4049
+ function valuesAt(args, path16) {
4050
4050
  let cursors = [args];
4051
- for (const rawSegment of path15.split(".")) {
4051
+ for (const rawSegment of path16.split(".")) {
4052
4052
  const isArray = rawSegment.endsWith("[]");
4053
4053
  const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
4054
4054
  const next = [];
@@ -4080,15 +4080,15 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
4080
4080
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
4081
4081
  if (!paths) return null;
4082
4082
  const exempt = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
4083
- for (const path15 of paths) {
4084
- for (const value of valuesAt(args, path15)) {
4083
+ for (const path16 of paths) {
4084
+ for (const value of valuesAt(args, path16)) {
4085
4085
  const host = hostOf(value);
4086
4086
  if (!host) continue;
4087
4087
  const m = classifySsrf(host);
4088
4088
  if (!m) continue;
4089
4089
  if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
4090
4090
  if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
4091
- return { ...m, argPath: path15, host, reason: ssrfReason(m, host) };
4091
+ return { ...m, argPath: path16, host, reason: ssrfReason(m, host) };
4092
4092
  }
4093
4093
  }
4094
4094
  return null;
@@ -4211,7 +4211,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4211
4211
  reason: dest.reason,
4212
4212
  ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
4213
4213
  ruleDescription: dest.reason,
4214
- tier: 3
4214
+ tier: 3,
4215
+ overridable: dest.overridable
4215
4216
  };
4216
4217
  }
4217
4218
  }
@@ -4360,7 +4361,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4360
4361
  reason: ssrf.reason,
4361
4362
  ruleName: `ssrf:${ssrf.tier}:${ssrf.binary}:${ssrf.host}`,
4362
4363
  ruleDescription: ssrf.reason,
4363
- tier: 3
4364
+ tier: 3,
4365
+ overridable: ssrf.overridable
4364
4366
  };
4365
4367
  }
4366
4368
  }
@@ -5866,6 +5868,45 @@ function isTrustedHost(host) {
5866
5868
  );
5867
5869
  }
5868
5870
 
5871
+ // src/auth/api-url.ts
5872
+ var DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
5873
+ var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]", "0.0.0.0"]);
5874
+ var HOST_ALLOW_ENV = "NODE9_API_HOST_ALLOW";
5875
+ function defaultHostSuffixes() {
5876
+ const host = new URL(DEFAULT_API_URL).hostname.toLowerCase();
5877
+ const parent = host.split(".").slice(1).join(".");
5878
+ return parent.includes(".") ? [host, parent] : [host];
5879
+ }
5880
+ function allowedSuffixes() {
5881
+ const extra = (process.env[HOST_ALLOW_ENV] ?? "").split(",").map(
5882
+ (s) => s.trim().toLowerCase().replace(/^\*?\./, "")
5883
+ ).filter(Boolean);
5884
+ return [...defaultHostSuffixes(), ...extra];
5885
+ }
5886
+ function validateApiUrl(raw) {
5887
+ if (typeof raw !== "string" || raw.length === 0 || raw.length > 2048) return null;
5888
+ let u;
5889
+ try {
5890
+ u = new URL(raw);
5891
+ } catch {
5892
+ return null;
5893
+ }
5894
+ if (u.username || u.password) return null;
5895
+ if (u.protocol !== "https:" && u.protocol !== "http:") return null;
5896
+ const host = u.hostname.toLowerCase();
5897
+ if (LOOPBACK.has(host)) return u;
5898
+ if (u.protocol !== "https:") return null;
5899
+ const suffixes = allowedSuffixes();
5900
+ const ok = suffixes.some((s) => host === s || host.endsWith("." + s));
5901
+ return ok ? u : null;
5902
+ }
5903
+ function safeApiUrl(raw, onReject) {
5904
+ const ok = validateApiUrl(raw);
5905
+ if (ok) return typeof raw === "string" ? raw : ok.toString();
5906
+ onReject?.(raw);
5907
+ return DEFAULT_API_URL;
5908
+ }
5909
+
5869
5910
  // src/config/index.ts
5870
5911
  function sanitizeSsrfAllow(entries, source) {
5871
5912
  const kept = [];
@@ -6154,12 +6195,27 @@ var ADVISORY_SMART_RULES = [
6154
6195
  }
6155
6196
  ];
6156
6197
  var cachedConfig = null;
6198
+ var rejectedApiUrlsSeen = /* @__PURE__ */ new Set();
6199
+ function noteRejectedApiUrl(raw) {
6200
+ const key = String(raw).slice(0, 200);
6201
+ if (rejectedApiUrlsSeen.has(key)) return;
6202
+ rejectedApiUrlsSeen.add(key);
6203
+ try {
6204
+ import_fs4.default.appendFileSync(
6205
+ import_path4.default.join(import_os4.default.homedir(), ".node9", "hook-debug.log"),
6206
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] REFUSED apiUrl, using the default instead: ${String(raw).slice(0, 200).replace(/[\r\n]+/g, " ")}
6207
+ `
6208
+ );
6209
+ } catch {
6210
+ }
6211
+ }
6157
6212
  function getCredentials() {
6158
- const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
6159
6213
  if (process.env.NODE9_API_KEY) {
6160
6214
  return {
6161
6215
  apiKey: process.env.NODE9_API_KEY,
6162
- apiUrl: process.env.NODE9_API_URL || DEFAULT_API_URL
6216
+ // NODE9_API_URL is env, and a hook inherits the agent's env, so it is
6217
+ // no more trusted than the file below.
6218
+ apiUrl: safeApiUrl(process.env.NODE9_API_URL || DEFAULT_API_URL, noteRejectedApiUrl)
6163
6219
  };
6164
6220
  }
6165
6221
  try {
@@ -6171,14 +6227,14 @@ function getCredentials() {
6171
6227
  if (profile?.apiKey) {
6172
6228
  return {
6173
6229
  apiKey: profile.apiKey,
6174
- apiUrl: profile.apiUrl || DEFAULT_API_URL,
6230
+ apiUrl: safeApiUrl(profile.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
6175
6231
  localOnly: profile.localOnly === true || profileName !== "default"
6176
6232
  };
6177
6233
  }
6178
6234
  if (creds.apiKey) {
6179
6235
  return {
6180
6236
  apiKey: creds.apiKey,
6181
- apiUrl: creds.apiUrl || DEFAULT_API_URL,
6237
+ apiUrl: safeApiUrl(creds.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
6182
6238
  localOnly: creds.localOnly === true
6183
6239
  };
6184
6240
  }
@@ -6611,13 +6667,13 @@ function getConfig(cwd) {
6611
6667
  }
6612
6668
  if (Array.isArray(mc.jailPaths)) {
6613
6669
  for (const jp of mc.jailPaths) {
6614
- const path15 = typeof jp?.path === "string" ? jp.path.trim() : "";
6615
- if (!path15) continue;
6670
+ const path16 = typeof jp?.path === "string" ? jp.path.trim() : "";
6671
+ if (!path16) continue;
6616
6672
  const verdict = jp?.verdict === "review" ? "review" : "block";
6617
- for (const r of pathRules(path15, verdict, "org-managed jail")) {
6673
+ for (const r of pathRules(path16, verdict, "org-managed jail")) {
6618
6674
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
6619
6675
  }
6620
- mergedPolicy.managedJailPaths.push({ path: path15, verdict });
6676
+ mergedPolicy.managedJailPaths.push({ path: path16, verdict });
6621
6677
  }
6622
6678
  }
6623
6679
  if (Array.isArray(mc.trustedHosts)) {
@@ -6954,11 +7010,41 @@ function isIgnoredTool2(toolName) {
6954
7010
  }
6955
7011
 
6956
7012
  // src/auth/state.ts
7013
+ var import_fs8 = __toESM(require("fs"));
7014
+ var import_path8 = __toESM(require("path"));
7015
+ var import_os6 = __toESM(require("os"));
7016
+
7017
+ // src/utils/atomic-write.ts
6957
7018
  var import_fs7 = __toESM(require("fs"));
6958
7019
  var import_path7 = __toESM(require("path"));
6959
- var import_os6 = __toESM(require("os"));
6960
- var PAUSED_FILE = import_path7.default.join(import_os6.default.homedir(), ".node9", "PAUSED");
6961
- var TRUST_FILE = import_path7.default.join(import_os6.default.homedir(), ".node9", "trust.json");
7020
+ var import_crypto5 = require("crypto");
7021
+ function atomicWriteSync(filePath, data, options) {
7022
+ const dir = import_path7.default.dirname(filePath);
7023
+ if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
7024
+ const tmpPath = `${filePath}.${(0, import_crypto5.randomUUID)()}.tmp`;
7025
+ try {
7026
+ import_fs7.default.writeFileSync(tmpPath, data, options);
7027
+ } catch (err) {
7028
+ try {
7029
+ import_fs7.default.unlinkSync(tmpPath);
7030
+ } catch {
7031
+ }
7032
+ throw err;
7033
+ }
7034
+ try {
7035
+ import_fs7.default.renameSync(tmpPath, filePath);
7036
+ } catch (err) {
7037
+ try {
7038
+ import_fs7.default.unlinkSync(tmpPath);
7039
+ } catch {
7040
+ }
7041
+ throw err;
7042
+ }
7043
+ }
7044
+
7045
+ // src/auth/state.ts
7046
+ var PAUSED_FILE = import_path8.default.join(import_os6.default.homedir(), ".node9", "PAUSED");
7047
+ var TRUST_FILE = import_path8.default.join(import_os6.default.homedir(), ".node9", "trust.json");
6962
7048
  function extractCommandPattern(toolName, args) {
6963
7049
  const lower = toolName.toLowerCase();
6964
7050
  if (lower !== "bash" && lower !== "execute_bash" && lower !== "shell") return void 0;
@@ -6970,11 +7056,11 @@ function extractCommandPattern(toolName, args) {
6970
7056
  }
6971
7057
  function checkPause() {
6972
7058
  try {
6973
- if (!import_fs7.default.existsSync(PAUSED_FILE)) return { paused: false };
6974
- const state = JSON.parse(import_fs7.default.readFileSync(PAUSED_FILE, "utf-8"));
7059
+ if (!import_fs8.default.existsSync(PAUSED_FILE)) return { paused: false };
7060
+ const state = JSON.parse(import_fs8.default.readFileSync(PAUSED_FILE, "utf-8"));
6975
7061
  if (state.expiry > 0 && Date.now() >= state.expiry) {
6976
7062
  try {
6977
- import_fs7.default.unlinkSync(PAUSED_FILE);
7063
+ import_fs8.default.unlinkSync(PAUSED_FILE);
6978
7064
  } catch {
6979
7065
  }
6980
7066
  return { paused: false };
@@ -6984,21 +7070,14 @@ function checkPause() {
6984
7070
  return { paused: false };
6985
7071
  }
6986
7072
  }
6987
- function atomicWriteSync(filePath, data, options) {
6988
- const dir = import_path7.default.dirname(filePath);
6989
- if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
6990
- const tmpPath = `${filePath}.${import_os6.default.hostname()}.${process.pid}.tmp`;
6991
- import_fs7.default.writeFileSync(tmpPath, data, options);
6992
- import_fs7.default.renameSync(tmpPath, filePath);
6993
- }
6994
7073
  function getActiveTrustSession(toolName, args) {
6995
7074
  try {
6996
- if (!import_fs7.default.existsSync(TRUST_FILE)) return false;
6997
- const trust = JSON.parse(import_fs7.default.readFileSync(TRUST_FILE, "utf-8"));
7075
+ if (!import_fs8.default.existsSync(TRUST_FILE)) return false;
7076
+ const trust = JSON.parse(import_fs8.default.readFileSync(TRUST_FILE, "utf-8"));
6998
7077
  const now = Date.now();
6999
7078
  const active = trust.entries.filter((e) => e.expiry > now);
7000
7079
  if (active.length !== trust.entries.length) {
7001
- import_fs7.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
7080
+ atomicWriteSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
7002
7081
  }
7003
7082
  return active.some((e) => {
7004
7083
  if (!(e.tool === toolName || matchesPattern(toolName, e.tool))) return false;
@@ -7017,8 +7096,8 @@ function writeTrustSession(toolName, durationMs, args) {
7017
7096
  try {
7018
7097
  let trust = { entries: [] };
7019
7098
  try {
7020
- if (import_fs7.default.existsSync(TRUST_FILE)) {
7021
- trust = JSON.parse(import_fs7.default.readFileSync(TRUST_FILE, "utf-8"));
7099
+ if (import_fs8.default.existsSync(TRUST_FILE)) {
7100
+ trust = JSON.parse(import_fs8.default.readFileSync(TRUST_FILE, "utf-8"));
7022
7101
  }
7023
7102
  } catch {
7024
7103
  }
@@ -7040,9 +7119,9 @@ function writeTrustSession(toolName, durationMs, args) {
7040
7119
  }
7041
7120
  function getPersistentDecision(toolName) {
7042
7121
  try {
7043
- const file = import_path7.default.join(import_os6.default.homedir(), ".node9", "decisions.json");
7044
- if (!import_fs7.default.existsSync(file)) return null;
7045
- const decisions = JSON.parse(import_fs7.default.readFileSync(file, "utf-8"));
7122
+ const file = import_path8.default.join(import_os6.default.homedir(), ".node9", "decisions.json");
7123
+ if (!import_fs8.default.existsSync(file)) return null;
7124
+ const decisions = JSON.parse(import_fs8.default.readFileSync(file, "utf-8"));
7046
7125
  const d = decisions[toolName];
7047
7126
  if (d === "allow" || d === "deny") return d;
7048
7127
  } catch {
@@ -7051,11 +7130,11 @@ function getPersistentDecision(toolName) {
7051
7130
  }
7052
7131
 
7053
7132
  // src/auth/daemon.ts
7054
- var import_fs8 = __toESM(require("fs"));
7133
+ var import_fs9 = __toESM(require("fs"));
7055
7134
  var import_net = __toESM(require("net"));
7056
- var import_path8 = __toESM(require("path"));
7135
+ var import_path9 = __toESM(require("path"));
7057
7136
  var import_os7 = __toESM(require("os"));
7058
- var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path8.default.join(import_os7.default.tmpdir(), "node9-activity.sock");
7137
+ var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path9.default.join(import_os7.default.tmpdir(), "node9-activity.sock");
7059
7138
  function notifyActivitySocket(data) {
7060
7139
  return new Promise((resolve) => {
7061
7140
  try {
@@ -7088,9 +7167,9 @@ var DAEMON_PORT = 7391;
7088
7167
  var DAEMON_HOST = "127.0.0.1";
7089
7168
  function getInternalToken() {
7090
7169
  try {
7091
- const pidFile = import_path8.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
7092
- if (!import_fs8.default.existsSync(pidFile)) return null;
7093
- const data = JSON.parse(import_fs8.default.readFileSync(pidFile, "utf-8"));
7170
+ const pidFile = import_path9.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
7171
+ if (!import_fs9.default.existsSync(pidFile)) return null;
7172
+ const data = JSON.parse(import_fs9.default.readFileSync(pidFile, "utf-8"));
7094
7173
  process.kill(data.pid, 0);
7095
7174
  return data.internalToken ?? null;
7096
7175
  } catch {
@@ -7098,12 +7177,12 @@ function getInternalToken() {
7098
7177
  }
7099
7178
  }
7100
7179
  function isDaemonRunning() {
7101
- const pidFile = import_path8.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
7102
- if (import_fs8.default.existsSync(pidFile)) {
7180
+ const pidFile = import_path9.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
7181
+ if (import_fs9.default.existsSync(pidFile)) {
7103
7182
  let pid;
7104
7183
  let port;
7105
7184
  try {
7106
- const data = JSON.parse(import_fs8.default.readFileSync(pidFile, "utf-8"));
7185
+ const data = JSON.parse(import_fs9.default.readFileSync(pidFile, "utf-8"));
7107
7186
  pid = data.pid;
7108
7187
  port = data.port;
7109
7188
  } catch {
@@ -7121,7 +7200,7 @@ function isDaemonRunning() {
7121
7200
  } catch (err) {
7122
7201
  if (err instanceof Error && "code" in err && err.code === "ESRCH") {
7123
7202
  try {
7124
- import_fs8.default.unlinkSync(pidFile);
7203
+ import_fs9.default.unlinkSync(pidFile);
7125
7204
  } catch {
7126
7205
  }
7127
7206
  }
@@ -7287,14 +7366,14 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
7287
7366
  }
7288
7367
 
7289
7368
  // src/auth/orchestrator.ts
7290
- var import_crypto5 = require("crypto");
7369
+ var import_crypto6 = require("crypto");
7291
7370
 
7292
7371
  // src/ui/native.ts
7293
7372
  var import_child_process = require("child_process");
7294
- var import_path10 = __toESM(require("path"));
7373
+ var import_path11 = __toESM(require("path"));
7295
7374
 
7296
7375
  // src/context-sniper.ts
7297
- var import_path9 = __toESM(require("path"));
7376
+ var import_path10 = __toESM(require("path"));
7298
7377
  function smartTruncate(str, maxLen = 500) {
7299
7378
  if (str.length <= maxLen) return str;
7300
7379
  const edge = Math.floor(maxLen / 2) - 3;
@@ -7362,7 +7441,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
7362
7441
  intent = "EDIT";
7363
7442
  if (obj.file_path) {
7364
7443
  editFilePath = String(obj.file_path);
7365
- editFileName = import_path9.default.basename(editFilePath);
7444
+ editFileName = import_path10.default.basename(editFilePath);
7366
7445
  }
7367
7446
  const result = extractContext(String(obj.new_string), matchedWord);
7368
7447
  contextSnippet = result.snippet;
@@ -7426,7 +7505,7 @@ function formatArgs(args, matchedField, matchedWord) {
7426
7505
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
7427
7506
  const obj = parsed;
7428
7507
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
7429
- const file = obj.file_path ? import_path10.default.basename(String(obj.file_path)) : "file";
7508
+ const file = obj.file_path ? import_path11.default.basename(String(obj.file_path)) : "file";
7430
7509
  const oldPreview = smartTruncate(String(obj.old_string), 120);
7431
7510
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
7432
7511
  return {
@@ -7624,22 +7703,22 @@ end run`;
7624
7703
  }
7625
7704
 
7626
7705
  // src/canary/registry.ts
7627
- var import_fs10 = __toESM(require("fs"));
7706
+ var import_fs11 = __toESM(require("fs"));
7628
7707
  var import_os9 = __toESM(require("os"));
7629
- var import_path12 = __toESM(require("path"));
7708
+ var import_path13 = __toESM(require("path"));
7630
7709
 
7631
7710
  // src/shields/jail.ts
7632
- var import_fs9 = __toESM(require("fs"));
7711
+ var import_fs10 = __toESM(require("fs"));
7633
7712
  var import_os8 = __toESM(require("os"));
7634
- var import_path11 = __toESM(require("path"));
7713
+ var import_path12 = __toESM(require("path"));
7635
7714
  var USER_JAIL_SHIELD = "user-jail";
7636
7715
  function jailStorePath() {
7637
- return import_path11.default.join(import_os8.default.homedir(), ".node9", "jail-paths.json");
7716
+ return import_path12.default.join(import_os8.default.homedir(), ".node9", "jail-paths.json");
7638
7717
  }
7639
7718
  function readJailPaths() {
7640
7719
  let text;
7641
7720
  try {
7642
- text = import_fs9.default.readFileSync(jailStorePath(), "utf8");
7721
+ text = import_fs10.default.readFileSync(jailStorePath(), "utf8");
7643
7722
  } catch (err) {
7644
7723
  if (err.code === "ENOENT") return [];
7645
7724
  throw err;
@@ -7668,7 +7747,7 @@ function findJailedPathIn(candidate, paths) {
7668
7747
 
7669
7748
  // src/canary/registry.ts
7670
7749
  function canaryStorePath() {
7671
- return import_path12.default.join(import_os9.default.homedir(), ".node9", "canaries.json");
7750
+ return import_path13.default.join(import_os9.default.homedir(), ".node9", "canaries.json");
7672
7751
  }
7673
7752
  function isRecord(x) {
7674
7753
  if (!x || typeof x !== "object") return false;
@@ -7679,7 +7758,7 @@ function loadCanaries(opts) {
7679
7758
  const p = canaryStorePath();
7680
7759
  let raw;
7681
7760
  try {
7682
- raw = import_fs10.default.readFileSync(p, "utf-8");
7761
+ raw = import_fs11.default.readFileSync(p, "utf-8");
7683
7762
  } catch (e) {
7684
7763
  if (e.code === "ENOENT") return [];
7685
7764
  throw e;
@@ -7705,10 +7784,51 @@ function canaryValues() {
7705
7784
  init_audit();
7706
7785
 
7707
7786
  // src/auth/cloud.ts
7708
- var import_fs11 = __toESM(require("fs"));
7787
+ var import_fs13 = __toESM(require("fs"));
7709
7788
  var import_os10 = __toESM(require("os"));
7710
- var import_path13 = __toESM(require("path"));
7789
+ var import_path14 = __toESM(require("path"));
7711
7790
  init_audit();
7791
+
7792
+ // src/utils/read-capped.ts
7793
+ var import_fs12 = __toESM(require("fs"));
7794
+ function readCapped(file, maxBytes) {
7795
+ if (maxBytes < 0) return null;
7796
+ let fd;
7797
+ try {
7798
+ fd = import_fs12.default.openSync(file, "r");
7799
+ } catch {
7800
+ return null;
7801
+ }
7802
+ try {
7803
+ const st = import_fs12.default.fstatSync(fd);
7804
+ if (!st.isFile()) return null;
7805
+ const want = Math.min(st.size, maxBytes + 1);
7806
+ const buf = Buffer.alloc(want);
7807
+ let read = 0;
7808
+ while (read < want) {
7809
+ const n = import_fs12.default.readSync(fd, buf, read, want - read, read);
7810
+ if (n <= 0) break;
7811
+ read += n;
7812
+ }
7813
+ return {
7814
+ bytes: buf.subarray(0, Math.min(read, maxBytes)),
7815
+ truncated: read > maxBytes
7816
+ };
7817
+ } catch {
7818
+ return null;
7819
+ } finally {
7820
+ try {
7821
+ import_fs12.default.closeSync(fd);
7822
+ } catch {
7823
+ }
7824
+ }
7825
+ }
7826
+ function readCappedText(file, maxBytes) {
7827
+ const r = readCapped(file, maxBytes);
7828
+ return r ? { text: r.bytes.toString("utf8"), truncated: r.truncated } : null;
7829
+ }
7830
+
7831
+ // src/auth/cloud.ts
7712
7832
  async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
7713
7833
  const controller = new AbortController();
7714
7834
  const timeout = setTimeout(() => controller.abort(), 1e4);
@@ -7716,10 +7836,11 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
7716
7836
  let ciContext;
7717
7837
  if (process.env.CI) {
7718
7838
  try {
7719
- const ciContextPath = import_path13.default.join(import_os10.default.homedir(), ".node9", "ci-context.json");
7720
- const stats = import_fs11.default.statSync(ciContextPath);
7721
- if (stats.size > 1e4) throw new Error("ci-context.json exceeds 10 KB");
7722
- const raw = import_fs11.default.readFileSync(ciContextPath, "utf8");
7839
+ const ciContextPath = import_path14.default.join(import_os10.default.homedir(), ".node9", "ci-context.json");
7840
+ const ci = readCappedText(ciContextPath, 1e4);
7841
+ if (!ci) throw new Error("ci-context.json unreadable");
7842
+ if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
7843
+ const raw = ci.text;
7723
7844
  const parsed = JSON.parse(raw);
7724
7845
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
7725
7846
  throw new Error("ci-context.json is not a plain object");
@@ -7818,14 +7939,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
7818
7939
  });
7819
7940
  clearTimeout(timer);
7820
7941
  if (!res.ok) {
7821
- import_fs11.default.appendFileSync(
7942
+ import_fs13.default.appendFileSync(
7822
7943
  HOOK_DEBUG_LOG,
7823
7944
  `[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
7824
7945
  `
7825
7946
  );
7826
7947
  }
7827
7948
  } catch (err) {
7828
- import_fs11.default.appendFileSync(
7949
+ import_fs13.default.appendFileSync(
7829
7950
  HOOK_DEBUG_LOG,
7830
7951
  `[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
7831
7952
  `
@@ -7834,16 +7955,16 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
7834
7955
  }
7835
7956
 
7836
7957
  // src/loop-detector.ts
7837
- var import_fs12 = __toESM(require("fs"));
7838
- var import_path14 = __toESM(require("path"));
7958
+ var import_fs14 = __toESM(require("fs"));
7959
+ var import_path15 = __toESM(require("path"));
7839
7960
  var import_os11 = __toESM(require("os"));
7840
7961
  function loopStateFile() {
7841
- return import_path14.default.join(import_os11.default.homedir(), ".node9", "loop-state.json");
7962
+ return import_path15.default.join(import_os11.default.homedir(), ".node9", "loop-state.json");
7842
7963
  }
7843
7964
  function readState() {
7844
7965
  try {
7845
- if (!import_fs12.default.existsSync(loopStateFile())) return [];
7846
- const raw = import_fs12.default.readFileSync(loopStateFile(), "utf-8");
7966
+ if (!import_fs14.default.existsSync(loopStateFile())) return [];
7967
+ const raw = import_fs14.default.readFileSync(loopStateFile(), "utf-8");
7847
7968
  const parsed = JSON.parse(raw);
7848
7969
  if (!Array.isArray(parsed)) return [];
7849
7970
  return parsed;
@@ -7852,11 +7973,11 @@ function readState() {
7852
7973
  }
7853
7974
  }
7854
7975
  function writeState(records) {
7855
- const dir = import_path14.default.dirname(loopStateFile());
7856
- if (!import_fs12.default.existsSync(dir)) import_fs12.default.mkdirSync(dir, { recursive: true });
7976
+ const dir = import_path15.default.dirname(loopStateFile());
7977
+ if (!import_fs14.default.existsSync(dir)) import_fs14.default.mkdirSync(dir, { recursive: true });
7857
7978
  const tmpPath = `${loopStateFile()}.${import_os11.default.hostname()}.${process.pid}.tmp`;
7858
- import_fs12.default.writeFileSync(tmpPath, JSON.stringify(records));
7859
- import_fs12.default.renameSync(tmpPath, loopStateFile());
7979
+ import_fs14.default.writeFileSync(tmpPath, JSON.stringify(records));
7980
+ import_fs14.default.renameSync(tmpPath, loopStateFile());
7860
7981
  }
7861
7982
  function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
7862
7983
  try {
@@ -7924,9 +8045,13 @@ async function hasReachableHumanApprover(opts) {
7924
8045
  if (nativeReachable) return true;
7925
8046
  return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
7926
8047
  }
8048
+ function mayDowngradeHardBlock(opts) {
8049
+ if (opts.overridable === false) return false;
8050
+ return opts.daemonUp && !opts.isTestEnv && opts.humanApproverReachable;
8051
+ }
7927
8052
  async function authorizeHeadless(toolName, args, meta, options) {
7928
8053
  if (!options?.calledFromDaemon) {
7929
- const actId = (0, import_crypto5.randomUUID)();
8054
+ const actId = (0, import_crypto6.randomUUID)();
7930
8055
  const actTs = Date.now();
7931
8056
  const sanitizedAgent = meta?.agent ? safeMessage(meta.agent, 80) : void 0;
7932
8057
  const sanitizedMcpServer = meta?.mcpServer ? safeMessage(meta.mcpServer, 40) : void 0;
@@ -8314,7 +8439,12 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
8314
8439
  calledFromDaemon: options?.calledFromDaemon
8315
8440
  });
8316
8441
  }
8317
- const mayDowngrade = daemonUp && !isTestEnv2 && humanApproverReachable;
8442
+ const mayDowngrade = mayDowngradeHardBlock({
8443
+ daemonUp,
8444
+ isTestEnv: isTestEnv2,
8445
+ humanApproverReachable,
8446
+ overridable: policyResult.overridable
8447
+ });
8318
8448
  const hardBlock = () => {
8319
8449
  if (!isManual)
8320
8450
  appendLocalAudit(