@bnbagent/studio-cli 0.0.13-alpha.7 → 0.0.13

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 (29) hide show
  1. package/dist/_agentcoreName-DZDWEYD3.js +0 -0
  2. package/dist/_twak-4XF4H5PL.js +0 -0
  3. package/dist/bag.js +353 -71
  4. package/dist/{chunk-YZ4WH5MD.js → chunk-H4X2OOLA.js} +59 -17
  5. package/dist/chunk-RO726HJG.js +0 -0
  6. package/dist/chunk-U7IDQ3K5.js +0 -0
  7. package/dist/{deployCli-A4EW5FTF.js → deployCli-22NMZ4G7.js} +3 -1
  8. package/package.json +15 -13
  9. package/recipes/agent/code/{{PKG}}/signing.ts.tmpl +2 -2
  10. package/recipes/agent/recipe.toml +1 -1
  11. package/recipes/mpp-buyer/recipe.toml +1 -1
  12. package/recipes/runtimes/agentcore/code/{{PKG}}/dualMain.ts.tmpl +2 -0
  13. package/recipes/runtimes/agentcore/code/{{PKG}}/executor.ts.tmpl +14 -6
  14. package/recipes/runtimes/agentcore/code/{{PKG}}/mcpMain.ts.tmpl +63 -30
  15. package/recipes/runtimes/agentcore/code/{{PKG}}/requestLimits.ts.tmpl +178 -0
  16. package/recipes/runtimes/agentcore/code/{{PKG}}/sellerCore.ts.tmpl +3 -0
  17. package/recipes/runtimes/agentcore/code/{{PKG}}/unifiedMain.ts.tmpl +2 -0
  18. package/recipes/runtimes/agentcore/recipe.toml +1 -1
  19. package/recipes/runtimes/azure-foundry/code/{{PKG}}/executor.ts.tmpl +14 -6
  20. package/recipes/runtimes/azure-foundry/code/{{PKG}}/mcpMain.ts.tmpl +63 -30
  21. package/recipes/runtimes/azure-foundry/code/{{PKG}}/requestLimits.ts.tmpl +178 -0
  22. package/recipes/runtimes/azure-foundry/code/{{PKG}}/sellerCore.ts.tmpl +3 -0
  23. package/recipes/runtimes/azure-foundry/code/{{PKG}}/unifiedMain.ts.tmpl +2 -0
  24. package/recipes/runtimes/azure-foundry/recipe.toml +1 -1
  25. package/recipes/wallet/recipe.toml +0 -1
  26. package/recipes/x402-buyer/recipe.toml +1 -1
  27. package/skills/references/bnbagent-studio-selling-via-b402.md +1 -1
  28. package/skills/references/bnbagent-studio-using-altana-wallet.md +3 -2
  29. package/LICENSE +0 -201
@@ -9,6 +9,7 @@ import {
9
9
 
10
10
  // src/cli/_deploy/deployCli.ts
11
11
  import * as fs5 from "fs";
12
+ import { createRequire } from "module";
12
13
  import * as os from "os";
13
14
  import * as path6 from "path";
14
15
  import {
@@ -415,10 +416,10 @@ function packageVersionOf(entry, packageName) {
415
416
  async function pack(buildRoot, outZip) {
416
417
  fs.rmSync(outZip, { force: true });
417
418
  const ZipArchive = await loadZipArchive();
418
- await new Promise((resolve, reject) => {
419
+ await new Promise((resolve2, reject) => {
419
420
  const output = fs.createWriteStream(outZip);
420
421
  const archive = new ZipArchive({ zlib: { level: 9 } });
421
- output.on("close", () => resolve());
422
+ output.on("close", () => resolve2());
422
423
  archive.on("error", (err) => reject(err));
423
424
  archive.pipe(output);
424
425
  for (const file of listFiles(buildRoot).sort()) {
@@ -571,6 +572,21 @@ function printErr(line) {
571
572
  `);
572
573
  }
573
574
 
575
+ // src/cli/_deploy/commandOverride.ts
576
+ function isVitestProcess(environment, argv) {
577
+ return environment === process.env && environment.VITEST === "true" && argv.some(
578
+ (arg) => /(?:^|[/\\])vitest(?:\.mjs|\.js)?$/u.test(arg) || /(?:^|[/\\])tinypool[/\\]dist[/\\]entry[/\\](?:process|worker)\.js$/u.test(
579
+ arg
580
+ )
581
+ );
582
+ }
583
+ function deployCommandOverrideAllowed(environment = process.env, argv = process.argv) {
584
+ if (isVitestProcess(environment, argv)) return true;
585
+ const ci = (environment.CI ?? "").trim().toLowerCase();
586
+ const inCi = ci !== "" && !["0", "false", "no"].includes(ci);
587
+ return !inCi && ["development", "test"].includes(environment.NODE_ENV ?? "");
588
+ }
589
+
574
590
  // src/cli/preflight.ts
575
591
  var OK = "ok";
576
592
  var WARN = "warn";
@@ -615,29 +631,30 @@ function agentcoreFlavor(binPath) {
615
631
  async function checkBunx() {
616
632
  const override = (process.env.BNBAGENT_DEPLOY_COMMAND ?? "").trim();
617
633
  if (override) {
634
+ const allowed = deployCommandOverrideAllowed();
618
635
  return {
619
636
  name: "bnbagent-deploy",
620
- ok: true,
621
- level: OK,
622
- detail: `command overridden via BNBAGENT_DEPLOY_COMMAND (${override})`,
623
- fix: ""
637
+ ok: allowed,
638
+ level: allowed ? OK : WARN,
639
+ detail: allowed ? `local development/test command override (${override})` : "BNBAGENT_DEPLOY_COMMAND is disabled outside local development/tests and in CI",
640
+ fix: allowed ? "" : "unset BNBAGENT_DEPLOY_COMMAND"
624
641
  };
625
642
  }
626
- const found = await whichBin("bunx");
643
+ const found = await whichBin("bun");
627
644
  if (found === null) {
628
645
  return {
629
646
  name: "bnbagent-deploy",
630
647
  ok: false,
631
648
  level: WARN,
632
- detail: "bunx not found (deploys delegate to the pinned @bnbagent/deploy-cli via bunx)",
633
- fix: "install Bun 1.3+ (https://bun.sh) or set BNBAGENT_DEPLOY_COMMAND"
649
+ detail: "bun not found (required by the declared @bnbagent/deploy-cli dependency)",
650
+ fix: "install Bun 1.3+ (https://bun.sh)"
634
651
  };
635
652
  }
636
653
  return {
637
654
  name: "bnbagent-deploy",
638
655
  ok: true,
639
656
  level: OK,
640
- detail: `bunx on PATH (${found}); deploys delegate to the pinned @bnbagent/deploy-cli`,
657
+ detail: `bun on PATH (${found}); deploys use the declared @bnbagent/deploy-cli dependency`,
641
658
  fix: ""
642
659
  };
643
660
  }
@@ -791,8 +808,8 @@ function packageRoot() {
791
808
  }
792
809
  }
793
810
  function studioCliVersion() {
794
- if ("0.0.13-alpha.7") {
795
- return "0.0.13-alpha.7";
811
+ if ("0.0.13") {
812
+ return "0.0.13";
796
813
  }
797
814
  const file = path3.join(packageRoot(), "package.json");
798
815
  const pkg = JSON.parse(fs3.readFileSync(file, "utf-8"));
@@ -1035,7 +1052,7 @@ function x402DeploySummaryFromSnapshot(snapshot, runtime, destination, publicUrl
1035
1052
  const settlement = [
1036
1053
  "B402 settlement can add significant latency; buyer timeout must be at least 120 s.",
1037
1054
  "Settlement happens before work. If work later fails, the payment is retained and is not refunded automatically.",
1038
- "B402 classifies an asynchronous pending response or other ambiguous settlement outcome as unknown; reconcile it and do not replay the paid request."
1055
+ "Compatibility note: @bnb-chain/b402@0.2.1 guards credential replays, but an asynchronous pending response or otherwise ambiguous settlement is outcome unknown; reconcile it and do not replay the paid request."
1039
1056
  ];
1040
1057
  if (protocol === "mpp") {
1041
1058
  settlement.push(
@@ -1073,6 +1090,21 @@ function isTable(value) {
1073
1090
  // src/cli/_deploy/deployCli.ts
1074
1091
  var DEPLOY_CLI_VERSION = "0.5.15";
1075
1092
  var DEPLOY_CLI_PACKAGE = `@bnbagent/deploy-cli@${DEPLOY_CLI_VERSION}`;
1093
+ var require2 = createRequire(import.meta.url);
1094
+ function resolveLocalDeployCli() {
1095
+ try {
1096
+ const manifest = require2.resolve("@bnbagent/deploy-cli/package.json");
1097
+ const pkg = JSON.parse(fs5.readFileSync(manifest, "utf-8"));
1098
+ if (pkg.version !== DEPLOY_CLI_VERSION) return null;
1099
+ const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["bnbagent-deploy"];
1100
+ if (typeof bin !== "string") return null;
1101
+ const entry = path6.resolve(path6.dirname(manifest), bin);
1102
+ return fs5.statSync(entry).isFile() ? entry : null;
1103
+ } catch {
1104
+ return null;
1105
+ }
1106
+ }
1107
+ var LOCAL_DEPLOY_CLI_ENTRY = resolveLocalDeployCli();
1076
1108
  var BNB_PLATFORM_API_URL = "https://bnbagent-api.bnbchain.world";
1077
1109
  var BNB_PLATFORM_API_URL_ENV = "BNBAGENT_API_URL";
1078
1110
  var LEGACY_BNB_PLATFORM_API_URL_ENV = "BAG_PLATFORM_API_BASE";
@@ -1135,8 +1167,16 @@ function bnbPlatformApiUrl(environment = process.env) {
1135
1167
  function deployCommand(environment = process.env) {
1136
1168
  const override = (environment.BNBAGENT_DEPLOY_COMMAND ?? "").trim();
1137
1169
  if (override) {
1170
+ if (!deployCommandOverrideAllowed(environment)) {
1171
+ throw new Error(
1172
+ "BNBAGENT_DEPLOY_COMMAND is allowed only for local development/tests and is disabled in CI"
1173
+ );
1174
+ }
1138
1175
  return override.split(/\s+/u);
1139
1176
  }
1177
+ if (LOCAL_DEPLOY_CLI_ENTRY !== null) {
1178
+ return ["bun", LOCAL_DEPLOY_CLI_ENTRY];
1179
+ }
1140
1180
  return ["bunx", "--bun", DEPLOY_CLI_PACKAGE];
1141
1181
  }
1142
1182
  function providerPassthrough(studio, table4) {
@@ -1351,11 +1391,11 @@ async function withDeployFiles(root, opts, fn) {
1351
1391
  fs5.rmSync(scratch, { recursive: true, force: true });
1352
1392
  }
1353
1393
  }
1354
- var BUNX_HINT = "error: could not start bnbagent-deploy (bunx not found); install Bun 1.3+ or set BNBAGENT_DEPLOY_COMMAND.";
1394
+ var DEPLOY_CLI_HINT = "error: could not start the lockfile-pinned bnbagent-deploy; install Bun 1.3+ and reinstall @bnbagent/studio-cli.";
1355
1395
  async function ensureRunnable(environment = process.env) {
1356
1396
  const [bin] = deployCommand(environment);
1357
1397
  if (bin && await whichBin(bin) === null) {
1358
- printErr(BUNX_HINT);
1398
+ printErr(DEPLOY_CLI_HINT);
1359
1399
  return false;
1360
1400
  }
1361
1401
  return true;
@@ -1365,8 +1405,8 @@ var BUNX_TREE_HINT = [
1365
1405
  `error: ${DEPLOY_CLI_PACKAGE} could not load its own dependencies \u2014 its `,
1366
1406
  "bunx install directory is incomplete. Remove it and retry:\n",
1367
1407
  ' rm -rf "${TMPDIR:-/tmp}"/bunx-*-@bnbagent\n',
1368
- "If it persists, clear Bun's package cache (`bun pm cache rm`), or point ",
1369
- "BNBAGENT_DEPLOY_COMMAND at a local checkout."
1408
+ "If it persists, clear Bun's package cache (`bun pm cache rm`) and reinstall ",
1409
+ "@bnbagent/studio-cli."
1370
1410
  ].join("");
1371
1411
  function bunxInstallDir(environment = process.env) {
1372
1412
  if (deployCommand(environment)[0] !== "bunx") {
@@ -1521,6 +1561,7 @@ export {
1521
1561
  x402SellerUsesB402,
1522
1562
  x402SellerIsFree,
1523
1563
  b402PaymentProtocol,
1564
+ deployCommandOverrideAllowed,
1524
1565
  whichBin,
1525
1566
  agentcoreFlavor,
1526
1567
  checkBunx,
@@ -1542,6 +1583,7 @@ export {
1542
1583
  x402DeploySummaryFromSnapshot,
1543
1584
  DEPLOY_CLI_VERSION,
1544
1585
  DEPLOY_CLI_PACKAGE,
1586
+ LOCAL_DEPLOY_CLI_ENTRY,
1545
1587
  BNB_PLATFORM_API_URL,
1546
1588
  BNB_PLATFORM_API_URL_ENV,
1547
1589
  LEGACY_BNB_PLATFORM_API_URL_ENV,
File without changes
File without changes
@@ -5,6 +5,7 @@ import {
5
5
  DEPLOY_CLI_PACKAGE,
6
6
  DEPLOY_CLI_VERSION,
7
7
  LEGACY_BNB_PLATFORM_API_URL_ENV,
8
+ LOCAL_DEPLOY_CLI_ENTRY,
8
9
  bnbEnv,
9
10
  bnbPlatformApiUrl,
10
11
  buildDeploySpec,
@@ -18,7 +19,7 @@ import {
18
19
  runPlatformAccountCommand,
19
20
  trialFromDeployCliJson,
20
21
  withDeployFiles
21
- } from "./chunk-YZ4WH5MD.js";
22
+ } from "./chunk-H4X2OOLA.js";
22
23
  import "./chunk-RO726HJG.js";
23
24
  export {
24
25
  BNB_PLATFORM_API_URL,
@@ -26,6 +27,7 @@ export {
26
27
  DEPLOY_CLI_PACKAGE,
27
28
  DEPLOY_CLI_VERSION,
28
29
  LEGACY_BNB_PLATFORM_API_URL_ENV,
30
+ LOCAL_DEPLOY_CLI_ENTRY,
29
31
  bnbEnv,
30
32
  bnbPlatformApiUrl,
31
33
  buildDeploySpec,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bnbagent/studio-cli",
3
- "version": "0.0.13-alpha.7",
3
+ "version": "0.0.13",
4
4
  "description": "Skills-first toolkit and bag CLI for BNB Chain seller agents: ERC-8004 identity, ERC-8183 escrowed commerce, and x402 payments.",
5
5
  "keywords": [
6
6
  "bnb-chain",
@@ -25,6 +25,7 @@
25
25
  "directory": "packages/studio-cli"
26
26
  },
27
27
  "type": "module",
28
+ "packageManager": "pnpm@10.24.0",
28
29
  "engines": {
29
30
  "node": ">=22"
30
31
  },
@@ -41,8 +42,18 @@
41
42
  "bin": {
42
43
  "bag": "./dist/bag.js"
43
44
  },
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "pretest": "pnpm --filter @bnbagent/studio-runtime build",
48
+ "test": "vitest run",
49
+ "lint": "biome check src tests",
50
+ "typecheck": "tsc --noEmit",
51
+ "check": "pnpm typecheck && pnpm lint && pnpm test && pnpm build"
52
+ },
44
53
  "dependencies": {
45
- "@bnbagent/sdk": "0.5.3",
54
+ "@bnbagent/deploy-cli": "0.5.15",
55
+ "@bnbagent/sdk": "0.5.5",
56
+ "@bnbagent/studio-runtime": "0.0.13",
46
57
  "ai": "^7.0.29",
47
58
  "archiver": "^8.0.0",
48
59
  "commander": "^15.0.0",
@@ -53,8 +64,7 @@
53
64
  "smol-toml": "^1.3.0",
54
65
  "tar": "^7.4.0",
55
66
  "viem": "^2.54.0",
56
- "yaml": "^2.9.0",
57
- "@bnbagent/studio-runtime": "0.0.13-alpha.7"
67
+ "yaml": "^2.9.0"
58
68
  },
59
69
  "devDependencies": {
60
70
  "@a2a-js/sdk": "^0.3.14",
@@ -70,13 +80,5 @@
70
80
  "typescript": "^5.5.0",
71
81
  "vitest": "^2.0.0",
72
82
  "zod": "^3.25.76"
73
- },
74
- "scripts": {
75
- "build": "tsup",
76
- "pretest": "pnpm --filter @bnbagent/studio-runtime build",
77
- "test": "vitest run",
78
- "lint": "biome check src tests",
79
- "typecheck": "tsc --noEmit",
80
- "check": "pnpm typecheck && pnpm lint && pnpm test && pnpm build"
81
83
  }
82
- }
84
+ }
@@ -229,8 +229,8 @@ export async function signQuote(
229
229
  estimatedCompletionSeconds: est,
230
230
  });
231
231
 
232
- // NegotiationHandler signs non-fatally: if signMessage failed it returns an
233
- // accepted result WITHOUT provider_sig. Never relay an unsigned "accepted".
232
+ // SDK 0.5.4 throws QuoteSigningError when signing fails. Keep this shape
233
+ // check as defense in depth for injected handlers and mixed deployments.
234
234
  if (result.accepted && (!result.negotiationHash || !result.providerSig)) {
235
235
  throw new Error(
236
236
  "quote accepted but provider_sig is missing (wallet sign failed); " +
@@ -11,7 +11,7 @@ node = [
11
11
  # neutral. The agent project's serving deps (@a2a-js/sdk / MCP SDK / ai)
12
12
  # come from the runtimes/<R>/ recipe selected at `bag init` time.
13
13
  "@bnbagent/studio-runtime",
14
- "@bnbagent/sdk@0.5.3",
14
+ "@bnbagent/sdk@0.5.5",
15
15
  ]
16
16
 
17
17
  [env]
@@ -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",
11
11
  "zod@^3.25.0",
12
12
  ]
@@ -80,6 +80,7 @@ import { buildAgentCard } from "./agentCard.js";
80
80
  import { SellerAgentExecutor } from "./executor.js";
81
81
  import { buildMcpServer } from "./mcpMain.js";
82
82
  import { buildModel } from "./model.js";
83
+ import { requestLimitContext } from "./requestLimits.js";
83
84
  import type { RunWork } from "./sellerCore.js";
84
85
  import { LLM_READ_TOOLS } from "./tools.js";
85
86
 
@@ -296,6 +297,7 @@ export async function buildDualApp(): Promise<{
296
297
  );
297
298
 
298
299
  const app = express();
300
+ app.use(requestLimitContext);
299
301
 
300
302
  // GET /ping status fed to AgentCore: HEALTHY_BUSY while a background
301
303
  // delivery is in flight, else HEALTHY.
@@ -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(