@bnbagent/studio-cli 0.0.13-alpha.6 → 0.0.13-alpha.8

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 (26) hide show
  1. package/dist/bag.js +354 -72
  2. package/dist/{chunk-LEBIBQWN.js → chunk-NTDWVEW2.js} +59 -17
  3. package/dist/{deployCli-H65YVV3V.js → deployCli-ZESBUWQB.js} +3 -1
  4. package/package.json +4 -3
  5. package/recipes/agent/code/{{PKG}}/signing.ts.tmpl +2 -2
  6. package/recipes/agent/recipe.toml +1 -1
  7. package/recipes/mpp-buyer/recipe.toml +1 -1
  8. package/recipes/runtimes/agentcore/code/{{PKG}}/dualMain.ts.tmpl +2 -0
  9. package/recipes/runtimes/agentcore/code/{{PKG}}/executor.ts.tmpl +14 -6
  10. package/recipes/runtimes/agentcore/code/{{PKG}}/mcpMain.ts.tmpl +63 -30
  11. package/recipes/runtimes/agentcore/code/{{PKG}}/requestLimits.ts.tmpl +178 -0
  12. package/recipes/runtimes/agentcore/code/{{PKG}}/sellerCore.ts.tmpl +3 -0
  13. package/recipes/runtimes/agentcore/code/{{PKG}}/unifiedMain.ts.tmpl +2 -0
  14. package/recipes/runtimes/agentcore/recipe.toml +1 -1
  15. package/recipes/runtimes/azure-foundry/code/{{PKG}}/executor.ts.tmpl +14 -6
  16. package/recipes/runtimes/azure-foundry/code/{{PKG}}/mcpMain.ts.tmpl +63 -30
  17. package/recipes/runtimes/azure-foundry/code/{{PKG}}/requestLimits.ts.tmpl +178 -0
  18. package/recipes/runtimes/azure-foundry/code/{{PKG}}/sellerCore.ts.tmpl +3 -0
  19. package/recipes/runtimes/azure-foundry/code/{{PKG}}/unifiedMain.ts.tmpl +2 -0
  20. package/recipes/runtimes/azure-foundry/recipe.toml +1 -1
  21. package/recipes/wallet/recipe.toml +0 -1
  22. package/recipes/x402-buyer/recipe.toml +1 -1
  23. package/skills/references/bnbagent-studio-adding-to-project.md +1 -1
  24. package/skills/references/bnbagent-studio-operating.md +1 -1
  25. package/skills/references/bnbagent-studio-selling-via-b402.md +3 -3
  26. package/skills/references/bnbagent-studio-using-altana-wallet.md +3 -2
@@ -40,6 +40,7 @@
40
40
  import { ERC8183JobOps } from "@bnbagent/sdk/erc8183";
41
41
  import { SubmitPermanentlyUnsupportedError } from "@bnbagent/studio-runtime/erc8183";
42
42
  import { getWallet } from "@bnbagent/studio-runtime/wallet";
43
+ import { limitCommerceOperation } from "./requestLimits.js";
43
44
  import * as defaultSigning from "./signing.js";
44
45
 
45
46
  const log = {
@@ -227,6 +228,7 @@ export class SellerCore {
227
228
  data: Record<string, unknown>,
228
229
  ): Promise<Record<string, unknown>> {
229
230
  this.requireCommerceRail();
231
+ await limitCommerceOperation("negotiate");
230
232
  let request = data.request;
231
233
  if (request === null || typeof request !== "object" || Array.isArray(request)) {
232
234
  const picked: Record<string, unknown> = {};
@@ -262,6 +264,7 @@ export class SellerCore {
262
264
  data: Record<string, unknown>,
263
265
  ): Promise<Record<string, unknown>> {
264
266
  this.requireCommerceRail();
267
+ await limitCommerceOperation("notify_funded");
265
268
  const raw = data.job_id;
266
269
  if (raw === undefined || raw === null || String(raw) === "") {
267
270
  this.spawn(() => this.sweep()); // bare notify → just scan stragglers
@@ -95,6 +95,7 @@ import express from "express";
95
95
  import { buildAgentCard } from "./agentCard.js";
96
96
  import { SellerAgentExecutor } from "./executor.js";
97
97
  import { buildModel } from "./model.js";
98
+ import { requestLimitContext } from "./requestLimits.js";
98
99
  import type { RunWork } from "./sellerCore.js";
99
100
  import { LLM_READ_TOOLS } from "./tools.js";
100
101
 
@@ -500,6 +501,7 @@ async function main(): Promise<void> {
500
501
  );
501
502
 
502
503
  const app = express();
504
+ app.use(requestLimitContext);
503
505
 
504
506
  // GET /ping status fed to AgentCore: HEALTHY_BUSY while a background
505
507
  // delivery is in flight, else HEALTHY.
@@ -32,7 +32,7 @@ node = [
32
32
  # BNBAGENT_RUNTIME_SECRET_ID is set (default secretsmanager mode + platform).
33
33
  "@aws-sdk/client-secrets-manager@^3.600.0",
34
34
  "@bnbagent/studio-runtime",
35
- "@bnbagent/sdk@0.5.3",
35
+ "@bnbagent/sdk@0.5.5",
36
36
  # The LLM work hook (generateText + tools) and the model factory.
37
37
  "ai@^7.0.29",
38
38
  # Tool input schemas (AI SDK tools + MCP registerTool).
@@ -43,6 +43,7 @@ import {
43
43
  type ExecutionEventBus,
44
44
  type RequestContext,
45
45
  } from "@a2a-js/sdk/server";
46
+ import { isCommerceRateLimitError } from "./requestLimits.js";
46
47
  import { SellerCore } from "./sellerCore.js";
47
48
 
48
49
  const log = {
@@ -94,9 +95,10 @@ export class SellerAgentExecutor extends SellerCore implements AgentExecutor {
94
95
  } catch (e) {
95
96
  // a skill failure must still ACK the buyer
96
97
  log.error(`skill ${JSON.stringify(skill)} failed`, e);
97
- const name = e instanceof Error ? e.constructor.name : "Error";
98
- const msg = e instanceof Error ? e.message : String(e);
99
- return { error: `${name}: ${msg}`, skill };
98
+ if (isCommerceRateLimitError(e)) {
99
+ return { status: "retry", error: "seller rate limit exceeded", skill };
100
+ }
101
+ return { error: "seller operation failed; retry later", skill };
100
102
  }
101
103
  }
102
104
 
@@ -135,9 +137,15 @@ export class SellerAgentExecutor extends SellerCore implements AgentExecutor {
135
137
  // returned as a result above (peer of the MCP runtime: faults →
136
138
  // isError, business outcomes → result).
137
139
  log.error(`skill ${JSON.stringify(skill)} failed`, e);
138
- const name = e instanceof Error ? e.constructor.name : "Error";
139
- const msg = e instanceof Error ? e.message : String(e);
140
- throw A2AError.internalError(`${name}: ${msg}`);
140
+ if (isCommerceRateLimitError(e)) {
141
+ result = {
142
+ status: "retry",
143
+ error: "seller rate limit exceeded",
144
+ skill,
145
+ };
146
+ } else {
147
+ throw A2AError.internalError("seller operation failed; retry later");
148
+ }
141
149
  }
142
150
  reply(eventBus, context, result);
143
151
  };
@@ -79,6 +79,11 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
79
79
  import { generateText, stepCountIs } from "ai";
80
80
  import express from "express";
81
81
  import { z } from "zod";
82
+ import {
83
+ isCommerceRateLimitError,
84
+ limitCommerceOperation,
85
+ requestLimitContext,
86
+ } from "./requestLimits.js";
82
87
  import * as signing from "./signing.js";
83
88
 
84
89
  const APP_NAME = "agent";
@@ -88,6 +93,11 @@ const log = {
88
93
  console.error(`[seller-agent.mcp] ERROR ${msg}`, e ?? ""),
89
94
  };
90
95
 
96
+ function protocolFailure(scope: string, error: unknown): never {
97
+ log.error(scope, error);
98
+ throw new Error("seller operation failed; retry later");
99
+ }
100
+
91
101
  // ── Runtime secrets ───────────────────────────────────────────────────────────
92
102
  // Keep plaintext secrets OUT of agentcore.json. When BNBAGENT_RUNTIME_SECRET_ID
93
103
  // is set (deployed runtime), pull a JSON {ENV_NAME: value} blob from AWS
@@ -296,16 +306,24 @@ export function buildMcpServer(
296
306
  annotations: COMMERCE_ANNOTATIONS,
297
307
  },
298
308
  // Error contract (unified with the A2A executor): an unexpected fault
299
- // here (wallet/signing failure) is left to PROPAGATE the MCP SDK turns
300
- // it into a tool result with `isError: true` (MCP's tool-execution-error
301
- // channel), the peer of the A2A executor throwing
302
- // `A2AError.internalError` → JSON-RPC -32603. Only CLASSIFIED business
303
- // outcomes are returned as a normal result. So do NOT wrap this in a
304
- // try/catch that masks a fault as a successful quote.
309
+ // becomes an MCP `isError` result with a generic public message; its full
310
+ // detail is logged server-side. Classified quota exhaustion is returned
311
+ // as a normal retry result and never as a fake quote.
305
312
  async ({ task_description, terms }) => {
306
- const request = { task_description, terms: terms ?? {} };
307
- const clamped = signing.clampPrice(signing.listPrice());
308
- return toolResult(await signing.signQuote(request, clamped));
313
+ try {
314
+ await limitCommerceOperation("negotiate");
315
+ const request = { task_description, terms: terms ?? {} };
316
+ const clamped = signing.clampPrice(signing.listPrice());
317
+ return toolResult(await signing.signQuote(request, clamped));
318
+ } catch (e) {
319
+ if (isCommerceRateLimitError(e)) {
320
+ return toolResult({
321
+ status: "retry",
322
+ reason: "seller rate limit exceeded",
323
+ });
324
+ }
325
+ return protocolFailure("negotiate failed", e);
326
+ }
309
327
  },
310
328
  );
311
329
 
@@ -327,6 +345,17 @@ export function buildMcpServer(
327
345
  annotations: COMMERCE_ANNOTATIONS,
328
346
  },
329
347
  async ({ job_id }, extra) => {
348
+ try {
349
+ await limitCommerceOperation("notify_funded");
350
+ } catch (e) {
351
+ if (isCommerceRateLimitError(e)) {
352
+ return toolResult({
353
+ status: "retry",
354
+ reason: "seller rate limit exceeded",
355
+ });
356
+ }
357
+ return protocolFailure("notify_funded limiter failed", e);
358
+ }
330
359
  let jid: number;
331
360
  try {
332
361
  jid = parseJobId(job_id);
@@ -348,40 +377,43 @@ export function buildMcpServer(
348
377
  } catch (e) {
349
378
  // a failed verify is transient; tell the buyer to retry
350
379
  log.error(`verify of job ${jid} failed`, e);
351
- const name = e instanceof Error ? e.constructor.name : "Error";
352
- const msg = e instanceof Error ? e.message : String(e);
353
380
  return toolResult({
354
381
  status: "retry",
355
382
  job_id: jid,
356
- reason: `${name}: ${msg}`,
383
+ reason: "chain verification temporarily unavailable",
357
384
  });
358
385
  }
359
386
  if (!verdict.ok) {
360
387
  return toolResult({
361
388
  status: verdict.permanent ? "rejected" : "retry",
362
389
  job_id: jid,
363
- reason: verdict.reason,
390
+ reason: verdict.permanent
391
+ ? verdict.reason
392
+ : "chain verification temporarily unavailable",
364
393
  });
365
394
  }
366
395
 
367
396
  // 2/4 — produce the deliverable (THE ONLY LLM CALL; specialise the
368
397
  // prompt here)
369
398
  await reportProgress(extra, 2, 4);
370
- const spec = await signing.jobSpec(jid);
371
- const task =
372
- spec !== null
373
- ? JSON.stringify({ task: spec.task, terms: spec.terms })
374
- : `job ${jid}`;
375
- const prompt =
376
- "You accepted and were paid for the following job. Produce the deliverable " +
377
- `now. Be complete and self-contained.\n\nJOB CONTEXT:\n${task}`;
378
- // An unexpected fault below (LLM unavailable, RPC/submit hiccup) is
379
- // left to PROPAGATE the MCP SDK returns it as an isError tool result
380
- // (the peer of the A2A executor's internalError/-32603). Only the
381
- // deterministic, classified outcome SubmitPermanentlyUnsupportedError
382
- // is a "rejected" business result.
383
- const work = await runLlm(prompt);
384
-
399
+ let work: string;
400
+ try {
401
+ const spec = await signing.jobSpec(jid);
402
+ const task =
403
+ spec !== null
404
+ ? JSON.stringify({ task: spec.task, terms: spec.terms })
405
+ : `job ${jid}`;
406
+ const prompt =
407
+ "You accepted and were paid for the following job. Produce the deliverable " +
408
+ `now. Be complete and self-contained.\n\nJOB CONTEXT:\n${task}`;
409
+ work = await runLlm(prompt);
410
+ } catch (e) {
411
+ return protocolFailure(`delivery preparation for job ${jid} failed`, e);
412
+ }
413
+ // Unexpected LLM/RPC faults are logged in full, then surfaced through
414
+ // MCP's isError channel with a generic public message. Only the
415
+ // deterministic SubmitPermanentlyUnsupportedError is a classified
416
+ // "rejected" business result.
385
417
  // 3/4 — sign + broadcast the on-chain submit (re-verifies FUNDED inside)
386
418
  await reportProgress(extra, 3, 4);
387
419
  let res: { submitTx: string; deliverableUrl: string | null };
@@ -401,10 +433,10 @@ export function buildMcpServer(
401
433
  status: "rejected",
402
434
  job_id: jid,
403
435
  skip: true,
404
- reason: e.message,
436
+ reason: "seller wallet does not support result submission",
405
437
  });
406
438
  }
407
- throw e;
439
+ return protocolFailure(`submit of job ${jid} failed`, e);
408
440
  }
409
441
 
410
442
  // 4/4 — done
@@ -606,6 +638,7 @@ async function main(): Promise<void> {
606
638
  });
607
639
 
608
640
  const app = express();
641
+ app.use(requestLimitContext);
609
642
 
610
643
  if (seller.state !== "disabled") {
611
644
  app.all(
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Application-level quotas for the two public seller operations.
3
+ *
4
+ * A process-wide bucket is always enforced, including when the hosting
5
+ * platform exposes no trustworthy caller identity. A second per-caller
6
+ * bucket is enabled only when the operator names a header that its trusted
7
+ * edge sets after stripping caller-supplied values. Request payload fields
8
+ * and forwarded IP headers are deliberately never treated as identities.
9
+ * The defaults are process-local; multi-replica owners can inject async
10
+ * shared limiters without making that infrastructure mandatory.
11
+ */
12
+
13
+ import { AsyncLocalStorage } from "node:async_hooks";
14
+ import { RateLimitExceeded, SlidingWindowLimiter } from "@bnbagent/sdk/utils";
15
+ import type { NextFunction, Request, Response } from "express";
16
+
17
+ type CommerceOperation = "negotiate" | "notify_funded";
18
+
19
+ const DEFAULT_GLOBAL_MAX = 120;
20
+ const DEFAULT_CALLER_MAX = 20;
21
+ const DEFAULT_WINDOW_SECONDS = 60;
22
+ const DEFAULT_MAX_CALLERS = 10_000;
23
+ const SHARED_LIMITER_TIMEOUT_MS = 5_000;
24
+
25
+ export interface CommerceRateLimiter {
26
+ /** Consume one request; honor cancellation and reject denied requests. */
27
+ check(key: string, signal?: AbortSignal): void | Promise<void>;
28
+ }
29
+
30
+ export interface CommerceRateLimiters {
31
+ readonly global: CommerceRateLimiter;
32
+ readonly caller: CommerceRateLimiter;
33
+ }
34
+
35
+ interface CachedLimiters extends CommerceRateLimiters {
36
+ readonly configKey: string;
37
+ }
38
+
39
+ const callerContext = new AsyncLocalStorage<string | undefined>();
40
+ let cached: CachedLimiters | undefined;
41
+ let injected: CommerceRateLimiters | undefined;
42
+ let warnedProcessLocal = false;
43
+
44
+ /** Replace process-local counters with application-owned shared limiters. */
45
+ export function setCommerceRateLimiters(
46
+ value: CommerceRateLimiters | null,
47
+ ): void {
48
+ injected = value ?? undefined;
49
+ }
50
+
51
+ function positiveEnv(name: string, fallback: number): number {
52
+ const raw = process.env[name];
53
+ if (raw === undefined || !/^\d+$/u.test(raw)) return fallback;
54
+ const value = Number(raw);
55
+ return Number.isSafeInteger(value) && value > 0 ? value : fallback;
56
+ }
57
+
58
+ function limiters(): CommerceRateLimiters {
59
+ if (injected) return injected;
60
+ const environment = (
61
+ process.env.ENV ||
62
+ process.env.ENVIRONMENT ||
63
+ process.env.NODE_ENV ||
64
+ ""
65
+ )
66
+ .trim()
67
+ .toLowerCase();
68
+ if (
69
+ !warnedProcessLocal &&
70
+ !["dev", "development", "test"].includes(environment)
71
+ ) {
72
+ warnedProcessLocal = true;
73
+ console.warn(
74
+ "[seller-agent] this process is using process-local rate limits; " +
75
+ "inject shared limiters or enforce equivalent limits at a trusted edge before scaling out.",
76
+ );
77
+ }
78
+ const names = [
79
+ "SELLER_RATE_LIMIT_GLOBAL_MAX_REQUESTS",
80
+ "SELLER_RATE_LIMIT_CALLER_MAX_REQUESTS",
81
+ "SELLER_RATE_LIMIT_WINDOW_SECONDS",
82
+ "SELLER_RATE_LIMIT_MAX_CALLERS",
83
+ ] as const;
84
+ const configKey = names.map((name) => process.env[name] ?? "").join("\0");
85
+ if (cached?.configKey === configKey) return cached;
86
+
87
+ const windowSeconds = positiveEnv(
88
+ "SELLER_RATE_LIMIT_WINDOW_SECONDS",
89
+ DEFAULT_WINDOW_SECONDS,
90
+ );
91
+ cached = {
92
+ configKey,
93
+ global: new SlidingWindowLimiter(
94
+ positiveEnv("SELLER_RATE_LIMIT_GLOBAL_MAX_REQUESTS", DEFAULT_GLOBAL_MAX),
95
+ windowSeconds,
96
+ 2,
97
+ ),
98
+ caller: new SlidingWindowLimiter(
99
+ positiveEnv("SELLER_RATE_LIMIT_CALLER_MAX_REQUESTS", DEFAULT_CALLER_MAX),
100
+ windowSeconds,
101
+ positiveEnv("SELLER_RATE_LIMIT_MAX_CALLERS", DEFAULT_MAX_CALLERS),
102
+ ),
103
+ };
104
+ return cached;
105
+ }
106
+
107
+ async function checkLimiter(
108
+ limiter: CommerceRateLimiter,
109
+ key: string,
110
+ ): Promise<void> {
111
+ let timer: ReturnType<typeof setTimeout> | undefined;
112
+ const controller = new AbortController();
113
+ try {
114
+ await Promise.race([
115
+ Promise.resolve(limiter.check(key, controller.signal)),
116
+ new Promise<never>((_resolve, reject) => {
117
+ timer = setTimeout(
118
+ () => {
119
+ controller.abort();
120
+ reject(new RateLimitExceeded("Seller rate limiter unavailable"));
121
+ },
122
+ SHARED_LIMITER_TIMEOUT_MS,
123
+ );
124
+ timer.unref?.();
125
+ }),
126
+ ]);
127
+ } finally {
128
+ if (timer !== undefined) clearTimeout(timer);
129
+ }
130
+ }
131
+
132
+ function trustedCaller(headers: Request["headers"]): string | undefined {
133
+ const header = (process.env.SELLER_TRUSTED_CALLER_HEADER ?? "")
134
+ .trim()
135
+ .toLowerCase();
136
+ if (!/^[a-z0-9-]+$/u.test(header)) return undefined;
137
+
138
+ const raw = headers[header];
139
+ if (typeof raw !== "string") return undefined;
140
+ const value = raw.trim();
141
+ if (value.length === 0 || value.length > 256 || /[\r\n]/u.test(value)) {
142
+ return undefined;
143
+ }
144
+ return value;
145
+ }
146
+
147
+ /** Carry only an operator-configured, edge-authenticated identity. */
148
+ export function withTrustedCaller<T>(
149
+ headers: Request["headers"],
150
+ work: () => T,
151
+ ): T {
152
+ return callerContext.run(trustedCaller(headers), work);
153
+ }
154
+
155
+ /** Express middleware that makes the trusted identity available to handlers. */
156
+ export function requestLimitContext(
157
+ req: Request,
158
+ _res: Response,
159
+ next: NextFunction,
160
+ ): void {
161
+ withTrustedCaller(req.headers, next);
162
+ }
163
+
164
+ /** Consume both the mandatory process bucket and optional caller bucket. */
165
+ export async function limitCommerceOperation(
166
+ operation: CommerceOperation,
167
+ ): Promise<void> {
168
+ const active = limiters();
169
+ await checkLimiter(active.global, operation);
170
+ const caller = callerContext.getStore();
171
+ if (caller !== undefined) {
172
+ await checkLimiter(active.caller, `${operation}:${caller}`);
173
+ }
174
+ }
175
+
176
+ export function isCommerceRateLimitError(error: unknown): boolean {
177
+ return error instanceof RateLimitExceeded;
178
+ }
@@ -40,6 +40,7 @@
40
40
  import { ERC8183JobOps } from "@bnbagent/sdk/erc8183";
41
41
  import { SubmitPermanentlyUnsupportedError } from "@bnbagent/studio-runtime/erc8183";
42
42
  import { getWallet } from "@bnbagent/studio-runtime/wallet";
43
+ import { limitCommerceOperation } from "./requestLimits.js";
43
44
  import * as defaultSigning from "./signing.js";
44
45
 
45
46
  const log = {
@@ -227,6 +228,7 @@ export class SellerCore {
227
228
  data: Record<string, unknown>,
228
229
  ): Promise<Record<string, unknown>> {
229
230
  this.requireCommerceRail();
231
+ await limitCommerceOperation("negotiate");
230
232
  let request = data.request;
231
233
  if (request === null || typeof request !== "object" || Array.isArray(request)) {
232
234
  const picked: Record<string, unknown> = {};
@@ -262,6 +264,7 @@ export class SellerCore {
262
264
  data: Record<string, unknown>,
263
265
  ): Promise<Record<string, unknown>> {
264
266
  this.requireCommerceRail();
267
+ await limitCommerceOperation("notify_funded");
265
268
  const raw = data.job_id;
266
269
  if (raw === undefined || raw === null || String(raw) === "") {
267
270
  this.spawn(() => this.sweep()); // bare notify → just scan stragglers
@@ -95,6 +95,7 @@ import express from "express";
95
95
  import { buildAgentCard } from "./agentCard.js";
96
96
  import { SellerAgentExecutor } from "./executor.js";
97
97
  import { buildModel } from "./model.js";
98
+ import { requestLimitContext } from "./requestLimits.js";
98
99
  import type { RunWork } from "./sellerCore.js";
99
100
  import { LLM_READ_TOOLS } from "./tools.js";
100
101
 
@@ -500,6 +501,7 @@ async function main(): Promise<void> {
500
501
  );
501
502
 
502
503
  const app = express();
504
+ app.use(requestLimitContext);
503
505
 
504
506
  // GET /ping status fed to AgentCore: HEALTHY_BUSY while a background
505
507
  // delivery is in flight, else HEALTHY.
@@ -29,7 +29,7 @@ node = [
29
29
  # on either cloud.
30
30
  "@aws-sdk/client-secrets-manager@^3.600.0",
31
31
  "@bnbagent/studio-runtime",
32
- "@bnbagent/sdk@0.5.3",
32
+ "@bnbagent/sdk@0.5.5",
33
33
  # The LLM work hook (generateText + tools) and the model factory
34
34
  # (model.ts buildModel — studio.toml [llm] + the provider key env).
35
35
  "ai@^7.0.29",
@@ -7,7 +7,6 @@ node = ["@bnbagent/studio-runtime"]
7
7
 
8
8
  [dependencies.altana]
9
9
  node = ["@altananetwork/sdk@0.7.1"]
10
-
11
10
  [dependencies.turnkey]
12
11
  node = ["@turnkey/sdk-server@8.1.0", "@turnkey/viem@0.14.34"]
13
12
 
@@ -6,7 +6,7 @@ status = "v0.0.x"
6
6
  [dependencies]
7
7
  node = [
8
8
  "@bnbagent/studio-runtime",
9
- "@bnbagent/sdk@0.5.3",
9
+ "@bnbagent/sdk@0.5.5",
10
10
  "ai@^7.0.29", # AI SDK `tool` wrappers around the buyer functions
11
11
  "zod@^3.25.0", # tool input schemas
12
12
  ]
@@ -74,7 +74,7 @@ Tune the price in `app/agent/studio.toml` (`[payments.erc8183]` `min_price`/ `ma
74
74
 
75
75
  Use `bag config set payments.erc8183.price 0` only for an explicit FREE product decision. Studio stores ERC-8183 amounts as decimal strings and reports FREE in `bag doctor`; the canonical stack supports zero funding. If a custom deployment is selected, set all three `ERC8183_*_ADDRESS` overrides from that same stack. The buyer still runs `setBudget(0)` and `fund(0)`, but no ERC-20 approval or token escrow occurs. Require `bag doctor` and `bag deploy prepare` to pass.
76
76
 
77
- For an X402 face, choose its request price independently. Use `bag config set payments.x402_seller.price_usd 0` only when the existing agent is intentionally becoming an unrestricted anonymous FREE API. This path bypasses B402 verify/settle, payment, and settlement audit; it needs no merchant credentials and Studio will not synchronize any configured B402 secrets. Positive prices retain the paid B402 onboarding and settle-before-work flow. Verify the choice with `bag x402 sell status`, `bag doctor`, and `bag deploy prepare`.
77
+ For an X402 face, choose its request price independently. Use `bag config set payments.b402_seller.price_usd 0` only when the existing agent is intentionally becoming an unrestricted anonymous FREE API. This path bypasses B402 verify/settle, payment, and settlement audit; it needs no merchant credentials and Studio will not synchronize any configured B402 secrets. Positive prices retain the paid B402 onboarding and settle-before-work flow. Verify the choice with `bag x402 sell status`, `bag doctor`, and `bag deploy prepare`.
78
78
 
79
79
  ## Step 4c - LLM credit continuity (automatic, NOT an LLM tool)
80
80
 
@@ -164,7 +164,7 @@ JobStatus enum: `OPEN` (0) → `FUNDED` (1) → `SUBMITTED` (2) → `COMPLETED`
164
164
  | Job stays `FUNDED`, never reaches `SUBMITTED` after an `accepted` ack | Background delivery failed (`runWork` / `submitResult` raised) - **not** visible in the A2A reply | The ack only confirms verify passed; delivery runs in the background. Observe the failure via the chain (job never leaves `FUNDED`) + CloudWatch logs; a later `notify_funded` re-attempts it via the sweep |
165
165
  | `ERC8183JobOps` has no such export from `@bnbagent/sdk` | package.json pinned an old `@bnbagent/sdk` (missing class) | Bump the dependency and reinstall |
166
166
  | ERC-8183 contract override is incomplete | Only one or two of commerce/router/policy were selected | Set or remove all three together; never mix stacks |
167
- | `/x402` is public without a 402 challenge | `payments.x402_seller.price_usd = "0"` selected anonymous FREE passthrough | If payment is intended, set a positive decimal price, configure the complete B402 credential set, rerun `bag doctor`, and redeploy |
167
+ | `/x402` is public without a 402 challenge | `payments.b402_seller.price_usd = "0"` selected anonymous FREE passthrough | If payment is intended, set a positive decimal price, configure the complete B402 credential set, rerun `bag doctor`, and redeploy |
168
168
  | B402 credentials are missing but x402 reports FREE | Expected: FREE bypasses B402 and does not synchronize its secrets | No credential fix is needed; change to a positive price only when the route should charge |
169
169
  | `OPENROUTER_API_KEY env var is required` | Loading the entrypoint triggers the emitted `buildModel()` factory | Set the env var even for `bag dev --help` smoke |
170
170
  | RPC `limit exceeded` | Public RPC throttle | Retry, or set `STUDIO_BSC_TESTNET_RPC=<private rpc>` |
@@ -14,7 +14,7 @@ Use this playbook to activate the x402 seller rail for one agent. First choose P
14
14
  - The agent wallet already exists. In PAID mode its address receives U.
15
15
  - The project targets the managed platform or self-hosted AgentCore (azure-foundry cannot activate the rail).
16
16
  - PAID managed platform only: an interactive GitHub-login session from `bag platform login` is available for reading the platform Relay egress IPs. A `bnbk_…` CI token does not satisfy this endpoint's GitHub-user check.
17
- - `[payments.x402_seller]` exists. If not, run `bag x402 sell init`.
17
+ - `[payments.b402_seller]` exists. If not, run `bag x402 sell init`.
18
18
 
19
19
  The PAID application uses the **agent wallet address**, not a developer treasury, buyer wallet, or platform wallet.
20
20
 
@@ -25,7 +25,7 @@ Use one of these explicit boundaries:
25
25
  ```bash
26
26
  bag init <name> --rails b402 --b402-price 0
27
27
  bag x402 sell init --price-usd 0
28
- bag config set payments.x402_seller.price_usd 0
28
+ bag config set payments.b402_seller.price_usd 0
29
29
  ```
30
30
 
31
31
  `"0"` means anonymous FREE passthrough. The runtime returns work directly and does not issue a 402 challenge, call B402 `/supported`/verify/settle, transfer U, or write an `x402_sell` settlement audit. B402 credentials are ignored and not synchronized. Run `bag x402 sell status`, `bag doctor`, and `bag deploy prepare`; all must label the route FREE.
@@ -161,7 +161,7 @@ On the managed platform the deploy summary must say `x402 rail is ACTIVE` (or `A
161
161
  - Never log or print any B402 value or private key.
162
162
  - Never put a B402 value in `studio.toml` or a deploy descriptor.
163
163
  - Never replay a paid HTTP request whose outcome is unknown. Follow `docs/guides/x402-selling.md` and reconcile `(nonce, network, payer)` first.
164
- - Binance `/settle` is asynchronous. A parseable `success: false` response with a transaction is pending and requires an idempotent poll with the same settlement payload. Studio v0.0.6 and the latest `@bnb-chain/b402@0.1.0` do not yet perform that poll; they classify pending as unknown. Do not claim current mainnet readiness until this is updated.
164
+ - Binance `/settle` is asynchronous. A parseable `success: false` response with a transaction is pending and requires an idempotent poll with the same settlement payload. Studio with `@bnb-chain/b402@0.2.1` does not yet perform that poll; it classifies pending as unknown. Version 0.2.1 separately guards credential replays through an atomic store. Do not claim current mainnet readiness until polling is implemented.
165
165
  - Settlement happens before work. A later work failure retains the payment and does not trigger an automatic refund.
166
166
  - The rail activates on AgentCore and Azure Foundry targets (managed platform or self-hosted); self-hosted targets have no anonymous URL and need an operator-run envelope-v1 front.
167
167
  - Every supported `wallet.kind` can be the PAID B402 payout wallet: `evm-local`, `twak`, `turnkey`, and `altana`. The payout lands at the configured `pay_to` or, by default, `[wallet].address`; for altana that address is the admin EOA (EIP-7702 — the smart account address equals the admin address), so the locally-held admin keystore can always move the revenue. Register the B402 merchant credentials for that exact address. FREE x402 bypasses B402 and has no payout.
@@ -11,6 +11,7 @@ Altana separates trusted administration from runtime authority:
11
11
  - `.studio/wallets/altana-session.json` is the one bounded, expiring runtime session and must stay mode `0600`.
12
12
  - `WALLET_PASSWORD` is admin-only. The Agent gets `ALTANA_SESSION`, never the password or admin keystore.
13
13
  - Generic signing is refused. ERC-8183 uses `sessionQuoteSigner()` and the approved quote checker.
14
+ - The generated project pins `@bnbagent/sdk@0.5.5` and `@altananetwork/sdk@0.7.1`; doctor, readiness, and runtime loading reject version drift. SDK 0.5.4 introduced selector-bound calls, removed session-key token approvals, and requires an admin-provisioned bounded Commerce allowance. Projects upgrading from an older SDK must update it, re-grant with `bag wallet session grant --force`, and redeploy.
14
15
  - Deployment ships ONLY the serialized session as the `ALTANA_SESSION` runtime secret; the admin keystore and `WALLET_PASSWORD` never leave the operator machine. Renewal after expiry: `bag wallet session grant --force`, then re-run `bag deploy`. Readiness fails on a missing/expired/address-mismatched session, a group/world-readable session file, a session inside the artifact root, or an unresolvable project-local `@altananetwork/sdk`; it warns under 7 days remaining. `bag deploy verify` needs `--skip-register` (no generic signing for the ERC-8004 register).
15
16
  - Altana refuses generic message signing, so Pieverse SIWE cannot authenticate `bag llm activate` or runtime credit renewal. `bag init --wallet-kind altana --llm-provider pieverse-llm` is rejected outright; use OpenRouter, OpenAI, or Anthropic (API-key providers). `bag llm activate` and `bag doctor` also flag the combination on projects edited by hand.
16
17
 
@@ -34,13 +35,13 @@ bag dev
34
35
 
35
36
  Interactive grant recommendations are 10 U/day, 30 days, register=yes. In a non-TTY, pass `--budget-u`, `--expiry-days`, optional `--no-register`, and `--yes`. Stdout from a successful grant is only the session public key.
36
37
 
37
- If quote-checker approval fails after the paid grant, the owner-only session file is preserved. Repair it with:
38
+ The grant persists the owner-only session, provisions a Commerce allowance no higher than its U-token cap, and approves the quote checker. Every relay-backed management write must return `CONFIRMED`; `PENDING` fails closed and preserves the session file. If either setup step fails after the paid grant, repair both with:
38
39
 
39
40
  ```bash
40
41
  bag wallet session grant --approve-only
41
42
  ```
42
43
 
43
- Replace with `grant --force`; the old on-chain revoke must succeed before the new grant begins. Revoke with `bag wallet session revoke --yes`; the file is deleted only after chain success.
44
+ `--approve-only`, runtime loading, and deploy readiness reject pre-0.5.4 target-wide sessions. Replace one with `grant --force`; Studio zeros the old Commerce allowance before revoking, and both operations must be confirmed before the new grant begins. Revoke with `bag wallet session revoke --yes`; the file is deleted only after both confirmations.
44
45
 
45
46
  x402 buying remains separate and exact-bounded:
46
47