@bnbagent/studio-cli 0.0.6-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 (55) hide show
  1. package/DISCLAIMER.md +48 -0
  2. package/LICENSE +201 -0
  3. package/dist/_agentcoreName-DZDWEYD3.js +7 -0
  4. package/dist/_twak-5XQMOFUC.js +25 -0
  5. package/dist/bag.js +19358 -0
  6. package/dist/chunk-7RAKL4AS.js +172 -0
  7. package/dist/chunk-M3ODFCA7.js +1053 -0
  8. package/dist/chunk-U7IDQ3K5.js +14 -0
  9. package/dist/deployCli-N6TPN6XA.js +40 -0
  10. package/package.json +64 -0
  11. package/recipes/agent/code/{{PKG}}/signing.ts.tmpl +287 -0
  12. package/recipes/agent/recipe.toml +35 -0
  13. package/recipes/providers/pieverse-llm/recipe.toml +16 -0
  14. package/recipes/providers/pieverse-llm/skills/funding-pieverse-llm.md +203 -0
  15. package/recipes/runtimes/agentcore/code/{{PKG}}/.dockerignore.tmpl +8 -0
  16. package/recipes/runtimes/agentcore/code/{{PKG}}/Dockerfile.tmpl +50 -0
  17. package/recipes/runtimes/agentcore/code/{{PKG}}/agentCard.ts.tmpl +135 -0
  18. package/recipes/runtimes/agentcore/code/{{PKG}}/dualMain.ts.tmpl +402 -0
  19. package/recipes/runtimes/agentcore/code/{{PKG}}/executor.ts.tmpl +147 -0
  20. package/recipes/runtimes/agentcore/code/{{PKG}}/main.ts.tmpl +344 -0
  21. package/recipes/runtimes/agentcore/code/{{PKG}}/mcpMain.ts.tmpl +677 -0
  22. package/recipes/runtimes/agentcore/code/{{PKG}}/model.ts.tmpl +117 -0
  23. package/recipes/runtimes/agentcore/code/{{PKG}}/sellerCore.ts.tmpl +503 -0
  24. package/recipes/runtimes/agentcore/code/{{PKG}}/tools.ts.tmpl +157 -0
  25. package/recipes/runtimes/agentcore/recipe.toml +97 -0
  26. package/recipes/runtimes/azure-foundry/code/{{PKG}}/.dockerignore.tmpl +8 -0
  27. package/recipes/runtimes/azure-foundry/code/{{PKG}}/Dockerfile.tmpl +47 -0
  28. package/recipes/runtimes/azure-foundry/code/{{PKG}}/agentCard.ts.tmpl +131 -0
  29. package/recipes/runtimes/azure-foundry/code/{{PKG}}/executor.ts.tmpl +504 -0
  30. package/recipes/runtimes/azure-foundry/code/{{PKG}}/foundryMain.ts.tmpl +300 -0
  31. package/recipes/runtimes/azure-foundry/code/{{PKG}}/main.ts.tmpl +196 -0
  32. package/recipes/runtimes/azure-foundry/code/{{PKG}}/mcpMain.ts.tmpl +562 -0
  33. package/recipes/runtimes/azure-foundry/code/{{PKG}}/model.ts.tmpl +117 -0
  34. package/recipes/runtimes/azure-foundry/code/{{PKG}}/tools.ts.tmpl +157 -0
  35. package/recipes/runtimes/azure-foundry/recipe.toml +88 -0
  36. package/recipes/tools-chain/code/{{PKG}}/chainTools.ts.tmpl +166 -0
  37. package/recipes/tools-chain/recipe.toml +11 -0
  38. package/recipes/wallet/recipe.toml +20 -0
  39. package/recipes/x402-buyer/code/{{PKG}}/x402Buyer.ts.tmpl +175 -0
  40. package/recipes/x402-buyer/recipe.toml +15 -0
  41. package/skills/bnbagent-studio.md +107 -0
  42. package/skills/references/bnbagent-studio-adding-to-project.md +241 -0
  43. package/skills/references/bnbagent-studio-buying-from-bazaar.md +169 -0
  44. package/skills/references/bnbagent-studio-buying-via-8183.md +222 -0
  45. package/skills/references/bnbagent-studio-extending-signing.md +227 -0
  46. package/skills/references/bnbagent-studio-operating.md +211 -0
  47. package/skills/references/bnbagent-studio-scaffolding-agent.md +536 -0
  48. package/skills/references/bnbagent-studio-selling-via-8183.md +271 -0
  49. package/skills/references/bnbagent-studio-selling-via-b402.md +194 -0
  50. package/skills/references/bnbagent-studio-use-aws-agentcore.md +208 -0
  51. package/skills/references/bnbagent-studio-use-azure-foundry.md +164 -0
  52. package/skills/references/bnbagent-studio-use-bnb-trial.md +92 -0
  53. package/skills/references/bnbagent-studio-using-altana-wallet.md +68 -0
  54. package/skills/references/bnbagent-studio-using-twak-wallet.md +260 -0
  55. package/skills/references/bnbagent-studio-wiring-llm-tools.md +338 -0
@@ -0,0 +1,147 @@
1
+ /**
2
+ * A2A executor — the seller agent's outward A2A surface (two fixed-code
3
+ * skills).
4
+ *
5
+ * The agent serves A2A directly (an `@a2a-js/sdk` express app on the
6
+ * AgentCore A2A runtime contract). This module is ONLY the a2a wire:
7
+ * {@link SellerAgentExecutor} inherits all of the seller logic +
8
+ * background-delivery machinery from `sellerCore.ts` `SellerCore` (which
9
+ * imports nothing from `@a2a-js/sdk`) and adds the a2a-specific
10
+ * {@link SellerAgentExecutor.execute} / {@link SellerAgentExecutor.cancelTask}
11
+ * entrypoints plus the request/response wire helpers. `execute` reads the
12
+ * inbound message's data part and dispatches on its `skill`:
13
+ *
14
+ * negotiate → `SellerCore.negotiate` (rule-based price clamp + EIP-191 sign)
15
+ * notify_funded → `SellerCore.notifyFunded` (fast on-chain gate) → ACK at
16
+ * once, then in the BACKGROUND: LLM work → `signing.submitResult`
17
+ *
18
+ * `notify_funded` is the buyer's "I funded job X — please deliver"
19
+ * notification. Because the work takes time, the executor does NOT block the
20
+ * caller: the core verifies the funded job synchronously (a couple of
21
+ * eth_calls) to ACK accepted/rejected, then runs the slow LLM work + on-chain
22
+ * `submit` in a background task and replies immediately. The buyer reads the
23
+ * deliverable back from the CHAIN (SUBMITTED / `getDeliverableUrl`) — the
24
+ * chain is the source of truth. While any background delivery is in flight
25
+ * `isBusy` (from `SellerCore`) reports busy, which `main.ts` feeds to
26
+ * AgentCore's `/ping` as `HEALTHY_BUSY` so the scale-to-zero runtime stays
27
+ * warm until the work lands (within the session max-lifetime).
28
+ *
29
+ * ALL signing is FIXED code in `signing.ts` — NEVER an LLM-callable tool
30
+ * (money is never in the LLM; the LLM only produces the work text, via the
31
+ * `runWork` hook). See `sellerCore.ts` for the negotiate / notifyFunded /
32
+ * sweep logic.
33
+ *
34
+ * You own this file — specialise the work hook / dispatch in `sellerCore.ts`,
35
+ * but keep signing OUT of the LLM tool list.
36
+ */
37
+
38
+ import { randomUUID } from "node:crypto";
39
+ import type { DataPart, Message } from "@a2a-js/sdk";
40
+ import {
41
+ A2AError,
42
+ type AgentExecutor,
43
+ type ExecutionEventBus,
44
+ type RequestContext,
45
+ } from "@a2a-js/sdk/server";
46
+ import { SellerCore } from "./sellerCore.js";
47
+
48
+ const log = {
49
+ error: (msg: string, e?: unknown) =>
50
+ console.error(`[seller-agent.a2a] ERROR ${msg}`, e ?? ""),
51
+ };
52
+
53
+ /**
54
+ * ERC-8183 seller A2A executor: the a2a wire over `SellerCore`.
55
+ *
56
+ * All seller logic (negotiate, notifyFunded, background delivery, `isBusy`,
57
+ * the constructor bookkeeping, the `runWork` hook) lives in
58
+ * `sellerCore.ts` `SellerCore`; this class adds only the A2A entrypoints and
59
+ * request/response wire helpers.
60
+ *
61
+ * The agent exposes ONLY the two paid, structured skills — there is no
62
+ * free-form chat skill. A plain text message (no `{"skill": ...}` DataPart)
63
+ * is rejected: negotiate / notify_funded always need a structured DataPart,
64
+ * so prose never triggers an LLM call or a paid action.
65
+ */
66
+ export class SellerAgentExecutor extends SellerCore implements AgentExecutor {
67
+ // ── A2A entrypoints ───────────────────────────────────────────────────────
68
+ execute = async (
69
+ context: RequestContext,
70
+ eventBus: ExecutionEventBus,
71
+ ): Promise<void> => {
72
+ const data = inbound(context);
73
+ const skill = data.skill;
74
+ let result: Record<string, unknown>;
75
+ try {
76
+ if (skill === "negotiate") {
77
+ result = await this.negotiate(data);
78
+ } else if (skill === "notify_funded") {
79
+ result = await this.notifyFunded(data);
80
+ } else {
81
+ // Includes a plain text message (no DataPart → skill is undefined):
82
+ // the seller has no free-form skill, so prose is rejected here.
83
+ result = {
84
+ error: `unknown skill: ${JSON.stringify(skill)}`,
85
+ skills: this.skills(),
86
+ };
87
+ if (skill === undefined) {
88
+ // Most common cause: the caller put the JSON envelope in a
89
+ // "text" part. Structured skill calls must ride in a DataPart.
90
+ result.hint =
91
+ 'send the skill envelope as an A2A data part: parts:[{"kind":"data","data":{"skill":"negotiate",...}}]';
92
+ }
93
+ }
94
+ } catch (e) {
95
+ // A genuine internal fault is surfaced as a JSON-RPC error, NOT masked
96
+ // as a successful result. Throwing `A2AError.internalError` is caught
97
+ // by @a2a-js/sdk's request handler and serialized to a proper -32603
98
+ // carrying the request id. CLASSIFIED business outcomes are
99
+ // returned as a result above (peer of the MCP runtime: faults →
100
+ // isError, business outcomes → result).
101
+ log.error(`skill ${JSON.stringify(skill)} failed`, e);
102
+ const name = e instanceof Error ? e.constructor.name : "Error";
103
+ const msg = e instanceof Error ? e.message : String(e);
104
+ throw A2AError.internalError(`${name}: ${msg}`);
105
+ }
106
+ reply(eventBus, context, result);
107
+ };
108
+
109
+ cancelTask = async (
110
+ _taskId: string,
111
+ _eventBus: ExecutionEventBus,
112
+ ): Promise<void> => {
113
+ // negotiate is synchronous; notify_funded acks then delivers on-chain in
114
+ // the background — once submitted it is anchored on-chain and cannot be
115
+ // cancelled via A2A. Nothing to cancel here. (@a2a-js/sdk hands cancel
116
+ // only a taskId — no message to reply to — so this surfaces as the
117
+ // standard JSON-RPC unsupported-operation error.)
118
+ throw A2AError.unsupportedOperation("cancel");
119
+ };
120
+ }
121
+
122
+ // ── wire helpers ──────────────────────────────────────────────────────────────
123
+
124
+ function inbound(context: RequestContext): Record<string, unknown> {
125
+ const parts = context.userMessage?.parts ?? [];
126
+ const dataPart = parts.find((p): p is DataPart => p.kind === "data");
127
+ return dataPart?.data ?? {};
128
+ }
129
+
130
+ function reply(
131
+ eventBus: ExecutionEventBus,
132
+ context: RequestContext,
133
+ data: Record<string, unknown>,
134
+ ): void {
135
+ const message: Message = {
136
+ kind: "message",
137
+ role: "agent",
138
+ messageId: randomUUID(),
139
+ parts: [{ kind: "data", data }],
140
+ contextId: context.contextId,
141
+ taskId: context.taskId,
142
+ };
143
+ // publish + finished() — without finished() the event stream never closes
144
+ // and the caller hangs.
145
+ eventBus.publish(message);
146
+ eventBus.finished();
147
+ }
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Single A2A seller agent entrypoint. Generated by `bag recipe code agent`.
3
+ *
4
+ * This is the VALUABLE agent AND the SOLE key-holder/signer. It serves its
5
+ * two ERC-8183 seller skills DIRECTLY over the A2A protocol on AWS Bedrock
6
+ * AgentCore (`--protocol A2A`): an `@a2a-js/sdk` express app exposes the
7
+ * agent card at `/.well-known/agent-card.json` + JSON-RPC `message/send` on
8
+ * `0.0.0.0:9000` (`AGENT_PORT` overrides locally), plus `GET /ping` for the
9
+ * AgentCore liveness contract. There is no separate forwarding service — the
10
+ * agent IS the seller (see executor.ts + agentCard.ts).
11
+ *
12
+ * A2A skills (executor.ts):
13
+ *
14
+ * negotiate → read the FIXED list price → CLAMP to [min,max] → EIP-191 SIGN
15
+ * the offer (no LLM, no tools) → return the signed offer (or reject)
16
+ * notify_funded → re-verify the funded job on-chain (fast) → ACK accepted at once,
17
+ * then in the BACKGROUND: LLM work → manifest → storage →
18
+ * submitResult (SIGN + broadcast). The buyer polls the chain for
19
+ * the deliverable. Each notify also sweeps other FUNDED jobs
20
+ * (buyer-push fallback). While background work is in flight the
21
+ * `/ping` handler reports HEALTHY_BUSY so AgentCore keeps the
22
+ * scale-to-zero runtime warm until it lands.
23
+ *
24
+ * Buyers reach this endpoint with an OAuth2 (Cognito) bearer — AgentCore A2A
25
+ * mandates inbound auth (see agentCard.ts + `bag deploy provision-cognito`).
26
+ *
27
+ * ## Boundaries (do NOT cross — they are the whole point)
28
+ *
29
+ * - The agent does ALL deterministic SIGNING (quote-sign + submit + settle +
30
+ * automatic Pieverse LLM-credit auto-renew). ALL signing is FIXED code in
31
+ * `signing.ts` — NEVER an LLM-callable tool (money never in the LLM).
32
+ * - The price is a FIXED list price from studio.toml (clamped before
33
+ * signing) — the LLM never prices; it only PRODUCES the work text in the
34
+ * delivery step.
35
+ * - Chain access for the LLM is READ-ONLY tools only (`tools.ts`).
36
+ * - `settle` (claim payment after the dispute window) is operator-driven —
37
+ * run `bag erc8183 settle <job_id>`; it is deliberately NOT an A2A skill.
38
+ */
39
+
40
+ import { createHash } from "node:crypto";
41
+ import { pathToFileURL } from "node:url";
42
+ import {
43
+ GetSecretValueCommand,
44
+ SecretsManagerClient,
45
+ } from "@aws-sdk/client-secrets-manager";
46
+ import { DefaultRequestHandler, InMemoryTaskStore } from "@a2a-js/sdk/server";
47
+ import {
48
+ agentCardHandler,
49
+ jsonRpcHandler,
50
+ UserBuilder,
51
+ } from "@a2a-js/sdk/server/express";
52
+ import {
53
+ loadStudioToml,
54
+ type TomlTable,
55
+ } from "@bnbagent/studio-runtime/config";
56
+ import {
57
+ ensureAltanaSessionLoaded,
58
+ ensureKeystoreMaterialized,
59
+ ensureTwakMaterialized,
60
+ getWallet,
61
+ } from "@bnbagent/studio-runtime/wallet";
62
+ import {
63
+ createEnvelopeMiddleware,
64
+ X402_SELL_PATH,
65
+ type X402HttpRequest,
66
+ type X402RunWork,
67
+ X402Seller,
68
+ } from "@bnbagent/studio-runtime/x402";
69
+ import { generateText, stepCountIs } from "ai";
70
+ import express from "express";
71
+ import { buildAgentCard } from "./agentCard.js";
72
+ import { SellerAgentExecutor } from "./executor.js";
73
+ import { buildModel } from "./model.js";
74
+ import type { RunWork } from "./sellerCore.js";
75
+ import { LLM_READ_TOOLS } from "./tools.js";
76
+
77
+ const APP_NAME = "agent";
78
+
79
+ /**
80
+ * Deliverable `generator` label: this seller's own name, read from
81
+ * studio.toml `[project].name` (minus the `-agent` suffix) so each delivered
82
+ * manifest is self-identifying. Best-effort — falls back to `APP_NAME` if
83
+ * the config can't be read.
84
+ */
85
+ function generatorTag(): string {
86
+ let name = "";
87
+ try {
88
+ const cfg = loadStudioToml();
89
+ name = String(((cfg.project ?? {}) as Record<string, unknown>).name ?? "");
90
+ } catch {
91
+ // a metadata label must never break delivery
92
+ return APP_NAME;
93
+ }
94
+ return name.endsWith("-agent")
95
+ ? name.slice(0, -"-agent".length)
96
+ : name || APP_NAME;
97
+ }
98
+
99
+ // ── Runtime secrets ───────────────────────────────────────────────────────────
100
+ // Keep plaintext secrets OUT of agentcore.json. When BNBAGENT_RUNTIME_SECRET_ID
101
+ // is set (deployed runtime), pull a JSON {ENV_NAME: value} blob from AWS
102
+ // Secrets Manager into the process env BEFORE anything reads it (keystore
103
+ // unlock, provider key, buildModel, Cognito OAuth env). No-op locally, where
104
+ // .env.local already populated the environment. In a deployed runtime the
105
+ // managed secret bundle is authoritative and replaces any stale spec-level
106
+ // value left by an earlier runtime revision.
107
+ async function loadRuntimeSecrets(): Promise<void> {
108
+ const secretId = process.env.BNBAGENT_RUNTIME_SECRET_ID;
109
+ if (!secretId) {
110
+ return;
111
+ }
112
+ const resp = await new SecretsManagerClient({}).send(
113
+ new GetSecretValueCommand({ SecretId: secretId }),
114
+ );
115
+ const bundle = JSON.parse(resp.SecretString ?? "{}") as Record<
116
+ string,
117
+ unknown
118
+ >;
119
+ for (const [key, value] of Object.entries(bundle)) {
120
+ process.env[key] = String(value);
121
+ }
122
+ const pieverseKey = process.env.PIEVERSE_LLM_API_KEY;
123
+ if (pieverseKey) {
124
+ const fingerprint = createHash("sha256")
125
+ .update(pieverseKey, "utf-8")
126
+ .digest("hex")
127
+ .slice(0, 12);
128
+ console.info(
129
+ `[runtime-secrets] PIEVERSE_LLM_API_KEY source=secretsmanager sha256=${fingerprint}…`,
130
+ );
131
+ }
132
+ }
133
+
134
+ /** studio.toml `[network].default` (best-effort; used by the funded sweep). */
135
+ function defaultNetwork(): string {
136
+ try {
137
+ const cfg = loadStudioToml();
138
+ return String(
139
+ ((cfg.network ?? {}) as Record<string, unknown>).default ?? "bsc-testnet",
140
+ );
141
+ } catch {
142
+ return "bsc-testnet";
143
+ }
144
+ }
145
+
146
+ // ── One-shot LLM helper (the executor's delivery work hook) ──────────────────
147
+ // LLM credit auto-renew (Pieverse path): `buildModel()` (in model.ts) returns
148
+ // a model wrapped with a middleware that auto-tops up the active Pieverse key
149
+ // before each generate call when [llm.auto_renew] is enabled. That top-up is
150
+ // the ONLY automatic signing path outside signing.ts — it is budget-gated and
151
+ // is NOT an LLM tool. It rides transparently into the delivery step.
152
+ //
153
+ // The LLM runs ONLY in the delivery step (the value hook). `negotiate` is
154
+ // rule-based and never touches the LLM. The read-only chain tools are
155
+ // attached so the work can read on-chain context if it needs to — drop them
156
+ // from `tools.ts` if your work doesn't read chain. Signing / settle are NEVER
157
+ // tools — they are fixed code in signing.ts, triggered by the A2A skills,
158
+ // never callable by the LLM. (The one deliberate exception: the x402-buyer
159
+ // recipe's PAID fetch tools — see the `tools:` note below — the LLM picks the
160
+ // URL, but who gets paid and the per-call/daily caps stay locked in
161
+ // studio.toml.)
162
+ export function buildRunWork(): RunWork {
163
+ // The model is resolved LAZILY on first delivery, not at boot: a seller
164
+ // with no provider key yet must still serve negotiate (which never calls
165
+ // the LLM) — missing-key errors surface at notify_funded delivery time.
166
+ let model: ReturnType<typeof buildModel> | undefined;
167
+ return async (prompt, { abortSignal }) => {
168
+ model ??= buildModel(); // managed model with the auto-renew hook (delivery only)
169
+ const result = await generateText({
170
+ model,
171
+ system:
172
+ "You are a seller agent. You do the actual work once a job is funded. " +
173
+ "Be concrete and concise. Use the read-only chain tools when on-chain " +
174
+ "context helps. If a paid-data tool such as `buy_with_x402` is available " +
175
+ "to you, USE IT to fetch the data a task needs — those merchants (e.g. " +
176
+ "CoinMarketCap) charge via on-chain wallet payment, NOT an API key; never " +
177
+ "reply that you cannot complete the task for lack of an API key.",
178
+ prompt,
179
+ // LLM_READ_TOOLS = read-only chain tools (wallet, balances,
180
+ // ERC-8004/8183 queries). Edit `tools.ts` to add/remove. These are
181
+ // READ-ONLY — the agent never signs via a tool; all signing is in
182
+ // signing.ts (fixed code).
183
+ // To let the agent BUY paid data at work time (e.g. CMC market data
184
+ // after `bag x402 trust cmc` + `bag recipe code x402-buyer`), spread
185
+ // the emitted tool set — payee + per-call/daily caps stay locked in
186
+ // studio.toml:
187
+ // import { X402_BUYER_TOOLS } from "./x402Buyer.js";
188
+ // tools: { ...LLM_READ_TOOLS, ...X402_BUYER_TOOLS },
189
+ tools: LLM_READ_TOOLS,
190
+ stopWhen: stepCountIs(8), // bounded tool-call loop, then final text
191
+ abortSignal,
192
+ });
193
+ return result.text.trim();
194
+ };
195
+ }
196
+
197
+ function hasErc8183Rail(cfg: TomlTable): boolean {
198
+ const payments = asTable(cfg.payments);
199
+ return asTable(payments?.erc8183) !== null;
200
+ }
201
+
202
+ function asTable(value: unknown): TomlTable | null {
203
+ return value !== null && typeof value === "object" && !Array.isArray(value)
204
+ ? (value as TomlTable)
205
+ : null;
206
+ }
207
+
208
+ function flatHeaders(
209
+ headers: Record<string, string | string[] | undefined>,
210
+ ): Record<string, string> {
211
+ const out: Record<string, string> = {};
212
+ for (const [name, value] of Object.entries(headers)) {
213
+ if (typeof value === "string") out[name] = value;
214
+ else if (value !== undefined) out[name] = value[0] ?? "";
215
+ }
216
+ return out;
217
+ }
218
+
219
+ function flatQuery(query: Record<string, unknown>): Record<string, string> {
220
+ const out: Record<string, string> = {};
221
+ for (const [name, value] of Object.entries(query)) {
222
+ if (typeof value === "string") out[name] = value;
223
+ }
224
+ return out;
225
+ }
226
+
227
+ function x402Work(runWork: RunWork): X402RunWork {
228
+ return ({ prompt }) => runWork(prompt, { sessionId: "x402" });
229
+ }
230
+
231
+ // ── serving ───────────────────────────────────────────────────────────────────
232
+
233
+ async function main(): Promise<void> {
234
+ await loadRuntimeSecrets();
235
+
236
+ // Wallet material is NEVER bundled into the deploy artifact. `bag deploy`
237
+ // injects it via Secrets Manager and these calls (run once at cold start,
238
+ // before any signing) materialize it on disk. Each is a no-op for the
239
+ // other wallet kind and locally, where the wallet already lives on disk:
240
+ // - evm-local: WALLET_KEYSTORE_JSON → keystore file (unlocked with WALLET_PASSWORD)
241
+ // - twak: TWAK_WALLET_JSON / TWAK_CREDENTIALS_JSON → $TMPDIR/twak-home/.twak
242
+ // (exported as TWAK_HOME_DIR; twak reads TWAK_WALLET_PASSWORD itself)
243
+ ensureKeystoreMaterialized();
244
+ ensureTwakMaterialized();
245
+ await ensureAltanaSessionLoaded();
246
+
247
+ const cfg = loadStudioToml();
248
+ const rails = { erc8183: hasErc8183Rail(cfg) };
249
+ const host = process.env.AGENT_BIND_HOST || "0.0.0.0";
250
+ const port = Number(process.env.AGENT_PORT || "9000");
251
+ const runWork = buildRunWork();
252
+
253
+ // The executor backs the seller skills with signing.ts fixed code (NEVER an
254
+ // LLM tool). The express app hosts the agent card + JSON-RPC message/send
255
+ // on 0.0.0.0:9000 and GET /ping for AgentCore's liveness probe.
256
+ const executor = new SellerAgentExecutor({
257
+ runWork,
258
+ generator: generatorTag(),
259
+ network: defaultNetwork(),
260
+ commerceSkills: rails.erc8183,
261
+ });
262
+ const agentCard = buildAgentCard({ commerceSkills: rails.erc8183 });
263
+ const seller = await X402Seller.create({
264
+ cfg,
265
+ runWork: x402Work(runWork),
266
+ walletAddress: getWallet().address,
267
+ resourceUrl: `${
268
+ process.env.AGENTCORE_RUNTIME_URL ?? `http://localhost:${port}`
269
+ }${X402_SELL_PATH}`,
270
+ });
271
+
272
+ const handler = new DefaultRequestHandler(
273
+ agentCard,
274
+ new InMemoryTaskStore(),
275
+ executor,
276
+ );
277
+
278
+ const app = express();
279
+
280
+ // GET /ping status fed to AgentCore: HEALTHY_BUSY while a background
281
+ // delivery is in flight, else HEALTHY.
282
+ //
283
+ // notify_funded acks immediately and runs the slow work (LLM + on-chain
284
+ // submit) in the background. Reporting HEALTHY_BUSY tells AgentCore the
285
+ // runtime is still working, so the scale-to-zero runtime is NOT reaped on
286
+ // idle before delivery lands (bounded by the session max-lifetime; ≤8h).
287
+ app.get("/ping", (_req, res) => {
288
+ res.json({ status: executor.isBusy() ? "HEALTHY_BUSY" : "HEALTHY" });
289
+ });
290
+
291
+ if (seller.state !== "disabled") {
292
+ app.all(
293
+ X402_SELL_PATH,
294
+ express.text({ type: "*/*", limit: "1mb" }),
295
+ async (req, res) => {
296
+ const request: X402HttpRequest = {
297
+ method: req.method,
298
+ path: req.path,
299
+ query: flatQuery(req.query),
300
+ headers: flatHeaders(req.headers),
301
+ body:
302
+ typeof req.body === "string"
303
+ ? req.body
304
+ : JSON.stringify(req.body ?? ""),
305
+ };
306
+ const out = await seller.handle(request);
307
+ res.status(out.status).set(out.headers).send(out.body);
308
+ },
309
+ );
310
+ }
311
+
312
+ app.use(express.json({ limit: "8mb" }));
313
+ app.use(createEnvelopeMiddleware({ port }));
314
+
315
+ app.use(
316
+ "/.well-known/agent-card.json",
317
+ agentCardHandler({ agentCardProvider: handler }),
318
+ );
319
+ app.use(
320
+ jsonRpcHandler({
321
+ requestHandler: handler,
322
+ userBuilder: UserBuilder.noAuthentication,
323
+ }),
324
+ );
325
+
326
+ // AgentCore's A2A contract is 0.0.0.0:9000. Do not honor the HTTP
327
+ // protocol's $PORT=8080 convention here; AGENT_PORT is the local-dev /
328
+ // rendered-container override.
329
+ app.listen(port, host, () => {
330
+ console.log(`[seller-agent] A2A serving on ${host}:${port}`);
331
+ });
332
+ }
333
+
334
+ // Run only as an entrypoint (`node main.js` / the AgentCore runtime), never
335
+ // on import — tests import the builders above without starting a server.
336
+ const isMain =
337
+ process.argv[1] !== undefined &&
338
+ import.meta.url === pathToFileURL(process.argv[1]).href;
339
+ if (isMain) {
340
+ main().catch((e) => {
341
+ console.error("[seller-agent] fatal:", e);
342
+ process.exit(1);
343
+ });
344
+ }