@blockrun/mcp 0.23.2 → 0.24.1
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 +551 -384
- 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,
|
|
@@ -20,13 +20,41 @@ import {
|
|
|
20
20
|
loadSolanaWallet,
|
|
21
21
|
getPaymentLinks,
|
|
22
22
|
formatWalletCreatedMessage,
|
|
23
|
-
formatNeedsFundingMessage
|
|
24
|
-
SOLANA_WALLET_FILE_PATH
|
|
23
|
+
formatNeedsFundingMessage
|
|
25
24
|
} from "@blockrun/llm";
|
|
26
|
-
|
|
25
|
+
|
|
26
|
+
// src/utils/constants.ts
|
|
27
|
+
import * as path from "path";
|
|
28
|
+
import * as os from "os";
|
|
29
|
+
var WALLET_DIR = path.join(os.homedir(), ".blockrun");
|
|
30
|
+
var WALLET_FILE = path.join(WALLET_DIR, ".session");
|
|
31
|
+
var QR_FILE = path.join(WALLET_DIR, "qr.png");
|
|
32
|
+
var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
33
|
+
var BASE_CHAIN_ID = "8453";
|
|
34
|
+
var BASE_RPC_URLS = [
|
|
35
|
+
"https://mainnet.base.org",
|
|
36
|
+
"https://base.llamarpc.com",
|
|
37
|
+
"https://1rpc.io/base"
|
|
38
|
+
];
|
|
39
|
+
var MODEL_TIERS = {
|
|
40
|
+
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"],
|
|
41
|
+
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"],
|
|
42
|
+
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"],
|
|
43
|
+
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"],
|
|
44
|
+
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"],
|
|
45
|
+
// 2026-06-07 sweep: dropped qwen3-next (NVIDIA EOL, 410), mistral-small-4-119b
|
|
46
|
+
// (timing out), deepseek-v3.2 + glm-4.7 (NIM hung). All redirect server-side
|
|
47
|
+
// anyway; these are the free models actually serving themselves.
|
|
48
|
+
free: ["nvidia/llama-4-maverick", "nvidia/qwen3-coder-480b", "nvidia/deepseek-v4-flash", "nvidia/gpt-oss-120b", "nvidia/gpt-oss-20b"],
|
|
49
|
+
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"],
|
|
50
|
+
glm: ["zai/glm-5", "zai/glm-5-turbo"]
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/utils/wallet.ts
|
|
54
|
+
var BLOCKRUN_DIR = path2.join(os2.homedir(), ".blockrun");
|
|
27
55
|
var CHAIN_PREFERENCE_FILES = [
|
|
28
|
-
|
|
29
|
-
|
|
56
|
+
path2.join(BLOCKRUN_DIR, ".chain"),
|
|
57
|
+
path2.join(BLOCKRUN_DIR, "payment-chain")
|
|
30
58
|
];
|
|
31
59
|
var _evmClient = null;
|
|
32
60
|
var _imageClient = null;
|
|
@@ -51,12 +79,12 @@ function getChain() {
|
|
|
51
79
|
if (preferred) return preferred;
|
|
52
80
|
if (process.env.SOLANA_WALLET_KEY) return "solana";
|
|
53
81
|
try {
|
|
54
|
-
if (
|
|
82
|
+
if (loadSolanaWallet()) return "solana";
|
|
55
83
|
} catch {
|
|
56
84
|
}
|
|
57
85
|
return "base";
|
|
58
86
|
}
|
|
59
|
-
var CHAIN_FILE =
|
|
87
|
+
var CHAIN_FILE = path2.join(BLOCKRUN_DIR, ".chain");
|
|
60
88
|
function resetChainCaches() {
|
|
61
89
|
_evmClient = null;
|
|
62
90
|
_solanaClient = null;
|
|
@@ -125,6 +153,10 @@ function buildClientWithTimeout(timeoutMs) {
|
|
|
125
153
|
const privateKey = getOrCreateWalletKey();
|
|
126
154
|
return new LLMClient({ privateKey, timeout: timeoutMs });
|
|
127
155
|
}
|
|
156
|
+
function buildClient() {
|
|
157
|
+
if (getChain() === "solana") return buildSolanaClient();
|
|
158
|
+
return new LLMClient({ privateKey: getOrCreateWalletKey() });
|
|
159
|
+
}
|
|
128
160
|
function getAnthropicClient() {
|
|
129
161
|
if (!_anthropicClient) {
|
|
130
162
|
const privateKey = getOrCreateWalletKey();
|
|
@@ -185,17 +217,15 @@ async function getSolanaUsdcBalance() {
|
|
|
185
217
|
return null;
|
|
186
218
|
}
|
|
187
219
|
}
|
|
220
|
+
function parseBaseUsdcCallResult(raw) {
|
|
221
|
+
if (typeof raw !== "string" || !/^0x[0-9a-fA-F]+$/.test(raw)) return null;
|
|
222
|
+
return Number(BigInt(raw)) / 1e6;
|
|
223
|
+
}
|
|
188
224
|
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
225
|
const data = {
|
|
196
226
|
jsonrpc: "2.0",
|
|
197
227
|
method: "eth_call",
|
|
198
|
-
params: [{ to:
|
|
228
|
+
params: [{ to: USDC_ADDRESS, data: `0x70a08231000000000000000000000000${address.slice(2)}` }, "latest"],
|
|
199
229
|
id: 1
|
|
200
230
|
};
|
|
201
231
|
for (const rpcUrl of BASE_RPC_URLS) {
|
|
@@ -203,10 +233,12 @@ async function getBaseUsdcBalance(address) {
|
|
|
203
233
|
const response = await fetch(rpcUrl, {
|
|
204
234
|
method: "POST",
|
|
205
235
|
headers: { "Content-Type": "application/json" },
|
|
206
|
-
body: JSON.stringify(data)
|
|
236
|
+
body: JSON.stringify(data),
|
|
237
|
+
signal: AbortSignal.timeout(8e3)
|
|
207
238
|
});
|
|
208
239
|
const result = await response.json();
|
|
209
|
-
|
|
240
|
+
const usd = parseBaseUsdcCallResult(result.result);
|
|
241
|
+
if (usd !== null) return usd;
|
|
210
242
|
} catch {
|
|
211
243
|
continue;
|
|
212
244
|
}
|
|
@@ -220,7 +252,7 @@ async function getChainBalance(chain, address) {
|
|
|
220
252
|
// src/utils/model-cache.ts
|
|
221
253
|
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
222
254
|
async function loadModels(llm, cache) {
|
|
223
|
-
if (
|
|
255
|
+
if (cache.models === null || cache.models.length === 0) {
|
|
224
256
|
cache.models = llm.listAllModels ? await llm.listAllModels() : await llm.listModels();
|
|
225
257
|
setTimeout(() => {
|
|
226
258
|
cache.models = null;
|
|
@@ -255,6 +287,25 @@ function checkBudget(budget, agentId, estimatedCost = 1e-3) {
|
|
|
255
287
|
}
|
|
256
288
|
return { allowed: true };
|
|
257
289
|
}
|
|
290
|
+
function reserveBudget(budget, agentId, estimatedCost = 1e-3) {
|
|
291
|
+
const check = checkBudget(budget, agentId, estimatedCost);
|
|
292
|
+
if (!check.allowed) return { allowed: false, reason: check.reason, release: () => {
|
|
293
|
+
} };
|
|
294
|
+
const cost = Math.max(0, estimatedCost);
|
|
295
|
+
budget.spent += cost;
|
|
296
|
+
const agentBudget = agentId ? budget.agents.get(agentId) : void 0;
|
|
297
|
+
if (agentBudget) agentBudget.spent += cost;
|
|
298
|
+
let released = false;
|
|
299
|
+
return {
|
|
300
|
+
allowed: true,
|
|
301
|
+
release: () => {
|
|
302
|
+
if (released) return;
|
|
303
|
+
released = true;
|
|
304
|
+
budget.spent -= cost;
|
|
305
|
+
if (agentBudget) agentBudget.spent -= cost;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
258
309
|
function recordSpending(budget, cost, agentId) {
|
|
259
310
|
budget.spent += cost;
|
|
260
311
|
budget.calls += 1;
|
|
@@ -289,30 +340,6 @@ import QRCode from "qrcode";
|
|
|
289
340
|
import open from "open";
|
|
290
341
|
import * as fs2 from "fs";
|
|
291
342
|
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
343
|
var SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
317
344
|
var sharpModule;
|
|
318
345
|
async function loadSharp() {
|
|
@@ -423,9 +450,13 @@ ${parts.join("\n")}`;
|
|
|
423
450
|
}
|
|
424
451
|
return base;
|
|
425
452
|
}
|
|
453
|
+
function isPaymentRejectionError(message) {
|
|
454
|
+
const m = message.toLowerCase();
|
|
455
|
+
return m.includes("insufficient") || m.includes("balance") || m.includes("rejected");
|
|
456
|
+
}
|
|
426
457
|
function formatError(message, opts) {
|
|
427
458
|
const msgLower = message.toLowerCase();
|
|
428
|
-
const hasStatus = (code) => new RegExp(`(^|[^0-9.])${code}([^0-9]
|
|
459
|
+
const hasStatus = (code) => new RegExp(`(^|[^0-9.])${code}($|[^0-9.])`).test(msgLower);
|
|
429
460
|
const isPaymentError = hasStatus("402") || msgLower.includes("balance") || msgLower.includes("insufficient") || msgLower.includes("payment") && !hasStatus("500");
|
|
430
461
|
const isModelUnavailable = msgLower.includes("not active for requested provider") || msgLower.includes("not found or not active");
|
|
431
462
|
const isServerError = hasStatus("500") || msgLower.includes("api error after payment");
|
|
@@ -814,6 +845,7 @@ async function handleAnthropicNative(args) {
|
|
|
814
845
|
temperature,
|
|
815
846
|
stop,
|
|
816
847
|
thinking,
|
|
848
|
+
responseFormat,
|
|
817
849
|
budget,
|
|
818
850
|
agentId,
|
|
819
851
|
estimatedCost
|
|
@@ -829,6 +861,9 @@ async function handleAnthropicNative(args) {
|
|
|
829
861
|
}
|
|
830
862
|
apiMessages.push({ role: m.role, content: toAnthropicContent(m.content) });
|
|
831
863
|
}
|
|
864
|
+
if (responseFormat?.type === "json_object") {
|
|
865
|
+
systemParts.push("Respond with only valid JSON. Do not wrap it in markdown code fences or add any prose before or after.");
|
|
866
|
+
}
|
|
832
867
|
if (message.trim()) apiMessages.push({ role: "user", content: message });
|
|
833
868
|
if (apiMessages.length === 0) {
|
|
834
869
|
return { content: [{ type: "text", text: "No message content to send." }], isError: true };
|
|
@@ -898,7 +933,6 @@ ${thinkingText}` });
|
|
|
898
933
|
function estimateChatCost(maxTokens, mode, model, routing, routingProfile) {
|
|
899
934
|
if (mode === "free") return 0;
|
|
900
935
|
if (model?.startsWith("nvidia/")) return 0;
|
|
901
|
-
if (routing === "smart" && routingProfile === "free") return 0;
|
|
902
936
|
const out = Math.max(maxTokens ?? 1024, 256);
|
|
903
937
|
const frontierReserve = Math.max(0.01, out / 1e6 * 20);
|
|
904
938
|
if (routing === "smart") {
|
|
@@ -944,7 +978,7 @@ Run blockrun_models to see all available models with pricing.`,
|
|
|
944
978
|
model: z2.string().optional().describe("Specific model ID (e.g., 'zai/glm-5', 'openai/o3')"),
|
|
945
979
|
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)"),
|
|
946
980
|
routing: z2.enum(["smart"]).optional().describe('Set to "smart" to auto-select the optimal model via ClawRouter (14-dimension AI routing)'),
|
|
947
|
-
routing_profile: z2.enum(["free", "eco", "auto", "premium"]).optional().default("auto").describe('Cost/quality profile for ClawRouter: "
|
|
981
|
+
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".'),
|
|
948
982
|
system: z2.string().optional().describe("Optional system prompt"),
|
|
949
983
|
max_tokens: z2.number().optional().default(1024).describe("Max tokens in response"),
|
|
950
984
|
temperature: z2.number().optional().default(1).describe("Creativity 0-2"),
|
|
@@ -968,144 +1002,155 @@ Run blockrun_models to see all available models with pricing.`,
|
|
|
968
1002
|
}
|
|
969
1003
|
},
|
|
970
1004
|
async ({ message, model, mode, routing, routing_profile, system, max_tokens, temperature, response_format, stop, thinking, agent_id, messages }) => {
|
|
971
|
-
const llm =
|
|
1005
|
+
const llm = buildClient();
|
|
972
1006
|
const responseFormat = response_format ? { type: response_format } : void 0;
|
|
973
1007
|
const estimatedCost = estimateChatCost(max_tokens, mode, model, routing, routing_profile);
|
|
974
|
-
const
|
|
975
|
-
if (!
|
|
1008
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
1009
|
+
if (!gate.allowed) {
|
|
976
1010
|
return {
|
|
977
|
-
content: [{ type: "text", text: `${
|
|
1011
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet with action: "report" to see usage, or action: "delegate" to increase agent budget.` }],
|
|
978
1012
|
isError: true
|
|
979
1013
|
};
|
|
980
1014
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
messages,
|
|
992
|
-
maxTokens: max_tokens,
|
|
993
|
-
temperature,
|
|
994
|
-
stop,
|
|
995
|
-
thinking,
|
|
996
|
-
budget,
|
|
997
|
-
agentId: agent_id,
|
|
998
|
-
estimatedCost
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
if (routing === "smart") {
|
|
1002
|
-
if (!(llm instanceof LLMClient2)) {
|
|
1003
|
-
return {
|
|
1004
|
-
content: [{ type: "text", text: "Smart routing (ClawRouter) is not available on Solana. Use a specific model or mode instead." }],
|
|
1005
|
-
isError: true
|
|
1006
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
try {
|
|
1009
|
-
const { result, settledUsd } = await withSettledCost(llm, () => llm.smartChat(message, {
|
|
1015
|
+
try {
|
|
1016
|
+
if (model && isAnthropicModel(model)) {
|
|
1017
|
+
const solanaBlock = baseOnlyMessage("Native Anthropic (claude-*) calls");
|
|
1018
|
+
if (solanaBlock) {
|
|
1019
|
+
return { content: [{ type: "text", text: solanaBlock }], isError: true };
|
|
1020
|
+
}
|
|
1021
|
+
return await handleAnthropicNative({
|
|
1022
|
+
client: getAnthropicClient(),
|
|
1023
|
+
model,
|
|
1024
|
+
message,
|
|
1010
1025
|
system,
|
|
1026
|
+
messages,
|
|
1011
1027
|
maxTokens: max_tokens,
|
|
1012
|
-
maxOutputTokens: max_tokens,
|
|
1013
1028
|
temperature,
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
// omit it and let ClawRouter pick (matches the SDK upgrade path).
|
|
1017
|
-
routingProfile: routing_profile === "free" ? void 0 : routing_profile,
|
|
1029
|
+
stop,
|
|
1030
|
+
thinking,
|
|
1018
1031
|
responseFormat,
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1032
|
+
budget,
|
|
1033
|
+
agentId: agent_id,
|
|
1034
|
+
estimatedCost
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
if (routing === "smart") {
|
|
1038
|
+
if (messages && messages.length > 0) {
|
|
1039
|
+
return {
|
|
1040
|
+
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".') }],
|
|
1041
|
+
isError: true
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
if (!(llm instanceof LLMClient2)) {
|
|
1045
|
+
return {
|
|
1046
|
+
content: [{ type: "text", text: "Smart routing (ClawRouter) is not available on Solana. Use a specific model or mode instead." }],
|
|
1047
|
+
isError: true
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
const { result, settledUsd } = await withSettledCost(llm, () => llm.smartChat(message, {
|
|
1052
|
+
system,
|
|
1053
|
+
maxTokens: max_tokens,
|
|
1054
|
+
maxOutputTokens: max_tokens,
|
|
1055
|
+
temperature,
|
|
1056
|
+
// @blockrun/llm 2.x dropped the "free" routing profile; the gateway
|
|
1057
|
+
// already routes to the most cost-effective model by default, so we
|
|
1058
|
+
// omit it and let ClawRouter pick (matches the SDK upgrade path).
|
|
1059
|
+
routingProfile: routing_profile === "free" ? void 0 : routing_profile,
|
|
1060
|
+
responseFormat,
|
|
1061
|
+
stop
|
|
1062
|
+
}));
|
|
1063
|
+
recordActualSpend(budget, settledUsd, result.routing.costEstimate || estimatedCost, agent_id);
|
|
1064
|
+
return {
|
|
1065
|
+
content: [{ type: "text", text: `[${result.model} | ${result.routing.tier} | $${result.routing.costEstimate.toFixed(4)} | ${Math.round((result.routing.savings ?? 0) * 100)}% savings]
|
|
1024
1066
|
|
|
1025
1067
|
${result.response}` }],
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1068
|
+
structuredContent: {
|
|
1069
|
+
model_used: result.model,
|
|
1070
|
+
response: result.response,
|
|
1071
|
+
routing: result.routing
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
|
|
1076
|
+
}
|
|
1034
1077
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
content: [{ type: "text", text: `[${targetModel} | ${fullMessages.length} msgs]
|
|
1078
|
+
if (messages && messages.length > 0) {
|
|
1079
|
+
const targetModel = model || MODEL_TIERS[mode ?? "balanced"]?.[0] || "openai/gpt-5.5";
|
|
1080
|
+
const fullMessages = [
|
|
1081
|
+
...system ? [{ role: "system", content: system }] : [],
|
|
1082
|
+
...messages,
|
|
1083
|
+
{ role: "user", content: message }
|
|
1084
|
+
];
|
|
1085
|
+
try {
|
|
1086
|
+
const { result, settledUsd } = await withSettledCost(llm, () => llm.chatCompletion(targetModel, fullMessages, {
|
|
1087
|
+
maxTokens: max_tokens,
|
|
1088
|
+
temperature,
|
|
1089
|
+
responseFormat,
|
|
1090
|
+
stop
|
|
1091
|
+
}));
|
|
1092
|
+
const reply = result.choices?.[0]?.message?.content || "";
|
|
1093
|
+
recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
|
|
1094
|
+
return {
|
|
1095
|
+
content: [{ type: "text", text: `[${targetModel} | ${fullMessages.length} msgs]
|
|
1054
1096
|
|
|
1055
1097
|
${reply}` }],
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1098
|
+
structuredContent: { model_used: targetModel, response: reply, message_count: fullMessages.length }
|
|
1099
|
+
};
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
|
|
1102
|
+
}
|
|
1060
1103
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
}
|
|
1104
|
+
if (model) {
|
|
1105
|
+
try {
|
|
1106
|
+
const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(model, message, {
|
|
1107
|
+
system,
|
|
1108
|
+
maxTokens: max_tokens,
|
|
1109
|
+
temperature,
|
|
1110
|
+
responseFormat,
|
|
1111
|
+
stop
|
|
1112
|
+
}));
|
|
1113
|
+
recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
|
|
1114
|
+
return { content: [{ type: "text", text: response }] };
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
return {
|
|
1117
|
+
content: [{ type: "text", text: formatError(extractErrorMessage(error)) }],
|
|
1118
|
+
isError: true
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1078
1121
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
content: [{ type: "text", text: `[${m}]
|
|
1122
|
+
const routingMode = mode || "balanced";
|
|
1123
|
+
const models = MODEL_TIERS[routingMode];
|
|
1124
|
+
let lastError = null;
|
|
1125
|
+
for (const m of models) {
|
|
1126
|
+
try {
|
|
1127
|
+
const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(m, message, {
|
|
1128
|
+
system,
|
|
1129
|
+
maxTokens: max_tokens,
|
|
1130
|
+
temperature,
|
|
1131
|
+
responseFormat,
|
|
1132
|
+
stop
|
|
1133
|
+
}));
|
|
1134
|
+
recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
|
|
1135
|
+
return {
|
|
1136
|
+
content: [{ type: "text", text: `[${m}]
|
|
1095
1137
|
|
|
1096
1138
|
${response}` }],
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1139
|
+
structuredContent: { model_used: m, response }
|
|
1140
|
+
};
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
lastError = error;
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1102
1145
|
}
|
|
1146
|
+
const errorMessage = lastError ? extractErrorMessage(lastError) : "All models failed";
|
|
1147
|
+
return {
|
|
1148
|
+
content: [{ type: "text", text: formatError(errorMessage) }],
|
|
1149
|
+
isError: true
|
|
1150
|
+
};
|
|
1151
|
+
} finally {
|
|
1152
|
+
gate.release();
|
|
1103
1153
|
}
|
|
1104
|
-
const errorMessage = lastError ? extractErrorMessage(lastError) : "All models failed";
|
|
1105
|
-
return {
|
|
1106
|
-
content: [{ type: "text", text: formatError(errorMessage) }],
|
|
1107
|
-
isError: true
|
|
1108
|
-
};
|
|
1109
1154
|
}
|
|
1110
1155
|
);
|
|
1111
1156
|
}
|
|
@@ -1174,6 +1219,41 @@ ${lines.join("\n")}` }],
|
|
|
1174
1219
|
// src/tools/image.ts
|
|
1175
1220
|
import { z as z4 } from "zod";
|
|
1176
1221
|
import { PaymentError } from "@blockrun/llm";
|
|
1222
|
+
|
|
1223
|
+
// src/utils/ssrf.ts
|
|
1224
|
+
function ipv4Blocked(host) {
|
|
1225
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
1226
|
+
if (!m) return null;
|
|
1227
|
+
const o = m.slice(1).map(Number);
|
|
1228
|
+
if (o.some((n) => n > 255)) return true;
|
|
1229
|
+
const [a, b] = o;
|
|
1230
|
+
return a === 0 || // 0.0.0.0/8
|
|
1231
|
+
a === 127 || // loopback
|
|
1232
|
+
a === 10 || // private
|
|
1233
|
+
a === 172 && b >= 16 && b <= 31 || // private
|
|
1234
|
+
a === 192 && b === 168 || // private
|
|
1235
|
+
a === 169 && b === 254 || // link-local (incl. metadata)
|
|
1236
|
+
a === 100 && b >= 64 && b <= 127;
|
|
1237
|
+
}
|
|
1238
|
+
function isBlockedFetchHost(hostname) {
|
|
1239
|
+
let host = hostname.trim().toLowerCase();
|
|
1240
|
+
if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
|
|
1241
|
+
if (!host) return true;
|
|
1242
|
+
if (host === "localhost" || host.endsWith(".localhost")) return true;
|
|
1243
|
+
if (host.endsWith(".internal") || host.endsWith(".local")) return true;
|
|
1244
|
+
const v4 = ipv4Blocked(host);
|
|
1245
|
+
if (v4 !== null) return v4;
|
|
1246
|
+
if (host.includes(":")) {
|
|
1247
|
+
if (host === "::1" || host === "::") return true;
|
|
1248
|
+
if (host.startsWith("fc") || host.startsWith("fd") || host.startsWith("fe80")) return true;
|
|
1249
|
+
const mapped = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(host);
|
|
1250
|
+
if (mapped) return ipv4Blocked(mapped[1]) === true;
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/tools/image.ts
|
|
1177
1257
|
import { readFile } from "fs/promises";
|
|
1178
1258
|
var REFERENCE_IMAGE_MAX_BYTES = 4e6;
|
|
1179
1259
|
var IMAGE_EXT_MIME = {
|
|
@@ -1189,10 +1269,29 @@ async function toImageDataUri(ref) {
|
|
|
1189
1269
|
const ctrl = new AbortController();
|
|
1190
1270
|
const timeout = setTimeout(() => ctrl.abort(), 3e4);
|
|
1191
1271
|
try {
|
|
1192
|
-
|
|
1272
|
+
let url = ref;
|
|
1273
|
+
let res;
|
|
1274
|
+
for (let hop = 0; ; hop++) {
|
|
1275
|
+
const host = new URL(url).hostname;
|
|
1276
|
+
if (isBlockedFetchHost(host)) {
|
|
1277
|
+
throw new Error(`refusing to fetch a private/loopback/link-local address: ${host}`);
|
|
1278
|
+
}
|
|
1279
|
+
res = await fetch(url, { signal: ctrl.signal, redirect: "manual" });
|
|
1280
|
+
const location = res.headers.get("location");
|
|
1281
|
+
if (res.status >= 300 && res.status < 400 && location) {
|
|
1282
|
+
if (hop >= 5) throw new Error("too many redirects");
|
|
1283
|
+
url = new URL(location, url).toString();
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1193
1288
|
if (!res.ok) throw new Error(`fetch failed: ${res.status} ${res.statusText}`);
|
|
1194
1289
|
const mime2 = (res.headers.get("content-type") || "").toLowerCase().split(";")[0].trim();
|
|
1195
1290
|
if (!mime2.startsWith("image/")) throw new Error(`URL returned non-image content-type: ${mime2 || "(none)"}`);
|
|
1291
|
+
const advertised = Number(res.headers.get("content-length"));
|
|
1292
|
+
if (Number.isFinite(advertised) && advertised > REFERENCE_IMAGE_MAX_BYTES) {
|
|
1293
|
+
throw new Error(`image too large: ${(advertised / 1e6).toFixed(1)}MB > ${REFERENCE_IMAGE_MAX_BYTES / 1e6}MB cap`);
|
|
1294
|
+
}
|
|
1196
1295
|
const buffer2 = Buffer.from(await res.arrayBuffer());
|
|
1197
1296
|
if (buffer2.byteLength > REFERENCE_IMAGE_MAX_BYTES) {
|
|
1198
1297
|
throw new Error(`image too large: ${(buffer2.byteLength / 1e6).toFixed(1)}MB > ${REFERENCE_IMAGE_MAX_BYTES / 1e6}MB cap`);
|
|
@@ -1349,34 +1448,42 @@ Source images and masks accept a base64 data URI, an http(s) URL, or a local fil
|
|
|
1349
1448
|
};
|
|
1350
1449
|
}
|
|
1351
1450
|
const estimatedCost = estimateCost(selectedModel, size);
|
|
1352
|
-
const
|
|
1353
|
-
if (!
|
|
1451
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
1452
|
+
if (!gate.allowed) {
|
|
1354
1453
|
return {
|
|
1355
|
-
content: [{ type: "text", text: `${
|
|
1454
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
1356
1455
|
isError: true
|
|
1357
1456
|
};
|
|
1358
1457
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1458
|
+
try {
|
|
1459
|
+
response = await getImageClient().edit(prompt, normalizedImage, {
|
|
1460
|
+
model: selectedModel,
|
|
1461
|
+
size,
|
|
1462
|
+
...normalizedMask ? { mask: normalizedMask } : {}
|
|
1463
|
+
});
|
|
1464
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
1465
|
+
} finally {
|
|
1466
|
+
gate.release();
|
|
1467
|
+
}
|
|
1365
1468
|
} else {
|
|
1366
1469
|
const estimatedCost = estimateCost(selectedModel, size);
|
|
1367
|
-
const
|
|
1368
|
-
if (!
|
|
1470
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
1471
|
+
if (!gate.allowed) {
|
|
1369
1472
|
return {
|
|
1370
|
-
content: [{ type: "text", text: `${
|
|
1473
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
1371
1474
|
isError: true
|
|
1372
1475
|
};
|
|
1373
1476
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1477
|
+
try {
|
|
1478
|
+
response = await getImageClient().generate(prompt, {
|
|
1479
|
+
model: selectedModel,
|
|
1480
|
+
size,
|
|
1481
|
+
quality
|
|
1482
|
+
});
|
|
1483
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
1484
|
+
} finally {
|
|
1485
|
+
gate.release();
|
|
1486
|
+
}
|
|
1380
1487
|
}
|
|
1381
1488
|
const imageUrl = response.data?.[0]?.url;
|
|
1382
1489
|
if (!imageUrl) {
|
|
@@ -1417,11 +1524,7 @@ async function fetchWithTimeout(url, options, timeoutMs) {
|
|
|
1417
1524
|
const controller = new AbortController();
|
|
1418
1525
|
const id = setTimeout(() => controller.abort(), timeoutMs);
|
|
1419
1526
|
id.unref?.();
|
|
1420
|
-
|
|
1421
|
-
return await fetch(url, { ...options, signal: controller.signal });
|
|
1422
|
-
} finally {
|
|
1423
|
-
clearTimeout(id);
|
|
1424
|
-
}
|
|
1527
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
1425
1528
|
}
|
|
1426
1529
|
function isTimeoutError(err) {
|
|
1427
1530
|
const name = err instanceof Error ? err.name : "";
|
|
@@ -1460,6 +1563,7 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
|
|
|
1460
1563
|
}
|
|
1461
1564
|
},
|
|
1462
1565
|
async ({ prompt, instrumental, lyrics, model, agent_id }) => {
|
|
1566
|
+
let gate;
|
|
1463
1567
|
try {
|
|
1464
1568
|
if (getChain() !== "base") {
|
|
1465
1569
|
return {
|
|
@@ -1473,10 +1577,10 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
|
|
|
1473
1577
|
isError: true
|
|
1474
1578
|
};
|
|
1475
1579
|
}
|
|
1476
|
-
|
|
1477
|
-
if (!
|
|
1580
|
+
gate = reserveBudget(budget, agent_id, MUSIC_COST);
|
|
1581
|
+
if (!gate.allowed) {
|
|
1478
1582
|
return {
|
|
1479
|
-
content: [{ type: "text", text: `${
|
|
1583
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
1480
1584
|
isError: true
|
|
1481
1585
|
};
|
|
1482
1586
|
}
|
|
@@ -1491,8 +1595,8 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
|
|
|
1491
1595
|
body: JSON.stringify(body)
|
|
1492
1596
|
}, 15e3);
|
|
1493
1597
|
if (resp402.status !== 402) {
|
|
1494
|
-
const data2 = await resp402.json();
|
|
1495
|
-
throw new Error(`
|
|
1598
|
+
const data2 = await resp402.json().catch(() => ({}));
|
|
1599
|
+
throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${JSON.stringify(data2)}`);
|
|
1496
1600
|
}
|
|
1497
1601
|
const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
|
|
1498
1602
|
if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
|
|
@@ -1553,7 +1657,7 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
|
|
|
1553
1657
|
};
|
|
1554
1658
|
} catch (err) {
|
|
1555
1659
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1556
|
-
if (
|
|
1660
|
+
if (isPaymentRejectionError(errMsg)) {
|
|
1557
1661
|
return {
|
|
1558
1662
|
content: [{ type: "text", text: `Music generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
|
|
1559
1663
|
Error: ${errMsg}` }],
|
|
@@ -1571,6 +1675,8 @@ Error: ${errMsg}` }],
|
|
|
1571
1675
|
content: [{ type: "text", text: formatError(`Music generation failed: ${errMsg}`) }],
|
|
1572
1676
|
isError: true
|
|
1573
1677
|
};
|
|
1678
|
+
} finally {
|
|
1679
|
+
gate?.release();
|
|
1574
1680
|
}
|
|
1575
1681
|
}
|
|
1576
1682
|
);
|
|
@@ -1640,6 +1746,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
|
|
|
1640
1746
|
}
|
|
1641
1747
|
},
|
|
1642
1748
|
async ({ action, input, voice, model, response_format, speed, duration_seconds, prompt_influence, agent_id }) => {
|
|
1749
|
+
let gate;
|
|
1643
1750
|
try {
|
|
1644
1751
|
if (action === "voices") {
|
|
1645
1752
|
return await listVoices();
|
|
@@ -1684,10 +1791,10 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
|
|
|
1684
1791
|
if (speed !== void 0) body.speed = speed;
|
|
1685
1792
|
cost = speechCost(model, input.length);
|
|
1686
1793
|
}
|
|
1687
|
-
|
|
1688
|
-
if (!
|
|
1794
|
+
gate = reserveBudget(budget, agent_id, cost);
|
|
1795
|
+
if (!gate.allowed) {
|
|
1689
1796
|
return {
|
|
1690
|
-
content: [{ type: "text", text: `${
|
|
1797
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
1691
1798
|
isError: true
|
|
1692
1799
|
};
|
|
1693
1800
|
}
|
|
@@ -1700,7 +1807,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
|
|
|
1700
1807
|
}, 15e3);
|
|
1701
1808
|
if (resp402.status !== 402) {
|
|
1702
1809
|
const data2 = await resp402.json().catch(() => ({}));
|
|
1703
|
-
throw new Error(`
|
|
1810
|
+
throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${JSON.stringify(data2)}`);
|
|
1704
1811
|
}
|
|
1705
1812
|
const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
|
|
1706
1813
|
if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
|
|
@@ -1766,7 +1873,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
|
|
|
1766
1873
|
};
|
|
1767
1874
|
} catch (err) {
|
|
1768
1875
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1769
|
-
if (
|
|
1876
|
+
if (isPaymentRejectionError(errMsg)) {
|
|
1770
1877
|
return {
|
|
1771
1878
|
content: [{ type: "text", text: `Speech generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
|
|
1772
1879
|
Error: ${errMsg}` }],
|
|
@@ -1784,6 +1891,8 @@ Error: ${errMsg}` }],
|
|
|
1784
1891
|
content: [{ type: "text", text: formatError(`Speech generation failed: ${errMsg}`) }],
|
|
1785
1892
|
isError: true
|
|
1786
1893
|
};
|
|
1894
|
+
} finally {
|
|
1895
|
+
gate?.release();
|
|
1787
1896
|
}
|
|
1788
1897
|
}
|
|
1789
1898
|
);
|
|
@@ -1884,6 +1993,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
1884
1993
|
}
|
|
1885
1994
|
},
|
|
1886
1995
|
async ({ prompt, image_url, real_face_asset_id, duration_seconds, generate_audio, resolution, aspect_ratio, last_frame_url, model, agent_id }) => {
|
|
1996
|
+
let gate;
|
|
1887
1997
|
try {
|
|
1888
1998
|
if (getChain() !== "base") {
|
|
1889
1999
|
return {
|
|
@@ -1924,10 +2034,10 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
1924
2034
|
const hasImageInput = Boolean(image_url || real_face_asset_id);
|
|
1925
2035
|
const perSecond = (hasImageInput ? VIDEO_PRICE_PER_SECOND_IMAGE[selectedModel] : void 0) ?? VIDEO_PRICE_PER_SECOND[selectedModel] ?? 0.05;
|
|
1926
2036
|
const estimatedCost = perSecond * billedSeconds;
|
|
1927
|
-
|
|
1928
|
-
if (!
|
|
2037
|
+
gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
2038
|
+
if (!gate.allowed) {
|
|
1929
2039
|
return {
|
|
1930
|
-
content: [{ type: "text", text: `${
|
|
2040
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
1931
2041
|
isError: true
|
|
1932
2042
|
};
|
|
1933
2043
|
}
|
|
@@ -1948,14 +2058,21 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
1948
2058
|
body: JSON.stringify(body)
|
|
1949
2059
|
}, 15e3);
|
|
1950
2060
|
if (resp402.status !== 402) {
|
|
1951
|
-
const data = await resp402.json();
|
|
1952
|
-
throw new Error(`
|
|
2061
|
+
const data = await resp402.json().catch(() => ({}));
|
|
2062
|
+
throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${JSON.stringify(data)}`);
|
|
1953
2063
|
}
|
|
1954
2064
|
const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
|
|
1955
2065
|
if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
|
|
1956
2066
|
const paymentRequired = parsePaymentRequired3(prHeader);
|
|
1957
2067
|
const details = extractPaymentDetails3(paymentRequired);
|
|
1958
2068
|
const settledUsd = amountToUsd(details.amount);
|
|
2069
|
+
if (settledUsd !== null && settledUsd > estimatedCost) {
|
|
2070
|
+
gate?.release();
|
|
2071
|
+
gate = reserveBudget(budget, agent_id, settledUsd);
|
|
2072
|
+
if (!gate.allowed) {
|
|
2073
|
+
return { content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }], isError: true };
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
1959
2076
|
const paymentPayload = await createPaymentPayload3(
|
|
1960
2077
|
privateKey,
|
|
1961
2078
|
account.address,
|
|
@@ -2032,7 +2149,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
2032
2149
|
const lines = [
|
|
2033
2150
|
`\u{1F3AC} Video ready!`,
|
|
2034
2151
|
`URL: ${completed.url}`,
|
|
2035
|
-
`Duration: ${completed.duration_seconds
|
|
2152
|
+
`Duration: ${completed.duration_seconds ?? billedSeconds}s`,
|
|
2036
2153
|
`Model: ${completed.modelReturned || selectedModel}`,
|
|
2037
2154
|
...completed.backed_up ? [`Backed up to BlockRun storage (URL is permanent)`] : completed.source_url ? [`Source URL: ${completed.source_url}`] : [],
|
|
2038
2155
|
...completed.request_id ? [`Request ID: ${completed.request_id}`] : [],
|
|
@@ -2053,7 +2170,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
2053
2170
|
};
|
|
2054
2171
|
} catch (err) {
|
|
2055
2172
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2056
|
-
if (
|
|
2173
|
+
if (isPaymentRejectionError(errMsg)) {
|
|
2057
2174
|
return {
|
|
2058
2175
|
content: [{ type: "text", text: `Video generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
|
|
2059
2176
|
Error: ${errMsg}` }],
|
|
@@ -2071,6 +2188,8 @@ Error: ${errMsg}` }],
|
|
|
2071
2188
|
content: [{ type: "text", text: formatError(`Video generation failed: ${errMsg}`, { altModels: "bytedance/seedance-2.0, azure/sora-2" }) }],
|
|
2072
2189
|
isError: true
|
|
2073
2190
|
};
|
|
2191
|
+
} finally {
|
|
2192
|
+
gate?.release();
|
|
2074
2193
|
}
|
|
2075
2194
|
}
|
|
2076
2195
|
);
|
|
@@ -2096,7 +2215,7 @@ async function payAndPostJson(url, reqBody, fallbackDescription) {
|
|
|
2096
2215
|
}, 15e3);
|
|
2097
2216
|
if (resp402.status !== 402) {
|
|
2098
2217
|
const data2 = await resp402.json().catch(() => ({}));
|
|
2099
|
-
throw new Error(`
|
|
2218
|
+
throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${data2.message || data2.error || JSON.stringify(data2)}`);
|
|
2100
2219
|
}
|
|
2101
2220
|
const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
|
|
2102
2221
|
if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
|
|
@@ -2157,6 +2276,7 @@ Privacy: BlockRun does not store face/liveness data \u2014 only the asset id, na
|
|
|
2157
2276
|
}
|
|
2158
2277
|
},
|
|
2159
2278
|
async ({ action, name, group_id, image_url, agent_id }) => {
|
|
2279
|
+
let gate;
|
|
2160
2280
|
try {
|
|
2161
2281
|
if (action === "init") {
|
|
2162
2282
|
if (!name) {
|
|
@@ -2283,9 +2403,9 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
|
|
|
2283
2403
|
if (!name || !image_url) {
|
|
2284
2404
|
return { content: [{ type: "text", text: formatError("portrait requires name and image_url (public HTTPS URL of an AI-generated character image).") }], isError: true };
|
|
2285
2405
|
}
|
|
2286
|
-
|
|
2287
|
-
if (!
|
|
2288
|
-
return { content: [{ type: "text", text: `${
|
|
2406
|
+
gate = reserveBudget(budget, agent_id, ENROLLMENT_PRICE_USD);
|
|
2407
|
+
if (!gate.allowed) {
|
|
2408
|
+
return { content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }], isError: true };
|
|
2289
2409
|
}
|
|
2290
2410
|
const { status, data, settledUsd } = await payAndPostJson(
|
|
2291
2411
|
`${BLOCKRUN_API4}/v1/portrait/enroll`,
|
|
@@ -2333,61 +2453,26 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
|
|
|
2333
2453
|
if (!name || !image_url || !group_id) {
|
|
2334
2454
|
return { content: [{ type: "text", text: formatError("enroll requires name, image_url, and group_id (from init, after the group is active).") }], isError: true };
|
|
2335
2455
|
}
|
|
2336
|
-
|
|
2337
|
-
if (!
|
|
2338
|
-
return { content: [{ type: "text", text: `${
|
|
2456
|
+
gate = reserveBudget(budget, agent_id, ENROLLMENT_PRICE_USD);
|
|
2457
|
+
if (!gate.allowed) {
|
|
2458
|
+
return { content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }], isError: true };
|
|
2339
2459
|
}
|
|
2340
|
-
const
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
const resp402 = await fetchWithTimeout(enrollUrl, {
|
|
2345
|
-
method: "POST",
|
|
2346
|
-
headers: { "Content-Type": "application/json" },
|
|
2347
|
-
body: reqBody
|
|
2348
|
-
}, 15e3);
|
|
2349
|
-
if (resp402.status !== 402) {
|
|
2350
|
-
const data2 = await resp402.json().catch(() => ({}));
|
|
2351
|
-
throw new Error(`Expected 402, got ${resp402.status}: ${data2.message || data2.error || JSON.stringify(data2)}`);
|
|
2352
|
-
}
|
|
2353
|
-
const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
|
|
2354
|
-
if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
|
|
2355
|
-
const paymentRequired = parsePaymentRequired4(prHeader);
|
|
2356
|
-
const details = extractPaymentDetails4(paymentRequired);
|
|
2357
|
-
const settledUsd = amountToUsd(details.amount);
|
|
2358
|
-
const paymentPayload = await createPaymentPayload4(
|
|
2359
|
-
privateKey,
|
|
2360
|
-
account.address,
|
|
2361
|
-
details.recipient,
|
|
2362
|
-
details.amount,
|
|
2363
|
-
details.network || "eip155:8453",
|
|
2364
|
-
{
|
|
2365
|
-
resourceUrl: details.resource?.url || enrollUrl,
|
|
2366
|
-
resourceDescription: details.resource?.description || "BlockRun RealFace enrollment",
|
|
2367
|
-
maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, 120),
|
|
2368
|
-
extra: details.extra
|
|
2369
|
-
}
|
|
2460
|
+
const { status, data, settledUsd } = await payAndPostJson(
|
|
2461
|
+
`${BLOCKRUN_API4}/v1/realface/enroll`,
|
|
2462
|
+
JSON.stringify({ name, image_url, group_id }),
|
|
2463
|
+
"BlockRun RealFace enrollment"
|
|
2370
2464
|
);
|
|
2371
|
-
|
|
2372
|
-
method: "POST",
|
|
2373
|
-
headers: {
|
|
2374
|
-
"Content-Type": "application/json",
|
|
2375
|
-
"PAYMENT-SIGNATURE": paymentPayload
|
|
2376
|
-
},
|
|
2377
|
-
body: reqBody
|
|
2378
|
-
}, 9e4);
|
|
2379
|
-
const data = await resp.json().catch(() => ({}));
|
|
2380
|
-
if (resp.status === 402) {
|
|
2465
|
+
if (status === 402) {
|
|
2381
2466
|
throw new Error("Payment rejected. Check your wallet balance.");
|
|
2382
2467
|
}
|
|
2383
|
-
if (
|
|
2468
|
+
if (status === 425) {
|
|
2384
2469
|
return { content: [{ type: "text", text: formatError(`Group not active yet \u2014 ${data.message || "finish the phone liveness check first"}. No payment taken.`) }], isError: true };
|
|
2385
2470
|
}
|
|
2386
|
-
if (
|
|
2471
|
+
if (status === 422) {
|
|
2387
2472
|
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 };
|
|
2388
2473
|
}
|
|
2389
|
-
if (
|
|
2390
|
-
throw new Error(`Enroll error ${
|
|
2474
|
+
if (status < 200 || status >= 300) {
|
|
2475
|
+
throw new Error(`Enroll error ${status}: ${data.error || JSON.stringify(data)}`);
|
|
2391
2476
|
}
|
|
2392
2477
|
const assetId = data.asset_id;
|
|
2393
2478
|
if (!assetId) throw new Error(`Enroll response missing asset_id: ${JSON.stringify(data)}`);
|
|
@@ -2416,7 +2501,7 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
|
|
|
2416
2501
|
return { content: [{ type: "text", text: formatError(`Unknown action: ${action}`) }], isError: true };
|
|
2417
2502
|
} catch (err) {
|
|
2418
2503
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2419
|
-
if (
|
|
2504
|
+
if (isPaymentRejectionError(errMsg)) {
|
|
2420
2505
|
return {
|
|
2421
2506
|
content: [{ type: "text", text: `RealFace enrollment requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
|
|
2422
2507
|
Error: ${errMsg}` }],
|
|
@@ -2424,6 +2509,8 @@ Error: ${errMsg}` }],
|
|
|
2424
2509
|
};
|
|
2425
2510
|
}
|
|
2426
2511
|
return { content: [{ type: "text", text: formatError(`RealFace ${action} failed: ${errMsg}`) }], isError: true };
|
|
2512
|
+
} finally {
|
|
2513
|
+
gate?.release();
|
|
2427
2514
|
}
|
|
2428
2515
|
}
|
|
2429
2516
|
);
|
|
@@ -2447,6 +2534,19 @@ function asStructuredContent(result) {
|
|
|
2447
2534
|
return typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result };
|
|
2448
2535
|
}
|
|
2449
2536
|
|
|
2537
|
+
// src/utils/path-safety.ts
|
|
2538
|
+
function hasPathTraversal(path5) {
|
|
2539
|
+
let decoded = path5;
|
|
2540
|
+
try {
|
|
2541
|
+
decoded = decodeURIComponent(path5);
|
|
2542
|
+
} catch {
|
|
2543
|
+
}
|
|
2544
|
+
return decoded.split(/[/\\]/).some((seg) => seg === ".." || seg === ".");
|
|
2545
|
+
}
|
|
2546
|
+
function isValidNetworkSlug(slug) {
|
|
2547
|
+
return /^[a-z0-9-]+$/.test(slug);
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2450
2550
|
// src/tools/search.ts
|
|
2451
2551
|
var SEARCH_PRICE_PER_SOURCE = 0.025;
|
|
2452
2552
|
var SEARCH_DEFAULT_MAX_RESULTS = 10;
|
|
@@ -2477,23 +2577,30 @@ Full request shape + worked examples in the \`search\` skill (\`skills/search/SK
|
|
|
2477
2577
|
async ({ path: path5, body, agent_id }) => {
|
|
2478
2578
|
try {
|
|
2479
2579
|
body = coerceBody(body);
|
|
2580
|
+
const cleanPath = (path5 ?? "").replace(/^\/+/, "").replace(/^v1\/search\/?/, "");
|
|
2581
|
+
if (hasPathTraversal(cleanPath)) {
|
|
2582
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
2583
|
+
}
|
|
2480
2584
|
const estimatedCost = estimateSearchCost(body);
|
|
2481
|
-
const
|
|
2482
|
-
if (!
|
|
2585
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
2586
|
+
if (!gate.allowed) {
|
|
2483
2587
|
return {
|
|
2484
|
-
content: [{ type: "text", text: `${
|
|
2588
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2485
2589
|
isError: true
|
|
2486
2590
|
};
|
|
2487
2591
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2592
|
+
try {
|
|
2593
|
+
const client = getClient();
|
|
2594
|
+
const endpoint = cleanPath ? `/v1/search/${cleanPath}` : "/v1/search";
|
|
2595
|
+
const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
|
|
2596
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
2597
|
+
return {
|
|
2598
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2599
|
+
structuredContent: asStructuredContent(result)
|
|
2600
|
+
};
|
|
2601
|
+
} finally {
|
|
2602
|
+
gate.release();
|
|
2603
|
+
}
|
|
2497
2604
|
} catch (err) {
|
|
2498
2605
|
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
2499
2606
|
}
|
|
@@ -2535,23 +2642,30 @@ Full request/response shapes + worked research workflows in the \`exa-research\`
|
|
|
2535
2642
|
async ({ path: path5, body, agent_id }) => {
|
|
2536
2643
|
try {
|
|
2537
2644
|
body = coerceBody(body);
|
|
2645
|
+
const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/exa\//, "");
|
|
2646
|
+
if (hasPathTraversal(cleanPath)) {
|
|
2647
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
2648
|
+
}
|
|
2538
2649
|
const estimatedCost = estimateExaCost(path5, body);
|
|
2539
|
-
const
|
|
2540
|
-
if (!
|
|
2650
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
2651
|
+
if (!gate.allowed) {
|
|
2541
2652
|
return {
|
|
2542
|
-
content: [{ type: "text", text: `${
|
|
2653
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2543
2654
|
isError: true
|
|
2544
2655
|
};
|
|
2545
2656
|
}
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2657
|
+
try {
|
|
2658
|
+
const client = getClient();
|
|
2659
|
+
const endpoint = `/v1/exa/${cleanPath}`;
|
|
2660
|
+
const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
|
|
2661
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
2662
|
+
return {
|
|
2663
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2664
|
+
structuredContent: asStructuredContent(result)
|
|
2665
|
+
};
|
|
2666
|
+
} finally {
|
|
2667
|
+
gate.release();
|
|
2668
|
+
}
|
|
2555
2669
|
} catch (err) {
|
|
2556
2670
|
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
2557
2671
|
}
|
|
@@ -2634,21 +2748,28 @@ Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. p
|
|
|
2634
2748
|
async ({ path: path5, params, body, agent_id }) => {
|
|
2635
2749
|
try {
|
|
2636
2750
|
body = coerceBody(body);
|
|
2751
|
+
if (hasPathTraversal(path5)) {
|
|
2752
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
2753
|
+
}
|
|
2637
2754
|
const estimatedCost = estimateMarketCost(path5, body);
|
|
2638
|
-
const
|
|
2639
|
-
if (!
|
|
2755
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
2756
|
+
if (!gate.allowed) {
|
|
2640
2757
|
return {
|
|
2641
|
-
content: [{ type: "text", text: `${
|
|
2758
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2642
2759
|
isError: true
|
|
2643
2760
|
};
|
|
2644
2761
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2762
|
+
try {
|
|
2763
|
+
const llm = getClient();
|
|
2764
|
+
const result = body !== void 0 ? await llm.pmQuery(path5, body) : await llm.pm(path5, params);
|
|
2765
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
2766
|
+
return {
|
|
2767
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2768
|
+
structuredContent: asStructuredContent(result)
|
|
2769
|
+
};
|
|
2770
|
+
} finally {
|
|
2771
|
+
gate.release();
|
|
2772
|
+
}
|
|
2652
2773
|
} catch (err) {
|
|
2653
2774
|
return {
|
|
2654
2775
|
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
@@ -2730,51 +2851,55 @@ Examples:
|
|
|
2730
2851
|
};
|
|
2731
2852
|
}
|
|
2732
2853
|
const estimatedCost = paid ? 1e-3 : 0;
|
|
2733
|
-
const
|
|
2734
|
-
if (!
|
|
2854
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
2855
|
+
if (!gate.allowed) {
|
|
2735
2856
|
return {
|
|
2736
|
-
content: [{ type: "text", text: `${
|
|
2857
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2737
2858
|
isError: true
|
|
2738
2859
|
};
|
|
2739
2860
|
}
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
if (
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
if (
|
|
2755
|
-
|
|
2756
|
-
|
|
2861
|
+
try {
|
|
2862
|
+
const priceClient = getPriceClient(paid);
|
|
2863
|
+
if (action === "price") {
|
|
2864
|
+
if (!symbol) throw new Error("symbol is required for action='price'");
|
|
2865
|
+
const result2 = await priceClient.price(category, symbol, {
|
|
2866
|
+
market,
|
|
2867
|
+
session
|
|
2868
|
+
});
|
|
2869
|
+
if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
|
|
2870
|
+
return {
|
|
2871
|
+
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }],
|
|
2872
|
+
structuredContent: result2
|
|
2873
|
+
};
|
|
2874
|
+
}
|
|
2875
|
+
if (action === "history") {
|
|
2876
|
+
if (!symbol) throw new Error("symbol is required for action='history'");
|
|
2877
|
+
if (from === void 0) throw new Error("from (unix seconds) is required for action='history'");
|
|
2878
|
+
const result2 = await priceClient.history(category, symbol, {
|
|
2879
|
+
market,
|
|
2880
|
+
session,
|
|
2881
|
+
resolution: resolution ?? "D",
|
|
2882
|
+
from,
|
|
2883
|
+
to
|
|
2884
|
+
});
|
|
2885
|
+
if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
|
|
2886
|
+
return {
|
|
2887
|
+
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }],
|
|
2888
|
+
structuredContent: result2
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
const result = await priceClient.listSymbols(category, {
|
|
2757
2892
|
market,
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
from,
|
|
2761
|
-
to
|
|
2893
|
+
query,
|
|
2894
|
+
limit
|
|
2762
2895
|
});
|
|
2763
|
-
if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
|
|
2764
2896
|
return {
|
|
2765
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
2766
|
-
structuredContent:
|
|
2897
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2898
|
+
structuredContent: result
|
|
2767
2899
|
};
|
|
2900
|
+
} finally {
|
|
2901
|
+
gate.release();
|
|
2768
2902
|
}
|
|
2769
|
-
const result = await priceClient.listSymbols(category, {
|
|
2770
|
-
market,
|
|
2771
|
-
query,
|
|
2772
|
-
limit
|
|
2773
|
-
});
|
|
2774
|
-
return {
|
|
2775
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2776
|
-
structuredContent: result
|
|
2777
|
-
};
|
|
2778
2903
|
} catch (err) {
|
|
2779
2904
|
return {
|
|
2780
2905
|
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
@@ -2823,7 +2948,7 @@ Examples:
|
|
|
2823
2948
|
isError: true
|
|
2824
2949
|
};
|
|
2825
2950
|
}
|
|
2826
|
-
const response = await
|
|
2951
|
+
const response = await fetchWithTimeout(url, {}, 8e3);
|
|
2827
2952
|
if (!response.ok) {
|
|
2828
2953
|
throw new Error(`DexScreener API error: ${response.status}`);
|
|
2829
2954
|
}
|
|
@@ -2905,22 +3030,29 @@ Full action shapes + GPU type details in the \`modal\` skill.`,
|
|
|
2905
3030
|
try {
|
|
2906
3031
|
body = coerceBody(body);
|
|
2907
3032
|
const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/modal\//, "");
|
|
3033
|
+
if (hasPathTraversal(cleanPath)) {
|
|
3034
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
3035
|
+
}
|
|
2908
3036
|
const estimatedCost = estimateModalCost(cleanPath);
|
|
2909
|
-
const
|
|
2910
|
-
if (!
|
|
3037
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
3038
|
+
if (!gate.allowed) {
|
|
2911
3039
|
return {
|
|
2912
|
-
content: [{ type: "text", text: `${
|
|
3040
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2913
3041
|
isError: true
|
|
2914
3042
|
};
|
|
2915
3043
|
}
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
3044
|
+
try {
|
|
3045
|
+
const client = buildClientWithTimeout(modalTimeoutMs(body));
|
|
3046
|
+
const endpoint = `/v1/modal/${cleanPath}`;
|
|
3047
|
+
const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
|
|
3048
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
3049
|
+
return {
|
|
3050
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3051
|
+
structuredContent: asStructuredContent(result)
|
|
3052
|
+
};
|
|
3053
|
+
} finally {
|
|
3054
|
+
gate.release();
|
|
3055
|
+
}
|
|
2924
3056
|
} catch (err) {
|
|
2925
3057
|
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
2926
3058
|
}
|
|
@@ -2971,22 +3103,29 @@ Voice call flow + voice preset details + full body shapes in the \`phone\` skill
|
|
|
2971
3103
|
try {
|
|
2972
3104
|
body = coerceBody(body);
|
|
2973
3105
|
const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\//, "");
|
|
3106
|
+
if (hasPathTraversal(cleanPath)) {
|
|
3107
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
3108
|
+
}
|
|
2974
3109
|
const estimatedCost = estimatePhoneCost(cleanPath, body !== void 0);
|
|
2975
|
-
const
|
|
2976
|
-
if (!
|
|
3110
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
3111
|
+
if (!gate.allowed) {
|
|
2977
3112
|
return {
|
|
2978
|
-
content: [{ type: "text", text: `${
|
|
3113
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2979
3114
|
isError: true
|
|
2980
3115
|
};
|
|
2981
3116
|
}
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
3117
|
+
try {
|
|
3118
|
+
const client = getClient();
|
|
3119
|
+
const endpoint = `/v1/${cleanPath}`;
|
|
3120
|
+
const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint);
|
|
3121
|
+
if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
|
|
3122
|
+
return {
|
|
3123
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3124
|
+
structuredContent: asStructuredContent(result)
|
|
3125
|
+
};
|
|
3126
|
+
} finally {
|
|
3127
|
+
gate.release();
|
|
3128
|
+
}
|
|
2990
3129
|
} catch (err) {
|
|
2991
3130
|
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
2992
3131
|
}
|
|
@@ -3069,22 +3208,29 @@ Each Surf endpoint pre-validates required params before settling \u2014 you get
|
|
|
3069
3208
|
try {
|
|
3070
3209
|
body = coerceBody(body);
|
|
3071
3210
|
const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/surf\//, "").replace(/^api\/v1\/surf\//, "");
|
|
3211
|
+
if (hasPathTraversal(cleanPath)) {
|
|
3212
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
3213
|
+
}
|
|
3072
3214
|
const estimatedCost = estimateSurfCost(cleanPath);
|
|
3073
|
-
const
|
|
3074
|
-
if (!
|
|
3215
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
3216
|
+
if (!gate.allowed) {
|
|
3075
3217
|
return {
|
|
3076
|
-
content: [{ type: "text", text: `${
|
|
3218
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
3077
3219
|
isError: true
|
|
3078
3220
|
};
|
|
3079
3221
|
}
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3222
|
+
try {
|
|
3223
|
+
const client = getClient();
|
|
3224
|
+
const endpoint = `/v1/surf/${cleanPath}`;
|
|
3225
|
+
const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint, params);
|
|
3226
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
3227
|
+
return {
|
|
3228
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3229
|
+
structuredContent: asStructuredContent(result)
|
|
3230
|
+
};
|
|
3231
|
+
} finally {
|
|
3232
|
+
gate.release();
|
|
3233
|
+
}
|
|
3088
3234
|
} catch (err) {
|
|
3089
3235
|
return {
|
|
3090
3236
|
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
@@ -3138,21 +3284,31 @@ Prefer blockrun_price (free quotes), blockrun_dex (free DEX data), or blockrun_s
|
|
|
3138
3284
|
}
|
|
3139
3285
|
const batchCount = Array.isArray(body) ? Math.max(body.length, 1) : 1;
|
|
3140
3286
|
const estimatedCost = RPC_PRICE_USD * batchCount;
|
|
3141
|
-
const
|
|
3142
|
-
if (!
|
|
3287
|
+
const cleanNetwork = network.trim().toLowerCase().replace(/^\/+|\/+$/g, "");
|
|
3288
|
+
if (!isValidNetworkSlug(cleanNetwork)) {
|
|
3143
3289
|
return {
|
|
3144
|
-
content: [{ type: "text", text:
|
|
3290
|
+
content: [{ type: "text", text: formatError(`Invalid network '${network}'. Use a chain slug like 'ethereum', 'base', or 'solana'.`) }],
|
|
3145
3291
|
isError: true
|
|
3146
3292
|
};
|
|
3147
3293
|
}
|
|
3148
|
-
const
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3294
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
3295
|
+
if (!gate.allowed) {
|
|
3296
|
+
return {
|
|
3297
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
3298
|
+
isError: true
|
|
3299
|
+
};
|
|
3300
|
+
}
|
|
3301
|
+
try {
|
|
3302
|
+
const client = getClient();
|
|
3303
|
+
const result = await client.requestWithPaymentRaw(`/v1/rpc/${cleanNetwork}`, body);
|
|
3304
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
3305
|
+
return {
|
|
3306
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3307
|
+
structuredContent: typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result }
|
|
3308
|
+
};
|
|
3309
|
+
} finally {
|
|
3310
|
+
gate.release();
|
|
3311
|
+
}
|
|
3156
3312
|
} catch (err) {
|
|
3157
3313
|
return {
|
|
3158
3314
|
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
@@ -3195,21 +3351,28 @@ Use blockrun_price (free) for plain spot quotes, blockrun_dex (free) for DEX pai
|
|
|
3195
3351
|
async ({ path: path5, agent_id }) => {
|
|
3196
3352
|
try {
|
|
3197
3353
|
const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/defillama\//, "").replace(/^api\/v1\/defillama\//, "");
|
|
3354
|
+
if (hasPathTraversal(cleanPath)) {
|
|
3355
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
3356
|
+
}
|
|
3198
3357
|
const estimatedCost = estimateDefiCost(cleanPath);
|
|
3199
|
-
const
|
|
3200
|
-
if (!
|
|
3358
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
3359
|
+
if (!gate.allowed) {
|
|
3201
3360
|
return {
|
|
3202
|
-
content: [{ type: "text", text: `${
|
|
3361
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
3203
3362
|
isError: true
|
|
3204
3363
|
};
|
|
3205
3364
|
}
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3365
|
+
try {
|
|
3366
|
+
const client = getClient();
|
|
3367
|
+
const result = await client.getWithPaymentRaw(`/v1/defillama/${cleanPath}`);
|
|
3368
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
3369
|
+
return {
|
|
3370
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3371
|
+
structuredContent: typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result }
|
|
3372
|
+
};
|
|
3373
|
+
} finally {
|
|
3374
|
+
gate.release();
|
|
3375
|
+
}
|
|
3213
3376
|
} catch (err) {
|
|
3214
3377
|
return {
|
|
3215
3378
|
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
@@ -3268,7 +3431,7 @@ function resolveProfileName(argv = process.argv.slice(2), env = process.env) {
|
|
|
3268
3431
|
}
|
|
3269
3432
|
function resolveTools(argv, env) {
|
|
3270
3433
|
const requested = resolveProfileName(argv, env);
|
|
3271
|
-
const spec = PROFILES[requested];
|
|
3434
|
+
const spec = Object.hasOwn(PROFILES, requested) ? PROFILES[requested] : void 0;
|
|
3272
3435
|
if (!spec) {
|
|
3273
3436
|
return { profile: DEFAULT_PROFILE, tools: new Set(ALL_TOOLS) };
|
|
3274
3437
|
}
|
|
@@ -3359,6 +3522,10 @@ function looksLikeRawPrivateKey(value) {
|
|
|
3359
3522
|
if (value.length >= 80 && value.length <= 100 && /^[1-9A-HJ-NP-Za-km-z]+$/.test(value)) return true;
|
|
3360
3523
|
return false;
|
|
3361
3524
|
}
|
|
3525
|
+
function looksLikeNamedSecretValue(value) {
|
|
3526
|
+
if (looksLikeRawPrivateKey(value)) return true;
|
|
3527
|
+
return typeof value === "string" && /^[0-9a-fA-F]{64}$/.test(value);
|
|
3528
|
+
}
|
|
3362
3529
|
function looksLikeSolanaSecretKeyArray(value) {
|
|
3363
3530
|
return Array.isArray(value) && value.length === 64 && value.every((n) => typeof n === "number" && Number.isInteger(n) && n >= 0 && n <= 255);
|
|
3364
3531
|
}
|
|
@@ -3373,7 +3540,7 @@ function walk(obj, file, jsonPath, out) {
|
|
|
3373
3540
|
}
|
|
3374
3541
|
for (const [k, v] of Object.entries(obj)) {
|
|
3375
3542
|
const next = jsonPath ? `${jsonPath}.${k}` : k;
|
|
3376
|
-
if (/wallet[-_ ]?key|private[-_ ]?key|secret/i.test(k) &&
|
|
3543
|
+
if (/wallet[-_ ]?key|private[-_ ]?key|secret/i.test(k) && looksLikeNamedSecretValue(v)) {
|
|
3377
3544
|
out.push({ file, path: next });
|
|
3378
3545
|
} else if (looksLikeRawPrivateKey(v)) {
|
|
3379
3546
|
out.push({ file, path: next });
|