@blockrun/mcp 0.23.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +492 -380
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
7
7
|
|
|
8
8
|
// src/utils/wallet.ts
|
|
9
9
|
import fs from "fs";
|
|
10
|
-
import
|
|
11
|
-
import
|
|
10
|
+
import path2 from "path";
|
|
11
|
+
import os2 from "os";
|
|
12
12
|
import {
|
|
13
13
|
LLMClient,
|
|
14
14
|
ImageClient,
|
|
@@ -23,10 +23,39 @@ import {
|
|
|
23
23
|
formatNeedsFundingMessage,
|
|
24
24
|
SOLANA_WALLET_FILE_PATH
|
|
25
25
|
} from "@blockrun/llm";
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
// src/utils/constants.ts
|
|
28
|
+
import * as path from "path";
|
|
29
|
+
import * as os from "os";
|
|
30
|
+
var WALLET_DIR = path.join(os.homedir(), ".blockrun");
|
|
31
|
+
var WALLET_FILE = path.join(WALLET_DIR, ".session");
|
|
32
|
+
var QR_FILE = path.join(WALLET_DIR, "qr.png");
|
|
33
|
+
var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
34
|
+
var BASE_CHAIN_ID = "8453";
|
|
35
|
+
var BASE_RPC_URLS = [
|
|
36
|
+
"https://mainnet.base.org",
|
|
37
|
+
"https://base.llamarpc.com",
|
|
38
|
+
"https://1rpc.io/base"
|
|
39
|
+
];
|
|
40
|
+
var MODEL_TIERS = {
|
|
41
|
+
fast: ["google/gemini-3.5-flash", "google/gemini-2.5-flash", "google/gemini-3.1-flash-lite", "openai/gpt-5-mini", "deepseek/deepseek-chat", "google/gemini-3-flash-preview"],
|
|
42
|
+
balanced: ["openai/gpt-5.5", "anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro", "moonshot/kimi-k2.6", "openai/gpt-5.3", "openai/gpt-5.4"],
|
|
43
|
+
powerful: ["anthropic/claude-opus-4.8", "openai/gpt-5.4-pro", "anthropic/claude-opus-4.7", "anthropic/claude-opus-4.6", "openai/o3", "openai/gpt-5.4"],
|
|
44
|
+
cheap: ["zai/glm-5", "zai/glm-5-turbo", "nvidia/gpt-oss-120b", "nvidia/deepseek-v4-flash", "google/gemini-2.5-flash", "deepseek/deepseek-chat", "openai/gpt-5.4-nano"],
|
|
45
|
+
reasoning: ["anthropic/claude-opus-4.8", "openai/o3", "openai/o1", "openai/o3-mini", "deepseek/deepseek-reasoner", "moonshot/kimi-k2.6", "openai/gpt-5.3-codex"],
|
|
46
|
+
// 2026-06-07 sweep: dropped qwen3-next (NVIDIA EOL, 410), mistral-small-4-119b
|
|
47
|
+
// (timing out), deepseek-v3.2 + glm-4.7 (NIM hung). All redirect server-side
|
|
48
|
+
// anyway; these are the free models actually serving themselves.
|
|
49
|
+
free: ["nvidia/llama-4-maverick", "nvidia/qwen3-coder-480b", "nvidia/deepseek-v4-flash", "nvidia/gpt-oss-120b", "nvidia/gpt-oss-20b"],
|
|
50
|
+
coding: ["anthropic/claude-opus-4.8", "zai/glm-5", "openai/gpt-5.3-codex", "moonshot/kimi-k2.6", "nvidia/qwen3-coder-480b", "anthropic/claude-sonnet-4.6", "openai/gpt-5.4"],
|
|
51
|
+
glm: ["zai/glm-5", "zai/glm-5-turbo"]
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/utils/wallet.ts
|
|
55
|
+
var BLOCKRUN_DIR = path2.join(os2.homedir(), ".blockrun");
|
|
27
56
|
var CHAIN_PREFERENCE_FILES = [
|
|
28
|
-
|
|
29
|
-
|
|
57
|
+
path2.join(BLOCKRUN_DIR, ".chain"),
|
|
58
|
+
path2.join(BLOCKRUN_DIR, "payment-chain")
|
|
30
59
|
];
|
|
31
60
|
var _evmClient = null;
|
|
32
61
|
var _imageClient = null;
|
|
@@ -56,7 +85,7 @@ function getChain() {
|
|
|
56
85
|
}
|
|
57
86
|
return "base";
|
|
58
87
|
}
|
|
59
|
-
var CHAIN_FILE =
|
|
88
|
+
var CHAIN_FILE = path2.join(BLOCKRUN_DIR, ".chain");
|
|
60
89
|
function resetChainCaches() {
|
|
61
90
|
_evmClient = null;
|
|
62
91
|
_solanaClient = null;
|
|
@@ -185,17 +214,15 @@ async function getSolanaUsdcBalance() {
|
|
|
185
214
|
return null;
|
|
186
215
|
}
|
|
187
216
|
}
|
|
217
|
+
function parseBaseUsdcCallResult(raw) {
|
|
218
|
+
if (typeof raw !== "string" || !/^0x[0-9a-fA-F]+$/.test(raw)) return null;
|
|
219
|
+
return Number(BigInt(raw)) / 1e6;
|
|
220
|
+
}
|
|
188
221
|
async function getBaseUsdcBalance(address) {
|
|
189
|
-
const USDC_ADDRESS2 = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
190
|
-
const BASE_RPC_URLS = [
|
|
191
|
-
"https://mainnet.base.org",
|
|
192
|
-
"https://base.llamarpc.com",
|
|
193
|
-
"https://1rpc.io/base"
|
|
194
|
-
];
|
|
195
222
|
const data = {
|
|
196
223
|
jsonrpc: "2.0",
|
|
197
224
|
method: "eth_call",
|
|
198
|
-
params: [{ to:
|
|
225
|
+
params: [{ to: USDC_ADDRESS, data: `0x70a08231000000000000000000000000${address.slice(2)}` }, "latest"],
|
|
199
226
|
id: 1
|
|
200
227
|
};
|
|
201
228
|
for (const rpcUrl of BASE_RPC_URLS) {
|
|
@@ -206,7 +233,8 @@ async function getBaseUsdcBalance(address) {
|
|
|
206
233
|
body: JSON.stringify(data)
|
|
207
234
|
});
|
|
208
235
|
const result = await response.json();
|
|
209
|
-
|
|
236
|
+
const usd = parseBaseUsdcCallResult(result.result);
|
|
237
|
+
if (usd !== null) return usd;
|
|
210
238
|
} catch {
|
|
211
239
|
continue;
|
|
212
240
|
}
|
|
@@ -255,6 +283,25 @@ function checkBudget(budget, agentId, estimatedCost = 1e-3) {
|
|
|
255
283
|
}
|
|
256
284
|
return { allowed: true };
|
|
257
285
|
}
|
|
286
|
+
function reserveBudget(budget, agentId, estimatedCost = 1e-3) {
|
|
287
|
+
const check = checkBudget(budget, agentId, estimatedCost);
|
|
288
|
+
if (!check.allowed) return { allowed: false, reason: check.reason, release: () => {
|
|
289
|
+
} };
|
|
290
|
+
const cost = Math.max(0, estimatedCost);
|
|
291
|
+
budget.spent += cost;
|
|
292
|
+
const agentBudget = agentId ? budget.agents.get(agentId) : void 0;
|
|
293
|
+
if (agentBudget) agentBudget.spent += cost;
|
|
294
|
+
let released = false;
|
|
295
|
+
return {
|
|
296
|
+
allowed: true,
|
|
297
|
+
release: () => {
|
|
298
|
+
if (released) return;
|
|
299
|
+
released = true;
|
|
300
|
+
budget.spent -= cost;
|
|
301
|
+
if (agentBudget) agentBudget.spent -= cost;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
258
305
|
function recordSpending(budget, cost, agentId) {
|
|
259
306
|
budget.spent += cost;
|
|
260
307
|
budget.calls += 1;
|
|
@@ -289,30 +336,6 @@ import QRCode from "qrcode";
|
|
|
289
336
|
import open from "open";
|
|
290
337
|
import * as fs2 from "fs";
|
|
291
338
|
import * as path3 from "path";
|
|
292
|
-
|
|
293
|
-
// src/utils/constants.ts
|
|
294
|
-
import * as path2 from "path";
|
|
295
|
-
import * as os2 from "os";
|
|
296
|
-
var WALLET_DIR = path2.join(os2.homedir(), ".blockrun");
|
|
297
|
-
var WALLET_FILE = path2.join(WALLET_DIR, ".session");
|
|
298
|
-
var QR_FILE = path2.join(WALLET_DIR, "qr.png");
|
|
299
|
-
var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
300
|
-
var BASE_CHAIN_ID = "8453";
|
|
301
|
-
var MODEL_TIERS = {
|
|
302
|
-
fast: ["google/gemini-3.5-flash", "google/gemini-2.5-flash", "google/gemini-3.1-flash-lite", "openai/gpt-5-mini", "deepseek/deepseek-chat", "google/gemini-3-flash-preview"],
|
|
303
|
-
balanced: ["openai/gpt-5.5", "anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro", "moonshot/kimi-k2.6", "openai/gpt-5.3", "openai/gpt-5.4"],
|
|
304
|
-
powerful: ["anthropic/claude-opus-4.8", "openai/gpt-5.4-pro", "anthropic/claude-opus-4.7", "anthropic/claude-opus-4.6", "openai/o3", "openai/gpt-5.4"],
|
|
305
|
-
cheap: ["zai/glm-5", "zai/glm-5-turbo", "nvidia/gpt-oss-120b", "nvidia/deepseek-v4-flash", "google/gemini-2.5-flash", "deepseek/deepseek-chat", "openai/gpt-5.4-nano"],
|
|
306
|
-
reasoning: ["anthropic/claude-opus-4.8", "openai/o3", "openai/o1", "openai/o3-mini", "deepseek/deepseek-reasoner", "moonshot/kimi-k2.6", "openai/gpt-5.3-codex"],
|
|
307
|
-
// 2026-06-07 sweep: dropped qwen3-next (NVIDIA EOL, 410), mistral-small-4-119b
|
|
308
|
-
// (timing out), deepseek-v3.2 + glm-4.7 (NIM hung). All redirect server-side
|
|
309
|
-
// anyway; these are the free models actually serving themselves.
|
|
310
|
-
free: ["nvidia/llama-4-maverick", "nvidia/qwen3-coder-480b", "nvidia/deepseek-v4-flash", "nvidia/gpt-oss-120b", "nvidia/gpt-oss-20b"],
|
|
311
|
-
coding: ["anthropic/claude-opus-4.8", "zai/glm-5", "openai/gpt-5.3-codex", "moonshot/kimi-k2.6", "nvidia/qwen3-coder-480b", "anthropic/claude-sonnet-4.6", "openai/gpt-5.4"],
|
|
312
|
-
glm: ["zai/glm-5", "zai/glm-5-turbo"]
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
// src/utils/qr.ts
|
|
316
339
|
var SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
317
340
|
var sharpModule;
|
|
318
341
|
async function loadSharp() {
|
|
@@ -423,17 +446,27 @@ ${parts.join("\n")}`;
|
|
|
423
446
|
}
|
|
424
447
|
return base;
|
|
425
448
|
}
|
|
426
|
-
function
|
|
449
|
+
function isPaymentRejectionError(message) {
|
|
450
|
+
const m = message.toLowerCase();
|
|
451
|
+
return m.includes("insufficient") || m.includes("balance") || m.includes("rejected");
|
|
452
|
+
}
|
|
453
|
+
function formatError(message, opts) {
|
|
427
454
|
const msgLower = message.toLowerCase();
|
|
428
|
-
const hasStatus = (code) => new RegExp(`(^|[^0-9.])${code}([^0-9]
|
|
455
|
+
const hasStatus = (code) => new RegExp(`(^|[^0-9.])${code}($|[^0-9.])`).test(msgLower);
|
|
429
456
|
const isPaymentError = hasStatus("402") || msgLower.includes("balance") || msgLower.includes("insufficient") || msgLower.includes("payment") && !hasStatus("500");
|
|
457
|
+
const isModelUnavailable = msgLower.includes("not active for requested provider") || msgLower.includes("not found or not active");
|
|
430
458
|
const isServerError = hasStatus("500") || msgLower.includes("api error after payment");
|
|
459
|
+
const altHint = opts?.altModels ? ` (e.g. ${opts.altModels})` : "";
|
|
431
460
|
let errorText = `Error: ${message}`;
|
|
432
|
-
if (
|
|
461
|
+
if (isModelUnavailable) {
|
|
462
|
+
errorText += `
|
|
463
|
+
|
|
464
|
+
This model is temporarily unavailable upstream` + (opts?.altModels ? `. Try a different model${altHint} \u2014 it should work right away.` : `. Try a different model, or retry shortly.`);
|
|
465
|
+
} else if (isServerError) {
|
|
433
466
|
errorText += `
|
|
434
467
|
|
|
435
468
|
This is a temporary API issue. The API may be experiencing problems.
|
|
436
|
-
Try again in a few minutes
|
|
469
|
+
Try again in a few minutes` + (opts?.altModels ? `, or use a different model${altHint}.` : `.`);
|
|
437
470
|
} else if (isPaymentError) {
|
|
438
471
|
const chain = getChain();
|
|
439
472
|
const network = chain === "solana" ? "Solana" : "Base";
|
|
@@ -808,6 +841,7 @@ async function handleAnthropicNative(args) {
|
|
|
808
841
|
temperature,
|
|
809
842
|
stop,
|
|
810
843
|
thinking,
|
|
844
|
+
responseFormat,
|
|
811
845
|
budget,
|
|
812
846
|
agentId,
|
|
813
847
|
estimatedCost
|
|
@@ -823,6 +857,9 @@ async function handleAnthropicNative(args) {
|
|
|
823
857
|
}
|
|
824
858
|
apiMessages.push({ role: m.role, content: toAnthropicContent(m.content) });
|
|
825
859
|
}
|
|
860
|
+
if (responseFormat?.type === "json_object") {
|
|
861
|
+
systemParts.push("Respond with only valid JSON. Do not wrap it in markdown code fences or add any prose before or after.");
|
|
862
|
+
}
|
|
826
863
|
if (message.trim()) apiMessages.push({ role: "user", content: message });
|
|
827
864
|
if (apiMessages.length === 0) {
|
|
828
865
|
return { content: [{ type: "text", text: "No message content to send." }], isError: true };
|
|
@@ -892,7 +929,6 @@ ${thinkingText}` });
|
|
|
892
929
|
function estimateChatCost(maxTokens, mode, model, routing, routingProfile) {
|
|
893
930
|
if (mode === "free") return 0;
|
|
894
931
|
if (model?.startsWith("nvidia/")) return 0;
|
|
895
|
-
if (routing === "smart" && routingProfile === "free") return 0;
|
|
896
932
|
const out = Math.max(maxTokens ?? 1024, 256);
|
|
897
933
|
const frontierReserve = Math.max(0.01, out / 1e6 * 20);
|
|
898
934
|
if (routing === "smart") {
|
|
@@ -938,7 +974,7 @@ Run blockrun_models to see all available models with pricing.`,
|
|
|
938
974
|
model: z2.string().optional().describe("Specific model ID (e.g., 'zai/glm-5', 'openai/o3')"),
|
|
939
975
|
mode: z2.enum(["fast", "balanced", "powerful", "cheap", "reasoning", "free", "coding", "glm"]).optional().describe("Routing mode: glm = Zhipu GLM-5/GLM-5-Turbo ($0.001/call, great for coding), coding = GLM-5 + code models, cheap = GLM-5 + budget, free = NVIDIA only (ignored if model specified)"),
|
|
940
976
|
routing: z2.enum(["smart"]).optional().describe('Set to "smart" to auto-select the optimal model via ClawRouter (14-dimension AI routing)'),
|
|
941
|
-
routing_profile: z2.enum(["free", "eco", "auto", "premium"]).optional().default("auto").describe('Cost/quality profile for ClawRouter: "
|
|
977
|
+
routing_profile: z2.enum(["free", "eco", "auto", "premium"]).optional().default("auto").describe('Cost/quality profile for ClawRouter: "eco" (budget), "auto" (balanced, default), "premium" (best quality). Note: "free" maps to "auto" (the SDK dropped the free profile) and still settles a PAID model \u2014 for zero-cost generation use mode:"free" or model:"nvidia/...". Only applies when routing:"smart".'),
|
|
942
978
|
system: z2.string().optional().describe("Optional system prompt"),
|
|
943
979
|
max_tokens: z2.number().optional().default(1024).describe("Max tokens in response"),
|
|
944
980
|
temperature: z2.number().optional().default(1).describe("Creativity 0-2"),
|
|
@@ -965,141 +1001,152 @@ Run blockrun_models to see all available models with pricing.`,
|
|
|
965
1001
|
const llm = getClient();
|
|
966
1002
|
const responseFormat = response_format ? { type: response_format } : void 0;
|
|
967
1003
|
const estimatedCost = estimateChatCost(max_tokens, mode, model, routing, routing_profile);
|
|
968
|
-
const
|
|
969
|
-
if (!
|
|
1004
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
1005
|
+
if (!gate.allowed) {
|
|
970
1006
|
return {
|
|
971
|
-
content: [{ type: "text", text: `${
|
|
1007
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet with action: "report" to see usage, or action: "delegate" to increase agent budget.` }],
|
|
972
1008
|
isError: true
|
|
973
1009
|
};
|
|
974
1010
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
messages,
|
|
986
|
-
maxTokens: max_tokens,
|
|
987
|
-
temperature,
|
|
988
|
-
stop,
|
|
989
|
-
thinking,
|
|
990
|
-
budget,
|
|
991
|
-
agentId: agent_id,
|
|
992
|
-
estimatedCost
|
|
993
|
-
});
|
|
994
|
-
}
|
|
995
|
-
if (routing === "smart") {
|
|
996
|
-
if (!(llm instanceof LLMClient2)) {
|
|
997
|
-
return {
|
|
998
|
-
content: [{ type: "text", text: "Smart routing (ClawRouter) is not available on Solana. Use a specific model or mode instead." }],
|
|
999
|
-
isError: true
|
|
1000
|
-
};
|
|
1001
|
-
}
|
|
1002
|
-
try {
|
|
1003
|
-
const { result, settledUsd } = await withSettledCost(llm, () => llm.smartChat(message, {
|
|
1011
|
+
try {
|
|
1012
|
+
if (model && isAnthropicModel(model)) {
|
|
1013
|
+
const solanaBlock = baseOnlyMessage("Native Anthropic (claude-*) calls");
|
|
1014
|
+
if (solanaBlock) {
|
|
1015
|
+
return { content: [{ type: "text", text: solanaBlock }], isError: true };
|
|
1016
|
+
}
|
|
1017
|
+
return await handleAnthropicNative({
|
|
1018
|
+
client: getAnthropicClient(),
|
|
1019
|
+
model,
|
|
1020
|
+
message,
|
|
1004
1021
|
system,
|
|
1022
|
+
messages,
|
|
1005
1023
|
maxTokens: max_tokens,
|
|
1006
|
-
maxOutputTokens: max_tokens,
|
|
1007
1024
|
temperature,
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
// omit it and let ClawRouter pick (matches the SDK upgrade path).
|
|
1011
|
-
routingProfile: routing_profile === "free" ? void 0 : routing_profile,
|
|
1025
|
+
stop,
|
|
1026
|
+
thinking,
|
|
1012
1027
|
responseFormat,
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1028
|
+
budget,
|
|
1029
|
+
agentId: agent_id,
|
|
1030
|
+
estimatedCost
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
if (routing === "smart") {
|
|
1034
|
+
if (messages && messages.length > 0) {
|
|
1035
|
+
return {
|
|
1036
|
+
content: [{ type: "text", text: formatError('routing:"smart" does not support multi-turn `messages` \u2014 smart routing answers a single prompt. Send the conversation via `messages` with an explicit `model`/`mode` (no routing), or send a single `message` with routing:"smart".') }],
|
|
1037
|
+
isError: true
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
if (!(llm instanceof LLMClient2)) {
|
|
1041
|
+
return {
|
|
1042
|
+
content: [{ type: "text", text: "Smart routing (ClawRouter) is not available on Solana. Use a specific model or mode instead." }],
|
|
1043
|
+
isError: true
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
try {
|
|
1047
|
+
const { result, settledUsd } = await withSettledCost(llm, () => llm.smartChat(message, {
|
|
1048
|
+
system,
|
|
1049
|
+
maxTokens: max_tokens,
|
|
1050
|
+
maxOutputTokens: max_tokens,
|
|
1051
|
+
temperature,
|
|
1052
|
+
// @blockrun/llm 2.x dropped the "free" routing profile; the gateway
|
|
1053
|
+
// already routes to the most cost-effective model by default, so we
|
|
1054
|
+
// omit it and let ClawRouter pick (matches the SDK upgrade path).
|
|
1055
|
+
routingProfile: routing_profile === "free" ? void 0 : routing_profile,
|
|
1056
|
+
responseFormat,
|
|
1057
|
+
stop
|
|
1058
|
+
}));
|
|
1059
|
+
recordActualSpend(budget, settledUsd, result.routing.costEstimate || estimatedCost, agent_id);
|
|
1060
|
+
return {
|
|
1061
|
+
content: [{ type: "text", text: `[${result.model} | ${result.routing.tier} | $${result.routing.costEstimate.toFixed(4)} | ${Math.round((result.routing.savings ?? 0) * 100)}% savings]
|
|
1018
1062
|
|
|
1019
1063
|
${result.response}` }],
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1064
|
+
structuredContent: {
|
|
1065
|
+
model_used: result.model,
|
|
1066
|
+
response: result.response,
|
|
1067
|
+
routing: result.routing
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
|
|
1072
|
+
}
|
|
1028
1073
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
content: [{ type: "text", text: `[${targetModel} | ${fullMessages.length} msgs]
|
|
1074
|
+
if (messages && messages.length > 0) {
|
|
1075
|
+
const targetModel = model || MODEL_TIERS[mode ?? "balanced"]?.[0] || "openai/gpt-5.5";
|
|
1076
|
+
const fullMessages = [
|
|
1077
|
+
...system ? [{ role: "system", content: system }] : [],
|
|
1078
|
+
...messages,
|
|
1079
|
+
{ role: "user", content: message }
|
|
1080
|
+
];
|
|
1081
|
+
try {
|
|
1082
|
+
const { result, settledUsd } = await withSettledCost(llm, () => llm.chatCompletion(targetModel, fullMessages, {
|
|
1083
|
+
maxTokens: max_tokens,
|
|
1084
|
+
temperature,
|
|
1085
|
+
responseFormat,
|
|
1086
|
+
stop
|
|
1087
|
+
}));
|
|
1088
|
+
const reply = result.choices?.[0]?.message?.content || "";
|
|
1089
|
+
recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
|
|
1090
|
+
return {
|
|
1091
|
+
content: [{ type: "text", text: `[${targetModel} | ${fullMessages.length} msgs]
|
|
1048
1092
|
|
|
1049
1093
|
${reply}` }],
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1094
|
+
structuredContent: { model_used: targetModel, response: reply, message_count: fullMessages.length }
|
|
1095
|
+
};
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
|
|
1098
|
+
}
|
|
1054
1099
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
}
|
|
1100
|
+
if (model) {
|
|
1101
|
+
try {
|
|
1102
|
+
const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(model, message, {
|
|
1103
|
+
system,
|
|
1104
|
+
maxTokens: max_tokens,
|
|
1105
|
+
temperature,
|
|
1106
|
+
responseFormat,
|
|
1107
|
+
stop
|
|
1108
|
+
}));
|
|
1109
|
+
recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
|
|
1110
|
+
return { content: [{ type: "text", text: response }] };
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
return {
|
|
1113
|
+
content: [{ type: "text", text: formatError(extractErrorMessage(error)) }],
|
|
1114
|
+
isError: true
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1072
1117
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
content: [{ type: "text", text: `[${m}]
|
|
1118
|
+
const routingMode = mode || "balanced";
|
|
1119
|
+
const models = MODEL_TIERS[routingMode];
|
|
1120
|
+
let lastError = null;
|
|
1121
|
+
for (const m of models) {
|
|
1122
|
+
try {
|
|
1123
|
+
const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(m, message, {
|
|
1124
|
+
system,
|
|
1125
|
+
maxTokens: max_tokens,
|
|
1126
|
+
temperature,
|
|
1127
|
+
responseFormat,
|
|
1128
|
+
stop
|
|
1129
|
+
}));
|
|
1130
|
+
recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
|
|
1131
|
+
return {
|
|
1132
|
+
content: [{ type: "text", text: `[${m}]
|
|
1089
1133
|
|
|
1090
1134
|
${response}` }],
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1135
|
+
structuredContent: { model_used: m, response }
|
|
1136
|
+
};
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
lastError = error;
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1096
1141
|
}
|
|
1142
|
+
const errorMessage = lastError ? extractErrorMessage(lastError) : "All models failed";
|
|
1143
|
+
return {
|
|
1144
|
+
content: [{ type: "text", text: formatError(errorMessage) }],
|
|
1145
|
+
isError: true
|
|
1146
|
+
};
|
|
1147
|
+
} finally {
|
|
1148
|
+
gate.release();
|
|
1097
1149
|
}
|
|
1098
|
-
const errorMessage = lastError ? extractErrorMessage(lastError) : "All models failed";
|
|
1099
|
-
return {
|
|
1100
|
-
content: [{ type: "text", text: formatError(errorMessage) }],
|
|
1101
|
-
isError: true
|
|
1102
|
-
};
|
|
1103
1150
|
}
|
|
1104
1151
|
);
|
|
1105
1152
|
}
|
|
@@ -1187,6 +1234,10 @@ async function toImageDataUri(ref) {
|
|
|
1187
1234
|
if (!res.ok) throw new Error(`fetch failed: ${res.status} ${res.statusText}`);
|
|
1188
1235
|
const mime2 = (res.headers.get("content-type") || "").toLowerCase().split(";")[0].trim();
|
|
1189
1236
|
if (!mime2.startsWith("image/")) throw new Error(`URL returned non-image content-type: ${mime2 || "(none)"}`);
|
|
1237
|
+
const advertised = Number(res.headers.get("content-length"));
|
|
1238
|
+
if (Number.isFinite(advertised) && advertised > REFERENCE_IMAGE_MAX_BYTES) {
|
|
1239
|
+
throw new Error(`image too large: ${(advertised / 1e6).toFixed(1)}MB > ${REFERENCE_IMAGE_MAX_BYTES / 1e6}MB cap`);
|
|
1240
|
+
}
|
|
1190
1241
|
const buffer2 = Buffer.from(await res.arrayBuffer());
|
|
1191
1242
|
if (buffer2.byteLength > REFERENCE_IMAGE_MAX_BYTES) {
|
|
1192
1243
|
throw new Error(`image too large: ${(buffer2.byteLength / 1e6).toFixed(1)}MB > ${REFERENCE_IMAGE_MAX_BYTES / 1e6}MB cap`);
|
|
@@ -1343,34 +1394,42 @@ Source images and masks accept a base64 data URI, an http(s) URL, or a local fil
|
|
|
1343
1394
|
};
|
|
1344
1395
|
}
|
|
1345
1396
|
const estimatedCost = estimateCost(selectedModel, size);
|
|
1346
|
-
const
|
|
1347
|
-
if (!
|
|
1397
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
1398
|
+
if (!gate.allowed) {
|
|
1348
1399
|
return {
|
|
1349
|
-
content: [{ type: "text", text: `${
|
|
1400
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
1350
1401
|
isError: true
|
|
1351
1402
|
};
|
|
1352
1403
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1404
|
+
try {
|
|
1405
|
+
response = await getImageClient().edit(prompt, normalizedImage, {
|
|
1406
|
+
model: selectedModel,
|
|
1407
|
+
size,
|
|
1408
|
+
...normalizedMask ? { mask: normalizedMask } : {}
|
|
1409
|
+
});
|
|
1410
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
1411
|
+
} finally {
|
|
1412
|
+
gate.release();
|
|
1413
|
+
}
|
|
1359
1414
|
} else {
|
|
1360
1415
|
const estimatedCost = estimateCost(selectedModel, size);
|
|
1361
|
-
const
|
|
1362
|
-
if (!
|
|
1416
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
1417
|
+
if (!gate.allowed) {
|
|
1363
1418
|
return {
|
|
1364
|
-
content: [{ type: "text", text: `${
|
|
1419
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
1365
1420
|
isError: true
|
|
1366
1421
|
};
|
|
1367
1422
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1423
|
+
try {
|
|
1424
|
+
response = await getImageClient().generate(prompt, {
|
|
1425
|
+
model: selectedModel,
|
|
1426
|
+
size,
|
|
1427
|
+
quality
|
|
1428
|
+
});
|
|
1429
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
1430
|
+
} finally {
|
|
1431
|
+
gate.release();
|
|
1432
|
+
}
|
|
1374
1433
|
}
|
|
1375
1434
|
const imageUrl = response.data?.[0]?.url;
|
|
1376
1435
|
if (!imageUrl) {
|
|
@@ -1395,7 +1454,7 @@ Error: ${errMsg}` }],
|
|
|
1395
1454
|
};
|
|
1396
1455
|
}
|
|
1397
1456
|
return {
|
|
1398
|
-
content: [{ type: "text", text: formatError(`Image generation failed: ${errMsg}
|
|
1457
|
+
content: [{ type: "text", text: formatError(`Image generation failed: ${errMsg}`, { altModels: "google/nano-banana, zai/cogview-4" }) }],
|
|
1399
1458
|
isError: true
|
|
1400
1459
|
};
|
|
1401
1460
|
}
|
|
@@ -1411,11 +1470,7 @@ async function fetchWithTimeout(url, options, timeoutMs) {
|
|
|
1411
1470
|
const controller = new AbortController();
|
|
1412
1471
|
const id = setTimeout(() => controller.abort(), timeoutMs);
|
|
1413
1472
|
id.unref?.();
|
|
1414
|
-
|
|
1415
|
-
return await fetch(url, { ...options, signal: controller.signal });
|
|
1416
|
-
} finally {
|
|
1417
|
-
clearTimeout(id);
|
|
1418
|
-
}
|
|
1473
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
1419
1474
|
}
|
|
1420
1475
|
function isTimeoutError(err) {
|
|
1421
1476
|
const name = err instanceof Error ? err.name : "";
|
|
@@ -1454,6 +1509,7 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
|
|
|
1454
1509
|
}
|
|
1455
1510
|
},
|
|
1456
1511
|
async ({ prompt, instrumental, lyrics, model, agent_id }) => {
|
|
1512
|
+
let gate;
|
|
1457
1513
|
try {
|
|
1458
1514
|
if (getChain() !== "base") {
|
|
1459
1515
|
return {
|
|
@@ -1467,10 +1523,10 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
|
|
|
1467
1523
|
isError: true
|
|
1468
1524
|
};
|
|
1469
1525
|
}
|
|
1470
|
-
|
|
1471
|
-
if (!
|
|
1526
|
+
gate = reserveBudget(budget, agent_id, MUSIC_COST);
|
|
1527
|
+
if (!gate.allowed) {
|
|
1472
1528
|
return {
|
|
1473
|
-
content: [{ type: "text", text: `${
|
|
1529
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
1474
1530
|
isError: true
|
|
1475
1531
|
};
|
|
1476
1532
|
}
|
|
@@ -1485,8 +1541,8 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
|
|
|
1485
1541
|
body: JSON.stringify(body)
|
|
1486
1542
|
}, 15e3);
|
|
1487
1543
|
if (resp402.status !== 402) {
|
|
1488
|
-
const data2 = await resp402.json();
|
|
1489
|
-
throw new Error(`
|
|
1544
|
+
const data2 = await resp402.json().catch(() => ({}));
|
|
1545
|
+
throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${JSON.stringify(data2)}`);
|
|
1490
1546
|
}
|
|
1491
1547
|
const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
|
|
1492
1548
|
if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
|
|
@@ -1547,7 +1603,7 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
|
|
|
1547
1603
|
};
|
|
1548
1604
|
} catch (err) {
|
|
1549
1605
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1550
|
-
if (
|
|
1606
|
+
if (isPaymentRejectionError(errMsg)) {
|
|
1551
1607
|
return {
|
|
1552
1608
|
content: [{ type: "text", text: `Music generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
|
|
1553
1609
|
Error: ${errMsg}` }],
|
|
@@ -1565,6 +1621,8 @@ Error: ${errMsg}` }],
|
|
|
1565
1621
|
content: [{ type: "text", text: formatError(`Music generation failed: ${errMsg}`) }],
|
|
1566
1622
|
isError: true
|
|
1567
1623
|
};
|
|
1624
|
+
} finally {
|
|
1625
|
+
gate?.release();
|
|
1568
1626
|
}
|
|
1569
1627
|
}
|
|
1570
1628
|
);
|
|
@@ -1634,6 +1692,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
|
|
|
1634
1692
|
}
|
|
1635
1693
|
},
|
|
1636
1694
|
async ({ action, input, voice, model, response_format, speed, duration_seconds, prompt_influence, agent_id }) => {
|
|
1695
|
+
let gate;
|
|
1637
1696
|
try {
|
|
1638
1697
|
if (action === "voices") {
|
|
1639
1698
|
return await listVoices();
|
|
@@ -1678,10 +1737,10 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
|
|
|
1678
1737
|
if (speed !== void 0) body.speed = speed;
|
|
1679
1738
|
cost = speechCost(model, input.length);
|
|
1680
1739
|
}
|
|
1681
|
-
|
|
1682
|
-
if (!
|
|
1740
|
+
gate = reserveBudget(budget, agent_id, cost);
|
|
1741
|
+
if (!gate.allowed) {
|
|
1683
1742
|
return {
|
|
1684
|
-
content: [{ type: "text", text: `${
|
|
1743
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
1685
1744
|
isError: true
|
|
1686
1745
|
};
|
|
1687
1746
|
}
|
|
@@ -1694,7 +1753,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
|
|
|
1694
1753
|
}, 15e3);
|
|
1695
1754
|
if (resp402.status !== 402) {
|
|
1696
1755
|
const data2 = await resp402.json().catch(() => ({}));
|
|
1697
|
-
throw new Error(`
|
|
1756
|
+
throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${JSON.stringify(data2)}`);
|
|
1698
1757
|
}
|
|
1699
1758
|
const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
|
|
1700
1759
|
if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
|
|
@@ -1760,7 +1819,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
|
|
|
1760
1819
|
};
|
|
1761
1820
|
} catch (err) {
|
|
1762
1821
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1763
|
-
if (
|
|
1822
|
+
if (isPaymentRejectionError(errMsg)) {
|
|
1764
1823
|
return {
|
|
1765
1824
|
content: [{ type: "text", text: `Speech generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
|
|
1766
1825
|
Error: ${errMsg}` }],
|
|
@@ -1778,6 +1837,8 @@ Error: ${errMsg}` }],
|
|
|
1778
1837
|
content: [{ type: "text", text: formatError(`Speech generation failed: ${errMsg}`) }],
|
|
1779
1838
|
isError: true
|
|
1780
1839
|
};
|
|
1840
|
+
} finally {
|
|
1841
|
+
gate?.release();
|
|
1781
1842
|
}
|
|
1782
1843
|
}
|
|
1783
1844
|
);
|
|
@@ -1878,6 +1939,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
1878
1939
|
}
|
|
1879
1940
|
},
|
|
1880
1941
|
async ({ prompt, image_url, real_face_asset_id, duration_seconds, generate_audio, resolution, aspect_ratio, last_frame_url, model, agent_id }) => {
|
|
1942
|
+
let gate;
|
|
1881
1943
|
try {
|
|
1882
1944
|
if (getChain() !== "base") {
|
|
1883
1945
|
return {
|
|
@@ -1918,10 +1980,10 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
1918
1980
|
const hasImageInput = Boolean(image_url || real_face_asset_id);
|
|
1919
1981
|
const perSecond = (hasImageInput ? VIDEO_PRICE_PER_SECOND_IMAGE[selectedModel] : void 0) ?? VIDEO_PRICE_PER_SECOND[selectedModel] ?? 0.05;
|
|
1920
1982
|
const estimatedCost = perSecond * billedSeconds;
|
|
1921
|
-
|
|
1922
|
-
if (!
|
|
1983
|
+
gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
1984
|
+
if (!gate.allowed) {
|
|
1923
1985
|
return {
|
|
1924
|
-
content: [{ type: "text", text: `${
|
|
1986
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
1925
1987
|
isError: true
|
|
1926
1988
|
};
|
|
1927
1989
|
}
|
|
@@ -1942,8 +2004,8 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
1942
2004
|
body: JSON.stringify(body)
|
|
1943
2005
|
}, 15e3);
|
|
1944
2006
|
if (resp402.status !== 402) {
|
|
1945
|
-
const data = await resp402.json();
|
|
1946
|
-
throw new Error(`
|
|
2007
|
+
const data = await resp402.json().catch(() => ({}));
|
|
2008
|
+
throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${JSON.stringify(data)}`);
|
|
1947
2009
|
}
|
|
1948
2010
|
const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
|
|
1949
2011
|
if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
|
|
@@ -2047,7 +2109,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
2047
2109
|
};
|
|
2048
2110
|
} catch (err) {
|
|
2049
2111
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2050
|
-
if (
|
|
2112
|
+
if (isPaymentRejectionError(errMsg)) {
|
|
2051
2113
|
return {
|
|
2052
2114
|
content: [{ type: "text", text: `Video generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
|
|
2053
2115
|
Error: ${errMsg}` }],
|
|
@@ -2062,9 +2124,11 @@ Error: ${errMsg}` }],
|
|
|
2062
2124
|
};
|
|
2063
2125
|
}
|
|
2064
2126
|
return {
|
|
2065
|
-
content: [{ type: "text", text: formatError(`Video generation failed: ${errMsg}
|
|
2127
|
+
content: [{ type: "text", text: formatError(`Video generation failed: ${errMsg}`, { altModels: "bytedance/seedance-2.0, azure/sora-2" }) }],
|
|
2066
2128
|
isError: true
|
|
2067
2129
|
};
|
|
2130
|
+
} finally {
|
|
2131
|
+
gate?.release();
|
|
2068
2132
|
}
|
|
2069
2133
|
}
|
|
2070
2134
|
);
|
|
@@ -2090,7 +2154,7 @@ async function payAndPostJson(url, reqBody, fallbackDescription) {
|
|
|
2090
2154
|
}, 15e3);
|
|
2091
2155
|
if (resp402.status !== 402) {
|
|
2092
2156
|
const data2 = await resp402.json().catch(() => ({}));
|
|
2093
|
-
throw new Error(`
|
|
2157
|
+
throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${data2.message || data2.error || JSON.stringify(data2)}`);
|
|
2094
2158
|
}
|
|
2095
2159
|
const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
|
|
2096
2160
|
if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
|
|
@@ -2151,6 +2215,7 @@ Privacy: BlockRun does not store face/liveness data \u2014 only the asset id, na
|
|
|
2151
2215
|
}
|
|
2152
2216
|
},
|
|
2153
2217
|
async ({ action, name, group_id, image_url, agent_id }) => {
|
|
2218
|
+
let gate;
|
|
2154
2219
|
try {
|
|
2155
2220
|
if (action === "init") {
|
|
2156
2221
|
if (!name) {
|
|
@@ -2277,9 +2342,9 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
|
|
|
2277
2342
|
if (!name || !image_url) {
|
|
2278
2343
|
return { content: [{ type: "text", text: formatError("portrait requires name and image_url (public HTTPS URL of an AI-generated character image).") }], isError: true };
|
|
2279
2344
|
}
|
|
2280
|
-
|
|
2281
|
-
if (!
|
|
2282
|
-
return { content: [{ type: "text", text: `${
|
|
2345
|
+
gate = reserveBudget(budget, agent_id, ENROLLMENT_PRICE_USD);
|
|
2346
|
+
if (!gate.allowed) {
|
|
2347
|
+
return { content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }], isError: true };
|
|
2283
2348
|
}
|
|
2284
2349
|
const { status, data, settledUsd } = await payAndPostJson(
|
|
2285
2350
|
`${BLOCKRUN_API4}/v1/portrait/enroll`,
|
|
@@ -2327,61 +2392,26 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
|
|
|
2327
2392
|
if (!name || !image_url || !group_id) {
|
|
2328
2393
|
return { content: [{ type: "text", text: formatError("enroll requires name, image_url, and group_id (from init, after the group is active).") }], isError: true };
|
|
2329
2394
|
}
|
|
2330
|
-
|
|
2331
|
-
if (!
|
|
2332
|
-
return { content: [{ type: "text", text: `${
|
|
2395
|
+
gate = reserveBudget(budget, agent_id, ENROLLMENT_PRICE_USD);
|
|
2396
|
+
if (!gate.allowed) {
|
|
2397
|
+
return { content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }], isError: true };
|
|
2333
2398
|
}
|
|
2334
|
-
const
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
const resp402 = await fetchWithTimeout(enrollUrl, {
|
|
2339
|
-
method: "POST",
|
|
2340
|
-
headers: { "Content-Type": "application/json" },
|
|
2341
|
-
body: reqBody
|
|
2342
|
-
}, 15e3);
|
|
2343
|
-
if (resp402.status !== 402) {
|
|
2344
|
-
const data2 = await resp402.json().catch(() => ({}));
|
|
2345
|
-
throw new Error(`Expected 402, got ${resp402.status}: ${data2.message || data2.error || JSON.stringify(data2)}`);
|
|
2346
|
-
}
|
|
2347
|
-
const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
|
|
2348
|
-
if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
|
|
2349
|
-
const paymentRequired = parsePaymentRequired4(prHeader);
|
|
2350
|
-
const details = extractPaymentDetails4(paymentRequired);
|
|
2351
|
-
const settledUsd = amountToUsd(details.amount);
|
|
2352
|
-
const paymentPayload = await createPaymentPayload4(
|
|
2353
|
-
privateKey,
|
|
2354
|
-
account.address,
|
|
2355
|
-
details.recipient,
|
|
2356
|
-
details.amount,
|
|
2357
|
-
details.network || "eip155:8453",
|
|
2358
|
-
{
|
|
2359
|
-
resourceUrl: details.resource?.url || enrollUrl,
|
|
2360
|
-
resourceDescription: details.resource?.description || "BlockRun RealFace enrollment",
|
|
2361
|
-
maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, 120),
|
|
2362
|
-
extra: details.extra
|
|
2363
|
-
}
|
|
2399
|
+
const { status, data, settledUsd } = await payAndPostJson(
|
|
2400
|
+
`${BLOCKRUN_API4}/v1/realface/enroll`,
|
|
2401
|
+
JSON.stringify({ name, image_url, group_id }),
|
|
2402
|
+
"BlockRun RealFace enrollment"
|
|
2364
2403
|
);
|
|
2365
|
-
|
|
2366
|
-
method: "POST",
|
|
2367
|
-
headers: {
|
|
2368
|
-
"Content-Type": "application/json",
|
|
2369
|
-
"PAYMENT-SIGNATURE": paymentPayload
|
|
2370
|
-
},
|
|
2371
|
-
body: reqBody
|
|
2372
|
-
}, 9e4);
|
|
2373
|
-
const data = await resp.json().catch(() => ({}));
|
|
2374
|
-
if (resp.status === 402) {
|
|
2404
|
+
if (status === 402) {
|
|
2375
2405
|
throw new Error("Payment rejected. Check your wallet balance.");
|
|
2376
2406
|
}
|
|
2377
|
-
if (
|
|
2407
|
+
if (status === 425) {
|
|
2378
2408
|
return { content: [{ type: "text", text: formatError(`Group not active yet \u2014 ${data.message || "finish the phone liveness check first"}. No payment taken.`) }], isError: true };
|
|
2379
2409
|
}
|
|
2380
|
-
if (
|
|
2410
|
+
if (status === 422) {
|
|
2381
2411
|
return { content: [{ type: "text", text: formatError(`Face match failed \u2014 ${data.hint || "use a clearer front-facing photo of the same person"}. No payment taken.`) }], isError: true };
|
|
2382
2412
|
}
|
|
2383
|
-
if (
|
|
2384
|
-
throw new Error(`Enroll error ${
|
|
2413
|
+
if (status < 200 || status >= 300) {
|
|
2414
|
+
throw new Error(`Enroll error ${status}: ${data.error || JSON.stringify(data)}`);
|
|
2385
2415
|
}
|
|
2386
2416
|
const assetId = data.asset_id;
|
|
2387
2417
|
if (!assetId) throw new Error(`Enroll response missing asset_id: ${JSON.stringify(data)}`);
|
|
@@ -2410,7 +2440,7 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
|
|
|
2410
2440
|
return { content: [{ type: "text", text: formatError(`Unknown action: ${action}`) }], isError: true };
|
|
2411
2441
|
} catch (err) {
|
|
2412
2442
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2413
|
-
if (
|
|
2443
|
+
if (isPaymentRejectionError(errMsg)) {
|
|
2414
2444
|
return {
|
|
2415
2445
|
content: [{ type: "text", text: `RealFace enrollment requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
|
|
2416
2446
|
Error: ${errMsg}` }],
|
|
@@ -2418,6 +2448,8 @@ Error: ${errMsg}` }],
|
|
|
2418
2448
|
};
|
|
2419
2449
|
}
|
|
2420
2450
|
return { content: [{ type: "text", text: formatError(`RealFace ${action} failed: ${errMsg}`) }], isError: true };
|
|
2451
|
+
} finally {
|
|
2452
|
+
gate?.release();
|
|
2421
2453
|
}
|
|
2422
2454
|
}
|
|
2423
2455
|
);
|
|
@@ -2441,6 +2473,19 @@ function asStructuredContent(result) {
|
|
|
2441
2473
|
return typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result };
|
|
2442
2474
|
}
|
|
2443
2475
|
|
|
2476
|
+
// src/utils/path-safety.ts
|
|
2477
|
+
function hasPathTraversal(path5) {
|
|
2478
|
+
let decoded = path5;
|
|
2479
|
+
try {
|
|
2480
|
+
decoded = decodeURIComponent(path5);
|
|
2481
|
+
} catch {
|
|
2482
|
+
}
|
|
2483
|
+
return decoded.split(/[/\\]/).some((seg) => seg === ".." || seg === ".");
|
|
2484
|
+
}
|
|
2485
|
+
function isValidNetworkSlug(slug) {
|
|
2486
|
+
return /^[a-z0-9-]+$/.test(slug);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2444
2489
|
// src/tools/search.ts
|
|
2445
2490
|
var SEARCH_PRICE_PER_SOURCE = 0.025;
|
|
2446
2491
|
var SEARCH_DEFAULT_MAX_RESULTS = 10;
|
|
@@ -2471,23 +2516,30 @@ Full request shape + worked examples in the \`search\` skill (\`skills/search/SK
|
|
|
2471
2516
|
async ({ path: path5, body, agent_id }) => {
|
|
2472
2517
|
try {
|
|
2473
2518
|
body = coerceBody(body);
|
|
2519
|
+
const cleanPath = (path5 ?? "").replace(/^\/+/, "").replace(/^v1\/search\/?/, "");
|
|
2520
|
+
if (hasPathTraversal(cleanPath)) {
|
|
2521
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
2522
|
+
}
|
|
2474
2523
|
const estimatedCost = estimateSearchCost(body);
|
|
2475
|
-
const
|
|
2476
|
-
if (!
|
|
2524
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
2525
|
+
if (!gate.allowed) {
|
|
2477
2526
|
return {
|
|
2478
|
-
content: [{ type: "text", text: `${
|
|
2527
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2479
2528
|
isError: true
|
|
2480
2529
|
};
|
|
2481
2530
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2531
|
+
try {
|
|
2532
|
+
const client = getClient();
|
|
2533
|
+
const endpoint = cleanPath ? `/v1/search/${cleanPath}` : "/v1/search";
|
|
2534
|
+
const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
|
|
2535
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
2536
|
+
return {
|
|
2537
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2538
|
+
structuredContent: asStructuredContent(result)
|
|
2539
|
+
};
|
|
2540
|
+
} finally {
|
|
2541
|
+
gate.release();
|
|
2542
|
+
}
|
|
2491
2543
|
} catch (err) {
|
|
2492
2544
|
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
2493
2545
|
}
|
|
@@ -2529,23 +2581,30 @@ Full request/response shapes + worked research workflows in the \`exa-research\`
|
|
|
2529
2581
|
async ({ path: path5, body, agent_id }) => {
|
|
2530
2582
|
try {
|
|
2531
2583
|
body = coerceBody(body);
|
|
2584
|
+
const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/exa\//, "");
|
|
2585
|
+
if (hasPathTraversal(cleanPath)) {
|
|
2586
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
2587
|
+
}
|
|
2532
2588
|
const estimatedCost = estimateExaCost(path5, body);
|
|
2533
|
-
const
|
|
2534
|
-
if (!
|
|
2589
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
2590
|
+
if (!gate.allowed) {
|
|
2535
2591
|
return {
|
|
2536
|
-
content: [{ type: "text", text: `${
|
|
2592
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2537
2593
|
isError: true
|
|
2538
2594
|
};
|
|
2539
2595
|
}
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2596
|
+
try {
|
|
2597
|
+
const client = getClient();
|
|
2598
|
+
const endpoint = `/v1/exa/${cleanPath}`;
|
|
2599
|
+
const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
|
|
2600
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
2601
|
+
return {
|
|
2602
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2603
|
+
structuredContent: asStructuredContent(result)
|
|
2604
|
+
};
|
|
2605
|
+
} finally {
|
|
2606
|
+
gate.release();
|
|
2607
|
+
}
|
|
2549
2608
|
} catch (err) {
|
|
2550
2609
|
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
2551
2610
|
}
|
|
@@ -2628,21 +2687,28 @@ Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. p
|
|
|
2628
2687
|
async ({ path: path5, params, body, agent_id }) => {
|
|
2629
2688
|
try {
|
|
2630
2689
|
body = coerceBody(body);
|
|
2690
|
+
if (hasPathTraversal(path5)) {
|
|
2691
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
2692
|
+
}
|
|
2631
2693
|
const estimatedCost = estimateMarketCost(path5, body);
|
|
2632
|
-
const
|
|
2633
|
-
if (!
|
|
2694
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
2695
|
+
if (!gate.allowed) {
|
|
2634
2696
|
return {
|
|
2635
|
-
content: [{ type: "text", text: `${
|
|
2697
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2636
2698
|
isError: true
|
|
2637
2699
|
};
|
|
2638
2700
|
}
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2701
|
+
try {
|
|
2702
|
+
const llm = getClient();
|
|
2703
|
+
const result = body !== void 0 ? await llm.pmQuery(path5, body) : await llm.pm(path5, params);
|
|
2704
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
2705
|
+
return {
|
|
2706
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2707
|
+
structuredContent: asStructuredContent(result)
|
|
2708
|
+
};
|
|
2709
|
+
} finally {
|
|
2710
|
+
gate.release();
|
|
2711
|
+
}
|
|
2646
2712
|
} catch (err) {
|
|
2647
2713
|
return {
|
|
2648
2714
|
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
@@ -2724,51 +2790,55 @@ Examples:
|
|
|
2724
2790
|
};
|
|
2725
2791
|
}
|
|
2726
2792
|
const estimatedCost = paid ? 1e-3 : 0;
|
|
2727
|
-
const
|
|
2728
|
-
if (!
|
|
2793
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
2794
|
+
if (!gate.allowed) {
|
|
2729
2795
|
return {
|
|
2730
|
-
content: [{ type: "text", text: `${
|
|
2796
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2731
2797
|
isError: true
|
|
2732
2798
|
};
|
|
2733
2799
|
}
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
if (
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
if (
|
|
2749
|
-
|
|
2750
|
-
|
|
2800
|
+
try {
|
|
2801
|
+
const priceClient = getPriceClient(paid);
|
|
2802
|
+
if (action === "price") {
|
|
2803
|
+
if (!symbol) throw new Error("symbol is required for action='price'");
|
|
2804
|
+
const result2 = await priceClient.price(category, symbol, {
|
|
2805
|
+
market,
|
|
2806
|
+
session
|
|
2807
|
+
});
|
|
2808
|
+
if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
|
|
2809
|
+
return {
|
|
2810
|
+
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }],
|
|
2811
|
+
structuredContent: result2
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
if (action === "history") {
|
|
2815
|
+
if (!symbol) throw new Error("symbol is required for action='history'");
|
|
2816
|
+
if (from === void 0) throw new Error("from (unix seconds) is required for action='history'");
|
|
2817
|
+
const result2 = await priceClient.history(category, symbol, {
|
|
2818
|
+
market,
|
|
2819
|
+
session,
|
|
2820
|
+
resolution: resolution ?? "D",
|
|
2821
|
+
from,
|
|
2822
|
+
to
|
|
2823
|
+
});
|
|
2824
|
+
if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
|
|
2825
|
+
return {
|
|
2826
|
+
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }],
|
|
2827
|
+
structuredContent: result2
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
const result = await priceClient.listSymbols(category, {
|
|
2751
2831
|
market,
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
from,
|
|
2755
|
-
to
|
|
2832
|
+
query,
|
|
2833
|
+
limit
|
|
2756
2834
|
});
|
|
2757
|
-
if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
|
|
2758
2835
|
return {
|
|
2759
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
2760
|
-
structuredContent:
|
|
2836
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2837
|
+
structuredContent: result
|
|
2761
2838
|
};
|
|
2839
|
+
} finally {
|
|
2840
|
+
gate.release();
|
|
2762
2841
|
}
|
|
2763
|
-
const result = await priceClient.listSymbols(category, {
|
|
2764
|
-
market,
|
|
2765
|
-
query,
|
|
2766
|
-
limit
|
|
2767
|
-
});
|
|
2768
|
-
return {
|
|
2769
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2770
|
-
structuredContent: result
|
|
2771
|
-
};
|
|
2772
2842
|
} catch (err) {
|
|
2773
2843
|
return {
|
|
2774
2844
|
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
@@ -2899,22 +2969,29 @@ Full action shapes + GPU type details in the \`modal\` skill.`,
|
|
|
2899
2969
|
try {
|
|
2900
2970
|
body = coerceBody(body);
|
|
2901
2971
|
const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/modal\//, "");
|
|
2972
|
+
if (hasPathTraversal(cleanPath)) {
|
|
2973
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
2974
|
+
}
|
|
2902
2975
|
const estimatedCost = estimateModalCost(cleanPath);
|
|
2903
|
-
const
|
|
2904
|
-
if (!
|
|
2976
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
2977
|
+
if (!gate.allowed) {
|
|
2905
2978
|
return {
|
|
2906
|
-
content: [{ type: "text", text: `${
|
|
2979
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2907
2980
|
isError: true
|
|
2908
2981
|
};
|
|
2909
2982
|
}
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2983
|
+
try {
|
|
2984
|
+
const client = buildClientWithTimeout(modalTimeoutMs(body));
|
|
2985
|
+
const endpoint = `/v1/modal/${cleanPath}`;
|
|
2986
|
+
const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
|
|
2987
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
2988
|
+
return {
|
|
2989
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2990
|
+
structuredContent: asStructuredContent(result)
|
|
2991
|
+
};
|
|
2992
|
+
} finally {
|
|
2993
|
+
gate.release();
|
|
2994
|
+
}
|
|
2918
2995
|
} catch (err) {
|
|
2919
2996
|
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
2920
2997
|
}
|
|
@@ -2965,22 +3042,29 @@ Voice call flow + voice preset details + full body shapes in the \`phone\` skill
|
|
|
2965
3042
|
try {
|
|
2966
3043
|
body = coerceBody(body);
|
|
2967
3044
|
const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\//, "");
|
|
3045
|
+
if (hasPathTraversal(cleanPath)) {
|
|
3046
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
3047
|
+
}
|
|
2968
3048
|
const estimatedCost = estimatePhoneCost(cleanPath, body !== void 0);
|
|
2969
|
-
const
|
|
2970
|
-
if (!
|
|
3049
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
3050
|
+
if (!gate.allowed) {
|
|
2971
3051
|
return {
|
|
2972
|
-
content: [{ type: "text", text: `${
|
|
3052
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2973
3053
|
isError: true
|
|
2974
3054
|
};
|
|
2975
3055
|
}
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
3056
|
+
try {
|
|
3057
|
+
const client = getClient();
|
|
3058
|
+
const endpoint = `/v1/${cleanPath}`;
|
|
3059
|
+
const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint);
|
|
3060
|
+
if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
|
|
3061
|
+
return {
|
|
3062
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3063
|
+
structuredContent: asStructuredContent(result)
|
|
3064
|
+
};
|
|
3065
|
+
} finally {
|
|
3066
|
+
gate.release();
|
|
3067
|
+
}
|
|
2984
3068
|
} catch (err) {
|
|
2985
3069
|
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
2986
3070
|
}
|
|
@@ -3063,22 +3147,29 @@ Each Surf endpoint pre-validates required params before settling \u2014 you get
|
|
|
3063
3147
|
try {
|
|
3064
3148
|
body = coerceBody(body);
|
|
3065
3149
|
const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/surf\//, "").replace(/^api\/v1\/surf\//, "");
|
|
3150
|
+
if (hasPathTraversal(cleanPath)) {
|
|
3151
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
3152
|
+
}
|
|
3066
3153
|
const estimatedCost = estimateSurfCost(cleanPath);
|
|
3067
|
-
const
|
|
3068
|
-
if (!
|
|
3154
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
3155
|
+
if (!gate.allowed) {
|
|
3069
3156
|
return {
|
|
3070
|
-
content: [{ type: "text", text: `${
|
|
3157
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
3071
3158
|
isError: true
|
|
3072
3159
|
};
|
|
3073
3160
|
}
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3161
|
+
try {
|
|
3162
|
+
const client = getClient();
|
|
3163
|
+
const endpoint = `/v1/surf/${cleanPath}`;
|
|
3164
|
+
const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint, params);
|
|
3165
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
3166
|
+
return {
|
|
3167
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3168
|
+
structuredContent: asStructuredContent(result)
|
|
3169
|
+
};
|
|
3170
|
+
} finally {
|
|
3171
|
+
gate.release();
|
|
3172
|
+
}
|
|
3082
3173
|
} catch (err) {
|
|
3083
3174
|
return {
|
|
3084
3175
|
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
@@ -3132,21 +3223,31 @@ Prefer blockrun_price (free quotes), blockrun_dex (free DEX data), or blockrun_s
|
|
|
3132
3223
|
}
|
|
3133
3224
|
const batchCount = Array.isArray(body) ? Math.max(body.length, 1) : 1;
|
|
3134
3225
|
const estimatedCost = RPC_PRICE_USD * batchCount;
|
|
3135
|
-
const
|
|
3136
|
-
if (!
|
|
3226
|
+
const cleanNetwork = network.trim().toLowerCase().replace(/^\/+|\/+$/g, "");
|
|
3227
|
+
if (!isValidNetworkSlug(cleanNetwork)) {
|
|
3137
3228
|
return {
|
|
3138
|
-
content: [{ type: "text", text:
|
|
3229
|
+
content: [{ type: "text", text: formatError(`Invalid network '${network}'. Use a chain slug like 'ethereum', 'base', or 'solana'.`) }],
|
|
3139
3230
|
isError: true
|
|
3140
3231
|
};
|
|
3141
3232
|
}
|
|
3142
|
-
const
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3233
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
3234
|
+
if (!gate.allowed) {
|
|
3235
|
+
return {
|
|
3236
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
3237
|
+
isError: true
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
try {
|
|
3241
|
+
const client = getClient();
|
|
3242
|
+
const result = await client.requestWithPaymentRaw(`/v1/rpc/${cleanNetwork}`, body);
|
|
3243
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
3244
|
+
return {
|
|
3245
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3246
|
+
structuredContent: typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result }
|
|
3247
|
+
};
|
|
3248
|
+
} finally {
|
|
3249
|
+
gate.release();
|
|
3250
|
+
}
|
|
3150
3251
|
} catch (err) {
|
|
3151
3252
|
return {
|
|
3152
3253
|
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
@@ -3189,21 +3290,28 @@ Use blockrun_price (free) for plain spot quotes, blockrun_dex (free) for DEX pai
|
|
|
3189
3290
|
async ({ path: path5, agent_id }) => {
|
|
3190
3291
|
try {
|
|
3191
3292
|
const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/defillama\//, "").replace(/^api\/v1\/defillama\//, "");
|
|
3293
|
+
if (hasPathTraversal(cleanPath)) {
|
|
3294
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
3295
|
+
}
|
|
3192
3296
|
const estimatedCost = estimateDefiCost(cleanPath);
|
|
3193
|
-
const
|
|
3194
|
-
if (!
|
|
3297
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
3298
|
+
if (!gate.allowed) {
|
|
3195
3299
|
return {
|
|
3196
|
-
content: [{ type: "text", text: `${
|
|
3300
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
3197
3301
|
isError: true
|
|
3198
3302
|
};
|
|
3199
3303
|
}
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3304
|
+
try {
|
|
3305
|
+
const client = getClient();
|
|
3306
|
+
const result = await client.getWithPaymentRaw(`/v1/defillama/${cleanPath}`);
|
|
3307
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
3308
|
+
return {
|
|
3309
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3310
|
+
structuredContent: typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result }
|
|
3311
|
+
};
|
|
3312
|
+
} finally {
|
|
3313
|
+
gate.release();
|
|
3314
|
+
}
|
|
3207
3315
|
} catch (err) {
|
|
3208
3316
|
return {
|
|
3209
3317
|
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
@@ -3262,7 +3370,7 @@ function resolveProfileName(argv = process.argv.slice(2), env = process.env) {
|
|
|
3262
3370
|
}
|
|
3263
3371
|
function resolveTools(argv, env) {
|
|
3264
3372
|
const requested = resolveProfileName(argv, env);
|
|
3265
|
-
const spec = PROFILES[requested];
|
|
3373
|
+
const spec = Object.hasOwn(PROFILES, requested) ? PROFILES[requested] : void 0;
|
|
3266
3374
|
if (!spec) {
|
|
3267
3375
|
return { profile: DEFAULT_PROFILE, tools: new Set(ALL_TOOLS) };
|
|
3268
3376
|
}
|
|
@@ -3353,6 +3461,10 @@ function looksLikeRawPrivateKey(value) {
|
|
|
3353
3461
|
if (value.length >= 80 && value.length <= 100 && /^[1-9A-HJ-NP-Za-km-z]+$/.test(value)) return true;
|
|
3354
3462
|
return false;
|
|
3355
3463
|
}
|
|
3464
|
+
function looksLikeNamedSecretValue(value) {
|
|
3465
|
+
if (looksLikeRawPrivateKey(value)) return true;
|
|
3466
|
+
return typeof value === "string" && /^[0-9a-fA-F]{64}$/.test(value);
|
|
3467
|
+
}
|
|
3356
3468
|
function looksLikeSolanaSecretKeyArray(value) {
|
|
3357
3469
|
return Array.isArray(value) && value.length === 64 && value.every((n) => typeof n === "number" && Number.isInteger(n) && n >= 0 && n <= 255);
|
|
3358
3470
|
}
|
|
@@ -3367,7 +3479,7 @@ function walk(obj, file, jsonPath, out) {
|
|
|
3367
3479
|
}
|
|
3368
3480
|
for (const [k, v] of Object.entries(obj)) {
|
|
3369
3481
|
const next = jsonPath ? `${jsonPath}.${k}` : k;
|
|
3370
|
-
if (/wallet[-_ ]?key|private[-_ ]?key|secret/i.test(k) &&
|
|
3482
|
+
if (/wallet[-_ ]?key|private[-_ ]?key|secret/i.test(k) && looksLikeNamedSecretValue(v)) {
|
|
3371
3483
|
out.push({ file, path: next });
|
|
3372
3484
|
} else if (looksLikeRawPrivateKey(v)) {
|
|
3373
3485
|
out.push({ file, path: next });
|