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