@blockrun/clawrouter 0.12.281 → 0.12.283

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
@@ -34678,6 +34678,11 @@ function getSharedSpendControl() {
34678
34678
  host.__clawrouterSharedSpendControl ??= new SpendControl();
34679
34679
  return host.__clawrouterSharedSpendControl;
34680
34680
  }
34681
+ function parseQuotedAmountMicros(selected) {
34682
+ const raw = selected.amount ?? selected.maxAmountRequired;
34683
+ if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) return void 0;
34684
+ return BigInt(raw);
34685
+ }
34681
34686
  function parseQuotedAmountUsd(selected) {
34682
34687
  const raw = selected.amount ?? selected.maxAmountRequired;
34683
34688
  if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) {
@@ -35443,6 +35448,16 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
35443
35448
  return est === void 0 ? void 0 : Number(est);
35444
35449
  };
35445
35450
  const needMicros = estimateMicros();
35451
+ const notifyDeclinedPayment = () => {
35452
+ if (!options?.onPaymentDeclined) return;
35453
+ try {
35454
+ options.onPaymentDeclined();
35455
+ } catch (error) {
35456
+ console.error(
35457
+ `[ClawRouter] onPaymentDeclined callback failed: ${error instanceof Error ? error.message : String(error)}`
35458
+ );
35459
+ }
35460
+ };
35446
35461
  const cached = !options?.skipPreAuth ? cache2.get(cacheKey2) : void 0;
35447
35462
  const preAuthCovers = cached !== void 0 && Date.now() - cached.cachedAt < ttlMs && cached.coverMicros !== void 0 && needMicros !== void 0 && needMicros <= cached.coverMicros;
35448
35463
  if (preAuthCovers) {
@@ -35461,6 +35476,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
35461
35476
  return response2;
35462
35477
  }
35463
35478
  paymentInFlight = false;
35479
+ notifyDeclinedPayment();
35464
35480
  cache2.delete(cacheKey2);
35465
35481
  } catch (err) {
35466
35482
  cache2.delete(cacheKey2);
@@ -35505,6 +35521,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
35505
35521
  clonedRequest.headers.set(key3, value);
35506
35522
  }
35507
35523
  const paidResponse = await baseFetch(clonedRequest);
35524
+ if (paidResponse.status === 402) notifyDeclinedPayment();
35508
35525
  notifyAcceptedPayment(paidResponse, payload);
35509
35526
  return paidResponse;
35510
35527
  };
@@ -64542,6 +64559,132 @@ var init_api_key = __esm({
64542
64559
  }
64543
64560
  });
64544
64561
 
64562
+ // src/session-spend.ts
64563
+ function parseMicros(raw) {
64564
+ if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
64565
+ return BigInt(raw);
64566
+ }
64567
+ function usdToMicros(usd) {
64568
+ return BigInt(Math.round(usd * 1e6));
64569
+ }
64570
+ function microsToUsd(micros) {
64571
+ return Number(micros) / 1e6;
64572
+ }
64573
+ var SessionSpend, CostCapError;
64574
+ var init_session_spend = __esm({
64575
+ "src/session-spend.ts"() {
64576
+ "use strict";
64577
+ init_spend_control();
64578
+ SessionSpend = class _SessionSpend {
64579
+ constructor(store, sessionId, capMicros, holdMicros) {
64580
+ this.store = store;
64581
+ this.sessionId = sessionId;
64582
+ this.capMicros = capMicros;
64583
+ this.heldMicros = holdMicros;
64584
+ if (holdMicros > 0n) store.adjustSessionHold(sessionId, holdMicros);
64585
+ }
64586
+ store;
64587
+ sessionId;
64588
+ capMicros;
64589
+ /** Budget this request still holds against the session (strict mode only). */
64590
+ heldMicros;
64591
+ /** Sum of payments signed by this request that still count. */
64592
+ signedMicros = 0n;
64593
+ /** The most recent signature not yet settled or voided — the one a decline undoes. */
64594
+ lastSignedMicros = 0n;
64595
+ released = false;
64596
+ /**
64597
+ * Strict mode: admit the request only if the session's spend, plus what its
64598
+ * other in-flight requests hold, plus this request's estimate fits the cap —
64599
+ * and hold the estimate in the same step.
64600
+ */
64601
+ static admit(store, sessionId, capMicros, estimateMicros) {
64602
+ const spentMicros = store.getSessionCostMicros(sessionId);
64603
+ const heldMicros = store.getSessionHoldMicros(sessionId);
64604
+ if (spentMicros + heldMicros + estimateMicros > capMicros) {
64605
+ return { admitted: false, spentMicros, heldMicros, estimateMicros };
64606
+ }
64607
+ return {
64608
+ admitted: true,
64609
+ spend: new _SessionSpend(store, sessionId, capMicros, estimateMicros)
64610
+ };
64611
+ }
64612
+ /** Graceful mode: track signed spend, enforce nothing. */
64613
+ static track(store, sessionId) {
64614
+ return new _SessionSpend(store, sessionId, void 0, 0n);
64615
+ }
64616
+ get isStrict() {
64617
+ return this.capMicros !== void 0;
64618
+ }
64619
+ /**
64620
+ * Before a payment is created: may this request sign `quoteMicros`?
64621
+ * Spend already recorded (including this request's own earlier signatures)
64622
+ * plus what OTHER in-flight requests hold, plus the quote, must fit the cap.
64623
+ * Always true in graceful mode.
64624
+ */
64625
+ coverPayment(quoteMicros) {
64626
+ if (this.capMicros === void 0) return true;
64627
+ const spent = this.store.getSessionCostMicros(this.sessionId);
64628
+ const heldByOthers = this.store.getSessionHoldMicros(this.sessionId) - this.heldMicros;
64629
+ if (spent + heldByOthers + quoteMicros > this.capMicros) return false;
64630
+ if (quoteMicros > this.heldMicros) {
64631
+ this.store.adjustSessionHold(this.sessionId, quoteMicros - this.heldMicros);
64632
+ this.heldMicros = quoteMicros;
64633
+ }
64634
+ return true;
64635
+ }
64636
+ /** A payment was signed: it is spend now, and replaces part of the hold. */
64637
+ recordSigned(amountMicros) {
64638
+ if (amountMicros <= 0n) return;
64639
+ this.signedMicros += amountMicros;
64640
+ this.lastSignedMicros = amountMicros;
64641
+ this.store.addSessionCost(this.sessionId, amountMicros);
64642
+ this.shrinkHold(amountMicros);
64643
+ }
64644
+ /**
64645
+ * The gateway answered the payment just signed with a 402: nothing settled.
64646
+ * Payments within one request are signed and sent one at a time, so the
64647
+ * decline always belongs to the latest signature. Undone at most once.
64648
+ */
64649
+ voidLastSigned() {
64650
+ const undo = this.lastSignedMicros;
64651
+ this.lastSignedMicros = 0n;
64652
+ if (undo <= 0n) return;
64653
+ this.signedMicros -= undo;
64654
+ this.store.addSessionCost(this.sessionId, -undo);
64655
+ }
64656
+ /**
64657
+ * An attempt succeeded. When nothing was signed for this request (the API
64658
+ * key rail), the local estimate is the only record of the spend. When a
64659
+ * payment was signed, it is already recorded at its real amount.
64660
+ */
64661
+ recordSuccess(estimateMicros) {
64662
+ if (this.signedMicros > 0n || estimateMicros <= 0n) return;
64663
+ this.store.addSessionCost(this.sessionId, estimateMicros);
64664
+ this.shrinkHold(estimateMicros);
64665
+ }
64666
+ /** The request is over: return whatever it still holds. Idempotent. */
64667
+ release() {
64668
+ if (this.released) return;
64669
+ this.released = true;
64670
+ this.shrinkHold(this.heldMicros);
64671
+ }
64672
+ shrinkHold(byMicros) {
64673
+ const drop = byMicros < this.heldMicros ? byMicros : this.heldMicros;
64674
+ if (drop <= 0n) return;
64675
+ this.heldMicros -= drop;
64676
+ this.store.adjustSessionHold(this.sessionId, -drop);
64677
+ }
64678
+ };
64679
+ CostCapError = class extends SpendPolicyError {
64680
+ constructor(reason) {
64681
+ super(reason);
64682
+ this.name = "CostCapError";
64683
+ }
64684
+ };
64685
+ }
64686
+ });
64687
+
64545
64688
  // src/compression/types.ts
64546
64689
  var DEFAULT_COMPRESSION_CONFIG;
64547
64690
  var init_types2 = __esm({
@@ -65389,6 +65532,9 @@ var init_session = __esm({
65389
65532
  };
65390
65533
  SessionStore = class {
65391
65534
  sessions = /* @__PURE__ */ new Map();
65535
+ // Kept apart from `sessions`: a hold belongs to requests still running, so it
65536
+ // must not vanish when the idle-session sweep drops an entry mid-request.
65537
+ sessionHolds = /* @__PURE__ */ new Map();
65392
65538
  config;
65393
65539
  cleanupInterval = null;
65394
65540
  constructor(config = {}) {
@@ -65564,14 +65710,34 @@ var init_session = __esm({
65564
65710
  this.sessions.set(sessionId, entry);
65565
65711
  }
65566
65712
  entry.sessionCostMicros += additionalMicros;
65713
+ if (entry.sessionCostMicros < 0n) entry.sessionCostMicros = 0n;
65567
65714
  }
65568
65715
  /**
65569
65716
  * Get the total accumulated cost for a session in USD.
65570
65717
  */
65571
65718
  getSessionCostUsd(sessionId) {
65572
- const entry = this.sessions.get(sessionId);
65573
- if (!entry) return 0;
65574
- return Number(entry.sessionCostMicros) / 1e6;
65719
+ return Number(this.getSessionCostMicros(sessionId)) / 1e6;
65720
+ }
65721
+ /**
65722
+ * Get the total accumulated cost for a session in USDC micros.
65723
+ */
65724
+ getSessionCostMicros(sessionId) {
65725
+ return this.sessions.get(sessionId)?.sessionCostMicros ?? 0n;
65726
+ }
65727
+ /**
65728
+ * Budget held by a session's in-flight requests (USDC micros) — admitted
65729
+ * against the strict cap but not yet spent. See session-spend.ts.
65730
+ */
65731
+ getSessionHoldMicros(sessionId) {
65732
+ return this.sessionHolds.get(sessionId) ?? 0n;
65733
+ }
65734
+ /**
65735
+ * Grow (positive) or release (negative) a session's in-flight hold.
65736
+ */
65737
+ adjustSessionHold(sessionId, deltaMicros) {
65738
+ const next = (this.sessionHolds.get(sessionId) ?? 0n) + deltaMicros;
65739
+ if (next > 0n) this.sessionHolds.set(sessionId, next);
65740
+ else this.sessionHolds.delete(sessionId);
65575
65741
  }
65576
65742
  /**
65577
65743
  * Stop the cleanup interval.
@@ -65931,6 +66097,156 @@ ${lines.join("\n")}`;
65931
66097
  }
65932
66098
  });
65933
66099
 
66100
+ // src/local-guard.ts
66101
+ import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readSync as readSync2, realpathSync } from "fs";
66102
+ import { homedir as homedir7 } from "os";
66103
+ import { join as join10 } from "path";
66104
+ function isLocalHostname(hostname) {
66105
+ const h = hostname.toLowerCase().replace(/\.$/, "");
66106
+ return h === "localhost" || h === "127.0.0.1" || h === "[::1]" || h === "::1";
66107
+ }
66108
+ function untrustedRequestReason(req) {
66109
+ const host = req.headers.host;
66110
+ if (host) {
66111
+ let hostname;
66112
+ try {
66113
+ hostname = new URL(`http://${host}`).hostname;
66114
+ } catch {
66115
+ return "malformed Host header";
66116
+ }
66117
+ if (!isLocalHostname(hostname)) return `non-local Host header (${hostname})`;
66118
+ }
66119
+ const origin2 = req.headers.origin;
66120
+ if (Array.isArray(origin2)) return "multiple Origin headers";
66121
+ if (origin2 !== void 0) {
66122
+ if (origin2 === "null") return "opaque Origin";
66123
+ let o3;
66124
+ try {
66125
+ o3 = new URL(origin2);
66126
+ } catch {
66127
+ return "malformed Origin header";
66128
+ }
66129
+ if (o3.protocol !== "http:" && o3.protocol !== "https:" || !isLocalHostname(o3.hostname)) {
66130
+ return `cross-site Origin (${origin2})`;
66131
+ }
66132
+ }
66133
+ const site = req.headers["sec-fetch-site"];
66134
+ if (site === "cross-site") return "cross-site browser request";
66135
+ return null;
66136
+ }
66137
+ function v4FromHex(hi, lo) {
66138
+ return `${hi >>> 8 & 255}.${hi & 255}.${lo >>> 8 & 255}.${lo & 255}`;
66139
+ }
66140
+ function isBlockedSsrfHost(hostname) {
66141
+ const h = hostname.toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.$/, "");
66142
+ if (!h) return true;
66143
+ if (h === "localhost" || h.endsWith(".localhost") || h.endsWith(".local")) return true;
66144
+ if (h.endsWith(".internal")) return true;
66145
+ if (METADATA_HOSTS.has(h)) return true;
66146
+ if (h.includes(":")) {
66147
+ if (h === "::1" || h === "::") return true;
66148
+ if (h.startsWith("fe80:") || h.startsWith("fc") || h.startsWith("fd")) return true;
66149
+ const sixToFour = h.match(/^2002:([0-9a-f]{1,4}):([0-9a-f]{1,4}):/);
66150
+ if (sixToFour) {
66151
+ return isBlockedSsrfHost(v4FromHex(parseInt(sixToFour[1], 16), parseInt(sixToFour[2], 16)));
66152
+ }
66153
+ const embedded = h.match(/^64:ff9b::(.+)$/) ?? h.match(/::ffff:(.+)$/);
66154
+ if (embedded) {
66155
+ const tail = embedded[1];
66156
+ if (tail.includes(".")) return isBlockedSsrfHost(tail);
66157
+ const seg = tail.split(":");
66158
+ if (seg.length === 2)
66159
+ return isBlockedSsrfHost(v4FromHex(parseInt(seg[0], 16), parseInt(seg[1], 16)));
66160
+ }
66161
+ return false;
66162
+ }
66163
+ const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
66164
+ if (m) {
66165
+ const a = Number(m[1]);
66166
+ const b = Number(m[2]);
66167
+ const c = Number(m[3]);
66168
+ if (a === 127 || a === 0 || a === 10) return true;
66169
+ if (a === 169 && b === 254) return true;
66170
+ if (a === 172 && b >= 16 && b <= 31) return true;
66171
+ if (a === 192 && b === 168) return true;
66172
+ if (a === 100 && b >= 64 && b <= 127) return true;
66173
+ if (a === 192 && b === 0 && c === 0) return true;
66174
+ }
66175
+ return false;
66176
+ }
66177
+ async function ssrfSafeFetch(url2, init2 = {}, maxHops = 5) {
66178
+ const { allowPrivate, ...fetchInit } = init2;
66179
+ let current = url2;
66180
+ for (let hop = 0; hop <= maxHops; hop++) {
66181
+ const u = new URL(current);
66182
+ if (u.protocol !== "http:" && u.protocol !== "https:") {
66183
+ throw new Error(`refusing to fetch non-http(s) URL (${u.protocol})`);
66184
+ }
66185
+ if (!allowPrivate && isBlockedSsrfHost(u.hostname)) {
66186
+ throw new Error(`refusing to fetch a private/loopback/metadata address: ${u.hostname}`);
66187
+ }
66188
+ const res = await fetch(current, { ...fetchInit, redirect: "manual" });
66189
+ if (res.status >= 300 && res.status < 400) {
66190
+ const loc = res.headers.get("location");
66191
+ if (!loc) return res;
66192
+ current = new URL(loc, current).href;
66193
+ continue;
66194
+ }
66195
+ return res;
66196
+ }
66197
+ throw new Error("too many redirects");
66198
+ }
66199
+ function readLocalImageAsDataUri(filePath) {
66200
+ const expanded = filePath.startsWith("~/") ? join10(homedir7(), filePath.slice(2)) : filePath;
66201
+ const refuse = () => new Error(`Not a readable PNG, JPEG or WebP image: ${filePath}`);
66202
+ let fd;
66203
+ try {
66204
+ fd = openSync2(realpathSync(expanded), "r");
66205
+ } catch {
66206
+ throw refuse();
66207
+ }
66208
+ try {
66209
+ const st = fstatSync2(fd);
66210
+ if (!st.isFile() || st.size === 0 || st.size > MAX_LOCAL_IMAGE_BYTES) throw refuse();
66211
+ const head = Buffer.alloc(12);
66212
+ readSync2(fd, head, 0, 12, 0);
66213
+ const sig = IMAGE_SIGNATURES.find((s3) => s3.test(head));
66214
+ if (!sig) throw refuse();
66215
+ const data = Buffer.alloc(st.size);
66216
+ let off = 0;
66217
+ while (off < st.size) {
66218
+ const n = readSync2(fd, data, off, st.size - off, off);
66219
+ if (n === 0) break;
66220
+ off += n;
66221
+ }
66222
+ return `data:${sig.mime};base64,${data.subarray(0, off).toString("base64")}`;
66223
+ } finally {
66224
+ closeSync2(fd);
66225
+ }
66226
+ }
66227
+ var METADATA_HOSTS, MAX_LOCAL_IMAGE_BYTES, IMAGE_SIGNATURES;
66228
+ var init_local_guard = __esm({
66229
+ "src/local-guard.ts"() {
66230
+ "use strict";
66231
+ METADATA_HOSTS = /* @__PURE__ */ new Set(["metadata", "metadata.goog", "instance-data"]);
66232
+ MAX_LOCAL_IMAGE_BYTES = 25 * 1024 * 1024;
66233
+ IMAGE_SIGNATURES = [
66234
+ {
66235
+ mime: "image/png",
66236
+ test: (b) => b.length >= 8 && b.readUInt32BE(0) === 2303741511 && b.readUInt32BE(4) === 218765834
66237
+ },
66238
+ {
66239
+ mime: "image/jpeg",
66240
+ test: (b) => b.length >= 3 && b[0] === 255 && b[1] === 216 && b[2] === 255
66241
+ },
66242
+ {
66243
+ mime: "image/webp",
66244
+ test: (b) => b.length >= 12 && b.toString("ascii", 0, 4) === "RIFF" && b.toString("ascii", 8, 12) === "WEBP"
66245
+ }
66246
+ ];
66247
+ }
66248
+ });
66249
+
65934
66250
  // node_modules/undici/lib/core/symbols.js
65935
66251
  var require_symbols = __commonJS({
65936
66252
  "node_modules/undici/lib/core/symbols.js"(exports, module) {
@@ -93324,8 +93640,8 @@ var init_textual_tool_calls = __esm({
93324
93640
 
93325
93641
  // src/response-store.ts
93326
93642
  import { appendFile as appendFile2, mkdir as mkdir5, readFile, readdir as readdir2 } from "fs/promises";
93327
- import { homedir as homedir7 } from "os";
93328
- import { join as join10 } from "path";
93643
+ import { homedir as homedir8 } from "os";
93644
+ import { join as join11 } from "path";
93329
93645
  import { randomBytes as randomBytes7 } from "crypto";
93330
93646
  function isEnabled() {
93331
93647
  const v = process.env.BLOCKRUN_RESPONSE_STORE;
@@ -93338,7 +93654,7 @@ async function ensureDir2() {
93338
93654
  }
93339
93655
  function dailyFile(date = /* @__PURE__ */ new Date()) {
93340
93656
  const iso = date.toISOString().slice(0, 10);
93341
- return join10(STORE_DIR, `responses-${iso}.jsonl`);
93657
+ return join11(STORE_DIR, `responses-${iso}.jsonl`);
93342
93658
  }
93343
93659
  function genId() {
93344
93660
  return `resp_${Date.now()}_${randomBytes7(3).toString("hex")}`;
@@ -93388,7 +93704,7 @@ async function listRecent(limit = 20, daysBack = 7) {
93388
93704
  d.setUTCDate(d.getUTCDate() - i);
93389
93705
  const iso = d.toISOString().slice(0, 10);
93390
93706
  const name = `responses-${iso}.jsonl`;
93391
- if (files.includes(name)) candidateFiles.push(join10(STORE_DIR, name));
93707
+ if (files.includes(name)) candidateFiles.push(join11(STORE_DIR, name));
93392
93708
  }
93393
93709
  const all3 = [];
93394
93710
  for (const f of candidateFiles) {
@@ -93417,7 +93733,7 @@ var STORE_DIR, dirReady2;
93417
93733
  var init_response_store = __esm({
93418
93734
  "src/response-store.ts"() {
93419
93735
  "use strict";
93420
- STORE_DIR = join10(homedir7(), ".openclaw", "blockrun", "responses");
93736
+ STORE_DIR = join11(homedir8(), ".openclaw", "blockrun", "responses");
93421
93737
  dirReady2 = false;
93422
93738
  }
93423
93739
  });
@@ -95054,10 +95370,10 @@ import { createHmac } from "crypto";
95054
95370
  import { createServer } from "http";
95055
95371
  import { finished, Readable } from "stream";
95056
95372
  import { pipeline } from "stream/promises";
95057
- import { homedir as homedir8 } from "os";
95058
- import { join as join11 } from "path";
95373
+ import { homedir as homedir9 } from "os";
95374
+ import { join as join12 } from "path";
95059
95375
  import { mkdir as mkdir6, writeFile as writeFile4, readFile as readFile2, stat as fsStat } from "fs/promises";
95060
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
95376
+ import { readFileSync as readFileSync2 } from "fs";
95061
95377
  async function loadGatewayCatalog(apiBase, apiKey) {
95062
95378
  try {
95063
95379
  const controller = new AbortController();
@@ -95951,22 +96267,6 @@ async function proxyPaidApiRequest(req, res, apiBase, payFetch, getActualPayment
95951
96267
  }).catch(() => {
95952
96268
  });
95953
96269
  }
95954
- function readImageFileAsDataUri(filePath) {
95955
- const resolved = filePath.startsWith("~/") ? join11(homedir8(), filePath.slice(2)) : filePath;
95956
- if (!existsSync2(resolved)) {
95957
- throw new Error(`Image file not found: ${resolved}`);
95958
- }
95959
- const ext = resolved.split(".").pop()?.toLowerCase() ?? "png";
95960
- const mimeMap = {
95961
- png: "image/png",
95962
- jpg: "image/jpeg",
95963
- jpeg: "image/jpeg",
95964
- webp: "image/webp"
95965
- };
95966
- const mime = mimeMap[ext] ?? "image/png";
95967
- const data = readFileSync2(resolved);
95968
- return `data:${mime};base64,${data.toString("base64")}`;
95969
- }
95970
96270
  async function uploadDataUriToHost(dataUri) {
95971
96271
  const match = dataUri.match(/^data:(image\/\w+);base64,(.+)$/);
95972
96272
  if (!match) throw new Error("Invalid data URI format");
@@ -96140,6 +96440,19 @@ async function startProxy(options) {
96140
96440
  }
96141
96441
  const account = walletKey ? privateKeyToAccount(walletKey) : void 0;
96142
96442
  const x402 = authMode === "wallet" ? new x402Client() : void 0;
96443
+ x402?.onBeforePaymentCreation(async (context) => {
96444
+ const spend = paymentStore.getStore()?.sessionSpend;
96445
+ if (!spend?.isStrict) return;
96446
+ const quote = parseQuotedAmountMicros(context.selectedRequirements);
96447
+ if (quote === void 0) {
96448
+ throw new CostCapError("ClawRouter cost cap: unreadable payment amount in the 402 quote");
96449
+ }
96450
+ if (!spend.coverPayment(quote)) {
96451
+ throw new CostCapError(
96452
+ `ClawRouter cost cap exceeded: signing $${microsToUsd(quote).toFixed(6)} would push this session past its maxCostPerRun limit`
96453
+ );
96454
+ }
96455
+ });
96143
96456
  if (x402 && account) {
96144
96457
  const evmPublicClient = createPublicClient({ chain: base, transport: http() });
96145
96458
  const evmSigner = toClientEvmSigner(account, evmPublicClient);
@@ -96182,6 +96495,11 @@ async function startProxy(options) {
96182
96495
  if (store) store.amountUsd = amountUsd;
96183
96496
  console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
96184
96497
  });
96498
+ x402?.onAfterPaymentCreation(async (context) => {
96499
+ const spend = paymentStore.getStore()?.sessionSpend;
96500
+ const amount = parseQuotedAmountMicros(context.selectedRequirements);
96501
+ if (spend && amount !== void 0) spend.recordSigned(amount);
96502
+ });
96185
96503
  const payFetch = authMode === "api-key" ? withSpendPolicy(
96186
96504
  createApiKeyFetch(apiKey, fetch, apiBase),
96187
96505
  options.spendControl ?? getSharedSpendControl(),
@@ -96200,7 +96518,10 @@ async function startProxy(options) {
96200
96518
  // Only the wallet rail settles per call. The API-key rail is billed
96201
96519
  // server-side against account credit, so there is no per-request
96202
96520
  // settlement for an observer to report.
96203
- onPayment: options.onPayment
96521
+ onPayment: options.onPayment,
96522
+ // A 402 answer to a signed payment means the gateway declined it and
96523
+ // nothing settled — take it back off the session's budget.
96524
+ onPaymentDeclined: () => paymentStore.getStore()?.sessionSpend?.voidLastSigned()
96204
96525
  });
96205
96526
  let balanceMonitor;
96206
96527
  if (options._balanceMonitorOverride) {
@@ -96243,6 +96564,13 @@ async function startProxy(options) {
96243
96564
  console.error(`[ClawRouter] Request finished with error: ${err.message}`);
96244
96565
  }
96245
96566
  });
96567
+ const untrusted = untrustedRequestReason(req);
96568
+ if (untrusted) {
96569
+ console.warn(`[ClawRouter] Refused request to ${req.url}: ${untrusted}`);
96570
+ res.writeHead(403, { "Content-Type": "application/json" });
96571
+ res.end(JSON.stringify({ error: "forbidden", details: untrusted }));
96572
+ return;
96573
+ }
96246
96574
  if (req.url === "/health" || req.url?.startsWith("/health?")) {
96247
96575
  const url2 = new URL(req.url, "http://localhost");
96248
96576
  const full = url2.searchParams.get("full") === "true";
@@ -96471,7 +96799,7 @@ async function startProxy(options) {
96471
96799
  res.end("Bad request");
96472
96800
  return;
96473
96801
  }
96474
- const filePath = join11(IMAGE_DIR, filename);
96802
+ const filePath = join12(IMAGE_DIR, filename);
96475
96803
  try {
96476
96804
  const s3 = await fsStat(filePath);
96477
96805
  if (!s3.isFile()) throw new Error("not a file");
@@ -96502,7 +96830,7 @@ async function startProxy(options) {
96502
96830
  res.end("Bad request");
96503
96831
  return;
96504
96832
  }
96505
- const filePath = join11(AUDIO_DIR, filename);
96833
+ const filePath = join12(AUDIO_DIR, filename);
96506
96834
  try {
96507
96835
  const s3 = await fsStat(filePath);
96508
96836
  if (!s3.isFile()) throw new Error("not a file");
@@ -96532,7 +96860,7 @@ async function startProxy(options) {
96532
96860
  res.end("Bad request");
96533
96861
  return;
96534
96862
  }
96535
- const filePath = join11(VIDEO_DIR, filename);
96863
+ const filePath = join12(VIDEO_DIR, filename);
96536
96864
  try {
96537
96865
  const s3 = await fsStat(filePath);
96538
96866
  if (!s3.isFile()) throw new Error("not a file");
@@ -96674,7 +97002,7 @@ async function startProxy(options) {
96674
97002
  const [, mimeType, b64] = dataUriMatch;
96675
97003
  const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
96676
97004
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
96677
- await writeFile4(join11(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
97005
+ await writeFile4(join12(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
96678
97006
  img.url = `http://localhost:${port2}/images/${filename}`;
96679
97007
  console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
96680
97008
  } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
@@ -96685,7 +97013,7 @@ async function startProxy(options) {
96685
97013
  const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
96686
97014
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
96687
97015
  const buf = Buffer.from(await imgResp.arrayBuffer());
96688
- await writeFile4(join11(IMAGE_DIR, filename), buf);
97016
+ await writeFile4(join12(IMAGE_DIR, filename), buf);
96689
97017
  img.url = `http://localhost:${port2}/images/${filename}`;
96690
97018
  console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
96691
97019
  }
@@ -96748,7 +97076,10 @@ async function startProxy(options) {
96748
97076
  if (typeof val !== "string" || !val) continue;
96749
97077
  if (val.startsWith("data:")) {
96750
97078
  } else if (val.startsWith("https://") || val.startsWith("http://")) {
96751
- const imgResp = await fetch(val, { signal: clientAbort.signal });
97079
+ const imgResp = await ssrfSafeFetch(val, {
97080
+ signal: clientAbort.signal,
97081
+ allowPrivate: process.env.CLAWROUTER_ALLOW_PRIVATE_FETCH === "1"
97082
+ });
96752
97083
  if (!imgResp.ok)
96753
97084
  throw new Error(`Failed to download ${field} from ${val}: HTTP ${imgResp.status}`);
96754
97085
  const contentType = imgResp.headers.get("content-type") ?? "image/png";
@@ -96758,7 +97089,7 @@ async function startProxy(options) {
96758
97089
  `[ClawRouter] img2img: downloaded ${field} URL \u2192 data URI (${buf.length} bytes)`
96759
97090
  );
96760
97091
  } else {
96761
- parsed[field] = readImageFileAsDataUri(val);
97092
+ parsed[field] = readLocalImageAsDataUri(val);
96762
97093
  console.log(`[ClawRouter] img2img: read ${field} file \u2192 data URI`);
96763
97094
  }
96764
97095
  }
@@ -96807,7 +97138,7 @@ async function startProxy(options) {
96807
97138
  const [, mimeType, b64] = dataUriMatch;
96808
97139
  const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
96809
97140
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
96810
- await writeFile4(join11(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
97141
+ await writeFile4(join12(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
96811
97142
  img.url = `http://localhost:${port2}/images/${filename}`;
96812
97143
  console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
96813
97144
  } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
@@ -96818,7 +97149,7 @@ async function startProxy(options) {
96818
97149
  const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
96819
97150
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
96820
97151
  const buf = Buffer.from(await imgResp.arrayBuffer());
96821
- await writeFile4(join11(IMAGE_DIR, filename), buf);
97152
+ await writeFile4(join12(IMAGE_DIR, filename), buf);
96822
97153
  img.url = `http://localhost:${port2}/images/${filename}`;
96823
97154
  console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
96824
97155
  }
@@ -96914,7 +97245,7 @@ async function startProxy(options) {
96914
97245
  const ext = contentType.includes("wav") ? "wav" : "mp3";
96915
97246
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
96916
97247
  const buf = Buffer.from(await audioResp.arrayBuffer());
96917
- await writeFile4(join11(AUDIO_DIR, filename), buf);
97248
+ await writeFile4(join12(AUDIO_DIR, filename), buf);
96918
97249
  track.url = `http://localhost:${port2}/audio/${filename}`;
96919
97250
  console.log(`[ClawRouter] Audio saved \u2192 ${track.url}`);
96920
97251
  }
@@ -97081,7 +97412,7 @@ async function startProxy(options) {
97081
97412
  const ext = contentType.includes("webm") ? "webm" : contentType.includes("quicktime") ? "mov" : "mp4";
97082
97413
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
97083
97414
  const buf = Buffer.from(await videoResp.arrayBuffer());
97084
- await writeFile4(join11(VIDEO_DIR, filename), buf);
97415
+ await writeFile4(join12(VIDEO_DIR, filename), buf);
97085
97416
  clip.url = `http://localhost:${port2}/videos/${filename}`;
97086
97417
  console.log(`[ClawRouter] Video saved \u2192 ${clip.url}`);
97087
97418
  }
@@ -97474,6 +97805,17 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
97474
97805
  return { success: true, response };
97475
97806
  } catch (err) {
97476
97807
  const errorMsg = err instanceof Error ? err.message : String(err);
97808
+ if (err instanceof CostCapError) {
97809
+ return {
97810
+ success: false,
97811
+ errorBody: JSON.stringify({
97812
+ error: { message: errorMsg, type: "cost_cap_exceeded", code: "cost_cap_exceeded" }
97813
+ }),
97814
+ errorStatus: 429,
97815
+ isProviderError: false,
97816
+ errorCategory: "payment_error"
97817
+ };
97818
+ }
97477
97819
  if (err instanceof SpendPolicyError) {
97478
97820
  return {
97479
97821
  success: false,
@@ -98047,8 +98389,8 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
98047
98389
  let imageDataUri;
98048
98390
  let maskDataUri;
98049
98391
  try {
98050
- imageDataUri = readImageFileAsDataUri(imagePath);
98051
- if (maskPath) maskDataUri = readImageFileAsDataUri(maskPath);
98392
+ imageDataUri = readLocalImageAsDataUri(imagePath);
98393
+ if (maskPath) maskDataUri = readLocalImageAsDataUri(maskPath);
98052
98394
  } catch (fileErr) {
98053
98395
  const fileErrMsg = fileErr instanceof Error ? fileErr.message : String(fileErr);
98054
98396
  sendImg2ImgText(`Failed to read image file: ${fileErrMsg}`);
@@ -98553,14 +98895,23 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
98553
98895
  }
98554
98896
  }
98555
98897
  }
98898
+ let sessionSpend;
98556
98899
  if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "strict") {
98557
- const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
98558
98900
  const thisReqEstStr = estimatedCostMicros !== void 0 ? estimatedCostMicros.toString() : modelId ? estimateAmount(modelId, body.length, maxTokens) : void 0;
98559
- const thisReqEstUsd = thisReqEstStr ? Number(thisReqEstStr) / 1e6 : 0;
98560
- const projectedCostUsd = runCostUsd + thisReqEstUsd;
98561
- if (projectedCostUsd > options.maxCostPerRunUsd) {
98901
+ const admission = SessionSpend.admit(
98902
+ sessionStore,
98903
+ effectiveSessionId,
98904
+ usdToMicros(options.maxCostPerRunUsd),
98905
+ parseMicros(thisReqEstStr) ?? 0n
98906
+ );
98907
+ if (!admission.admitted) {
98908
+ const spentUsd = microsToUsd(admission.spentMicros);
98909
+ const heldUsd = microsToUsd(admission.heldMicros);
98910
+ const estUsd = microsToUsd(admission.estimateMicros);
98911
+ const projectedCostUsd = spentUsd + heldUsd + estUsd;
98912
+ const breakdown = `spent $${spentUsd.toFixed(4)}${heldUsd > 0 ? ` + in flight $${heldUsd.toFixed(4)}` : ""} + est $${estUsd.toFixed(4)}`;
98562
98913
  console.log(
98563
- `[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (spent $${runCostUsd.toFixed(4)} + est $${thisReqEstUsd.toFixed(4)}) > $${options.maxCostPerRunUsd} limit`
98914
+ `[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (${breakdown}) > $${options.maxCostPerRunUsd} limit`
98564
98915
  );
98565
98916
  res.writeHead(429, {
98566
98917
  "Content-Type": "application/json",
@@ -98569,7 +98920,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
98569
98920
  res.end(
98570
98921
  JSON.stringify({
98571
98922
  error: {
98572
- message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (spent $${runCostUsd.toFixed(4)} + est $${thisReqEstUsd.toFixed(4)}) would exceed limit $${options.maxCostPerRunUsd}`,
98923
+ message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (${breakdown}) would exceed limit $${options.maxCostPerRunUsd}`,
98573
98924
  type: "cost_cap_exceeded",
98574
98925
  code: "cost_cap_exceeded"
98575
98926
  }
@@ -98578,7 +98929,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
98578
98929
  deduplicator.removeInflight(dedupKey);
98579
98930
  return;
98580
98931
  }
98932
+ sessionSpend = admission.spend;
98933
+ } else if (options.maxCostPerRunUsd && effectiveSessionId) {
98934
+ sessionSpend = SessionSpend.track(sessionStore, effectiveSessionId);
98581
98935
  }
98936
+ const requestStore = paymentStore.getStore();
98937
+ if (requestStore) requestStore.sessionSpend = sessionSpend;
98582
98938
  if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "graceful") {
98583
98939
  const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
98584
98940
  const remainingUsd = options.maxCostPerRunUsd - runCostUsd;
@@ -98974,11 +99330,9 @@ data: [DONE]
98974
99330
  actualModelUsed = tryModel;
98975
99331
  recordOutcome(tryModel, requestKind, true);
98976
99332
  console.log(`[ClawRouter] Success with model: ${tryModel}`);
98977
- if (options.maxCostPerRunUsd && effectiveSessionId && !FREE_MODELS.has(tryModel)) {
98978
- const costEst = estimateAmount(tryModel, body.length, maxTokens);
98979
- if (costEst) {
98980
- sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
98981
- }
99333
+ if (sessionSpend && !FREE_MODELS.has(tryModel)) {
99334
+ const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
99335
+ if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
98982
99336
  }
98983
99337
  break;
98984
99338
  }
@@ -99052,11 +99406,9 @@ data: [DONE]
99052
99406
  upstream = retryResult.response;
99053
99407
  actualModelUsed = tryModel;
99054
99408
  console.log(`[ClawRouter] Explicit-pin retry succeeded for: ${tryModel}`);
99055
- if (options.maxCostPerRunUsd && effectiveSessionId && !FREE_MODELS.has(tryModel)) {
99056
- const costEst = estimateAmount(tryModel, body.length, maxTokens);
99057
- if (costEst) {
99058
- sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
99059
- }
99409
+ if (sessionSpend && !FREE_MODELS.has(tryModel)) {
99410
+ const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
99411
+ if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
99060
99412
  }
99061
99413
  break;
99062
99414
  }
@@ -99123,11 +99475,9 @@ data: [DONE]
99123
99475
  upstream = retryResult.response;
99124
99476
  actualModelUsed = tryModel;
99125
99477
  console.log(`[ClawRouter] Rate-limit retry succeeded for: ${tryModel}`);
99126
- if (options.maxCostPerRunUsd && effectiveSessionId && !FREE_MODELS.has(tryModel)) {
99127
- const costEst = estimateAmount(tryModel, body.length, maxTokens);
99128
- if (costEst) {
99129
- sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
99130
- }
99478
+ if (sessionSpend && !FREE_MODELS.has(tryModel)) {
99479
+ const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
99480
+ if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
99131
99481
  }
99132
99482
  break;
99133
99483
  }
@@ -99221,6 +99571,7 @@ data: [DONE]
99221
99571
  );
99222
99572
  const rawErrBody = lastError?.body || structuredMessage;
99223
99573
  const errStatus = lastError?.status || 502;
99574
+ const costCapRefusal = errStatus === 429 && rawErrBody.includes('"cost_cap_exceeded"');
99224
99575
  const transformedErr = transformPaymentError(rawErrBody);
99225
99576
  requestHadError = true;
99226
99577
  if (headersSentEarly) {
@@ -99257,7 +99608,8 @@ data: [DONE]
99257
99608
  "Content-Type": "application/json",
99258
99609
  "x-context-used-kb": String(originalContextSizeKB),
99259
99610
  "x-context-limit-kb": String(CONTEXT_LIMIT_KB),
99260
- ...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {}
99611
+ ...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {},
99612
+ ...costCapRefusal ? { "X-ClawRouter-Cost-Cap-Exceeded": "1" } : {}
99261
99613
  });
99262
99614
  res.end(transformedErr);
99263
99615
  deduplicator.complete(dedupKey, {
@@ -99691,6 +100043,8 @@ data: [DONE]
99691
100043
  throw new Error(`Request timed out after ${timeoutMs}ms`, { cause: err });
99692
100044
  }
99693
100045
  throw err;
100046
+ } finally {
100047
+ sessionSpend?.release();
99694
100048
  }
99695
100049
  const logModel = routingDecision?.model ?? modelId;
99696
100050
  if (logModel) {
@@ -99777,6 +100131,7 @@ var init_proxy = __esm({
99777
100131
  init_auth();
99778
100132
  init_api_key();
99779
100133
  init_spend_control();
100134
+ init_session_spend();
99780
100135
  init_compression();
99781
100136
  init_errors5();
99782
100137
  init_version4();
@@ -99786,6 +100141,7 @@ var init_proxy = __esm({
99786
100141
  init_outcome_memory();
99787
100142
  init_config();
99788
100143
  init_journal();
100144
+ init_local_guard();
99789
100145
  init_upstream_proxy();
99790
100146
  init_textual_tool_calls();
99791
100147
  init_response_store();
@@ -99794,10 +100150,10 @@ var init_proxy = __esm({
99794
100150
  paymentStore = new AsyncLocalStorage();
99795
100151
  BLOCKRUN_API = "https://blockrun.ai/api";
99796
100152
  BLOCKRUN_SOLANA_API = "https://sol.blockrun.ai/api";
99797
- DESKTOP_SERVICE_TOKEN_FILE = process.env.CLAWROUTER_DESKTOP_TOKEN_FILE ?? join11(homedir8(), ".clawrouter-desktop", "service-token");
99798
- IMAGE_DIR = join11(homedir8(), ".openclaw", "blockrun", "images");
99799
- AUDIO_DIR = join11(homedir8(), ".openclaw", "blockrun", "audio");
99800
- VIDEO_DIR = join11(homedir8(), ".openclaw", "blockrun", "videos");
100153
+ DESKTOP_SERVICE_TOKEN_FILE = process.env.CLAWROUTER_DESKTOP_TOKEN_FILE ?? join12(homedir9(), ".clawrouter-desktop", "service-token");
100154
+ IMAGE_DIR = join12(homedir9(), ".openclaw", "blockrun", "images");
100155
+ AUDIO_DIR = join12(homedir9(), ".openclaw", "blockrun", "audio");
100156
+ VIDEO_DIR = join12(homedir9(), ".openclaw", "blockrun", "videos");
99801
100157
  AUTO_MODEL = "blockrun/auto";
99802
100158
  ROUTING_PROFILES = /* @__PURE__ */ new Set([
99803
100159
  "blockrun/eco",
@@ -101178,7 +101534,13 @@ function buildTool(service, proxyBaseUrl) {
101178
101534
  if (service.proxyPath === "/pm/__dynamic__") {
101179
101535
  const rawPath = typeof params.path === "string" ? params.path : "";
101180
101536
  const normalized = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
101181
- if (!normalized.startsWith("/pm/") || normalized.includes("..")) {
101537
+ let resolved;
101538
+ try {
101539
+ resolved = new URL(`/v1${normalized}`, "http://x").pathname;
101540
+ } catch {
101541
+ resolved = "";
101542
+ }
101543
+ if (!normalized.startsWith("/pm/") || normalized.includes("..") || /%2e|%2f|%5c|\\/i.test(normalized) || !resolved.startsWith("/v1/pm/")) {
101182
101544
  throw new Error(
101183
101545
  `predexon_endpoint_call: invalid path '${rawPath}' \u2014 must begin with '/pm/' and contain no '..'`
101184
101546
  );
@@ -122018,8 +122380,8 @@ var init_constants3 = __esm({
122018
122380
 
122019
122381
  // src/polymarket/wallet-adapter.ts
122020
122382
  import fs2 from "fs";
122021
- import { homedir as homedir9 } from "os";
122022
- import { join as join12 } from "path";
122383
+ import { homedir as homedir10 } from "os";
122384
+ import { join as join13 } from "path";
122023
122385
  function getOrCreateWalletKey() {
122024
122386
  const envKey = process.env.BLOCKRUN_WALLET_KEY?.trim();
122025
122387
  if (envKey && /^0x[0-9a-fA-F]{64}$/.test(envKey)) return envKey;
@@ -122067,7 +122429,7 @@ var init_wallet_adapter = __esm({
122067
122429
  init_esm();
122068
122430
  init_chains();
122069
122431
  init_constants3();
122070
- WALLET_FILE2 = join12(homedir9(), ".openclaw", "blockrun", "wallet.key");
122432
+ WALLET_FILE2 = join13(homedir10(), ".openclaw", "blockrun", "wallet.key");
122071
122433
  BASE_RPC_URLS = [
122072
122434
  "https://mainnet.base.org",
122073
122435
  "https://base.llamarpc.com",
@@ -122471,7 +122833,7 @@ var init_client5 = __esm({
122471
122833
  function resolveSpendControl(deps) {
122472
122834
  return deps?.spendControl ?? getSharedSpendControl();
122473
122835
  }
122474
- function usdToMicros(usd) {
122836
+ function usdToMicros2(usd) {
122475
122837
  return String(Math.ceil(usd * 1e6));
122476
122838
  }
122477
122839
  async function signUnderSpendPolicy(deps, quote, action, sign4) {
@@ -122737,7 +123099,7 @@ Re-call with confirm:true to sign and submit.`,
122737
123099
  payTo: negRisk ? NEG_RISK_CTF_EXCHANGE_V2 : CTF_EXCHANGE_V2,
122738
123100
  network: `eip155:${POLYGON_CHAIN_ID}`,
122739
123101
  asset: PUSD_COLLATERAL,
122740
- amount: usdToMicros(notional)
123102
+ amount: usdToMicros2(notional)
122741
123103
  };
122742
123104
  let r2;
122743
123105
  try {
@@ -232583,7 +232945,7 @@ var init_retry = __esm({
232583
232945
  // src/index.ts
232584
232946
  import {
232585
232947
  writeFileSync as writeFileSync3,
232586
- existsSync as existsSync3,
232948
+ existsSync as existsSync2,
232587
232949
  readdirSync,
232588
232950
  mkdirSync as mkdirSync3,
232589
232951
  copyFileSync,
@@ -232592,9 +232954,9 @@ import {
232592
232954
  unlinkSync
232593
232955
  } from "fs";
232594
232956
  import { readFile as readFileAsync } from "fs/promises";
232595
- import { homedir as homedir14 } from "os";
232957
+ import { homedir as homedir15 } from "os";
232596
232958
  import { randomUUID } from "crypto";
232597
- import { join as join17 } from "path";
232959
+ import { join as join18 } from "path";
232598
232960
  async function waitForProxyHealth(port, timeoutMs = 3e3) {
232599
232961
  const start = Date.now();
232600
232962
  while (Date.now() - start < timeoutMs) {
@@ -232614,12 +232976,12 @@ function writePrivateJsonSync(path5, value) {
232614
232976
  renameSync2(temporary, path5);
232615
232977
  chmodSync(path5, 384);
232616
232978
  } finally {
232617
- if (existsSync3(temporary)) unlinkSync(temporary);
232979
+ if (existsSync2(temporary)) unlinkSync(temporary);
232618
232980
  }
232619
232981
  }
232620
232982
  function hasConfiguredWallet() {
232621
232983
  return Boolean(
232622
- process["env"].BLOCKRUN_WALLET_KEY || existsSync3(CORE_WALLET_FILE) || existsSync3(WALLET_FILE)
232984
+ process["env"].BLOCKRUN_WALLET_KEY || existsSync2(CORE_WALLET_FILE) || existsSync2(WALLET_FILE)
232623
232985
  );
232624
232986
  }
232625
232987
  function isCompletionMode() {
@@ -232637,11 +232999,11 @@ function isBlockrunWebSearchDisabled(config) {
232637
232999
  }
232638
233000
  function legacyPackageIsBlockRun() {
232639
233001
  for (const dir of [
232640
- join17(homedir14(), ".openclaw", "extensions", "clawrouter"),
232641
- join17(homedir14(), ".openclaw", "npm", "node_modules", "@blockrun", "clawrouter")
233002
+ join18(homedir15(), ".openclaw", "extensions", "clawrouter"),
233003
+ join18(homedir15(), ".openclaw", "npm", "node_modules", "@blockrun", "clawrouter")
232642
233004
  ]) {
232643
233005
  try {
232644
- const metadata = JSON.parse(readTextFileSync(join17(dir, "package.json")));
233006
+ const metadata = JSON.parse(readTextFileSync(join18(dir, "package.json")));
232645
233007
  if (metadata.name === "@blockrun/clawrouter") return true;
232646
233008
  } catch {
232647
233009
  }
@@ -232649,11 +233011,11 @@ function legacyPackageIsBlockRun() {
232649
233011
  return false;
232650
233012
  }
232651
233013
  function injectModelsConfig(logger48, options = {}) {
232652
- const configDir = join17(homedir14(), ".openclaw");
232653
- const configPath = join17(configDir, "openclaw.json");
233014
+ const configDir = join18(homedir15(), ".openclaw");
233015
+ const configPath = join18(configDir, "openclaw.json");
232654
233016
  let config = {};
232655
233017
  let needsWrite = false;
232656
- if (!existsSync3(configDir)) {
233018
+ if (!existsSync2(configDir)) {
232657
233019
  try {
232658
233020
  mkdirSync3(configDir, { recursive: true });
232659
233021
  logger48.info("Created OpenClaw config directory");
@@ -232664,7 +233026,7 @@ function injectModelsConfig(logger48, options = {}) {
232664
233026
  return;
232665
233027
  }
232666
233028
  }
232667
- if (existsSync3(configPath)) {
233029
+ if (existsSync2(configPath)) {
232668
233030
  try {
232669
233031
  const content = readTextFileSync(configPath).trim();
232670
233032
  if (content) {
@@ -232871,8 +233233,8 @@ function injectModelsConfig(logger48, options = {}) {
232871
233233
  }
232872
233234
  function syncAgentModelCache(logger48, options = {}) {
232873
233235
  if (!isGatewayMode() && !options.forceWrite) return;
232874
- const agentsDir = join17(homedir14(), ".openclaw", "agents");
232875
- if (!existsSync3(agentsDir)) return;
233236
+ const agentsDir = join18(homedir15(), ".openclaw", "agents");
233237
+ if (!existsSync2(agentsDir)) return;
232876
233238
  let agentDirs;
232877
233239
  try {
232878
233240
  agentDirs = readdirSync(agentsDir);
@@ -232881,8 +233243,8 @@ function syncAgentModelCache(logger48, options = {}) {
232881
233243
  }
232882
233244
  const expectedIds = VISIBLE_OPENCLAW_MODELS.map((m) => m.id);
232883
233245
  for (const agent of agentDirs) {
232884
- const cachePath = join17(agentsDir, agent, "agent", "models.json");
232885
- if (!existsSync3(cachePath)) continue;
233246
+ const cachePath = join18(agentsDir, agent, "agent", "models.json");
233247
+ if (!existsSync2(cachePath)) continue;
232886
233248
  try {
232887
233249
  const raw = readTextFileSync(cachePath).trim();
232888
233250
  if (!raw) continue;
@@ -232910,8 +233272,8 @@ function syncAgentModelCache(logger48, options = {}) {
232910
233272
  }
232911
233273
  }
232912
233274
  function injectAuthProfile(logger48) {
232913
- const agentsDir = join17(homedir14(), ".openclaw", "agents");
232914
- if (!existsSync3(agentsDir)) {
233275
+ const agentsDir = join18(homedir15(), ".openclaw", "agents");
233276
+ if (!existsSync2(agentsDir)) {
232915
233277
  try {
232916
233278
  mkdirSync3(agentsDir, { recursive: true });
232917
233279
  } catch (err) {
@@ -232927,10 +233289,10 @@ function injectAuthProfile(logger48) {
232927
233289
  agents = ["main", ...agents];
232928
233290
  }
232929
233291
  for (const agentId of agents) {
232930
- const authDir = join17(agentsDir, agentId, "agent");
232931
- const authPath = join17(authDir, "auth-profiles.json");
232932
- const sqlitePath = join17(authDir, "openclaw-agent.sqlite");
232933
- if (existsSync3(sqlitePath)) {
233292
+ const authDir = join18(agentsDir, agentId, "agent");
233293
+ const authPath = join18(authDir, "auth-profiles.json");
233294
+ const sqlitePath = join18(authDir, "openclaw-agent.sqlite");
233295
+ if (existsSync2(sqlitePath)) {
232934
233296
  removeInjectedAuthPlaceholder(authPath, logger48, agentId);
232935
233297
  continue;
232936
233298
  }
@@ -232938,7 +233300,7 @@ function injectAuthProfile(logger48) {
232938
233300
  removeInjectedAuthPlaceholder(authPath, logger48, agentId);
232939
233301
  continue;
232940
233302
  }
232941
- if (!existsSync3(authDir)) {
233303
+ if (!existsSync2(authDir)) {
232942
233304
  try {
232943
233305
  mkdirSync3(authDir, { recursive: true });
232944
233306
  } catch {
@@ -232949,7 +233311,7 @@ function injectAuthProfile(logger48) {
232949
233311
  version: 1,
232950
233312
  profiles: {}
232951
233313
  };
232952
- if (existsSync3(authPath)) {
233314
+ if (existsSync2(authPath)) {
232953
233315
  try {
232954
233316
  const existing = JSON.parse(readTextFileSync(authPath));
232955
233317
  if (existing.version && existing.profiles) {
@@ -232982,7 +233344,7 @@ function injectAuthProfile(logger48) {
232982
233344
  }
232983
233345
  function removeInjectedAuthPlaceholder(authPath, logger48, agentId) {
232984
233346
  try {
232985
- if (!existsSync3(authPath)) return;
233347
+ if (!existsSync2(authPath)) return;
232986
233348
  const parsed = JSON.parse(readTextFileSync(authPath));
232987
233349
  const profiles = parsed?.profiles;
232988
233350
  if (!profiles || typeof profiles !== "object" || Array.isArray(profiles)) return;
@@ -233437,7 +233799,7 @@ function buildImageGenerationProvider() {
233437
233799
  (result.data ?? []).map(async (img) => {
233438
233800
  const filename = img.url?.split("/images/").pop();
233439
233801
  if (!filename) throw new Error(`Unexpected image URL format: ${img.url}`);
233440
- const filePath = join17(IMAGE_DIR2, filename);
233802
+ const filePath = join18(IMAGE_DIR2, filename);
233441
233803
  const buffer2 = await readFileAsync(filePath);
233442
233804
  const ext = filename.split(".").pop()?.toLowerCase() ?? "png";
233443
233805
  const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "webp" ? "image/webp" : "image/png";
@@ -233489,7 +233851,7 @@ function buildMusicGenerationProvider() {
233489
233851
  (result.data ?? []).map(async (track) => {
233490
233852
  const filename = track.url?.split("/audio/").pop();
233491
233853
  if (!filename) throw new Error(`Unexpected audio URL format: ${track.url}`);
233492
- const filePath = join17(AUDIO_DIR2, filename);
233854
+ const filePath = join18(AUDIO_DIR2, filename);
233493
233855
  const buffer2 = await readFileAsync(filePath);
233494
233856
  const ext = filename.split(".").pop()?.toLowerCase() ?? "mp3";
233495
233857
  const mimeType = ext === "wav" ? "audio/wav" : "audio/mpeg";
@@ -233565,7 +233927,7 @@ function buildVideoGenerationProvider() {
233565
233927
  (result.data ?? []).map(async (clip) => {
233566
233928
  const filename = clip.url?.split("/videos/").pop();
233567
233929
  if (!filename) throw new Error(`Unexpected video URL format: ${clip.url}`);
233568
- const filePath = join17(VIDEO_DIR2, filename);
233930
+ const filePath = join18(VIDEO_DIR2, filename);
233569
233931
  const buffer2 = await readFileAsync(filePath);
233570
233932
  const ext = filename.split(".").pop()?.toLowerCase() ?? "mp4";
233571
233933
  const mimeType = ext === "webm" ? "video/webm" : ext === "mov" ? "video/quicktime" : "video/mp4";
@@ -233666,7 +234028,7 @@ function createWalletCommand(api) {
233666
234028
  ];
233667
234029
  let hasMnemonic3 = false;
233668
234030
  try {
233669
- if (existsSync3(MNEMONIC_FILE)) {
234031
+ if (existsSync2(MNEMONIC_FILE)) {
233670
234032
  const mnemonic = readTextFileSync(MNEMONIC_FILE).trim();
233671
234033
  if (mnemonic) {
233672
234034
  hasMnemonic3 = true;
@@ -233737,7 +234099,7 @@ function createWalletCommand(api) {
233737
234099
  ].join("\n")
233738
234100
  };
233739
234101
  }
233740
- if (existsSync3(MNEMONIC_FILE)) {
234102
+ if (existsSync2(MNEMONIC_FILE)) {
233741
234103
  const existingMnemonic = readTextFileSync(MNEMONIC_FILE).trim();
233742
234104
  if (existingMnemonic) {
233743
234105
  await savePaymentChain("solana");
@@ -233929,9 +234291,9 @@ var init_index = __esm({
233929
234291
  init_partners();
233930
234292
  activeProxyHandle = null;
233931
234293
  pendingConfiguredStartupApi = null;
233932
- IMAGE_DIR2 = join17(homedir14(), ".openclaw", "blockrun", "images");
233933
- AUDIO_DIR2 = join17(homedir14(), ".openclaw", "blockrun", "audio");
233934
- VIDEO_DIR2 = join17(homedir14(), ".openclaw", "blockrun", "videos");
234294
+ IMAGE_DIR2 = join18(homedir15(), ".openclaw", "blockrun", "images");
234295
+ AUDIO_DIR2 = join18(homedir15(), ".openclaw", "blockrun", "audio");
234296
+ VIDEO_DIR2 = join18(homedir15(), ".openclaw", "blockrun", "videos");
233935
234297
  plugin = {
233936
234298
  // NOT "clawrouter". OpenClaw bundles its own plugin under that id since the
233937
234299
  // 2026.7.1 line (vendored at dist/extensions/clawrouter, provider `clawrouter/*`,
@@ -234071,7 +234433,8 @@ var init_index = __esm({
234071
234433
  name: "cr-imagegen",
234072
234434
  description: "Generate an image (BlockRun image models, paid via wallet)",
234073
234435
  acceptsArgs: true,
234074
- requireAuth: false,
234436
+ // Paid from the wallet: only authorized senders.
234437
+ requireAuth: true,
234075
234438
  handler: async (ctx) => {
234076
234439
  const parsed = parseGenArgs(ctx.args ?? "");
234077
234440
  if (!parsed.prompt) {
@@ -234123,7 +234486,8 @@ ${errText}`
234123
234486
  name: "videogen",
234124
234487
  description: "Generate a short video (Grok Imagine / Seedance, paid via wallet)",
234125
234488
  acceptsArgs: true,
234126
- requireAuth: false,
234489
+ // Paid from the wallet: only authorized senders.
234490
+ requireAuth: true,
234127
234491
  handler: async (ctx) => {
234128
234492
  const parsed = parseGenArgs(ctx.args ?? "");
234129
234493
  if (!parsed.prompt) {
@@ -234172,7 +234536,8 @@ ${errText}`
234172
234536
  name: "cr-call",
234173
234537
  description: "Place an AI voice call via BlockRun + Bland.ai (paid from wallet, $0.54/call)",
234174
234538
  acceptsArgs: true,
234175
- requireAuth: false,
234539
+ // Paid from the wallet: only authorized senders.
234540
+ requireAuth: true,
234176
234541
  handler: async (ctx) => {
234177
234542
  const parsed = parseCallArgs(ctx.args ?? "");
234178
234543
  if (!parsed.to || !parsed.task) {
@@ -234369,8 +234734,8 @@ ${errText}`
234369
234734
  }
234370
234735
  resetProxyStartupState();
234371
234736
  try {
234372
- const configPath = join17(homedir14(), ".openclaw", "openclaw.json");
234373
- if (existsSync3(configPath)) {
234737
+ const configPath = join18(homedir15(), ".openclaw", "openclaw.json");
234738
+ if (existsSync2(configPath)) {
234374
234739
  const config = JSON.parse(readTextFileSync(configPath));
234375
234740
  if (config.models?.providers?.blockrun) {
234376
234741
  delete config.models.providers.blockrun;
@@ -234411,12 +234776,12 @@ ${errText}`
234411
234776
  api.logger.warn(`Config cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
234412
234777
  }
234413
234778
  try {
234414
- const agentsDir = join17(homedir14(), ".openclaw", "agents");
234415
- if (existsSync3(agentsDir)) {
234779
+ const agentsDir = join18(homedir15(), ".openclaw", "agents");
234780
+ if (existsSync2(agentsDir)) {
234416
234781
  for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
234417
234782
  if (!entry.isDirectory()) continue;
234418
- const authPath = join17(agentsDir, entry.name, "agent", "auth-profiles.json");
234419
- if (!existsSync3(authPath)) continue;
234783
+ const authPath = join18(agentsDir, entry.name, "agent", "auth-profiles.json");
234784
+ if (!existsSync2(authPath)) continue;
234420
234785
  try {
234421
234786
  const store = JSON.parse(readTextFileSync(authPath));
234422
234787
  if (store.profiles?.["blockrun:default"]) {