@node9/proxy 2.14.1 → 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
  }
@@ -4484,6 +4486,15 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4484
4486
  function isIgnoredTool(toolName, config) {
4485
4487
  return matchesPattern(toolName, config.policy.ignoredTools);
4486
4488
  }
4489
+ var TERMINAL_ESCAPE_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
4490
+ function stripTerminalEscapes(s) {
4491
+ return s.replace(TERMINAL_ESCAPE_RE, "");
4492
+ }
4493
+ function safeMessage(value, max = 300) {
4494
+ const raw = typeof value === "string" ? value : value instanceof Error ? value.message : String(value ?? "");
4495
+ const s = stripTerminalEscapes(raw).replace(/\s+/g, " ").trim();
4496
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
4497
+ }
4487
4498
  var aws_default = {
4488
4499
  name: "aws",
4489
4500
  description: "Protects AWS infrastructure from destructive AI operations",
@@ -5857,6 +5868,45 @@ function isTrustedHost(host) {
5857
5868
  );
5858
5869
  }
5859
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
+
5860
5910
  // src/config/index.ts
5861
5911
  function sanitizeSsrfAllow(entries, source) {
5862
5912
  const kept = [];
@@ -6145,12 +6195,27 @@ var ADVISORY_SMART_RULES = [
6145
6195
  }
6146
6196
  ];
6147
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
+ }
6148
6212
  function getCredentials() {
6149
- const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
6150
6213
  if (process.env.NODE9_API_KEY) {
6151
6214
  return {
6152
6215
  apiKey: process.env.NODE9_API_KEY,
6153
- 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)
6154
6219
  };
6155
6220
  }
6156
6221
  try {
@@ -6162,14 +6227,14 @@ function getCredentials() {
6162
6227
  if (profile?.apiKey) {
6163
6228
  return {
6164
6229
  apiKey: profile.apiKey,
6165
- apiUrl: profile.apiUrl || DEFAULT_API_URL,
6230
+ apiUrl: safeApiUrl(profile.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
6166
6231
  localOnly: profile.localOnly === true || profileName !== "default"
6167
6232
  };
6168
6233
  }
6169
6234
  if (creds.apiKey) {
6170
6235
  return {
6171
6236
  apiKey: creds.apiKey,
6172
- apiUrl: creds.apiUrl || DEFAULT_API_URL,
6237
+ apiUrl: safeApiUrl(creds.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
6173
6238
  localOnly: creds.localOnly === true
6174
6239
  };
6175
6240
  }
@@ -6602,13 +6667,13 @@ function getConfig(cwd) {
6602
6667
  }
6603
6668
  if (Array.isArray(mc.jailPaths)) {
6604
6669
  for (const jp of mc.jailPaths) {
6605
- const path15 = typeof jp?.path === "string" ? jp.path.trim() : "";
6606
- if (!path15) continue;
6670
+ const path16 = typeof jp?.path === "string" ? jp.path.trim() : "";
6671
+ if (!path16) continue;
6607
6672
  const verdict = jp?.verdict === "review" ? "review" : "block";
6608
- for (const r of pathRules(path15, verdict, "org-managed jail")) {
6673
+ for (const r of pathRules(path16, verdict, "org-managed jail")) {
6609
6674
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
6610
6675
  }
6611
- mergedPolicy.managedJailPaths.push({ path: path15, verdict });
6676
+ mergedPolicy.managedJailPaths.push({ path: path16, verdict });
6612
6677
  }
6613
6678
  }
6614
6679
  if (Array.isArray(mc.trustedHosts)) {
@@ -6945,11 +7010,41 @@ function isIgnoredTool2(toolName) {
6945
7010
  }
6946
7011
 
6947
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
6948
7018
  var import_fs7 = __toESM(require("fs"));
6949
7019
  var import_path7 = __toESM(require("path"));
6950
- var import_os6 = __toESM(require("os"));
6951
- var PAUSED_FILE = import_path7.default.join(import_os6.default.homedir(), ".node9", "PAUSED");
6952
- 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");
6953
7048
  function extractCommandPattern(toolName, args) {
6954
7049
  const lower = toolName.toLowerCase();
6955
7050
  if (lower !== "bash" && lower !== "execute_bash" && lower !== "shell") return void 0;
@@ -6961,11 +7056,11 @@ function extractCommandPattern(toolName, args) {
6961
7056
  }
6962
7057
  function checkPause() {
6963
7058
  try {
6964
- if (!import_fs7.default.existsSync(PAUSED_FILE)) return { paused: false };
6965
- 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"));
6966
7061
  if (state.expiry > 0 && Date.now() >= state.expiry) {
6967
7062
  try {
6968
- import_fs7.default.unlinkSync(PAUSED_FILE);
7063
+ import_fs8.default.unlinkSync(PAUSED_FILE);
6969
7064
  } catch {
6970
7065
  }
6971
7066
  return { paused: false };
@@ -6975,21 +7070,14 @@ function checkPause() {
6975
7070
  return { paused: false };
6976
7071
  }
6977
7072
  }
6978
- function atomicWriteSync(filePath, data, options) {
6979
- const dir = import_path7.default.dirname(filePath);
6980
- if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
6981
- const tmpPath = `${filePath}.${import_os6.default.hostname()}.${process.pid}.tmp`;
6982
- import_fs7.default.writeFileSync(tmpPath, data, options);
6983
- import_fs7.default.renameSync(tmpPath, filePath);
6984
- }
6985
7073
  function getActiveTrustSession(toolName, args) {
6986
7074
  try {
6987
- if (!import_fs7.default.existsSync(TRUST_FILE)) return false;
6988
- 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"));
6989
7077
  const now = Date.now();
6990
7078
  const active = trust.entries.filter((e) => e.expiry > now);
6991
7079
  if (active.length !== trust.entries.length) {
6992
- import_fs7.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
7080
+ atomicWriteSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
6993
7081
  }
6994
7082
  return active.some((e) => {
6995
7083
  if (!(e.tool === toolName || matchesPattern(toolName, e.tool))) return false;
@@ -7008,8 +7096,8 @@ function writeTrustSession(toolName, durationMs, args) {
7008
7096
  try {
7009
7097
  let trust = { entries: [] };
7010
7098
  try {
7011
- if (import_fs7.default.existsSync(TRUST_FILE)) {
7012
- 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"));
7013
7101
  }
7014
7102
  } catch {
7015
7103
  }
@@ -7031,9 +7119,9 @@ function writeTrustSession(toolName, durationMs, args) {
7031
7119
  }
7032
7120
  function getPersistentDecision(toolName) {
7033
7121
  try {
7034
- const file = import_path7.default.join(import_os6.default.homedir(), ".node9", "decisions.json");
7035
- if (!import_fs7.default.existsSync(file)) return null;
7036
- 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"));
7037
7125
  const d = decisions[toolName];
7038
7126
  if (d === "allow" || d === "deny") return d;
7039
7127
  } catch {
@@ -7042,11 +7130,11 @@ function getPersistentDecision(toolName) {
7042
7130
  }
7043
7131
 
7044
7132
  // src/auth/daemon.ts
7045
- var import_fs8 = __toESM(require("fs"));
7133
+ var import_fs9 = __toESM(require("fs"));
7046
7134
  var import_net = __toESM(require("net"));
7047
- var import_path8 = __toESM(require("path"));
7135
+ var import_path9 = __toESM(require("path"));
7048
7136
  var import_os7 = __toESM(require("os"));
7049
- 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");
7050
7138
  function notifyActivitySocket(data) {
7051
7139
  return new Promise((resolve) => {
7052
7140
  try {
@@ -7079,9 +7167,9 @@ var DAEMON_PORT = 7391;
7079
7167
  var DAEMON_HOST = "127.0.0.1";
7080
7168
  function getInternalToken() {
7081
7169
  try {
7082
- const pidFile = import_path8.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
7083
- if (!import_fs8.default.existsSync(pidFile)) return null;
7084
- 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"));
7085
7173
  process.kill(data.pid, 0);
7086
7174
  return data.internalToken ?? null;
7087
7175
  } catch {
@@ -7089,12 +7177,12 @@ function getInternalToken() {
7089
7177
  }
7090
7178
  }
7091
7179
  function isDaemonRunning() {
7092
- const pidFile = import_path8.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
7093
- 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)) {
7094
7182
  let pid;
7095
7183
  let port;
7096
7184
  try {
7097
- const data = JSON.parse(import_fs8.default.readFileSync(pidFile, "utf-8"));
7185
+ const data = JSON.parse(import_fs9.default.readFileSync(pidFile, "utf-8"));
7098
7186
  pid = data.pid;
7099
7187
  port = data.port;
7100
7188
  } catch {
@@ -7112,7 +7200,7 @@ function isDaemonRunning() {
7112
7200
  } catch (err) {
7113
7201
  if (err instanceof Error && "code" in err && err.code === "ESRCH") {
7114
7202
  try {
7115
- import_fs8.default.unlinkSync(pidFile);
7203
+ import_fs9.default.unlinkSync(pidFile);
7116
7204
  } catch {
7117
7205
  }
7118
7206
  }
@@ -7278,14 +7366,14 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
7278
7366
  }
7279
7367
 
7280
7368
  // src/auth/orchestrator.ts
7281
- var import_crypto5 = require("crypto");
7369
+ var import_crypto6 = require("crypto");
7282
7370
 
7283
7371
  // src/ui/native.ts
7284
7372
  var import_child_process = require("child_process");
7285
- var import_path10 = __toESM(require("path"));
7373
+ var import_path11 = __toESM(require("path"));
7286
7374
 
7287
7375
  // src/context-sniper.ts
7288
- var import_path9 = __toESM(require("path"));
7376
+ var import_path10 = __toESM(require("path"));
7289
7377
  function smartTruncate(str, maxLen = 500) {
7290
7378
  if (str.length <= maxLen) return str;
7291
7379
  const edge = Math.floor(maxLen / 2) - 3;
@@ -7353,7 +7441,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
7353
7441
  intent = "EDIT";
7354
7442
  if (obj.file_path) {
7355
7443
  editFilePath = String(obj.file_path);
7356
- editFileName = import_path9.default.basename(editFilePath);
7444
+ editFileName = import_path10.default.basename(editFilePath);
7357
7445
  }
7358
7446
  const result = extractContext(String(obj.new_string), matchedWord);
7359
7447
  contextSnippet = result.snippet;
@@ -7417,7 +7505,7 @@ function formatArgs(args, matchedField, matchedWord) {
7417
7505
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
7418
7506
  const obj = parsed;
7419
7507
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
7420
- 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";
7421
7509
  const oldPreview = smartTruncate(String(obj.old_string), 120);
7422
7510
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
7423
7511
  return {
@@ -7483,7 +7571,7 @@ function escapePango(text) {
7483
7571
  function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
7484
7572
  const lines = [];
7485
7573
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
7486
- const safeAgent = (agent ?? "AI Agent").replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80);
7574
+ const safeAgent = safeMessage(agent ?? "AI Agent", 80);
7487
7575
  lines.push(`\u{1F916} ${safeAgent} | \u{1F527} ${toolName}`);
7488
7576
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
7489
7577
  if (ruleDescription) lines.push(`\u2139 ${ruleDescription}`);
@@ -7615,22 +7703,22 @@ end run`;
7615
7703
  }
7616
7704
 
7617
7705
  // src/canary/registry.ts
7618
- var import_fs10 = __toESM(require("fs"));
7706
+ var import_fs11 = __toESM(require("fs"));
7619
7707
  var import_os9 = __toESM(require("os"));
7620
- var import_path12 = __toESM(require("path"));
7708
+ var import_path13 = __toESM(require("path"));
7621
7709
 
7622
7710
  // src/shields/jail.ts
7623
- var import_fs9 = __toESM(require("fs"));
7711
+ var import_fs10 = __toESM(require("fs"));
7624
7712
  var import_os8 = __toESM(require("os"));
7625
- var import_path11 = __toESM(require("path"));
7713
+ var import_path12 = __toESM(require("path"));
7626
7714
  var USER_JAIL_SHIELD = "user-jail";
7627
7715
  function jailStorePath() {
7628
- 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");
7629
7717
  }
7630
7718
  function readJailPaths() {
7631
7719
  let text;
7632
7720
  try {
7633
- text = import_fs9.default.readFileSync(jailStorePath(), "utf8");
7721
+ text = import_fs10.default.readFileSync(jailStorePath(), "utf8");
7634
7722
  } catch (err) {
7635
7723
  if (err.code === "ENOENT") return [];
7636
7724
  throw err;
@@ -7659,7 +7747,7 @@ function findJailedPathIn(candidate, paths) {
7659
7747
 
7660
7748
  // src/canary/registry.ts
7661
7749
  function canaryStorePath() {
7662
- 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");
7663
7751
  }
7664
7752
  function isRecord(x) {
7665
7753
  if (!x || typeof x !== "object") return false;
@@ -7670,7 +7758,7 @@ function loadCanaries(opts) {
7670
7758
  const p = canaryStorePath();
7671
7759
  let raw;
7672
7760
  try {
7673
- raw = import_fs10.default.readFileSync(p, "utf-8");
7761
+ raw = import_fs11.default.readFileSync(p, "utf-8");
7674
7762
  } catch (e) {
7675
7763
  if (e.code === "ENOENT") return [];
7676
7764
  throw e;
@@ -7696,10 +7784,51 @@ function canaryValues() {
7696
7784
  init_audit();
7697
7785
 
7698
7786
  // src/auth/cloud.ts
7699
- var import_fs11 = __toESM(require("fs"));
7787
+ var import_fs13 = __toESM(require("fs"));
7700
7788
  var import_os10 = __toESM(require("os"));
7701
- var import_path13 = __toESM(require("path"));
7789
+ var import_path14 = __toESM(require("path"));
7702
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
7703
7832
  async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
7704
7833
  const controller = new AbortController();
7705
7834
  const timeout = setTimeout(() => controller.abort(), 1e4);
@@ -7707,10 +7836,11 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
7707
7836
  let ciContext;
7708
7837
  if (process.env.CI) {
7709
7838
  try {
7710
- const ciContextPath = import_path13.default.join(import_os10.default.homedir(), ".node9", "ci-context.json");
7711
- const stats = import_fs11.default.statSync(ciContextPath);
7712
- if (stats.size > 1e4) throw new Error("ci-context.json exceeds 10 KB");
7713
- 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;
7714
7844
  const parsed = JSON.parse(raw);
7715
7845
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
7716
7846
  throw new Error("ci-context.json is not a plain object");
@@ -7809,32 +7939,32 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
7809
7939
  });
7810
7940
  clearTimeout(timer);
7811
7941
  if (!res.ok) {
7812
- import_fs11.default.appendFileSync(
7942
+ import_fs13.default.appendFileSync(
7813
7943
  HOOK_DEBUG_LOG,
7814
- `[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
7944
+ `[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
7815
7945
  `
7816
7946
  );
7817
7947
  }
7818
7948
  } catch (err) {
7819
- import_fs11.default.appendFileSync(
7949
+ import_fs13.default.appendFileSync(
7820
7950
  HOOK_DEBUG_LOG,
7821
- `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
7951
+ `[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
7822
7952
  `
7823
7953
  );
7824
7954
  }
7825
7955
  }
7826
7956
 
7827
7957
  // src/loop-detector.ts
7828
- var import_fs12 = __toESM(require("fs"));
7829
- var import_path14 = __toESM(require("path"));
7958
+ var import_fs14 = __toESM(require("fs"));
7959
+ var import_path15 = __toESM(require("path"));
7830
7960
  var import_os11 = __toESM(require("os"));
7831
7961
  function loopStateFile() {
7832
- 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");
7833
7963
  }
7834
7964
  function readState() {
7835
7965
  try {
7836
- if (!import_fs12.default.existsSync(loopStateFile())) return [];
7837
- 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");
7838
7968
  const parsed = JSON.parse(raw);
7839
7969
  if (!Array.isArray(parsed)) return [];
7840
7970
  return parsed;
@@ -7843,11 +7973,11 @@ function readState() {
7843
7973
  }
7844
7974
  }
7845
7975
  function writeState(records) {
7846
- const dir = import_path14.default.dirname(loopStateFile());
7847
- 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 });
7848
7978
  const tmpPath = `${loopStateFile()}.${import_os11.default.hostname()}.${process.pid}.tmp`;
7849
- import_fs12.default.writeFileSync(tmpPath, JSON.stringify(records));
7850
- import_fs12.default.renameSync(tmpPath, loopStateFile());
7979
+ import_fs14.default.writeFileSync(tmpPath, JSON.stringify(records));
7980
+ import_fs14.default.renameSync(tmpPath, loopStateFile());
7851
7981
  }
7852
7982
  function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
7853
7983
  try {
@@ -7915,13 +8045,16 @@ async function hasReachableHumanApprover(opts) {
7915
8045
  if (nativeReachable) return true;
7916
8046
  return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
7917
8047
  }
8048
+ function mayDowngradeHardBlock(opts) {
8049
+ if (opts.overridable === false) return false;
8050
+ return opts.daemonUp && !opts.isTestEnv && opts.humanApproverReachable;
8051
+ }
7918
8052
  async function authorizeHeadless(toolName, args, meta, options) {
7919
8053
  if (!options?.calledFromDaemon) {
7920
- const actId = (0, import_crypto5.randomUUID)();
8054
+ const actId = (0, import_crypto6.randomUUID)();
7921
8055
  const actTs = Date.now();
7922
- const stripAnsi = (s) => s.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "");
7923
- const sanitizedAgent = meta?.agent ? stripAnsi(meta.agent).slice(0, 80) : void 0;
7924
- const sanitizedMcpServer = meta?.mcpServer ? stripAnsi(meta.mcpServer).slice(0, 40) : void 0;
8056
+ const sanitizedAgent = meta?.agent ? safeMessage(meta.agent, 80) : void 0;
8057
+ const sanitizedMcpServer = meta?.mcpServer ? safeMessage(meta.mcpServer, 40) : void 0;
7925
8058
  const socketOk = await notifyActivity({
7926
8059
  id: actId,
7927
8060
  ts: actTs,
@@ -8306,7 +8439,12 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
8306
8439
  calledFromDaemon: options?.calledFromDaemon
8307
8440
  });
8308
8441
  }
8309
- const mayDowngrade = daemonUp && !isTestEnv2 && humanApproverReachable;
8442
+ const mayDowngrade = mayDowngradeHardBlock({
8443
+ daemonUp,
8444
+ isTestEnv: isTestEnv2,
8445
+ humanApproverReachable,
8446
+ overridable: policyResult.overridable
8447
+ });
8310
8448
  const hardBlock = () => {
8311
8449
  if (!isManual)
8312
8450
  appendLocalAudit(