@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.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
  }
@@ -4454,6 +4456,15 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4454
4456
  function isIgnoredTool(toolName, config) {
4455
4457
  return matchesPattern(toolName, config.policy.ignoredTools);
4456
4458
  }
4459
+ var TERMINAL_ESCAPE_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
4460
+ function stripTerminalEscapes(s) {
4461
+ return s.replace(TERMINAL_ESCAPE_RE, "");
4462
+ }
4463
+ function safeMessage(value, max = 300) {
4464
+ const raw = typeof value === "string" ? value : value instanceof Error ? value.message : String(value ?? "");
4465
+ const s = stripTerminalEscapes(raw).replace(/\s+/g, " ").trim();
4466
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
4467
+ }
4457
4468
  var aws_default = {
4458
4469
  name: "aws",
4459
4470
  description: "Protects AWS infrastructure from destructive AI operations",
@@ -5827,6 +5838,45 @@ function isTrustedHost(host) {
5827
5838
  );
5828
5839
  }
5829
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
+
5830
5880
  // src/config/index.ts
5831
5881
  function sanitizeSsrfAllow(entries, source) {
5832
5882
  const kept = [];
@@ -6115,12 +6165,27 @@ var ADVISORY_SMART_RULES = [
6115
6165
  }
6116
6166
  ];
6117
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
+ }
6118
6182
  function getCredentials() {
6119
- const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
6120
6183
  if (process.env.NODE9_API_KEY) {
6121
6184
  return {
6122
6185
  apiKey: process.env.NODE9_API_KEY,
6123
- 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)
6124
6189
  };
6125
6190
  }
6126
6191
  try {
@@ -6132,14 +6197,14 @@ function getCredentials() {
6132
6197
  if (profile?.apiKey) {
6133
6198
  return {
6134
6199
  apiKey: profile.apiKey,
6135
- apiUrl: profile.apiUrl || DEFAULT_API_URL,
6200
+ apiUrl: safeApiUrl(profile.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
6136
6201
  localOnly: profile.localOnly === true || profileName !== "default"
6137
6202
  };
6138
6203
  }
6139
6204
  if (creds.apiKey) {
6140
6205
  return {
6141
6206
  apiKey: creds.apiKey,
6142
- apiUrl: creds.apiUrl || DEFAULT_API_URL,
6207
+ apiUrl: safeApiUrl(creds.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
6143
6208
  localOnly: creds.localOnly === true
6144
6209
  };
6145
6210
  }
@@ -6572,13 +6637,13 @@ function getConfig(cwd) {
6572
6637
  }
6573
6638
  if (Array.isArray(mc.jailPaths)) {
6574
6639
  for (const jp of mc.jailPaths) {
6575
- const path15 = typeof jp?.path === "string" ? jp.path.trim() : "";
6576
- if (!path15) continue;
6640
+ const path16 = typeof jp?.path === "string" ? jp.path.trim() : "";
6641
+ if (!path16) continue;
6577
6642
  const verdict = jp?.verdict === "review" ? "review" : "block";
6578
- for (const r of pathRules(path15, verdict, "org-managed jail")) {
6643
+ for (const r of pathRules(path16, verdict, "org-managed jail")) {
6579
6644
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
6580
6645
  }
6581
- mergedPolicy.managedJailPaths.push({ path: path15, verdict });
6646
+ mergedPolicy.managedJailPaths.push({ path: path16, verdict });
6582
6647
  }
6583
6648
  }
6584
6649
  if (Array.isArray(mc.trustedHosts)) {
@@ -6915,11 +6980,41 @@ function isIgnoredTool2(toolName) {
6915
6980
  }
6916
6981
 
6917
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
6918
6988
  import fs7 from "fs";
6919
6989
  import path7 from "path";
6920
- import os6 from "os";
6921
- var PAUSED_FILE = path7.join(os6.homedir(), ".node9", "PAUSED");
6922
- 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");
6923
7018
  function extractCommandPattern(toolName, args) {
6924
7019
  const lower = toolName.toLowerCase();
6925
7020
  if (lower !== "bash" && lower !== "execute_bash" && lower !== "shell") return void 0;
@@ -6931,11 +7026,11 @@ function extractCommandPattern(toolName, args) {
6931
7026
  }
6932
7027
  function checkPause() {
6933
7028
  try {
6934
- if (!fs7.existsSync(PAUSED_FILE)) return { paused: false };
6935
- 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"));
6936
7031
  if (state.expiry > 0 && Date.now() >= state.expiry) {
6937
7032
  try {
6938
- fs7.unlinkSync(PAUSED_FILE);
7033
+ fs8.unlinkSync(PAUSED_FILE);
6939
7034
  } catch {
6940
7035
  }
6941
7036
  return { paused: false };
@@ -6945,21 +7040,14 @@ function checkPause() {
6945
7040
  return { paused: false };
6946
7041
  }
6947
7042
  }
6948
- function atomicWriteSync(filePath, data, options) {
6949
- const dir = path7.dirname(filePath);
6950
- if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
6951
- const tmpPath = `${filePath}.${os6.hostname()}.${process.pid}.tmp`;
6952
- fs7.writeFileSync(tmpPath, data, options);
6953
- fs7.renameSync(tmpPath, filePath);
6954
- }
6955
7043
  function getActiveTrustSession(toolName, args) {
6956
7044
  try {
6957
- if (!fs7.existsSync(TRUST_FILE)) return false;
6958
- 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"));
6959
7047
  const now = Date.now();
6960
7048
  const active = trust.entries.filter((e) => e.expiry > now);
6961
7049
  if (active.length !== trust.entries.length) {
6962
- fs7.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
7050
+ atomicWriteSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
6963
7051
  }
6964
7052
  return active.some((e) => {
6965
7053
  if (!(e.tool === toolName || matchesPattern(toolName, e.tool))) return false;
@@ -6978,8 +7066,8 @@ function writeTrustSession(toolName, durationMs, args) {
6978
7066
  try {
6979
7067
  let trust = { entries: [] };
6980
7068
  try {
6981
- if (fs7.existsSync(TRUST_FILE)) {
6982
- 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"));
6983
7071
  }
6984
7072
  } catch {
6985
7073
  }
@@ -7001,9 +7089,9 @@ function writeTrustSession(toolName, durationMs, args) {
7001
7089
  }
7002
7090
  function getPersistentDecision(toolName) {
7003
7091
  try {
7004
- const file = path7.join(os6.homedir(), ".node9", "decisions.json");
7005
- if (!fs7.existsSync(file)) return null;
7006
- 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"));
7007
7095
  const d = decisions[toolName];
7008
7096
  if (d === "allow" || d === "deny") return d;
7009
7097
  } catch {
@@ -7012,11 +7100,11 @@ function getPersistentDecision(toolName) {
7012
7100
  }
7013
7101
 
7014
7102
  // src/auth/daemon.ts
7015
- import fs8 from "fs";
7103
+ import fs9 from "fs";
7016
7104
  import net from "net";
7017
- import path8 from "path";
7105
+ import path9 from "path";
7018
7106
  import os7 from "os";
7019
- 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");
7020
7108
  function notifyActivitySocket(data) {
7021
7109
  return new Promise((resolve) => {
7022
7110
  try {
@@ -7049,9 +7137,9 @@ var DAEMON_PORT = 7391;
7049
7137
  var DAEMON_HOST = "127.0.0.1";
7050
7138
  function getInternalToken() {
7051
7139
  try {
7052
- const pidFile = path8.join(os7.homedir(), ".node9", "daemon.pid");
7053
- if (!fs8.existsSync(pidFile)) return null;
7054
- 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"));
7055
7143
  process.kill(data.pid, 0);
7056
7144
  return data.internalToken ?? null;
7057
7145
  } catch {
@@ -7059,12 +7147,12 @@ function getInternalToken() {
7059
7147
  }
7060
7148
  }
7061
7149
  function isDaemonRunning() {
7062
- const pidFile = path8.join(os7.homedir(), ".node9", "daemon.pid");
7063
- if (fs8.existsSync(pidFile)) {
7150
+ const pidFile = path9.join(os7.homedir(), ".node9", "daemon.pid");
7151
+ if (fs9.existsSync(pidFile)) {
7064
7152
  let pid;
7065
7153
  let port;
7066
7154
  try {
7067
- const data = JSON.parse(fs8.readFileSync(pidFile, "utf-8"));
7155
+ const data = JSON.parse(fs9.readFileSync(pidFile, "utf-8"));
7068
7156
  pid = data.pid;
7069
7157
  port = data.port;
7070
7158
  } catch {
@@ -7082,7 +7170,7 @@ function isDaemonRunning() {
7082
7170
  } catch (err) {
7083
7171
  if (err instanceof Error && "code" in err && err.code === "ESRCH") {
7084
7172
  try {
7085
- fs8.unlinkSync(pidFile);
7173
+ fs9.unlinkSync(pidFile);
7086
7174
  } catch {
7087
7175
  }
7088
7176
  }
@@ -7248,14 +7336,14 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
7248
7336
  }
7249
7337
 
7250
7338
  // src/auth/orchestrator.ts
7251
- import { randomUUID } from "crypto";
7339
+ import { randomUUID as randomUUID2 } from "crypto";
7252
7340
 
7253
7341
  // src/ui/native.ts
7254
7342
  import { spawn } from "child_process";
7255
- import path10 from "path";
7343
+ import path11 from "path";
7256
7344
 
7257
7345
  // src/context-sniper.ts
7258
- import path9 from "path";
7346
+ import path10 from "path";
7259
7347
  function smartTruncate(str, maxLen = 500) {
7260
7348
  if (str.length <= maxLen) return str;
7261
7349
  const edge = Math.floor(maxLen / 2) - 3;
@@ -7323,7 +7411,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
7323
7411
  intent = "EDIT";
7324
7412
  if (obj.file_path) {
7325
7413
  editFilePath = String(obj.file_path);
7326
- editFileName = path9.basename(editFilePath);
7414
+ editFileName = path10.basename(editFilePath);
7327
7415
  }
7328
7416
  const result = extractContext(String(obj.new_string), matchedWord);
7329
7417
  contextSnippet = result.snippet;
@@ -7387,7 +7475,7 @@ function formatArgs(args, matchedField, matchedWord) {
7387
7475
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
7388
7476
  const obj = parsed;
7389
7477
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
7390
- 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";
7391
7479
  const oldPreview = smartTruncate(String(obj.old_string), 120);
7392
7480
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
7393
7481
  return {
@@ -7453,7 +7541,7 @@ function escapePango(text) {
7453
7541
  function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
7454
7542
  const lines = [];
7455
7543
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
7456
- const safeAgent = (agent ?? "AI Agent").replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80);
7544
+ const safeAgent = safeMessage(agent ?? "AI Agent", 80);
7457
7545
  lines.push(`\u{1F916} ${safeAgent} | \u{1F527} ${toolName}`);
7458
7546
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
7459
7547
  if (ruleDescription) lines.push(`\u2139 ${ruleDescription}`);
@@ -7585,22 +7673,22 @@ end run`;
7585
7673
  }
7586
7674
 
7587
7675
  // src/canary/registry.ts
7588
- import fs10 from "fs";
7676
+ import fs11 from "fs";
7589
7677
  import os9 from "os";
7590
- import path12 from "path";
7678
+ import path13 from "path";
7591
7679
 
7592
7680
  // src/shields/jail.ts
7593
- import fs9 from "fs";
7681
+ import fs10 from "fs";
7594
7682
  import os8 from "os";
7595
- import path11 from "path";
7683
+ import path12 from "path";
7596
7684
  var USER_JAIL_SHIELD = "user-jail";
7597
7685
  function jailStorePath() {
7598
- return path11.join(os8.homedir(), ".node9", "jail-paths.json");
7686
+ return path12.join(os8.homedir(), ".node9", "jail-paths.json");
7599
7687
  }
7600
7688
  function readJailPaths() {
7601
7689
  let text;
7602
7690
  try {
7603
- text = fs9.readFileSync(jailStorePath(), "utf8");
7691
+ text = fs10.readFileSync(jailStorePath(), "utf8");
7604
7692
  } catch (err) {
7605
7693
  if (err.code === "ENOENT") return [];
7606
7694
  throw err;
@@ -7629,7 +7717,7 @@ function findJailedPathIn(candidate, paths) {
7629
7717
 
7630
7718
  // src/canary/registry.ts
7631
7719
  function canaryStorePath() {
7632
- return path12.join(os9.homedir(), ".node9", "canaries.json");
7720
+ return path13.join(os9.homedir(), ".node9", "canaries.json");
7633
7721
  }
7634
7722
  function isRecord(x) {
7635
7723
  if (!x || typeof x !== "object") return false;
@@ -7640,7 +7728,7 @@ function loadCanaries(opts) {
7640
7728
  const p = canaryStorePath();
7641
7729
  let raw;
7642
7730
  try {
7643
- raw = fs10.readFileSync(p, "utf-8");
7731
+ raw = fs11.readFileSync(p, "utf-8");
7644
7732
  } catch (e) {
7645
7733
  if (e.code === "ENOENT") return [];
7646
7734
  throw e;
@@ -7667,9 +7755,50 @@ init_audit();
7667
7755
 
7668
7756
  // src/auth/cloud.ts
7669
7757
  init_audit();
7670
- import fs11 from "fs";
7758
+ import fs13 from "fs";
7671
7759
  import os10 from "os";
7672
- 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
7673
7802
  async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
7674
7803
  const controller = new AbortController();
7675
7804
  const timeout = setTimeout(() => controller.abort(), 1e4);
@@ -7677,10 +7806,11 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
7677
7806
  let ciContext;
7678
7807
  if (process.env.CI) {
7679
7808
  try {
7680
- const ciContextPath = path13.join(os10.homedir(), ".node9", "ci-context.json");
7681
- const stats = fs11.statSync(ciContextPath);
7682
- if (stats.size > 1e4) throw new Error("ci-context.json exceeds 10 KB");
7683
- 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;
7684
7814
  const parsed = JSON.parse(raw);
7685
7815
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
7686
7816
  throw new Error("ci-context.json is not a plain object");
@@ -7779,32 +7909,32 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
7779
7909
  });
7780
7910
  clearTimeout(timer);
7781
7911
  if (!res.ok) {
7782
- fs11.appendFileSync(
7912
+ fs13.appendFileSync(
7783
7913
  HOOK_DEBUG_LOG,
7784
- `[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
7914
+ `[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
7785
7915
  `
7786
7916
  );
7787
7917
  }
7788
7918
  } catch (err) {
7789
- fs11.appendFileSync(
7919
+ fs13.appendFileSync(
7790
7920
  HOOK_DEBUG_LOG,
7791
- `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
7921
+ `[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
7792
7922
  `
7793
7923
  );
7794
7924
  }
7795
7925
  }
7796
7926
 
7797
7927
  // src/loop-detector.ts
7798
- import fs12 from "fs";
7799
- import path14 from "path";
7928
+ import fs14 from "fs";
7929
+ import path15 from "path";
7800
7930
  import os11 from "os";
7801
7931
  function loopStateFile() {
7802
- return path14.join(os11.homedir(), ".node9", "loop-state.json");
7932
+ return path15.join(os11.homedir(), ".node9", "loop-state.json");
7803
7933
  }
7804
7934
  function readState() {
7805
7935
  try {
7806
- if (!fs12.existsSync(loopStateFile())) return [];
7807
- const raw = fs12.readFileSync(loopStateFile(), "utf-8");
7936
+ if (!fs14.existsSync(loopStateFile())) return [];
7937
+ const raw = fs14.readFileSync(loopStateFile(), "utf-8");
7808
7938
  const parsed = JSON.parse(raw);
7809
7939
  if (!Array.isArray(parsed)) return [];
7810
7940
  return parsed;
@@ -7813,11 +7943,11 @@ function readState() {
7813
7943
  }
7814
7944
  }
7815
7945
  function writeState(records) {
7816
- const dir = path14.dirname(loopStateFile());
7817
- 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 });
7818
7948
  const tmpPath = `${loopStateFile()}.${os11.hostname()}.${process.pid}.tmp`;
7819
- fs12.writeFileSync(tmpPath, JSON.stringify(records));
7820
- fs12.renameSync(tmpPath, loopStateFile());
7949
+ fs14.writeFileSync(tmpPath, JSON.stringify(records));
7950
+ fs14.renameSync(tmpPath, loopStateFile());
7821
7951
  }
7822
7952
  function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
7823
7953
  try {
@@ -7885,13 +8015,16 @@ async function hasReachableHumanApprover(opts) {
7885
8015
  if (nativeReachable) return true;
7886
8016
  return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
7887
8017
  }
8018
+ function mayDowngradeHardBlock(opts) {
8019
+ if (opts.overridable === false) return false;
8020
+ return opts.daemonUp && !opts.isTestEnv && opts.humanApproverReachable;
8021
+ }
7888
8022
  async function authorizeHeadless(toolName, args, meta, options) {
7889
8023
  if (!options?.calledFromDaemon) {
7890
- const actId = randomUUID();
8024
+ const actId = randomUUID2();
7891
8025
  const actTs = Date.now();
7892
- const stripAnsi = (s) => s.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "");
7893
- const sanitizedAgent = meta?.agent ? stripAnsi(meta.agent).slice(0, 80) : void 0;
7894
- const sanitizedMcpServer = meta?.mcpServer ? stripAnsi(meta.mcpServer).slice(0, 40) : void 0;
8026
+ const sanitizedAgent = meta?.agent ? safeMessage(meta.agent, 80) : void 0;
8027
+ const sanitizedMcpServer = meta?.mcpServer ? safeMessage(meta.mcpServer, 40) : void 0;
7895
8028
  const socketOk = await notifyActivity({
7896
8029
  id: actId,
7897
8030
  ts: actTs,
@@ -8276,7 +8409,12 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
8276
8409
  calledFromDaemon: options?.calledFromDaemon
8277
8410
  });
8278
8411
  }
8279
- const mayDowngrade = daemonUp && !isTestEnv2 && humanApproverReachable;
8412
+ const mayDowngrade = mayDowngradeHardBlock({
8413
+ daemonUp,
8414
+ isTestEnv: isTestEnv2,
8415
+ humanApproverReachable,
8416
+ overridable: policyResult.overridable
8417
+ });
8280
8418
  const hardBlock = () => {
8281
8419
  if (!isManual)
8282
8420
  appendLocalAudit(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "2.14.1",
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",