@bnbagent/studio-cli 0.0.13 → 0.0.14-alpha.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/LICENSE +201 -0
- package/README.md +8 -0
- package/dist/_agentcoreName-DZDWEYD3.js +0 -0
- package/dist/bag.js +3556 -1565
- package/dist/{chunk-H4X2OOLA.js → chunk-QVYWAJEK.js} +228 -72
- package/dist/chunk-U7IDQ3K5.js +0 -0
- package/dist/{deployCli-22NMZ4G7.js → deployCli-F3AOM5UO.js} +1 -2
- package/package.json +12 -13
- package/recipes/agent/code/{{PKG}}/deliveryPolicy.ts.tmpl +43 -0
- package/recipes/agent/code/{{PKG}}/readToolCatalog.ts.tmpl +102 -0
- package/recipes/agent/code/{{PKG}}/signing.ts.tmpl +7 -2
- package/recipes/agent/recipe.toml +4 -3
- package/recipes/runtimes/agentcore/code/{{PKG}}/dualMain.ts.tmpl +6 -2
- package/recipes/runtimes/agentcore/code/{{PKG}}/mcpMain.ts.tmpl +173 -134
- package/recipes/runtimes/agentcore/code/{{PKG}}/model.ts.tmpl +7 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/sellerCore.ts.tmpl +31 -46
- package/recipes/runtimes/agentcore/code/{{PKG}}/tools.ts.tmpl +19 -48
- package/recipes/runtimes/agentcore/code/{{PKG}}/unifiedMain.ts.tmpl +2 -1
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/mcpMain.ts.tmpl +173 -134
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/model.ts.tmpl +7 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/sellerCore.ts.tmpl +31 -46
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/tools.ts.tmpl +19 -48
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/unifiedMain.ts.tmpl +2 -1
- package/recipes/wallet/recipe.toml +3 -2
- package/skills/bnbagent-studio.md +12 -1
- package/skills/references/bnbagent-studio-operating.md +1 -0
- package/skills/references/bnbagent-studio-scaffolding-agent.md +11 -34
- package/skills/references/bnbagent-studio-selling-via-b402.md +3 -3
- package/skills/references/bnbagent-studio-using-altana-wallet.md +6 -1
- package/skills/references/bnbagent-studio-using-twak-wallet.md +24 -76
- package/dist/_twak-4XF4H5PL.js +0 -25
- package/dist/chunk-RO726HJG.js +0 -175
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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
|
-
//
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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(`
|
|
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
|
|
456
|
-
|
|
457
|
-
|
|
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("
|
|
530
|
+
roConfig("wallet_info"),
|
|
465
531
|
async () => toolResult(await cr.walletInfo()),
|
|
466
532
|
);
|
|
467
533
|
server.registerTool(
|
|
468
534
|
"wallet_list",
|
|
469
|
-
roConfig("
|
|
535
|
+
roConfig("wallet_list"),
|
|
470
536
|
async () => toolResult(await cr.walletList()),
|
|
471
537
|
);
|
|
472
538
|
server.registerTool(
|
|
473
539
|
"wallet_address",
|
|
474
|
-
roConfig("
|
|
540
|
+
roConfig("wallet_address"),
|
|
475
541
|
async () => toolResult({ address: await cr.walletAddress() }),
|
|
476
542
|
);
|
|
477
543
|
server.registerTool(
|
|
478
544
|
"balance_native",
|
|
479
|
-
roConfig("
|
|
480
|
-
|
|
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("
|
|
491
|
-
|
|
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
|
-
|
|
503
|
-
|
|
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("
|
|
510
|
-
|
|
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("
|
|
519
|
-
|
|
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("
|
|
530
|
-
|
|
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("
|
|
539
|
-
|
|
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("
|
|
555
|
-
async (a) =>
|
|
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("
|
|
560
|
-
|
|
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
|
-
|
|
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("
|
|
580
|
-
|
|
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("
|
|
600
|
-
async (a) =>
|
|
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 }) =>
|
|
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({
|
|
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") {
|
|
@@ -38,24 +38,38 @@
|
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
40
|
import { ERC8183JobOps } from "@bnbagent/sdk/erc8183";
|
|
41
|
+
import { maskUrlSecrets } from "@bnbagent/studio-runtime/audit";
|
|
41
42
|
import { SubmitPermanentlyUnsupportedError } from "@bnbagent/studio-runtime/erc8183";
|
|
42
43
|
import { getWallet } from "@bnbagent/studio-runtime/wallet";
|
|
44
|
+
import {
|
|
45
|
+
DeliveryTimeoutError,
|
|
46
|
+
deliveryTimeoutSeconds,
|
|
47
|
+
envSeconds,
|
|
48
|
+
minimumDeliveryWindowSeconds,
|
|
49
|
+
withTimeout,
|
|
50
|
+
} from "./deliveryPolicy.js";
|
|
43
51
|
import { limitCommerceOperation } from "./requestLimits.js";
|
|
44
52
|
import * as defaultSigning from "./signing.js";
|
|
45
53
|
|
|
54
|
+
function safeLogText(value: unknown): string {
|
|
55
|
+
const text =
|
|
56
|
+
value instanceof Error
|
|
57
|
+
? (value.stack ?? `${value.name}: ${value.message}`)
|
|
58
|
+
: String(value ?? "");
|
|
59
|
+
return maskUrlSecrets(text);
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
const log = {
|
|
47
|
-
info: (msg: string) => console.log(`[seller-agent.core] ${msg}`),
|
|
48
|
-
warn: (msg: string) =>
|
|
63
|
+
info: (msg: string) => console.log(`[seller-agent.core] ${safeLogText(msg)}`),
|
|
64
|
+
warn: (msg: string) =>
|
|
65
|
+
console.warn(`[seller-agent.core] WARNING ${safeLogText(msg)}`),
|
|
49
66
|
error: (msg: string, e?: unknown) =>
|
|
50
|
-
console.error(
|
|
67
|
+
console.error(
|
|
68
|
+
`[seller-agent.core] ERROR ${safeLogText(msg)}`,
|
|
69
|
+
safeLogText(e),
|
|
70
|
+
),
|
|
51
71
|
};
|
|
52
72
|
|
|
53
|
-
/** Read a positive timeout (seconds) from the env, falling back to `dflt`. */
|
|
54
|
-
function envSeconds(name: string, dflt: number): number {
|
|
55
|
-
const v = Number(process.env[name] || dflt);
|
|
56
|
-
return Number.isFinite(v) && v > 0 ? v : dflt;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
73
|
// Background-task ceilings. notifyFunded ACKs immediately and delivers in a
|
|
60
74
|
// BACKGROUND task; AgentCore keeps the scale-to-zero microVM warm
|
|
61
75
|
// (HEALTHY_BUSY) while isBusy() is true. A delivery (LLM text + on-chain
|
|
@@ -65,43 +79,10 @@ function envSeconds(name: string, dflt: number): number {
|
|
|
65
79
|
// billing memory the whole time. A timed-out job is treated as TRANSIENT
|
|
66
80
|
// (not dropped): the funded job stays on-chain and a later sweep re-delivers
|
|
67
81
|
// it idempotently. (Read lazily so tests can tune them via the env.)
|
|
68
|
-
const jobDeliveryTimeoutSeconds = () =>
|
|
69
|
-
envSeconds("NOTIFY_DELIVERY_TIMEOUT_SECONDS", 600);
|
|
70
82
|
const sweepTimeoutSeconds = () => envSeconds("NOTIFY_SWEEP_TIMEOUT_SECONDS", 60);
|
|
71
83
|
const preverifyTimeoutSeconds = () =>
|
|
72
84
|
envSeconds("NOTIFY_PREVERIFY_TIMEOUT_SECONDS", 30);
|
|
73
85
|
|
|
74
|
-
/** Rejection raised by {@link withTimeout} when the deadline fires. */
|
|
75
|
-
export class DeliveryTimeoutError extends Error {}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Race `work` against a deadline, aborting `controller` when it fires.
|
|
79
|
-
*
|
|
80
|
-
* JS cannot hard-cancel an arbitrary promise the way asyncio.wait_for
|
|
81
|
-
* cancels a coroutine: the abort signal stops the LLM call (the AI SDK
|
|
82
|
-
* honours it), and the on-chain layers are idempotent — `verifySignedJob`
|
|
83
|
-
* returns non-OK for an already-SUBMITTED job and `submitResult` re-verifies
|
|
84
|
-
* FUNDED — so an orphaned straggler can never double-deliver.
|
|
85
|
-
*/
|
|
86
|
-
async function withTimeout<T>(
|
|
87
|
-
work: Promise<T>,
|
|
88
|
-
seconds: number,
|
|
89
|
-
controller?: AbortController,
|
|
90
|
-
): Promise<T> {
|
|
91
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
92
|
-
const deadline = new Promise<never>((_, reject) => {
|
|
93
|
-
timer = setTimeout(() => {
|
|
94
|
-
controller?.abort();
|
|
95
|
-
reject(new DeliveryTimeoutError(`timed out after ${seconds}s`));
|
|
96
|
-
}, seconds * 1000);
|
|
97
|
-
});
|
|
98
|
-
try {
|
|
99
|
-
return await Promise.race([work, deadline]);
|
|
100
|
-
} finally {
|
|
101
|
-
clearTimeout(timer);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
86
|
/**
|
|
106
87
|
* The LLM work hook: produce the deliverable text for a prompt.
|
|
107
88
|
*
|
|
@@ -125,6 +106,7 @@ export interface SigningApi {
|
|
|
125
106
|
): Promise<Record<string, unknown>>;
|
|
126
107
|
verifySignedJob(
|
|
127
108
|
jobId: number,
|
|
109
|
+
minimumRemainingSeconds?: number,
|
|
128
110
|
): Promise<{ ok: boolean; reason: string; permanent: boolean }>;
|
|
129
111
|
jobSpec(
|
|
130
112
|
jobId: number,
|
|
@@ -284,7 +266,7 @@ export class SellerCore {
|
|
|
284
266
|
// Time-bounded: a hung RPC must not stall the ack path. On timeout we
|
|
285
267
|
// fall through to accept-and-re-verify below.
|
|
286
268
|
const v = await withTimeout(
|
|
287
|
-
this.signing.verifySignedJob(jobId),
|
|
269
|
+
this.signing.verifySignedJob(jobId, minimumDeliveryWindowSeconds()),
|
|
288
270
|
preverifyTimeoutSeconds(),
|
|
289
271
|
);
|
|
290
272
|
if (!v.ok && v.permanent) {
|
|
@@ -351,7 +333,7 @@ export class SellerCore {
|
|
|
351
333
|
verified
|
|
352
334
|
? this.doWorkAndSubmit(jobId, controller.signal)
|
|
353
335
|
: this.fulfillJob(jobId, controller.signal),
|
|
354
|
-
|
|
336
|
+
deliveryTimeoutSeconds(),
|
|
355
337
|
controller,
|
|
356
338
|
);
|
|
357
339
|
log.info(`notify_funded job ${jobId} → ${JSON.stringify(result)}`);
|
|
@@ -367,7 +349,7 @@ export class SellerCore {
|
|
|
367
349
|
if (e instanceof DeliveryTimeoutError) {
|
|
368
350
|
// Transient by design — leave terminal false so a later sweep retries.
|
|
369
351
|
log.warn(
|
|
370
|
-
`background delivery of job ${jobId} timed out after ${
|
|
352
|
+
`background delivery of job ${jobId} timed out after ${deliveryTimeoutSeconds()}s; will retry`,
|
|
371
353
|
);
|
|
372
354
|
} else {
|
|
373
355
|
log.error(`background delivery of job ${jobId} failed`, e);
|
|
@@ -399,7 +381,10 @@ export class SellerCore {
|
|
|
399
381
|
jobId: number,
|
|
400
382
|
abortSignal: AbortSignal,
|
|
401
383
|
): Promise<Record<string, unknown>> {
|
|
402
|
-
const v = await this.signing.verifySignedJob(
|
|
384
|
+
const v = await this.signing.verifySignedJob(
|
|
385
|
+
jobId,
|
|
386
|
+
minimumDeliveryWindowSeconds(),
|
|
387
|
+
);
|
|
403
388
|
if (!v.ok) {
|
|
404
389
|
return { ok: false, job_id: jobId, skip: v.permanent, reason: v.reason };
|
|
405
390
|
}
|