@bnbagent/studio-cli 0.0.13 → 0.0.14-alpha.2

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.
Files changed (36) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +8 -0
  3. package/dist/_agentcoreName-DZDWEYD3.js +0 -0
  4. package/dist/bag.js +3741 -1612
  5. package/dist/{chunk-H4X2OOLA.js → chunk-CFMETPKS.js} +263 -73
  6. package/dist/chunk-U7IDQ3K5.js +0 -0
  7. package/dist/{deployCli-22NMZ4G7.js → deployCli-AVWKY3GN.js} +5 -2
  8. package/package.json +13 -14
  9. package/recipes/agent/code/{{PKG}}/deliveryPolicy.ts.tmpl +43 -0
  10. package/recipes/agent/code/{{PKG}}/readToolCatalog.ts.tmpl +102 -0
  11. package/recipes/agent/code/{{PKG}}/signing.ts.tmpl +7 -2
  12. package/recipes/agent/recipe.toml +5 -4
  13. package/recipes/mpp-buyer/recipe.toml +1 -1
  14. package/recipes/runtimes/agentcore/code/{{PKG}}/dualMain.ts.tmpl +6 -2
  15. package/recipes/runtimes/agentcore/code/{{PKG}}/mcpMain.ts.tmpl +173 -134
  16. package/recipes/runtimes/agentcore/code/{{PKG}}/model.ts.tmpl +7 -0
  17. package/recipes/runtimes/agentcore/code/{{PKG}}/sellerCore.ts.tmpl +31 -46
  18. package/recipes/runtimes/agentcore/code/{{PKG}}/tools.ts.tmpl +19 -48
  19. package/recipes/runtimes/agentcore/code/{{PKG}}/unifiedMain.ts.tmpl +2 -1
  20. package/recipes/runtimes/agentcore/recipe.toml +1 -1
  21. package/recipes/runtimes/azure-foundry/code/{{PKG}}/mcpMain.ts.tmpl +173 -134
  22. package/recipes/runtimes/azure-foundry/code/{{PKG}}/model.ts.tmpl +7 -0
  23. package/recipes/runtimes/azure-foundry/code/{{PKG}}/sellerCore.ts.tmpl +31 -46
  24. package/recipes/runtimes/azure-foundry/code/{{PKG}}/tools.ts.tmpl +19 -48
  25. package/recipes/runtimes/azure-foundry/code/{{PKG}}/unifiedMain.ts.tmpl +2 -1
  26. package/recipes/runtimes/azure-foundry/recipe.toml +1 -1
  27. package/recipes/wallet/recipe.toml +3 -2
  28. package/recipes/x402-buyer/recipe.toml +1 -1
  29. package/skills/bnbagent-studio.md +12 -1
  30. package/skills/references/bnbagent-studio-operating.md +1 -0
  31. package/skills/references/bnbagent-studio-scaffolding-agent.md +11 -34
  32. package/skills/references/bnbagent-studio-selling-via-b402.md +3 -3
  33. package/skills/references/bnbagent-studio-using-altana-wallet.md +7 -2
  34. package/skills/references/bnbagent-studio-using-twak-wallet.md +24 -76
  35. package/dist/_twak-4XF4H5PL.js +0 -25
  36. package/dist/chunk-RO726HJG.js +0 -175
@@ -30,6 +30,7 @@ import * as cr from "@bnbagent/studio-runtime/tools";
30
30
  import { loadStudioToml } from "@bnbagent/studio-runtime/config";
31
31
  import { tool, type ToolSet } from "ai";
32
32
  import { z } from "zod";
33
+ import { READ_TOOL_CATALOG } from "./readToolCatalog.js";
33
34
 
34
35
  /**
35
36
  * The project-wide default network (`[network].default`) — tool calls that
@@ -46,51 +47,34 @@ function defaultNetwork(): string {
46
47
  }
47
48
  }
48
49
 
49
- const networkArg = z
50
- .string()
51
- .optional()
52
- .describe("studio network name (defaults to the project's [network].default)");
53
-
54
50
  export const LLM_READ_TOOLS: ToolSet = {
55
51
  // --- Wallet & chain basics ---
56
52
  wallet_info: tool({
57
- description:
58
- "Describe the agent's active wallet (address, kind, key location).",
59
- inputSchema: z.object({}),
53
+ description: READ_TOOL_CATALOG.wallet_info.description,
54
+ inputSchema: z.object(READ_TOOL_CATALOG.wallet_info.inputSchema),
60
55
  execute: async () => cr.walletInfo(),
61
56
  }),
62
57
  balance_native: tool({
63
- description:
64
- "Native BNB balance of an address (defaults to the agent's own wallet).",
65
- inputSchema: z.object({
66
- address: z.string().optional().describe("0x address; omit for own wallet"),
67
- network: networkArg,
68
- }),
58
+ description: READ_TOOL_CATALOG.balance_native.description,
59
+ inputSchema: z.object(READ_TOOL_CATALOG.balance_native.inputSchema),
69
60
  execute: async ({ address, network }) =>
70
61
  cr.balanceNative(address ?? null, network ?? defaultNetwork()),
71
62
  }),
72
63
  balance_u: tool({
73
64
  // requires [u_token] in studio.toml
74
- description:
75
- "$U (payment token) balance of an address (defaults to the agent's own wallet).",
76
- inputSchema: z.object({
77
- address: z.string().optional().describe("0x address; omit for own wallet"),
78
- network: networkArg,
79
- }),
65
+ description: READ_TOOL_CATALOG.balance_u.description,
66
+ inputSchema: z.object(READ_TOOL_CATALOG.balance_u.inputSchema),
80
67
  execute: async ({ address, network }) =>
81
68
  cr.balanceU(address ?? null, network ?? defaultNetwork()),
82
69
  }),
83
70
  network_info: tool({
84
- description: "Chain id / RPC / token info for a studio network.",
85
- inputSchema: z.object({ network: networkArg }),
71
+ description: READ_TOOL_CATALOG.network_info.description,
72
+ inputSchema: z.object(READ_TOOL_CATALOG.network_info.inputSchema),
86
73
  execute: async ({ network }) => cr.networkInfo(network ?? defaultNetwork()),
87
74
  }),
88
75
  tx_status: tool({
89
- description: "Status + receipt summary of a transaction hash.",
90
- inputSchema: z.object({
91
- tx_hash: z.string().describe("0x transaction hash"),
92
- network: networkArg,
93
- }),
76
+ description: READ_TOOL_CATALOG.tx_status.description,
77
+ inputSchema: z.object(READ_TOOL_CATALOG.tx_status.inputSchema),
94
78
  execute: async ({ tx_hash, network }) =>
95
79
  cr.txStatus(tx_hash, network ?? defaultNetwork()),
96
80
  }),
@@ -106,21 +90,15 @@ export const LLM_READ_TOOLS: ToolSet = {
106
90
  // --- ERC-8004 identity (read-only lookups the LLM may want for context) ---
107
91
  agent_info: tool({
108
92
  // requires [erc8004] in studio.toml
109
- description: "ERC-8004 identity record for an agent id.",
110
- inputSchema: z.object({
111
- agent_id: z.number().int().describe("ERC-8004 agent id"),
112
- network: networkArg,
113
- }),
93
+ description: READ_TOOL_CATALOG.agent_info.description,
94
+ inputSchema: z.object(READ_TOOL_CATALOG.agent_info.inputSchema),
114
95
  execute: async ({ agent_id, network }) =>
115
96
  cr.agentInfo(agent_id, network ?? defaultNetwork()),
116
97
  }),
117
98
  agent_by_address: tool({
118
99
  // requires [erc8004] in studio.toml
119
- description: "Look up an ERC-8004 agent registration by wallet address.",
120
- inputSchema: z.object({
121
- address: z.string().describe("0x wallet address"),
122
- network: networkArg,
123
- }),
100
+ description: READ_TOOL_CATALOG.agent_by_address.description,
101
+ inputSchema: z.object(READ_TOOL_CATALOG.agent_by_address.inputSchema),
124
102
  execute: async ({ address, network }) =>
125
103
  cr.agentByAddress(address, network ?? defaultNetwork()),
126
104
  }),
@@ -128,22 +106,15 @@ export const LLM_READ_TOOLS: ToolSet = {
128
106
  // --- ERC-8183 jobs (READ-ONLY status/list — writes live in signing.ts) ---
129
107
  job_status: tool({
130
108
  // requires [erc8183] in studio.toml
131
- description: "Read-only ERC-8183 job summary (status, budget, deliverable URL).",
132
- inputSchema: z.object({
133
- job_id: z.number().int().describe("on-chain job id"),
134
- network: networkArg,
135
- }),
109
+ description: READ_TOOL_CATALOG.job_status.description,
110
+ inputSchema: z.object(READ_TOOL_CATALOG.job_status.inputSchema),
136
111
  execute: async ({ job_id, network }) =>
137
112
  cr.jobStatus(job_id, network ?? defaultNetwork()),
138
113
  }),
139
114
  job_list: tool({
140
115
  // requires [erc8183] in studio.toml
141
- description: "List recent ERC-8183 jobs (optionally only this agent's).",
142
- inputSchema: z.object({
143
- limit: z.number().int().optional(),
144
- mine: z.boolean().optional().describe("only jobs assigned to this agent"),
145
- network: networkArg,
146
- }),
116
+ description: READ_TOOL_CATALOG.job_list.description,
117
+ inputSchema: z.object(READ_TOOL_CATALOG.job_list.inputSchema),
147
118
  execute: async ({ limit, mine, network }) =>
148
119
  cr.jobList({ limit, mine, network: network ?? defaultNetwork() }),
149
120
  }),
@@ -225,7 +225,8 @@ export function buildRunWork(): RunWork {
225
225
 
226
226
  function hasErc8183Rail(cfg: TomlTable): boolean {
227
227
  const payments = asTable(cfg.payments);
228
- return asTable(payments?.erc8183) !== null;
228
+ const rail = asTable(payments?.erc8183);
229
+ return rail !== null && rail.enabled !== false;
229
230
  }
230
231
 
231
232
  function asTable(value: unknown): TomlTable | null {
@@ -32,7 +32,7 @@ node = [
32
32
  # BNBAGENT_RUNTIME_SECRET_ID is set (default secretsmanager mode + platform).
33
33
  "@aws-sdk/client-secrets-manager@^3.600.0",
34
34
  "@bnbagent/studio-runtime",
35
- "@bnbagent/sdk@0.5.5",
35
+ "@bnbagent/sdk@0.5.6",
36
36
  # The LLM work hook (generateText + tools) and the model factory.
37
37
  "ai@^7.0.29",
38
38
  # Tool input schemas (AI SDK tools + MCP registerTool).
@@ -60,6 +60,7 @@ import {
60
60
  loadStudioToml,
61
61
  type TomlTable,
62
62
  } from "@bnbagent/studio-runtime/config";
63
+ import { maskUrlSecrets } from "@bnbagent/studio-runtime/audit";
63
64
  import * as cr from "@bnbagent/studio-runtime/tools";
64
65
  import {
65
66
  ensureAltanaSessionLoaded,
@@ -84,13 +85,31 @@ import {
84
85
  limitCommerceOperation,
85
86
  requestLimitContext,
86
87
  } from "./requestLimits.js";
88
+ import {
89
+ DeliveryTimeoutError,
90
+ deliveryTimeoutSeconds,
91
+ minimumDeliveryWindowSeconds,
92
+ withTimeout,
93
+ } from "./deliveryPolicy.js";
87
94
  import * as signing from "./signing.js";
95
+ import { READ_TOOL_CATALOG, type ReadToolName } from "./readToolCatalog.js";
88
96
 
89
97
  const APP_NAME = "agent";
98
+ function safeLogText(value: unknown): string {
99
+ const text =
100
+ value instanceof Error
101
+ ? (value.stack ?? `${value.name}: ${value.message}`)
102
+ : String(value ?? "");
103
+ return maskUrlSecrets(text);
104
+ }
105
+
90
106
  const log = {
91
- info: (msg: string) => console.log(`[seller-agent.mcp] ${msg}`),
107
+ info: (msg: string) => console.log(`[seller-agent.mcp] ${safeLogText(msg)}`),
92
108
  error: (msg: string, e?: unknown) =>
93
- console.error(`[seller-agent.mcp] ERROR ${msg}`, e ?? ""),
109
+ console.error(
110
+ `[seller-agent.mcp] ERROR ${safeLogText(msg)}`,
111
+ safeLogText(e),
112
+ ),
94
113
  };
95
114
 
96
115
  function protocolFailure(scope: string, error: unknown): never {
@@ -166,7 +185,8 @@ function generatorTag(): string {
166
185
 
167
186
  function hasErc8183Rail(cfg: TomlTable): boolean {
168
187
  const payments = asTable(cfg.payments);
169
- return asTable(payments?.erc8183) !== null;
188
+ const rail = asTable(payments?.erc8183);
189
+ return rail !== null && rail.enabled !== false;
170
190
  }
171
191
 
172
192
  function asTable(value: unknown): TomlTable | null {
@@ -198,39 +218,46 @@ function flatQuery(query: Record<string, unknown>): Record<string, string> {
198
218
  // Deferred construction keeps negotiate and unpaid payment challenge paths from
199
219
  // building the model, and keeps this module importable without the provider
200
220
  // env until a deliverable is actually produced.
201
- type RunLlm = (prompt: string) => Promise<string>;
202
- let cachedRunLlm: RunLlm | null = null;
203
-
204
- async function runLlm(prompt: string): Promise<string> {
205
- if (cachedRunLlm === null) {
206
- const { buildModel } = await import("./model.js");
207
- const { LLM_READ_TOOLS } = await import("./tools.js");
208
- const model = buildModel(); // managed model w/ budget-gated LLM-credit auto-renew
209
- cachedRunLlm = async (p: string) => {
210
- const result = await generateText({
211
- model,
212
- system:
213
- "You are a seller agent. The runtime has already authorized this task " +
214
- "through its configured commerce rail. Complete the user's task now; " +
215
- "do not ask for a job ID or additional payment. " +
216
- "Be concrete and concise. Use the read-only chain tools when on-chain " +
217
- "context helps. If a paid-data tool such as `buy_with_x402` is " +
218
- "available to you, USE IT to fetch the data a task needs — those " +
219
- "merchants (e.g. CoinMarketCap) charge via on-chain wallet payment, " +
220
- "NOT an API key; never reply that you cannot complete the task for " +
221
- "lack of an API key.",
222
- prompt: p,
223
- // READ-ONLY chain tools; signing is never an LLM tool. To add
224
- // PAID x402 fetch tools (bag x402 trust + x402-buyer recipe):
225
- // import { X402_BUYER_TOOLS } from "./x402Buyer.js";
226
- // tools: { ...LLM_READ_TOOLS, ...X402_BUYER_TOOLS },
227
- tools: LLM_READ_TOOLS,
228
- stopWhen: stepCountIs(8),
229
- });
230
- return result.text.trim();
231
- };
232
- }
233
- return cachedRunLlm(prompt);
221
+ export type McpRunWork = (
222
+ prompt: string,
223
+ context: { sessionId: string; abortSignal?: AbortSignal },
224
+ ) => Promise<string>;
225
+
226
+ /** The MCP delivery hook. Replace this builder for protocol-only projects. */
227
+ export function buildRunWork(): McpRunWork {
228
+ let run: McpRunWork | null = null;
229
+ return async (prompt, context) => {
230
+ if (run === null) {
231
+ const { buildModel } = await import("./model.js");
232
+ const { LLM_READ_TOOLS } = await import("./tools.js");
233
+ const model = buildModel(); // managed model w/ budget-gated LLM-credit auto-renew
234
+ run = async (p, { abortSignal: signal }) => {
235
+ const result = await generateText({
236
+ model,
237
+ system:
238
+ "You are a seller agent. The runtime has already authorized this task " +
239
+ "through its configured commerce rail. Complete the user's task now; " +
240
+ "do not ask for a job ID or additional payment. " +
241
+ "Be concrete and concise. Use the read-only chain tools when on-chain " +
242
+ "context helps. If a paid-data tool such as `buy_with_x402` is " +
243
+ "available to you, USE IT to fetch the data a task needs — those " +
244
+ "merchants (e.g. CoinMarketCap) charge via on-chain wallet payment, " +
245
+ "NOT an API key; never reply that you cannot complete the task for " +
246
+ "lack of an API key.",
247
+ prompt: p,
248
+ // READ-ONLY chain tools; signing is never an LLM tool. To add
249
+ // PAID x402 fetch tools (bag x402 trust + x402-buyer recipe):
250
+ // import { X402_BUYER_TOOLS } from "./x402Buyer.js";
251
+ // tools: { ...LLM_READ_TOOLS, ...X402_BUYER_TOOLS },
252
+ tools: LLM_READ_TOOLS,
253
+ stopWhen: stepCountIs(8),
254
+ abortSignal: signal,
255
+ });
256
+ return result.text.trim();
257
+ };
258
+ }
259
+ return run(prompt, context);
260
+ };
234
261
  }
235
262
 
236
263
  // ── MCP server ────────────────────────────────────────────────────────────────
@@ -282,9 +309,10 @@ async function reportProgress(
282
309
 
283
310
  /** Build the seller MCP server, gating commerce tools on the ERC-8183 rail. */
284
311
  export function buildMcpServer(
285
- opts: { commerceSkills?: boolean } = {},
312
+ opts: { commerceSkills?: boolean; runWork?: McpRunWork } = {},
286
313
  ): McpServer {
287
314
  const server = new McpServer({ name: "bnbagent-seller", version: "1.0.0" });
315
+ const runWork = opts.runWork ?? buildRunWork();
288
316
 
289
317
  // ── Commerce tools (signing is FIXED code in signing.ts) ──────────────────
290
318
  if (opts.commerceSkills !== false) {
@@ -366,6 +394,8 @@ export function buildMcpServer(
366
394
  });
367
395
  }
368
396
 
397
+ const deadlineMs = Date.now() + deliveryTimeoutSeconds() * 1000;
398
+
369
399
  // 1/4 — verify the funded job carries THIS agent's signed quote
370
400
  // (eth_calls). Honour the `permanent` flag: a permanent failure is
371
401
  // terminal ("rejected"); a transient one (chain read hiccup) is
@@ -373,7 +403,10 @@ export function buildMcpServer(
373
403
  await reportProgress(extra, 1, 4);
374
404
  let verdict: { ok: boolean; reason: string; permanent: boolean };
375
405
  try {
376
- verdict = await signing.verifySignedJob(jid);
406
+ verdict = await withTimeout(
407
+ signing.verifySignedJob(jid, minimumDeliveryWindowSeconds()),
408
+ Math.max(0.001, (deadlineMs - Date.now()) / 1000),
409
+ );
377
410
  } catch (e) {
378
411
  // a failed verify is transient; tell the buyer to retry
379
412
  log.error(`verify of job ${jid} failed`, e);
@@ -393,37 +426,68 @@ export function buildMcpServer(
393
426
  });
394
427
  }
395
428
 
396
- // 2/4 produce the deliverable (THE ONLY LLM CALL; specialise the
397
- // prompt here)
398
- await reportProgress(extra, 2, 4);
399
- let work: string;
400
- try {
401
- const spec = await signing.jobSpec(jid);
402
- const task =
403
- spec !== null
404
- ? JSON.stringify({ task: spec.task, terms: spec.terms })
405
- : `job ${jid}`;
406
- const prompt =
407
- "You accepted and were paid for the following job. Produce the deliverable " +
408
- `now. Be complete and self-contained.\n\nJOB CONTEXT:\n${task}`;
409
- work = await runLlm(prompt);
410
- } catch (e) {
411
- return protocolFailure(`delivery preparation for job ${jid} failed`, e);
412
- }
413
- // Unexpected LLM/RPC faults are logged in full, then surfaced through
414
- // MCP's isError channel with a generic public message. Only the
415
- // deterministic SubmitPermanentlyUnsupportedError is a classified
416
- // "rejected" business result.
417
- // 3/4 — sign + broadcast the on-chain submit (re-verifies FUNDED inside)
418
- await reportProgress(extra, 3, 4);
429
+ // Bound only the cancellable work phase. Once submit starts, its
430
+ // broadcast outcome can be ambiguous: the request still has a total
431
+ // deadline, but a submit timeout is UNKNOWN and must be reconciled on
432
+ // chain rather than reported as safe to retry.
433
+ const controller = new AbortController();
434
+ let submitStarted = false;
419
435
  let res: { submitTx: string; deliverableUrl: string | null };
420
436
  try {
421
- res = await signing.submitResult(jid, work, {
422
- job_id: jid,
423
- generator: generatorTag(),
424
- built_with: "https://github.com/bnb-chain/bnbagent-studio",
425
- });
437
+ const work = await withTimeout(
438
+ (async () => {
439
+ // 2/4 — produce the deliverable (THE ONLY LLM CALL).
440
+ await reportProgress(extra, 2, 4);
441
+ const spec = await signing.jobSpec(jid);
442
+ const task =
443
+ spec !== null
444
+ ? JSON.stringify({ task: spec.task, terms: spec.terms })
445
+ : `job ${jid}`;
446
+ const prompt =
447
+ "You accepted and were paid for the following job. Produce the deliverable " +
448
+ `now. Be complete and self-contained.\n\nJOB CONTEXT:\n${task}`;
449
+ return runWork(prompt, {
450
+ sessionId: String(jid),
451
+ abortSignal: controller.signal,
452
+ });
453
+ })(),
454
+ Math.max(0.001, (deadlineMs - Date.now()) / 1000),
455
+ controller,
456
+ );
457
+
458
+ // 3/4 — sign + broadcast submit (re-verifies FUNDED inside). Await
459
+ // the definitive result within the remaining request window. The
460
+ // opaque SDK operation cannot be cancelled after broadcast.
461
+ await reportProgress(extra, 3, 4);
462
+ const remainingSeconds = (deadlineMs - Date.now()) / 1000;
463
+ if (remainingSeconds <= 0) {
464
+ throw new DeliveryTimeoutError("delivery deadline reached");
465
+ }
466
+ submitStarted = true;
467
+ res = await withTimeout(
468
+ signing.submitResult(jid, work, {
469
+ job_id: jid,
470
+ generator: generatorTag(),
471
+ built_with: "https://github.com/bnb-chain/bnbagent-studio",
472
+ }),
473
+ remainingSeconds,
474
+ );
426
475
  } catch (e) {
476
+ if (e instanceof DeliveryTimeoutError) {
477
+ if (submitStarted) {
478
+ return toolResult({
479
+ status: "unknown",
480
+ job_id: jid,
481
+ reason:
482
+ "submit outcome is unknown; poll the job on-chain and do not retry until reconciled",
483
+ });
484
+ }
485
+ return toolResult({
486
+ status: "retry",
487
+ job_id: jid,
488
+ reason: `delivery timed out after ${deliveryTimeoutSeconds()}s`,
489
+ });
490
+ }
427
491
  if (
428
492
  e instanceof Error &&
429
493
  e.name === "SubmitPermanentlyUnsupportedError"
@@ -436,7 +500,7 @@ export function buildMcpServer(
436
500
  reason: "seller wallet does not support result submission",
437
501
  });
438
502
  }
439
- return protocolFailure(`submit of job ${jid} failed`, e);
503
+ return protocolFailure(`delivery of job ${jid} failed`, e);
440
504
  }
441
505
 
442
506
  // 4/4 — done
@@ -452,95 +516,76 @@ export function buildMcpServer(
452
516
  }
453
517
 
454
518
  // ── Read-only chain tools ──────────────────────────────────────────────────
455
- const network = z.string().optional().describe("studio network name");
456
- const roConfig = (description: string, inputSchema: z.ZodRawShape) => ({
457
- description,
458
- inputSchema,
519
+ const roConfig = <Name extends ReadToolName>(name: Name) => ({
520
+ description: READ_TOOL_CATALOG[name].description,
521
+ inputSchema: READ_TOOL_CATALOG[name].inputSchema,
459
522
  annotations: READONLY_ANNOTATIONS,
460
523
  });
524
+ type ReadArgs<Name extends ReadToolName> = z.infer<
525
+ z.ZodObject<(typeof READ_TOOL_CATALOG)[Name]["inputSchema"]>
526
+ >;
461
527
 
462
528
  server.registerTool(
463
529
  "wallet_info",
464
- roConfig("Active wallet summary.", {}),
530
+ roConfig("wallet_info"),
465
531
  async () => toolResult(await cr.walletInfo()),
466
532
  );
467
533
  server.registerTool(
468
534
  "wallet_list",
469
- roConfig("All local wallet addresses.", {}),
535
+ roConfig("wallet_list"),
470
536
  async () => toolResult(await cr.walletList()),
471
537
  );
472
538
  server.registerTool(
473
539
  "wallet_address",
474
- roConfig("The active wallet address.", {}),
540
+ roConfig("wallet_address"),
475
541
  async () => toolResult({ address: await cr.walletAddress() }),
476
542
  );
477
543
  server.registerTool(
478
544
  "balance_native",
479
- roConfig("Native BNB balance (defaults to own wallet).", {
480
- address: z.string().optional(),
481
- network,
482
- }),
483
- async (a) =>
545
+ roConfig("balance_native"),
546
+ async (a: ReadArgs<"balance_native">) =>
484
547
  toolResult(
485
548
  await cr.balanceNative(a.address ?? null, a.network ?? defaultNetwork()),
486
549
  ),
487
550
  );
488
551
  server.registerTool(
489
552
  "balance_u",
490
- roConfig("$U payment-token balance (defaults to own wallet).", {
491
- address: z.string().optional(),
492
- network,
493
- }),
494
- async (a) =>
553
+ roConfig("balance_u"),
554
+ async (a: ReadArgs<"balance_u">) =>
495
555
  toolResult(
496
556
  await cr.balanceU(a.address ?? null, a.network ?? defaultNetwork()),
497
557
  ),
498
558
  );
499
559
  server.registerTool(
500
560
  "pieverse_usage",
501
- roConfig(
502
- "Pieverse LLM usage/credit summary (SIWE personal_sign; no on-chain effect).",
503
- { days: z.number().int().optional() },
504
- ),
505
- async (a) => toolResult(await cr.pieverseUsage(a.days ?? 7)),
561
+ roConfig("pieverse_usage"),
562
+ async (a: ReadArgs<"pieverse_usage">) =>
563
+ toolResult(await cr.pieverseUsage(a.days ?? 7)),
506
564
  );
507
565
  server.registerTool(
508
566
  "agent_info",
509
- roConfig("ERC-8004 identity record for an agent id.", {
510
- agent_id: z.number().int(),
511
- network,
512
- }),
513
- async (a) =>
567
+ roConfig("agent_info"),
568
+ async (a: ReadArgs<"agent_info">) =>
514
569
  toolResult(await cr.agentInfo(a.agent_id, a.network ?? defaultNetwork())),
515
570
  );
516
571
  server.registerTool(
517
572
  "agent_by_address",
518
- roConfig("ERC-8004 registration lookup by wallet address.", {
519
- address: z.string(),
520
- network,
521
- }),
522
- async (a) =>
573
+ roConfig("agent_by_address"),
574
+ async (a: ReadArgs<"agent_by_address">) =>
523
575
  toolResult(
524
576
  await cr.agentByAddress(a.address, a.network ?? defaultNetwork()),
525
577
  ),
526
578
  );
527
579
  server.registerTool(
528
580
  "job_status",
529
- roConfig("Read-only ERC-8183 job summary.", {
530
- job_id: z.number().int(),
531
- network,
532
- }),
533
- async (a) =>
581
+ roConfig("job_status"),
582
+ async (a: ReadArgs<"job_status">) =>
534
583
  toolResult(await cr.jobStatus(a.job_id, a.network ?? defaultNetwork())),
535
584
  );
536
585
  server.registerTool(
537
586
  "job_list",
538
- roConfig("List recent ERC-8183 jobs.", {
539
- limit: z.number().int().optional(),
540
- mine: z.boolean().optional(),
541
- network,
542
- }),
543
- async (a) =>
587
+ roConfig("job_list"),
588
+ async (a: ReadArgs<"job_list">) =>
544
589
  toolResult(
545
590
  await cr.jobList({
546
591
  limit: a.limit,
@@ -551,39 +596,28 @@ export function buildMcpServer(
551
596
  );
552
597
  server.registerTool(
553
598
  "job_count",
554
- roConfig("Network-wide in-flight ERC-8183 job count.", { network }),
555
- async (a) => toolResult(await cr.jobCount(a.network ?? defaultNetwork())),
599
+ roConfig("job_count"),
600
+ async (a: ReadArgs<"job_count">) =>
601
+ toolResult(await cr.jobCount(a.network ?? defaultNetwork())),
556
602
  );
557
603
  server.registerTool(
558
604
  "tx_status",
559
- roConfig("Transaction status + receipt summary.", {
560
- tx_hash: z.string(),
561
- network,
562
- }),
563
- async (a) =>
605
+ roConfig("tx_status"),
606
+ async (a: ReadArgs<"tx_status">) =>
564
607
  toolResult(await cr.txStatus(a.tx_hash, a.network ?? defaultNetwork())),
565
608
  );
566
609
  server.registerTool(
567
610
  "block_info",
568
- roConfig(
569
- 'Block header summary ("latest"/"earliest"/"pending", decimal, or 0x hash).',
570
- { block: z.string().optional(), network },
571
- ),
572
- async (a) =>
611
+ roConfig("block_info"),
612
+ async (a: ReadArgs<"block_info">) =>
573
613
  toolResult(
574
614
  await cr.blockInfo(a.block ?? "latest", a.network ?? defaultNetwork()),
575
615
  ),
576
616
  );
577
617
  server.registerTool(
578
618
  "contract_call_view",
579
- roConfig("Call a read-only (view) contract function by signature.", {
580
- address: z.string(),
581
- function_signature: z.string(),
582
- args: z.array(z.unknown()).optional(),
583
- output_types: z.array(z.string()).optional(),
584
- network,
585
- }),
586
- async (a) =>
619
+ roConfig("contract_call_view"),
620
+ async (a: ReadArgs<"contract_call_view">) =>
587
621
  toolResult(
588
622
  await cr.contractCallView(
589
623
  a.address,
@@ -596,8 +630,9 @@ export function buildMcpServer(
596
630
  );
597
631
  server.registerTool(
598
632
  "network_info",
599
- roConfig("Chain id / RPC / token info for a studio network.", { network }),
600
- async (a) => toolResult(await cr.networkInfo(a.network ?? defaultNetwork())),
633
+ roConfig("network_info"),
634
+ async (a: ReadArgs<"network_info">) =>
635
+ toolResult(await cr.networkInfo(a.network ?? defaultNetwork())),
601
636
  );
602
637
 
603
638
  return server;
@@ -628,9 +663,10 @@ async function main(): Promise<void> {
628
663
  const sellPath = b402SellPath(cfg);
629
664
  const host = process.env.AGENT_BIND_HOST || "0.0.0.0";
630
665
  const port = Number(process.env.AGENT_PORT || "8000");
666
+ const runWork = buildRunWork();
631
667
  const seller = await B402Seller.create({
632
668
  cfg,
633
- runWork: ({ prompt }) => runLlm(prompt),
669
+ runWork: ({ prompt }) => runWork(prompt, { sessionId: "b402" }),
634
670
  walletAddress: getWallet().address,
635
671
  resourceUrl: `${
636
672
  process.env.AGENTCORE_RUNTIME_URL ?? `http://localhost:${port}`
@@ -690,7 +726,10 @@ async function main(): Promise<void> {
690
726
  delete transports[t.sessionId];
691
727
  }
692
728
  };
693
- await buildMcpServer({ commerceSkills: rails.erc8183 }).connect(t);
729
+ await buildMcpServer({
730
+ commerceSkills: rails.erc8183,
731
+ runWork,
732
+ }).connect(t);
694
733
  transport = t;
695
734
  }
696
735
  await transport.handleRequest(req, res, req.body);
@@ -57,6 +57,13 @@ import {
57
57
  export function buildModel(): LanguageModel {
58
58
  const cfg = loadStudioToml();
59
59
  const llmCfg = (cfg.llm ?? {}) as TomlTable;
60
+ if (String(llmCfg.provider ?? "openrouter") === "none") {
61
+ throw new Error(
62
+ "Protocol-only scaffold: no LLM provider is configured. " +
63
+ "Implement the generated work hook before accepting funded jobs, " +
64
+ "or set [llm].provider and [llm].model in studio.toml.",
65
+ );
66
+ }
60
67
  const inner = resolveModel(llmCfg);
61
68
 
62
69
  if (String(llmCfg.provider ?? "openrouter") !== "pieverse-llm") {