@bnbagent/studio-cli 0.0.10 → 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.
Files changed (36) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +2 -2
  3. package/dist/_agentcoreName-DZDWEYD3.js +0 -0
  4. package/dist/_twak-4XF4H5PL.js +0 -0
  5. package/dist/bag.js +317 -124
  6. package/dist/chunk-RO726HJG.js +0 -0
  7. package/dist/{chunk-YFEM4564.js → chunk-TTPOH453.js} +79 -37
  8. package/dist/chunk-U7IDQ3K5.js +0 -0
  9. package/dist/{deployCli-NJFCWBSF.js → deployCli-K55GXDVO.js} +1 -1
  10. package/package.json +11 -12
  11. package/recipes/runtimes/agentcore/code/{{PKG}}/Dockerfile.tmpl +20 -21
  12. package/recipes/runtimes/agentcore/code/{{PKG}}/executor.ts.tmpl +36 -0
  13. package/recipes/runtimes/agentcore/code/{{PKG}}/unifiedMain.ts.tmpl +620 -0
  14. package/recipes/runtimes/agentcore/recipe.toml +3 -3
  15. package/recipes/runtimes/azure-foundry/code/{{PKG}}/Dockerfile.tmpl +25 -23
  16. package/recipes/runtimes/azure-foundry/code/{{PKG}}/agentCard.ts.tmpl +16 -12
  17. package/recipes/runtimes/azure-foundry/code/{{PKG}}/executor.ts.tmpl +72 -393
  18. package/recipes/runtimes/azure-foundry/code/{{PKG}}/mcpMain.ts.tmpl +160 -43
  19. package/recipes/runtimes/azure-foundry/code/{{PKG}}/sellerCore.ts.tmpl +504 -0
  20. package/recipes/runtimes/azure-foundry/code/{{PKG}}/unifiedMain.ts.tmpl +620 -0
  21. package/recipes/runtimes/azure-foundry/recipe.toml +19 -11
  22. package/recipes/x402-buyer/code/{{PKG}}/x402Buyer.ts.tmpl +6 -4
  23. package/skills/bnbagent-studio.md +2 -2
  24. package/skills/references/bnbagent-studio-adding-to-project.md +1 -1
  25. package/skills/references/bnbagent-studio-buying-from-bazaar.md +1 -1
  26. package/skills/references/bnbagent-studio-operating.md +4 -4
  27. package/skills/references/bnbagent-studio-scaffolding-agent.md +4 -4
  28. package/skills/references/bnbagent-studio-selling-via-8183.md +3 -3
  29. package/skills/references/bnbagent-studio-selling-via-b402.md +2 -2
  30. package/skills/references/bnbagent-studio-use-aws-agentcore.md +1 -1
  31. package/skills/references/bnbagent-studio-use-azure-foundry.md +3 -3
  32. package/skills/references/bnbagent-studio-use-bnb-trial.md +1 -1
  33. package/skills/references/bnbagent-studio-wiring-llm-tools.md +3 -3
  34. package/recipes/runtimes/agentcore/code/{{PKG}}/main.ts.tmpl +0 -347
  35. package/recipes/runtimes/azure-foundry/code/{{PKG}}/foundryMain.ts.tmpl +0 -422
  36. 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. The MCP
9
- * server is cloud-neutral: a streamable-HTTP MCP server on `0.0.0.0:8000/mcp`
10
- * (port 8000, `AGENT_PORT` overrides locally) with no cloud-vendor serving
11
- * dependency. `bag dev` runs `node mcpMain.js` directly. There is no separate
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
- * An MCP server has no such ack-then-continue hook, so under MCP the
33
- * long-running pattern is **synchronous**: `notify_funded` does the whole
34
- * verify LLM work → submit **inside the single tool invocation**, using
35
- * MCP progress notifications as a heartbeat to keep the connection alive
36
- * across the steps. The runtime is
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 { loadStudioToml } from "@bnbagent/studio-runtime/config";
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
- // ── LLM work hook (lazy: built on first delivery; negotiate never needs it) ──
114
- // Deferred construction keeps the negotiate-only path (and a cold start that
115
- // only quotes) from building the model, and keeps this module importable
116
- // without the provider env until a deliverable is actually produced.
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. You do the actual work once a job is funded. " +
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: 2 commerce tools + the read-only chain tools. */
198
- export function buildMcpServer(): McpServer {
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
- server.registerTool(
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
- server.registerTool(
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
- * (0.0.0.0:8000/mcp; `AGENT_PORT` is the local override). Stateful — one
499
- * transport per `Mcp-Session-Id` — so progress notifications during the
500
- * multi-step `notify_funded` delivery reach the caller.
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 through a Foundry CustomKeys connection before process start;
505
- // these calls materialize it on disk before signing. Each is a no-op for
506
- // the other wallet kind and locally, where it already lives on disk.
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
- app.use(express.json());
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
  });