@bnbagent/studio-cli 0.0.9 → 0.0.11-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 +2 -2
- package/dist/_agentcoreName-DZDWEYD3.js +0 -0
- package/dist/_twak-4XF4H5PL.js +0 -0
- package/dist/bag.js +577 -370
- package/dist/chunk-RO726HJG.js +0 -0
- package/dist/{chunk-YFEM4564.js → chunk-TTPOH453.js} +79 -37
- package/dist/chunk-U7IDQ3K5.js +0 -0
- package/dist/{deployCli-NJFCWBSF.js → deployCli-K55GXDVO.js} +1 -1
- package/package.json +11 -12
- package/recipes/runtimes/agentcore/code/{{PKG}}/Dockerfile.tmpl +20 -21
- package/recipes/runtimes/agentcore/code/{{PKG}}/executor.ts.tmpl +36 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/unifiedMain.ts.tmpl +620 -0
- package/recipes/runtimes/agentcore/recipe.toml +3 -3
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/Dockerfile.tmpl +25 -23
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/agentCard.ts.tmpl +16 -12
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/executor.ts.tmpl +72 -393
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/mcpMain.ts.tmpl +160 -43
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/sellerCore.ts.tmpl +504 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/unifiedMain.ts.tmpl +620 -0
- package/recipes/runtimes/azure-foundry/recipe.toml +19 -11
- package/recipes/x402-buyer/code/{{PKG}}/x402Buyer.ts.tmpl +6 -4
- package/skills/bnbagent-studio.md +2 -2
- package/skills/references/bnbagent-studio-adding-to-project.md +1 -1
- package/skills/references/bnbagent-studio-buying-from-bazaar.md +1 -1
- package/skills/references/bnbagent-studio-operating.md +4 -4
- package/skills/references/bnbagent-studio-scaffolding-agent.md +4 -4
- package/skills/references/bnbagent-studio-selling-via-8183.md +3 -3
- package/skills/references/bnbagent-studio-selling-via-b402.md +2 -2
- package/skills/references/bnbagent-studio-use-aws-agentcore.md +1 -1
- package/skills/references/bnbagent-studio-use-azure-foundry.md +3 -3
- package/skills/references/bnbagent-studio-use-bnb-trial.md +1 -1
- package/skills/references/bnbagent-studio-wiring-llm-tools.md +3 -3
- package/recipes/runtimes/agentcore/code/{{PKG}}/main.ts.tmpl +0 -347
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/foundryMain.ts.tmpl +0 -422
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/main.ts.tmpl +0 -196
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Single MCP seller agent entrypoint (the `--protocol MCP` peer to main.ts).
|
|
3
3
|
*
|
|
4
|
-
* The x402 seller is intentionally dormant on Azure Foundry in v1. Use the
|
|
5
|
-
* platform deployment target for the anonymous HTTP-envelope tunnel.
|
|
6
|
-
*
|
|
7
4
|
* This is the VALUABLE agent AND the SOLE key-holder/signer, serving its
|
|
8
|
-
* seller surface over the **Model Context Protocol** instead of A2A.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* forwarding service — the agent IS the seller.
|
|
13
|
-
*
|
|
14
|
-
* (For the azure-foundry runtime, the DEPLOY host speaks the Invocations
|
|
15
|
-
* protocol — `foundryMain.ts`. This MCP entrypoint is for LOCAL run / dev and
|
|
16
|
-
* the eventual cross-cloud transport.)
|
|
5
|
+
* seller surface over the **Model Context Protocol** instead of A2A. AWS
|
|
6
|
+
* Bedrock AgentCore hosts MCP natively: it expects a streamable-HTTP MCP
|
|
7
|
+
* server on `0.0.0.0:8000/mcp` and wraps the protocol (session isolation,
|
|
8
|
+
* inbound OAuth/Cognito auth, scale-to-zero) exactly as it does for A2A.
|
|
9
|
+
* There is no separate forwarding service — the agent IS the seller.
|
|
17
10
|
*
|
|
18
11
|
* MCP tools (all backed by signing.ts fixed code — NEVER LLM-callable):
|
|
19
12
|
*
|
|
@@ -26,14 +19,16 @@
|
|
|
26
19
|
* + the read-only chain tools (wallet / balances / ERC-8004 / ERC-8183 /
|
|
27
20
|
* block / tx / contract-view), so an MCP client can inspect state.
|
|
28
21
|
*
|
|
29
|
-
* ## How delivery works under MCP (synchronous)
|
|
22
|
+
* ## How delivery works under MCP (synchronous, ≤ ~15 min)
|
|
30
23
|
*
|
|
31
|
-
* A2A acks then finishes the work + on-chain `submit` in a background task
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
24
|
+
* A2A acks then finishes the work + on-chain `submit` in a background task
|
|
25
|
+
* kept alive by reporting `HEALTHY_BUSY` to AgentCore's `/ping`. An MCP
|
|
26
|
+
* server has no such hook (its only liveness is the platform's `/mcp/`
|
|
27
|
+
* probe). So under MCP the documented long-running pattern is
|
|
28
|
+
* **synchronous**: `notify_funded` does the whole verify → LLM work → submit
|
|
29
|
+
* **inside the single tool invocation** (AgentCore allows a synchronous
|
|
30
|
+
* request to run up to ~15 minutes), using MCP progress notifications as a
|
|
31
|
+
* heartbeat to keep the connection alive across the steps. The runtime is
|
|
37
32
|
* therefore **stateful** (per-session `StreamableHTTPServerTransport`,
|
|
38
33
|
* routed by `Mcp-Session-Id`) so progress notifications work. Node's async
|
|
39
34
|
* chain/signing calls never block the event loop, so the platform's
|
|
@@ -51,17 +46,33 @@
|
|
|
51
46
|
*
|
|
52
47
|
* You own this file — specialise the work prompt / dispatch, but keep signing
|
|
53
48
|
* bounded to these two ops and keep the read tools read-only.
|
|
49
|
+
* In dual-face mode this file is imported as a library by dualMain.ts; main()
|
|
50
|
+
* runs only when mcpMain itself is the selected entrypoint.
|
|
54
51
|
*/
|
|
55
52
|
|
|
56
|
-
import { randomUUID } from "node:crypto";
|
|
53
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
57
54
|
import { pathToFileURL } from "node:url";
|
|
58
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
GetSecretValueCommand,
|
|
57
|
+
SecretsManagerClient,
|
|
58
|
+
} from "@aws-sdk/client-secrets-manager";
|
|
59
|
+
import {
|
|
60
|
+
loadStudioToml,
|
|
61
|
+
type TomlTable,
|
|
62
|
+
} from "@bnbagent/studio-runtime/config";
|
|
59
63
|
import * as cr from "@bnbagent/studio-runtime/tools";
|
|
60
64
|
import {
|
|
61
65
|
ensureAltanaSessionLoaded,
|
|
62
66
|
ensureKeystoreMaterialized,
|
|
63
67
|
ensureTwakMaterialized,
|
|
68
|
+
getWallet,
|
|
64
69
|
} from "@bnbagent/studio-runtime/wallet";
|
|
70
|
+
import {
|
|
71
|
+
createEnvelopeMiddleware,
|
|
72
|
+
X402_SELL_PATH,
|
|
73
|
+
type X402HttpRequest,
|
|
74
|
+
X402Seller,
|
|
75
|
+
} from "@bnbagent/studio-runtime/x402";
|
|
65
76
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
66
77
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
67
78
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -77,6 +88,39 @@ const log = {
|
|
|
77
88
|
console.error(`[seller-agent.mcp] ERROR ${msg}`, e ?? ""),
|
|
78
89
|
};
|
|
79
90
|
|
|
91
|
+
// ── Runtime secrets ───────────────────────────────────────────────────────────
|
|
92
|
+
// Keep plaintext secrets OUT of agentcore.json. When BNBAGENT_RUNTIME_SECRET_ID
|
|
93
|
+
// is set (deployed runtime), pull a JSON {ENV_NAME: value} blob from AWS
|
|
94
|
+
// Secrets Manager into the process env BEFORE anything reads it. No-op
|
|
95
|
+
// locally. In a deployed runtime the managed secret bundle is authoritative
|
|
96
|
+
// and replaces any stale spec-level value left by an earlier revision.
|
|
97
|
+
async function loadRuntimeSecrets(): Promise<void> {
|
|
98
|
+
const secretId = process.env.BNBAGENT_RUNTIME_SECRET_ID;
|
|
99
|
+
if (!secretId) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const resp = await new SecretsManagerClient({}).send(
|
|
103
|
+
new GetSecretValueCommand({ SecretId: secretId }),
|
|
104
|
+
);
|
|
105
|
+
const bundle = JSON.parse(resp.SecretString ?? "{}") as Record<
|
|
106
|
+
string,
|
|
107
|
+
unknown
|
|
108
|
+
>;
|
|
109
|
+
for (const [key, value] of Object.entries(bundle)) {
|
|
110
|
+
process.env[key] = String(value);
|
|
111
|
+
}
|
|
112
|
+
const pieverseKey = process.env.PIEVERSE_LLM_API_KEY;
|
|
113
|
+
if (pieverseKey) {
|
|
114
|
+
const fingerprint = createHash("sha256")
|
|
115
|
+
.update(pieverseKey, "utf-8")
|
|
116
|
+
.digest("hex")
|
|
117
|
+
.slice(0, 12);
|
|
118
|
+
log.info(
|
|
119
|
+
`runtime secret PIEVERSE_LLM_API_KEY source=secretsmanager sha256=${fingerprint}…`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
80
124
|
/**
|
|
81
125
|
* The project-wide default network (`[network].default`) — tool calls that
|
|
82
126
|
* omit `network` fall back to it, never to a hardcoded name.
|
|
@@ -110,10 +154,40 @@ function generatorTag(): string {
|
|
|
110
154
|
: name || APP_NAME;
|
|
111
155
|
}
|
|
112
156
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
157
|
+
function hasErc8183Rail(cfg: TomlTable): boolean {
|
|
158
|
+
const payments = asTable(cfg.payments);
|
|
159
|
+
return asTable(payments?.erc8183) !== null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function asTable(value: unknown): TomlTable | null {
|
|
163
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
164
|
+
? (value as TomlTable)
|
|
165
|
+
: null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function flatHeaders(
|
|
169
|
+
headers: Record<string, string | string[] | undefined>,
|
|
170
|
+
): Record<string, string> {
|
|
171
|
+
const out: Record<string, string> = {};
|
|
172
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
173
|
+
if (typeof value === "string") out[name] = value;
|
|
174
|
+
else if (value !== undefined) out[name] = value[0] ?? "";
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function flatQuery(query: Record<string, unknown>): Record<string, string> {
|
|
180
|
+
const out: Record<string, string> = {};
|
|
181
|
+
for (const [name, value] of Object.entries(query)) {
|
|
182
|
+
if (typeof value === "string") out[name] = value;
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── LLM work hook (lazy: built on first authorized task) ─────────────────────
|
|
188
|
+
// Deferred construction keeps negotiate and unpaid x402 quote paths from
|
|
189
|
+
// building the model, and keeps this module importable without the provider
|
|
190
|
+
// env until a deliverable is actually produced.
|
|
117
191
|
type RunLlm = (prompt: string) => Promise<string>;
|
|
118
192
|
let cachedRunLlm: RunLlm | null = null;
|
|
119
193
|
|
|
@@ -126,7 +200,9 @@ async function runLlm(prompt: string): Promise<string> {
|
|
|
126
200
|
const result = await generateText({
|
|
127
201
|
model,
|
|
128
202
|
system:
|
|
129
|
-
"You are a seller agent.
|
|
203
|
+
"You are a seller agent. The runtime has already authorized this task " +
|
|
204
|
+
"through its configured commerce rail. Complete the user's task now; " +
|
|
205
|
+
"do not ask for a job ID or additional payment. " +
|
|
130
206
|
"Be concrete and concise. Use the read-only chain tools when on-chain " +
|
|
131
207
|
"context helps. If a paid-data tool such as `buy_with_x402` is " +
|
|
132
208
|
"available to you, USE IT to fetch the data a task needs — those " +
|
|
@@ -194,12 +270,15 @@ async function reportProgress(
|
|
|
194
270
|
});
|
|
195
271
|
}
|
|
196
272
|
|
|
197
|
-
/** Build the seller MCP server
|
|
198
|
-
export function buildMcpServer(
|
|
273
|
+
/** Build the seller MCP server, gating commerce tools on the ERC-8183 rail. */
|
|
274
|
+
export function buildMcpServer(
|
|
275
|
+
opts: { commerceSkills?: boolean } = {},
|
|
276
|
+
): McpServer {
|
|
199
277
|
const server = new McpServer({ name: "bnbagent-seller", version: "1.0.0" });
|
|
200
278
|
|
|
201
279
|
// ── Commerce tools (signing is FIXED code in signing.ts) ──────────────────
|
|
202
|
-
|
|
280
|
+
if (opts.commerceSkills !== false) {
|
|
281
|
+
server.registerTool(
|
|
203
282
|
"negotiate",
|
|
204
283
|
{
|
|
205
284
|
description:
|
|
@@ -228,9 +307,9 @@ export function buildMcpServer(): McpServer {
|
|
|
228
307
|
const clamped = signing.clampPrice(signing.listPrice());
|
|
229
308
|
return toolResult(await signing.signQuote(request, clamped));
|
|
230
309
|
},
|
|
231
|
-
|
|
310
|
+
);
|
|
232
311
|
|
|
233
|
-
|
|
312
|
+
server.registerTool(
|
|
234
313
|
"notify_funded",
|
|
235
314
|
{
|
|
236
315
|
description:
|
|
@@ -337,7 +416,8 @@ export function buildMcpServer(): McpServer {
|
|
|
337
416
|
deliverable_url: res.deliverableUrl,
|
|
338
417
|
});
|
|
339
418
|
},
|
|
340
|
-
|
|
419
|
+
);
|
|
420
|
+
}
|
|
341
421
|
|
|
342
422
|
// ── Read-only chain tools ──────────────────────────────────────────────────
|
|
343
423
|
const network = z.string().optional().describe("studio network name");
|
|
@@ -494,22 +574,61 @@ export function buildMcpServer(): McpServer {
|
|
|
494
574
|
// ── serving ───────────────────────────────────────────────────────────────────
|
|
495
575
|
|
|
496
576
|
/**
|
|
497
|
-
* Serve the MCP server as **stateful** streamable-HTTP on `/mcp`
|
|
498
|
-
*
|
|
499
|
-
* transport per `Mcp-Session-Id` — so progress
|
|
500
|
-
* multi-step `notify_funded` delivery reach the
|
|
577
|
+
* Serve the MCP server as **stateful** streamable-HTTP on `/mcp` (the
|
|
578
|
+
* AgentCore MCP contract: 0.0.0.0:8000/mcp; `AGENT_PORT` is the local
|
|
579
|
+
* override). Stateful — one transport per `Mcp-Session-Id` — so progress
|
|
580
|
+
* notifications during the multi-step `notify_funded` delivery reach the
|
|
581
|
+
* caller; AgentCore routes the session to one microVM via the same header.
|
|
501
582
|
*/
|
|
502
583
|
async function main(): Promise<void> {
|
|
584
|
+
await loadRuntimeSecrets();
|
|
585
|
+
|
|
503
586
|
// Wallet material is NEVER bundled into the deploy artifact. `bag deploy`
|
|
504
|
-
// injects it
|
|
505
|
-
//
|
|
506
|
-
//
|
|
587
|
+
// injects it via Secrets Manager and these calls (run once at cold start,
|
|
588
|
+
// before any signing) materialize it on disk. Each is a no-op for the
|
|
589
|
+
// other wallet kind and locally, where the wallet already lives on disk.
|
|
507
590
|
ensureKeystoreMaterialized();
|
|
508
591
|
ensureTwakMaterialized();
|
|
509
592
|
await ensureAltanaSessionLoaded();
|
|
510
593
|
|
|
594
|
+
const cfg = loadStudioToml();
|
|
595
|
+
const rails = { erc8183: hasErc8183Rail(cfg) };
|
|
596
|
+
const host = process.env.AGENT_BIND_HOST || "0.0.0.0";
|
|
597
|
+
const port = Number(process.env.AGENT_PORT || "8000");
|
|
598
|
+
const seller = await X402Seller.create({
|
|
599
|
+
cfg,
|
|
600
|
+
runWork: ({ prompt }) => runLlm(prompt),
|
|
601
|
+
walletAddress: getWallet().address,
|
|
602
|
+
resourceUrl: `${
|
|
603
|
+
process.env.AGENTCORE_RUNTIME_URL ?? `http://localhost:${port}`
|
|
604
|
+
}${X402_SELL_PATH}`,
|
|
605
|
+
});
|
|
606
|
+
|
|
511
607
|
const app = express();
|
|
512
|
-
|
|
608
|
+
|
|
609
|
+
if (seller.state !== "disabled") {
|
|
610
|
+
app.all(
|
|
611
|
+
X402_SELL_PATH,
|
|
612
|
+
express.text({ type: "*/*", limit: "1mb" }),
|
|
613
|
+
async (req, res) => {
|
|
614
|
+
const request: X402HttpRequest = {
|
|
615
|
+
method: req.method,
|
|
616
|
+
path: req.path,
|
|
617
|
+
query: flatQuery(req.query),
|
|
618
|
+
headers: flatHeaders(req.headers),
|
|
619
|
+
body:
|
|
620
|
+
typeof req.body === "string"
|
|
621
|
+
? req.body
|
|
622
|
+
: JSON.stringify(req.body ?? ""),
|
|
623
|
+
};
|
|
624
|
+
const out = await seller.handle(request);
|
|
625
|
+
res.status(out.status).set(out.headers).send(out.body);
|
|
626
|
+
},
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
app.use(express.json({ limit: "8mb" }));
|
|
631
|
+
app.use(createEnvelopeMiddleware({ port }));
|
|
513
632
|
|
|
514
633
|
const transports: Record<string, StreamableHTTPServerTransport> = {};
|
|
515
634
|
|
|
@@ -537,14 +656,12 @@ async function main(): Promise<void> {
|
|
|
537
656
|
delete transports[t.sessionId];
|
|
538
657
|
}
|
|
539
658
|
};
|
|
540
|
-
await buildMcpServer().connect(t);
|
|
659
|
+
await buildMcpServer({ commerceSkills: rails.erc8183 }).connect(t);
|
|
541
660
|
transport = t;
|
|
542
661
|
}
|
|
543
662
|
await transport.handleRequest(req, res, req.body);
|
|
544
663
|
});
|
|
545
664
|
|
|
546
|
-
const host = process.env.AGENT_BIND_HOST || "0.0.0.0";
|
|
547
|
-
const port = Number(process.env.AGENT_PORT || "8000");
|
|
548
665
|
app.listen(port, host, () => {
|
|
549
666
|
log.info(`MCP serving on ${host}:${port}/mcp`);
|
|
550
667
|
});
|