@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,50 @@
1
+ # syntax=docker/dockerfile:1
2
+ # Agent image for wallet.kind = "twak" — SHIPPED BY `bag init`.
3
+ #
4
+ # Why a custom image: the managed AgentCore Node runtime (CodeZip) ships only
5
+ # the project's own bundle, and every signing / on-chain intent for a twak
6
+ # wallet shells out to the `twak` CLI (npm @trustwallet/cli), which must be
7
+ # installed globally in the image. agentcore.json points at this file via
8
+ # runtimes[].build = "Container" + dockerfile = "Dockerfile"; `bag deploy
9
+ # --provider aws` hands it to the pinned bnbagent-deploy, which builds the image
10
+ # locally with Docker and pushes it to ECR.
11
+ #
12
+ # AgentCore runtimes execute on linux/arm64 — pin the platform so an x86
13
+ # deploy machine cross-builds the right arch (needs docker buildx/containerd).
14
+ # buildkit may warn "FromPlatformFlagConstDisallowed" — expected; arm64 is pinned intentionally (see above).
15
+ FROM --platform=linux/arm64 public.ecr.aws/docker/library/node:22-slim
16
+
17
+ # The pinned twak CLI.
18
+ #
19
+ # ARCH WARNING: `npm install` MUST run with this image's own node (as below).
20
+ # Installing on the host (or any other arch) and COPYing node_modules in
21
+ # breaks at runtime: twak's `@napi-rs/keyring` native binding is selected at
22
+ # install time per-arch (known npm optional-deps bug) — an x64-installed tree
23
+ # crashes under arm64 node. Multi-arch images must install per-arch too.
24
+ RUN npm install -g @trustwallet/cli@{{TWAK_CLI_VERSION}} pnpm@{{PNPM_VERSION}} \
25
+ && npm cache clean --force
26
+
27
+ WORKDIR /app
28
+ # Install deps first (layer cache), then build. The lockfile is copied when
29
+ # present; a project without one falls back to a plain install.
30
+ COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml ./
31
+ COPY vendor ./vendor
32
+ RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
33
+ pnpm install
34
+
35
+ COPY . .
36
+ RUN pnpm build
37
+
38
+ # Runtime deps only in the final tree (drops typescript etc.).
39
+ RUN pnpm prune --prod
40
+
41
+ # AgentCore protocol contract:
42
+ # A2A → 0.0.0.0:9000 at /
43
+ # MCP → 0.0.0.0:8000/mcp
44
+ # BOTH → A2A-native 0.0.0.0:9000 at / + tunneled MCP at /mcp
45
+ # `bag init` renders {{CONTAINER_PORT}} / {{ENTRYPOINT}} from the selected
46
+ # protocol so the image build never falls back to the HTTP-only 8080 contract.
47
+ # ({{ENTRYPOINT}} is the BUILT entrypoint, e.g. dist/main.js.)
48
+ ENV AGENT_PORT={{CONTAINER_PORT}}
49
+ EXPOSE {{CONTAINER_PORT}}
50
+ CMD ["node", "{{ENTRYPOINT}}"]
@@ -0,0 +1,135 @@
1
+ /**
2
+ * A2A AgentCard — the seller agent's outward, discoverable identity.
3
+ *
4
+ * Built by `main.ts` and served at `/.well-known/agent-card.json`. When
5
+ * deployed, `main.ts` overwrites `card.url` at boot with the deployed
6
+ * AgentCore runtime URL (`$AGENTCORE_RUNTIME_URL`), so the `url` here is only
7
+ * a local-dev placeholder.
8
+ *
9
+ * The card advertises exactly two skills — `negotiate` and `notify_funded` —
10
+ * and the OAuth2 (Cognito) security scheme buyers must satisfy: AgentCore A2A
11
+ * endpoints require an inbound OAuth2 bearer (there is no anonymous mode).
12
+ * The token URL + scope come from the Cognito user pool
13
+ * `bag deploy provision-cognito` creates (env `OAUTH_TOKEN_URL` /
14
+ * `OAUTH_SCOPE`, injected at deploy); the runtime's inbound JWT authorizer
15
+ * validates the same pool. Locally (no Cognito env) the card omits the scheme
16
+ * so `bag dev` is reachable without a token.
17
+ *
18
+ * You own this file — edit the skill descriptions / card metadata for your
19
+ * seller.
20
+ */
21
+
22
+ import type { AgentCard, AgentSkill, SecurityScheme } from "@a2a-js/sdk";
23
+ import { loadStudioToml } from "@bnbagent/studio-runtime/config";
24
+
25
+ const NEGOTIATE: AgentSkill = {
26
+ id: "negotiate",
27
+ name: "Negotiate an ERC-8183 job",
28
+ description:
29
+ 'Send a data part {"skill": "negotiate", "task_description": "...", ' +
30
+ '"terms": {"deliverables": "...", "quality_standards": "..."}} (both ' +
31
+ "terms keys are REQUIRED) and receive a " +
32
+ "wallet-signed price quote (price, currency, negotiation_hash, provider_sig). " +
33
+ "Anchor the returned envelope on-chain via createJob + fund, then send the " +
34
+ "`notify_funded` skill with the job_id to request delivery.",
35
+ tags: ["erc8183", "negotiation", "bnb-chain"],
36
+ inputModes: ["application/json"],
37
+ outputModes: ["application/json"],
38
+ };
39
+
40
+ const NOTIFY_FUNDED: AgentSkill = {
41
+ id: "notify_funded",
42
+ name: "Notify the seller a job is funded (request delivery)",
43
+ description:
44
+ 'After you fund the job on-chain, send {"skill": "notify_funded", ' +
45
+ '"job_id": <int>} to tell the seller "I funded job X — please deliver". ' +
46
+ "The seller verifies the funded job carries its signed quote and replies " +
47
+ 'AT ONCE with {"status": "accepted"|"rejected", "job_id"}; delivery then ' +
48
+ "runs in the background (work takes time). Do NOT wait on this call for " +
49
+ "the result — read the deliverable back from the CHAIN once the job " +
50
+ "reaches SUBMITTED (the `submit` tx carries the deliverable_url; " +
51
+ "ERC-8183 `get_deliverable_url`). The agent serves no job-query endpoint.",
52
+ tags: ["erc8183", "delivery", "bnb-chain"],
53
+ inputModes: ["application/json"],
54
+ outputModes: ["application/json"],
55
+ };
56
+
57
+ /** Card name from studio.toml `[project].name` (best-effort). */
58
+ function agentName(): string {
59
+ let name = "";
60
+ try {
61
+ const cfg = loadStudioToml();
62
+ name = String(
63
+ ((cfg.project ?? {}) as Record<string, unknown>).name ?? "",
64
+ );
65
+ } catch {
66
+ // a card label must never break boot
67
+ }
68
+ return name || "bnbagent-seller";
69
+ }
70
+
71
+ /**
72
+ * OAuth2 (Cognito client-credentials) scheme from env, or null locally.
73
+ *
74
+ * `bag deploy provision-cognito` emits a Cognito user pool + app client and
75
+ * injects `OAUTH_TOKEN_URL` + `OAUTH_SCOPE`; the AgentCore runtime's inbound
76
+ * JWT authorizer is wired to the same pool. Absent (local `bag dev`) →
77
+ * return null so the card advertises no auth requirement.
78
+ */
79
+ function oauth2Scheme(): SecurityScheme | null {
80
+ const tokenUrl = process.env.OAUTH_TOKEN_URL;
81
+ const scope = process.env.OAUTH_SCOPE;
82
+ if (!tokenUrl || !scope) {
83
+ return null;
84
+ }
85
+ return {
86
+ type: "oauth2",
87
+ flows: {
88
+ clientCredentials: {
89
+ tokenUrl,
90
+ scopes: { [scope]: "Invoke the seller agent" },
91
+ },
92
+ },
93
+ };
94
+ }
95
+
96
+ /** Build the A2A AgentCard, gating ERC-8183 skills on the configured rail. */
97
+ export function buildAgentCard(
98
+ opts: { commerceSkills?: boolean } = {},
99
+ ): AgentCard {
100
+ const name = agentName();
101
+ const extra: Partial<AgentCard> = {};
102
+ const scheme = oauth2Scheme();
103
+ if (scheme !== null) {
104
+ const scope = process.env.OAUTH_SCOPE as string;
105
+ extra.securitySchemes = { oauth2: scheme };
106
+ extra.security = [{ oauth2: [scope] }];
107
+ }
108
+ return {
109
+ name,
110
+ description: `ERC-8183 seller agent (${name}) — negotiate + notify_funded over A2A.`,
111
+ // main.ts overwrites this with $AGENTCORE_RUNTIME_URL at boot.
112
+ // Local-dev fallback: a client-routable localhost URL (not the 0.0.0.0
113
+ // bind address). Host via AGENT_HOST (default localhost); port via the
114
+ // same AGENT_PORT → 9000 resolution main.ts serves on. Do not honor the
115
+ // AgentCore HTTP $PORT=8080 convention for this A2A runtime.
116
+ url:
117
+ process.env.AGENTCORE_RUNTIME_URL ??
118
+ `http://${process.env.AGENT_HOST ?? "localhost"}:${process.env.AGENT_PORT || "9000"}/`,
119
+ version: "1.0.0",
120
+ protocolVersion: "0.3.0",
121
+ preferredTransport: "JSONRPC",
122
+ // Non-streaming: negotiate / notify_funded are request/response
123
+ // (message/send). Do NOT flip this on to satisfy the AgentCore
124
+ // inspector's chat box — that box can't drive a seller agent (it can
125
+ // only send plain text, never the {"skill": ...} DataPart these skills
126
+ // require, and its streaming view expects Task events). Test locally
127
+ // with curl / an A2A client sending a DataPart (see the operating skill).
128
+ capabilities: { streaming: false },
129
+ defaultInputModes: ["application/json"],
130
+ defaultOutputModes: ["application/json"],
131
+ skills:
132
+ opts.commerceSkills === false ? [] : [NEGOTIATE, NOTIFY_FUNDED],
133
+ ...extra,
134
+ };
135
+ }
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Combined A2A-native + tunneled MCP seller entrypoint.
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: 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. The same app also exposes stateful
10
+ * streamable-HTTP MCP at `/mcp`; the platform gateway reaches that face
11
+ * through the HTTP envelope-v1 tunnel.
12
+ *
13
+ * A2A is deliberately the one native AgentCore protocol in dual mode:
14
+ * `HEALTHY_BUSY` on `/ping` is the only contract that keeps background A2A
15
+ * delivery alive. AgentCore accepts one serverProtocol and one data-plane
16
+ * port, so both faces share this A2A-native process and port.
17
+ *
18
+ * A2A skills (executor.ts):
19
+ *
20
+ * negotiate → read the FIXED list price → CLAMP to [min,max] → EIP-191 SIGN
21
+ * the offer (no LLM, no tools) → return the signed offer (or reject)
22
+ * notify_funded → re-verify the funded job on-chain (fast) → ACK accepted at once,
23
+ * then in the BACKGROUND: LLM work → manifest → storage →
24
+ * submitResult (SIGN + broadcast). The buyer polls the chain for
25
+ * the deliverable. Each notify also sweeps other FUNDED jobs
26
+ * (buyer-push fallback). While background work is in flight the
27
+ * `/ping` handler reports HEALTHY_BUSY so AgentCore keeps the
28
+ * scale-to-zero runtime warm until it lands.
29
+ *
30
+ * Buyers reach this endpoint with an OAuth2 (Cognito) bearer — AgentCore A2A
31
+ * mandates inbound auth (see agentCard.ts + `bag deploy provision-cognito`).
32
+ *
33
+ * ## Boundaries (do NOT cross — they are the whole point)
34
+ *
35
+ * - The agent does ALL deterministic SIGNING (quote-sign + submit + settle +
36
+ * automatic Pieverse LLM-credit auto-renew). ALL signing is FIXED code in
37
+ * `signing.ts` — NEVER an LLM-callable tool (money never in the LLM).
38
+ * - The price is a FIXED list price from studio.toml (clamped before
39
+ * signing) — the LLM never prices; it only PRODUCES the work text in the
40
+ * delivery step.
41
+ * - Chain access for the LLM is READ-ONLY tools only (`tools.ts`).
42
+ * - `settle` (claim payment after the dispute window) is operator-driven —
43
+ * run `bag erc8183 settle <job_id>`; it is deliberately NOT an A2A skill.
44
+ */
45
+
46
+ import { createHash, randomUUID } from "node:crypto";
47
+ import { pathToFileURL } from "node:url";
48
+ import {
49
+ GetSecretValueCommand,
50
+ SecretsManagerClient,
51
+ } from "@aws-sdk/client-secrets-manager";
52
+ import { DefaultRequestHandler, InMemoryTaskStore } from "@a2a-js/sdk/server";
53
+ import {
54
+ agentCardHandler,
55
+ jsonRpcHandler,
56
+ UserBuilder,
57
+ } from "@a2a-js/sdk/server/express";
58
+ import {
59
+ loadStudioToml,
60
+ type TomlTable,
61
+ } from "@bnbagent/studio-runtime/config";
62
+ import {
63
+ ensureAltanaSessionLoaded,
64
+ ensureKeystoreMaterialized,
65
+ ensureTwakMaterialized,
66
+ getWallet,
67
+ } from "@bnbagent/studio-runtime/wallet";
68
+ import {
69
+ createEnvelopeMiddleware,
70
+ X402_SELL_PATH,
71
+ type X402HttpRequest,
72
+ type X402RunWork,
73
+ X402Seller,
74
+ } from "@bnbagent/studio-runtime/x402";
75
+ import { generateText, stepCountIs } from "ai";
76
+ import express from "express";
77
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
78
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
79
+ import { buildAgentCard } from "./agentCard.js";
80
+ import { SellerAgentExecutor } from "./executor.js";
81
+ import { buildMcpServer } from "./mcpMain.js";
82
+ import { buildModel } from "./model.js";
83
+ import type { RunWork } from "./sellerCore.js";
84
+ import { LLM_READ_TOOLS } from "./tools.js";
85
+
86
+ const APP_NAME = "agent";
87
+
88
+ /**
89
+ * Deliverable `generator` label: this seller's own name, read from
90
+ * studio.toml `[project].name` (minus the `-agent` suffix) so each delivered
91
+ * manifest is self-identifying. Best-effort — falls back to `APP_NAME` if
92
+ * the config can't be read.
93
+ */
94
+ function generatorTag(): string {
95
+ let name = "";
96
+ try {
97
+ const cfg = loadStudioToml();
98
+ name = String(((cfg.project ?? {}) as Record<string, unknown>).name ?? "");
99
+ } catch {
100
+ // a metadata label must never break delivery
101
+ return APP_NAME;
102
+ }
103
+ return name.endsWith("-agent")
104
+ ? name.slice(0, -"-agent".length)
105
+ : name || APP_NAME;
106
+ }
107
+
108
+ // ── Runtime secrets ───────────────────────────────────────────────────────────
109
+ // Keep plaintext secrets OUT of agentcore.json. When BNBAGENT_RUNTIME_SECRET_ID
110
+ // is set (deployed runtime), pull a JSON {ENV_NAME: value} blob from AWS
111
+ // Secrets Manager into the process env BEFORE anything reads it (keystore
112
+ // unlock, provider key, buildModel, Cognito OAuth env). No-op locally, where
113
+ // .env.local already populated the environment. In a deployed runtime the
114
+ // managed secret bundle is authoritative and replaces any stale spec-level
115
+ // value left by an earlier runtime revision.
116
+ async function loadRuntimeSecrets(): Promise<void> {
117
+ const secretId = process.env.BNBAGENT_RUNTIME_SECRET_ID;
118
+ if (!secretId) {
119
+ return;
120
+ }
121
+ const resp = await new SecretsManagerClient({}).send(
122
+ new GetSecretValueCommand({ SecretId: secretId }),
123
+ );
124
+ const bundle = JSON.parse(resp.SecretString ?? "{}") as Record<
125
+ string,
126
+ unknown
127
+ >;
128
+ for (const [key, value] of Object.entries(bundle)) {
129
+ process.env[key] = String(value);
130
+ }
131
+ const pieverseKey = process.env.PIEVERSE_LLM_API_KEY;
132
+ if (pieverseKey) {
133
+ const fingerprint = createHash("sha256")
134
+ .update(pieverseKey, "utf-8")
135
+ .digest("hex")
136
+ .slice(0, 12);
137
+ console.info(
138
+ `[runtime-secrets] PIEVERSE_LLM_API_KEY source=secretsmanager sha256=${fingerprint}…`,
139
+ );
140
+ }
141
+ }
142
+
143
+ /** studio.toml `[network].default` (best-effort; used by the funded sweep). */
144
+ function defaultNetwork(): string {
145
+ try {
146
+ const cfg = loadStudioToml();
147
+ return String(
148
+ ((cfg.network ?? {}) as Record<string, unknown>).default ?? "bsc-testnet",
149
+ );
150
+ } catch {
151
+ return "bsc-testnet";
152
+ }
153
+ }
154
+
155
+ // ── One-shot LLM helper (the executor's delivery work hook) ──────────────────
156
+ // LLM credit auto-renew (Pieverse path): `buildModel()` (in model.ts) returns
157
+ // a model wrapped with a middleware that auto-tops up the active Pieverse key
158
+ // before each generate call when [llm.auto_renew] is enabled. That top-up is
159
+ // the ONLY automatic signing path outside signing.ts — it is budget-gated and
160
+ // is NOT an LLM tool. It rides transparently into the delivery step.
161
+ //
162
+ // The LLM runs ONLY in the delivery step (the value hook). `negotiate` is
163
+ // rule-based and never touches the LLM. The read-only chain tools are
164
+ // attached so the work can read on-chain context if it needs to — drop them
165
+ // from `tools.ts` if your work doesn't read chain. Signing / settle are NEVER
166
+ // tools — they are fixed code in signing.ts, triggered by the A2A skills,
167
+ // never callable by the LLM. (The one deliberate exception: the x402-buyer
168
+ // recipe's PAID fetch tools — see the `tools:` note below — the LLM picks the
169
+ // URL, but who gets paid and the per-call/daily caps stay locked in
170
+ // studio.toml.)
171
+ export function buildRunWork(): RunWork {
172
+ // The model is resolved LAZILY on first delivery, not at boot: a seller
173
+ // with no provider key yet must still serve negotiate (which never calls
174
+ // the LLM) — missing-key errors surface at notify_funded delivery time.
175
+ let model: ReturnType<typeof buildModel> | undefined;
176
+ return async (prompt, { abortSignal }) => {
177
+ model ??= buildModel(); // managed model with the auto-renew hook (delivery only)
178
+ const result = await generateText({
179
+ model,
180
+ system:
181
+ "You are a seller agent. You do the actual work once a job is funded. " +
182
+ "Be concrete and concise. Use the read-only chain tools when on-chain " +
183
+ "context helps. If a paid-data tool such as `buy_with_x402` is available " +
184
+ "to you, USE IT to fetch the data a task needs — those merchants (e.g. " +
185
+ "CoinMarketCap) charge via on-chain wallet payment, NOT an API key; never " +
186
+ "reply that you cannot complete the task for lack of an API key.",
187
+ prompt,
188
+ // LLM_READ_TOOLS = read-only chain tools (wallet, balances,
189
+ // ERC-8004/8183 queries). Edit `tools.ts` to add/remove. These are
190
+ // READ-ONLY — the agent never signs via a tool; all signing is in
191
+ // signing.ts (fixed code).
192
+ // To let the agent BUY paid data at work time (e.g. CMC market data
193
+ // after `bag x402 trust cmc` + `bag recipe code x402-buyer`), spread
194
+ // the emitted tool set — payee + per-call/daily caps stay locked in
195
+ // studio.toml:
196
+ // import { X402_BUYER_TOOLS } from "./x402Buyer.js";
197
+ // tools: { ...LLM_READ_TOOLS, ...X402_BUYER_TOOLS },
198
+ tools: LLM_READ_TOOLS,
199
+ stopWhen: stepCountIs(8), // bounded tool-call loop, then final text
200
+ abortSignal,
201
+ });
202
+ return result.text.trim();
203
+ };
204
+ }
205
+
206
+ function hasErc8183Rail(cfg: TomlTable): boolean {
207
+ const payments = asTable(cfg.payments);
208
+ return asTable(payments?.erc8183) !== null;
209
+ }
210
+
211
+ function asTable(value: unknown): TomlTable | null {
212
+ return value !== null && typeof value === "object" && !Array.isArray(value)
213
+ ? (value as TomlTable)
214
+ : null;
215
+ }
216
+
217
+ function flatHeaders(
218
+ headers: Record<string, string | string[] | undefined>,
219
+ ): Record<string, string> {
220
+ const out: Record<string, string> = {};
221
+ for (const [name, value] of Object.entries(headers)) {
222
+ if (typeof value === "string") out[name] = value;
223
+ else if (value !== undefined) out[name] = value[0] ?? "";
224
+ }
225
+ return out;
226
+ }
227
+
228
+ function flatQuery(query: Record<string, unknown>): Record<string, string> {
229
+ const out: Record<string, string> = {};
230
+ for (const [name, value] of Object.entries(query)) {
231
+ if (typeof value === "string") out[name] = value;
232
+ }
233
+ return out;
234
+ }
235
+
236
+ function x402Work(runWork: RunWork): X402RunWork {
237
+ return ({ prompt }) => runWork(prompt, { sessionId: "x402" });
238
+ }
239
+
240
+ // ── serving ───────────────────────────────────────────────────────────────────
241
+
242
+ /**
243
+ * Build the single-port dual-face app. Tests import this builder without
244
+ * opening a listener; the runtime entrypoint calls it once and listens on
245
+ * AgentCore's A2A port.
246
+ */
247
+ export async function buildDualApp(): Promise<{
248
+ app: express.Express;
249
+ executor: SellerAgentExecutor;
250
+ }> {
251
+ await loadRuntimeSecrets();
252
+
253
+ // Wallet material is NEVER bundled into the deploy artifact. `bag deploy`
254
+ // injects it via Secrets Manager and these calls (run once at cold start,
255
+ // before any signing) materialize it on disk. Each is a no-op for the
256
+ // other wallet kind and locally, where the wallet already lives on disk:
257
+ // - evm-local: WALLET_KEYSTORE_JSON → keystore file (unlocked with WALLET_PASSWORD)
258
+ // - twak: TWAK_WALLET_JSON / TWAK_CREDENTIALS_JSON → $TMPDIR/twak-home/.twak
259
+ // (exported as TWAK_HOME_DIR; twak reads TWAK_WALLET_PASSWORD itself)
260
+ ensureKeystoreMaterialized();
261
+ ensureTwakMaterialized();
262
+ await ensureAltanaSessionLoaded();
263
+
264
+ const cfg = loadStudioToml();
265
+ const rails = { erc8183: hasErc8183Rail(cfg) };
266
+ const port = Number(process.env.AGENT_PORT || "9000");
267
+ const runWork = buildRunWork();
268
+
269
+ // The executor backs the seller skills with signing.ts fixed code (NEVER an
270
+ // LLM tool). The express app hosts the agent card + JSON-RPC message/send
271
+ // on 0.0.0.0:9000 and GET /ping for AgentCore's liveness probe.
272
+ const executor = new SellerAgentExecutor({
273
+ runWork,
274
+ generator: generatorTag(),
275
+ network: defaultNetwork(),
276
+ commerceSkills: rails.erc8183,
277
+ });
278
+ const agentCard = buildAgentCard({ commerceSkills: rails.erc8183 });
279
+ const seller = await X402Seller.create({
280
+ cfg,
281
+ runWork: x402Work(runWork),
282
+ walletAddress: getWallet().address,
283
+ resourceUrl: `${
284
+ process.env.AGENTCORE_RUNTIME_URL ?? `http://localhost:${port}`
285
+ }${X402_SELL_PATH}`,
286
+ });
287
+
288
+ const handler = new DefaultRequestHandler(
289
+ agentCard,
290
+ new InMemoryTaskStore(),
291
+ executor,
292
+ );
293
+
294
+ const app = express();
295
+
296
+ // GET /ping status fed to AgentCore: HEALTHY_BUSY while a background
297
+ // delivery is in flight, else HEALTHY.
298
+ //
299
+ // notify_funded acks immediately and runs the slow work (LLM + on-chain
300
+ // submit) in the background. Reporting HEALTHY_BUSY tells AgentCore the
301
+ // runtime is still working, so the scale-to-zero runtime is NOT reaped on
302
+ // idle before delivery lands (bounded by the session max-lifetime; ≤8h).
303
+ app.get("/ping", (_req, res) => {
304
+ res.json({ status: executor.isBusy() ? "HEALTHY_BUSY" : "HEALTHY" });
305
+ });
306
+
307
+ if (seller.state !== "disabled") {
308
+ app.all(
309
+ X402_SELL_PATH,
310
+ express.text({ type: "*/*", limit: "1mb" }),
311
+ async (req, res) => {
312
+ const request: X402HttpRequest = {
313
+ method: req.method,
314
+ path: req.path,
315
+ query: flatQuery(req.query),
316
+ headers: flatHeaders(req.headers),
317
+ body:
318
+ typeof req.body === "string"
319
+ ? req.body
320
+ : JSON.stringify(req.body ?? ""),
321
+ };
322
+ const out = await seller.handle(request);
323
+ res.status(out.status).set(out.headers).send(out.body);
324
+ },
325
+ );
326
+ }
327
+
328
+ app.use(express.json({ limit: "8mb" }));
329
+ app.use(createEnvelopeMiddleware({ port }));
330
+
331
+ app.use(
332
+ "/.well-known/agent-card.json",
333
+ agentCardHandler({ agentCardProvider: handler }),
334
+ );
335
+
336
+ const transports: Record<string, StreamableHTTPServerTransport> = {};
337
+ app.all("/mcp", async (req, res) => {
338
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
339
+ let transport = sessionId ? transports[sessionId] : undefined;
340
+ if (transport === undefined) {
341
+ if (req.method !== "POST" || !isInitializeRequest(req.body)) {
342
+ res.status(400).json({
343
+ jsonrpc: "2.0",
344
+ error: { code: -32000, message: "Bad Request: no valid session" },
345
+ id: null,
346
+ });
347
+ return;
348
+ }
349
+ const t = new StreamableHTTPServerTransport({
350
+ sessionIdGenerator: () => randomUUID(),
351
+ enableJsonResponse: true,
352
+ onsessioninitialized: (sid) => {
353
+ transports[sid] = t;
354
+ },
355
+ });
356
+ t.onclose = () => {
357
+ if (t.sessionId !== undefined) {
358
+ delete transports[t.sessionId];
359
+ }
360
+ };
361
+ await buildMcpServer({ commerceSkills: rails.erc8183 }).connect(t);
362
+ transport = t;
363
+ }
364
+ await transport.handleRequest(req, res, req.body);
365
+ });
366
+
367
+ app.use(
368
+ jsonRpcHandler({
369
+ requestHandler: handler,
370
+ userBuilder: UserBuilder.noAuthentication,
371
+ }),
372
+ );
373
+
374
+ return { app, executor };
375
+ }
376
+
377
+ async function main(): Promise<void> {
378
+ const host = process.env.AGENT_BIND_HOST || "0.0.0.0";
379
+ const port = Number(process.env.AGENT_PORT || "9000");
380
+ const { app } = await buildDualApp();
381
+
382
+ // AgentCore's A2A contract is 0.0.0.0:9000. Do not honor the HTTP
383
+ // protocol's $PORT=8080 convention here; AGENT_PORT is the local-dev /
384
+ // rendered-container override.
385
+ app.listen(port, host, () => {
386
+ console.log(
387
+ `[seller-agent] A2A native + MCP tunneled serving on ${host}:${port}`,
388
+ );
389
+ });
390
+ }
391
+
392
+ // Run only as an entrypoint (`node dualMain.js` / the AgentCore runtime), never
393
+ // on import — tests import the builders above without starting a server.
394
+ const isMain =
395
+ process.argv[1] !== undefined &&
396
+ import.meta.url === pathToFileURL(process.argv[1]).href;
397
+ if (isMain) {
398
+ main().catch((e) => {
399
+ console.error("[seller-agent] fatal:", e);
400
+ process.exit(1);
401
+ });
402
+ }