@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.
- package/DISCLAIMER.md +48 -0
- package/LICENSE +201 -0
- package/dist/_agentcoreName-DZDWEYD3.js +7 -0
- package/dist/_twak-5XQMOFUC.js +25 -0
- package/dist/bag.js +19358 -0
- package/dist/chunk-7RAKL4AS.js +172 -0
- package/dist/chunk-M3ODFCA7.js +1053 -0
- package/dist/chunk-U7IDQ3K5.js +14 -0
- package/dist/deployCli-N6TPN6XA.js +40 -0
- package/package.json +64 -0
- package/recipes/agent/code/{{PKG}}/signing.ts.tmpl +287 -0
- package/recipes/agent/recipe.toml +35 -0
- package/recipes/providers/pieverse-llm/recipe.toml +16 -0
- package/recipes/providers/pieverse-llm/skills/funding-pieverse-llm.md +203 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/.dockerignore.tmpl +8 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/Dockerfile.tmpl +50 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/agentCard.ts.tmpl +135 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/dualMain.ts.tmpl +402 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/executor.ts.tmpl +147 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/main.ts.tmpl +344 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/mcpMain.ts.tmpl +677 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/model.ts.tmpl +117 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/sellerCore.ts.tmpl +503 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/tools.ts.tmpl +157 -0
- package/recipes/runtimes/agentcore/recipe.toml +97 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/.dockerignore.tmpl +8 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/Dockerfile.tmpl +47 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/agentCard.ts.tmpl +131 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/executor.ts.tmpl +504 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/foundryMain.ts.tmpl +300 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/main.ts.tmpl +196 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/mcpMain.ts.tmpl +562 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/model.ts.tmpl +117 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/tools.ts.tmpl +157 -0
- package/recipes/runtimes/azure-foundry/recipe.toml +88 -0
- package/recipes/tools-chain/code/{{PKG}}/chainTools.ts.tmpl +166 -0
- package/recipes/tools-chain/recipe.toml +11 -0
- package/recipes/wallet/recipe.toml +20 -0
- package/recipes/x402-buyer/code/{{PKG}}/x402Buyer.ts.tmpl +175 -0
- package/recipes/x402-buyer/recipe.toml +15 -0
- package/skills/bnbagent-studio.md +107 -0
- package/skills/references/bnbagent-studio-adding-to-project.md +241 -0
- package/skills/references/bnbagent-studio-buying-from-bazaar.md +169 -0
- package/skills/references/bnbagent-studio-buying-via-8183.md +222 -0
- package/skills/references/bnbagent-studio-extending-signing.md +227 -0
- package/skills/references/bnbagent-studio-operating.md +211 -0
- package/skills/references/bnbagent-studio-scaffolding-agent.md +536 -0
- package/skills/references/bnbagent-studio-selling-via-8183.md +271 -0
- package/skills/references/bnbagent-studio-selling-via-b402.md +194 -0
- package/skills/references/bnbagent-studio-use-aws-agentcore.md +208 -0
- package/skills/references/bnbagent-studio-use-azure-foundry.md +164 -0
- package/skills/references/bnbagent-studio-use-bnb-trial.md +92 -0
- package/skills/references/bnbagent-studio-using-altana-wallet.md +68 -0
- package/skills/references/bnbagent-studio-using-twak-wallet.md +260 -0
- 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
|
+
}
|