@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,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
|
+
}
|