@bondedhq/sdk 0.1.6 → 0.1.7

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/README.md CHANGED
@@ -388,6 +388,27 @@ if (isBondedEpisode()) {
388
388
  `isBondedEpisode()` is true when the runner set valid `BONDED_*` variables. `fromBondedEnv` reads
389
389
  and checks them, naming every missing or malformed one (`BondedError`, code `EPISODE_ENV`).
390
390
 
391
+ That is the vault world, where ratings run. An agent that trades through a broker can also be
392
+ tested on the brokerage world (a simulated brokerage account, no vault; docs/BROKERAGE.md), with a
393
+ runner started with `--world brokerage`. Its episode is read with `fromBondedBrokerageEnv()`, and
394
+ `bondedEpisodeWorld()` says which world the runner set up:
395
+
396
+ ```ts
397
+ import { bondedEpisodeWorld, fromBondedBrokerageEnv, fromBondedEnv } from "@bondedhq/sdk";
398
+
399
+ const world = bondedEpisodeWorld(); // "vault", "brokerage", or undefined outside an episode
400
+ if (world === "brokerage") {
401
+ const episode = fromBondedBrokerageEnv(); // { world, episodeId, task, mode: "tools", endpoints, deadlineMs }
402
+ } else if (world === "vault") {
403
+ const episode = fromBondedEnv();
404
+ }
405
+ ```
406
+
407
+ `isBondedEpisode()` is true in both worlds, so an agent under test never falls through to its
408
+ production tools. In a brokerage episode `fromBondedEnv` and the vault tool helpers throw
409
+ `EPISODE_ENV` rather than guess. By Agent URL, `createBondedHandler` runs brokerage episodes with
410
+ `onBrokerageEpisode` and refuses them (422) without it.
411
+
391
412
  The mode decides how the agent acts:
392
413
 
393
414
  - **tools**: reads and writes are tool calls; `swap`, `transfer` and `approve` are tools, and the
@@ -1,5 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- import type { BondedEpisode } from "./episode.js";
2
+ import type { BondedBrokerageEpisode, BondedEpisode } from "./episode.js";
3
3
  import type { EpisodeUsage } from "./mcp.js";
4
4
  /** For your own handler: the proof a verify answer carries, and the whole answer. */
5
5
  export { agentUrlVerifyProof, answerAgentUrlVerify } from "@bondedhq/shared/agent-url";
@@ -40,6 +40,14 @@ export interface BondedHandlerOptions {
40
40
  secret: string | string[];
41
41
  /** Runs one episode. Resolve when the agent is finished; the handler then posts `/done`. */
42
42
  onEpisode(episode: BondedEpisode, context: EpisodeContext): Promise<EpisodeOutcome | void> | EpisodeOutcome | void;
43
+ /**
44
+ * Runs one brokerage episode (a simulated brokerage account; docs/BROKERAGE.md). Without it, the
45
+ * handler refuses brokerage episodes with 422, so an agent built for the vault world never gets
46
+ * one by surprise. Errors are passed to `onBrokerageError` (default `console.error`).
47
+ */
48
+ onBrokerageEpisode?(episode: BondedBrokerageEpisode, context: EpisodeContext): Promise<EpisodeOutcome | void> | EpisodeOutcome | void;
49
+ /** Called when `onBrokerageEpisode` throws (default: `console.error`). `/done` is still posted. */
50
+ onBrokerageError?: (error: unknown, episode: BondedBrokerageEpisode) => void;
43
51
  /**
44
52
  * Your agent's version (a release tag, a git commit). Reported when Bonded verifies the URL;
45
53
  * it becomes the rating's code hash when you don't declare one.
package/dist/agent-url.js CHANGED
@@ -32,17 +32,29 @@ export function createBondedHandler(options) {
32
32
  /** Signatures already accepted, until they fall out of the window: replays are refused. */
33
33
  const replay = new AgentUrlReplayGuard(tolerance);
34
34
  const running = new Map();
35
- const run = async (episode, controller) => {
35
+ const onBrokerageError = options.onBrokerageError ??
36
+ ((error, episode) => console.error(`Bonded brokerage episode ${episode.episodeId} failed:`, error));
37
+ const run = async (episode, controller, work, reportError) => {
36
38
  const deadline = setTimeout(() => controller.abort(new Error("the episode's deadline passed")), episode.deadlineMs);
39
+ // A reporter that throws must not stop `/done` from being posted: the gateway would wait out
40
+ // the deadline for an episode that has already failed.
41
+ const report = (error) => {
42
+ try {
43
+ reportError(error, episode);
44
+ }
45
+ catch (reporterError) {
46
+ console.error(`Bonded episode ${episode.episodeId}: the error reporter threw:`, reporterError);
47
+ }
48
+ };
37
49
  let outcome;
38
50
  let failed = false;
39
51
  try {
40
- outcome = await options.onEpisode(episode, { signal: controller.signal });
52
+ outcome = await work(episode, { signal: controller.signal });
41
53
  }
42
54
  catch (error) {
43
55
  failed = true;
44
56
  if (!controller.signal.aborted)
45
- onError(error, episode);
57
+ report(error);
46
58
  outcome = {
47
59
  finalMessage: `the agent failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 4_000),
48
60
  };
@@ -64,7 +76,7 @@ export function createBondedHandler(options) {
64
76
  }
65
77
  catch (error) {
66
78
  if (!failed)
67
- onError(error, episode);
79
+ report(error);
68
80
  }
69
81
  };
70
82
  return async (request, context) => {
@@ -92,20 +104,25 @@ export function createBondedHandler(options) {
92
104
  controller?.abort(new Error("Bonded cancelled the episode"));
93
105
  return json(200, { ok: true, cancelled: controller !== undefined });
94
106
  }
95
- const episode = toEpisode(message);
96
- if (running.has(episode.episodeId))
97
- return json(200, { ok: true, episodeId: episode.episodeId, alreadyRunning: true });
107
+ if (message.world === "brokerage" && !options.onBrokerageEpisode)
108
+ return json(422, {
109
+ error: "this agent doesn't take brokerage episodes (no onBrokerageEpisode)",
110
+ });
111
+ if (running.has(message.episodeId))
112
+ return json(200, { ok: true, episodeId: message.episodeId, alreadyRunning: true });
98
113
  if (running.size >= maxConcurrent)
99
114
  return json(429, { error: `already running ${running.size} episodes` });
100
115
  const controller = new AbortController();
101
- running.set(episode.episodeId, controller);
102
- const work = run(episode, controller);
116
+ running.set(message.episodeId, controller);
117
+ const work = message.world === "brokerage"
118
+ ? run(toBrokerageEpisode(message), controller, (e, c) => options.onBrokerageEpisode(e, c), onBrokerageError)
119
+ : run(toEpisode(message), controller, (e, c) => options.onEpisode(e, c), onError);
103
120
  // Called as a method: Cloudflare's ctx.waitUntil needs its `this`.
104
121
  if (context?.waitUntil)
105
122
  context.waitUntil(work);
106
123
  else
107
124
  options.waitUntil?.(work);
108
- return json(202, { ok: true, episodeId: episode.episodeId });
125
+ return json(202, { ok: true, episodeId: message.episodeId });
109
126
  };
110
127
  }
111
128
  /**
@@ -122,6 +139,17 @@ async function signingSecret(secrets, body, headers, nowMs, toleranceSeconds) {
122
139
  }
123
140
  return secrets[0];
124
141
  }
142
+ /** A brokerage episode as the agent sees it (the same shape `fromBondedBrokerageEnv` returns). */
143
+ function toBrokerageEpisode(message) {
144
+ return {
145
+ world: "brokerage",
146
+ episodeId: message.episodeId,
147
+ task: message.task,
148
+ mode: "tools",
149
+ endpoints: message.endpoints,
150
+ deadlineMs: message.deadlineMs,
151
+ };
152
+ }
125
153
  /** The episode as the agent sees it (the same shape `fromBondedEnv` returns). */
126
154
  function toEpisode(message) {
127
155
  // The runner protocol's message, minus `type`, `nonce` and `teeKeyId` (TEE-bound agents use
package/dist/episode.d.ts CHANGED
@@ -41,6 +41,7 @@ export interface BondedEpisode {
41
41
  * next to `episodeEnv` that writes them, so the runner, the gateway and the SDK can't drift.
42
42
  */
43
43
  export declare const bondedEpisodeEnv: import("zod").ZodObject<{
44
+ BONDED_WORLD: import("zod").ZodOptional<import("zod").ZodLiteral<"vault">>;
44
45
  BONDED_EPISODE_ID: import("zod").ZodString;
45
46
  BONDED_MODE: import("zod").ZodEnum<{
46
47
  tools: "tools";
@@ -57,6 +58,7 @@ export declare const bondedEpisodeEnv: import("zod").ZodObject<{
57
58
  approvalAllowlist: import("zod").ZodArray<import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodTransform<`0x${string}`, string>>>;
58
59
  }, import("zod/v4/core").$strip>>;
59
60
  maxSteps: import("zod").ZodNumber;
61
+ tools: import("zod").ZodOptional<import("zod").ZodReadonly<import("zod").ZodArray<import("zod").ZodString>>>;
60
62
  }, import("zod/v4/core").$strip>>;
61
63
  BONDED_MCP_URL: import("zod").ZodURL;
62
64
  BONDED_TOOLS_URL: import("zod").ZodURL;
@@ -84,6 +86,11 @@ export declare const bondedEpisodeEnv: import("zod").ZodObject<{
84
86
  * ```ts
85
87
  * const tools = isBondedEpisode() ? await bondedLangChainTools() : productionTools;
86
88
  * ```
89
+ *
90
+ * True in every world, so an agent under test never falls through to its production tools. In a
91
+ * world the agent wasn't built for, the helpers then fail loudly: in a brokerage episode
92
+ * `fromBondedEnv` and the vault tool helpers throw `EPISODE_ENV`. `bondedEpisodeWorld()` says
93
+ * which world it is.
87
94
  */
88
95
  export declare function isBondedEpisode(env?: Record<string, string | undefined>): boolean;
89
96
  /**
@@ -105,4 +112,48 @@ export declare function fromBondedEnv(env?: Record<string, string | undefined>):
105
112
  * or when the key is held by a TEE (no session key): use your own signer there.
106
113
  */
107
114
  export declare function episodeVault(episode: BondedEpisode): BondedVault;
115
+ /**
116
+ * A brokerage episode: the agent manages a simulated brokerage account (stocks, filings, orders,
117
+ * cash withdrawals) through the episode's tools. There is no chain, vault or key: every read and
118
+ * write is a tool call, and the Arena judges it by the brokerage ledger. Needs runner protocol 2
119
+ * (`@bondedhq/runner` 0.1.4 or later), started with `--world brokerage`.
120
+ */
121
+ export interface BondedBrokerageEpisode {
122
+ world: "brokerage";
123
+ episodeId: string;
124
+ task: AgentTask;
125
+ mode: "tools";
126
+ endpoints: {
127
+ /** MCP over streamable HTTP. */
128
+ mcp: string;
129
+ /** Plain HTTP: `GET {tools}` lists them, `POST {tools}/{name}` with JSON args calls one. */
130
+ tools: string;
131
+ openapi: string;
132
+ /** `POST` here when finished (`{ finalMessage? }`). */
133
+ done: string;
134
+ };
135
+ /** How long the agent has, in milliseconds from when it starts. */
136
+ deadlineMs: number;
137
+ }
138
+ /**
139
+ * Which world the runner's episode is for: `vault` (on-chain trading from a Mandate Vault, read
140
+ * with `fromBondedEnv`), `brokerage` (read with `fromBondedBrokerageEnv`), or undefined when this
141
+ * process isn't running a Bonded episode.
142
+ *
143
+ * ```ts
144
+ * const world = bondedEpisodeWorld();
145
+ * const tools =
146
+ * world === "brokerage"
147
+ * ? await myBrokerageTools(fromBondedBrokerageEnv())
148
+ * : world === "vault"
149
+ * ? await bondedLangChainTools()
150
+ * : productionTools;
151
+ * ```
152
+ */
153
+ export declare function bondedEpisodeWorld(env?: Record<string, string | undefined>): "vault" | "brokerage" | undefined;
154
+ /**
155
+ * Reads a brokerage episode the Bonded runner set up from the environment. Throws `BondedError`
156
+ * (`EPISODE_ENV`) naming every missing or malformed variable.
157
+ */
158
+ export declare function fromBondedBrokerageEnv(env?: Record<string, string | undefined>): BondedBrokerageEpisode;
108
159
  //# sourceMappingURL=episode.d.ts.map
package/dist/episode.js CHANGED
@@ -1,4 +1,4 @@
1
- import { EpisodeEnv, parseEpisodeEnv } from "@bondedhq/shared";
1
+ import { EpisodeEnv, parseBrokerageEpisodeEnv, parseEpisodeEnv, } from "@bondedhq/shared";
2
2
  import { defineChain } from "viem";
3
3
  import { privateKeyToAccount } from "viem/accounts";
4
4
  import { BondedError } from "./errors.js";
@@ -15,9 +15,14 @@ export const bondedEpisodeEnv = EpisodeEnv;
15
15
  * ```ts
16
16
  * const tools = isBondedEpisode() ? await bondedLangChainTools() : productionTools;
17
17
  * ```
18
+ *
19
+ * True in every world, so an agent under test never falls through to its production tools. In a
20
+ * world the agent wasn't built for, the helpers then fail loudly: in a brokerage episode
21
+ * `fromBondedEnv` and the vault tool helpers throw `EPISODE_ENV`. `bondedEpisodeWorld()` says
22
+ * which world it is.
18
23
  */
19
24
  export function isBondedEpisode(env = typeof process === "undefined" ? {} : process.env) {
20
- return parseEpisodeEnv(env).ok;
25
+ return bondedEpisodeWorld(env) !== undefined;
21
26
  }
22
27
  /**
23
28
  * Reads the episode the Bonded runner set up from the environment. Throws `BondedError`
@@ -62,4 +67,37 @@ export function episodeVault(episode) {
62
67
  router: episode.contracts.exchange,
63
68
  });
64
69
  }
70
+ /**
71
+ * Which world the runner's episode is for: `vault` (on-chain trading from a Mandate Vault, read
72
+ * with `fromBondedEnv`), `brokerage` (read with `fromBondedBrokerageEnv`), or undefined when this
73
+ * process isn't running a Bonded episode.
74
+ *
75
+ * ```ts
76
+ * const world = bondedEpisodeWorld();
77
+ * const tools =
78
+ * world === "brokerage"
79
+ * ? await myBrokerageTools(fromBondedBrokerageEnv())
80
+ * : world === "vault"
81
+ * ? await bondedLangChainTools()
82
+ * : productionTools;
83
+ * ```
84
+ */
85
+ export function bondedEpisodeWorld(env = typeof process === "undefined" ? {} : process.env) {
86
+ // Fails closed: once the runner has named the brokerage world for an episode, that is the
87
+ // answer even if another variable is malformed. `fromBondedBrokerageEnv` then names the bad
88
+ // variable, where "not an episode" would send the agent to its production tools.
89
+ if (env.BONDED_WORLD === "brokerage" && env.BONDED_EPISODE_ID?.trim())
90
+ return "brokerage";
91
+ return parseEpisodeEnv(env).ok ? "vault" : undefined;
92
+ }
93
+ /**
94
+ * Reads a brokerage episode the Bonded runner set up from the environment. Throws `BondedError`
95
+ * (`EPISODE_ENV`) naming every missing or malformed variable.
96
+ */
97
+ export function fromBondedBrokerageEnv(env = process.env) {
98
+ const parsed = parseBrokerageEpisodeEnv(env);
99
+ if (!parsed.ok)
100
+ throw new BondedError("EPISODE_ENV", `not a valid Bonded brokerage episode environment: ${parsed.problems.join("; ")}`);
101
+ return parsed.episode;
102
+ }
65
103
  //# sourceMappingURL=episode.js.map
package/dist/index.d.ts CHANGED
@@ -24,6 +24,6 @@ export { BONDED_API_URL, PaymentGate, paymentGate, type GateAnswer, type GateChe
24
24
  export { guardPayments, guardVendorChanges, isPaymentRefusal, PaymentGuard, paymentGuard, type GuardPaymentsOptions, type GuardVendorChangesOptions, type PaymentBreach, type PaymentGuardOptions, type PaymentGuardState, type PaymentMandate, type PaymentSource, type PaymentVendor, type PaymentVerdict, } from "./payments.js";
25
25
  export { describePaymentMandate, GATE_RULE_WORDS, GATE_RULES, PAYMENT_RULE_WORDS, PAYMENT_RULES, type GateRule, type PaidPayment, type PayableInvoice, type PayableRefund, type PaymentAttempt, type PaymentRule, type VerifiedChange, } from "@bondedhq/shared";
26
26
  export { BONDED_VAULT_TOOLS, bondedVaultFromEnv, bondedVaultTools, type BondedVaultTool, type BondedVaultToolName, type BondedVaultToolsOptions, } from "./vault-tools.js";
27
- export { bondedEpisodeEnv, episodeVault, fromBondedEnv, isBondedEpisode, type BondedEpisode, } from "./episode.js";
27
+ export { bondedEpisodeEnv, bondedEpisodeWorld, episodeVault, fromBondedBrokerageEnv, fromBondedEnv, isBondedEpisode, type BondedBrokerageEpisode, type BondedEpisode, } from "./episode.js";
28
28
  export { checkUnprotectedAllowances, deploymentTokens, unprotectedAllowancesWarning, type AllowanceCheck, type AllowanceFinding, type TokenRef, installMandateModule, type InstallMandateModuleInput, type ModuleAccountType, type ModuleInstallPlan, } from "./module.js";
29
29
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -23,6 +23,6 @@ export { BONDED_API_URL, PaymentGate, paymentGate, } from "./gate.js";
23
23
  export { guardPayments, guardVendorChanges, isPaymentRefusal, PaymentGuard, paymentGuard, } from "./payments.js";
24
24
  export { describePaymentMandate, GATE_RULE_WORDS, GATE_RULES, PAYMENT_RULE_WORDS, PAYMENT_RULES, } from "@bondedhq/shared";
25
25
  export { BONDED_VAULT_TOOLS, bondedVaultFromEnv, bondedVaultTools, } from "./vault-tools.js";
26
- export { bondedEpisodeEnv, episodeVault, fromBondedEnv, isBondedEpisode, } from "./episode.js";
26
+ export { bondedEpisodeEnv, bondedEpisodeWorld, episodeVault, fromBondedBrokerageEnv, fromBondedEnv, isBondedEpisode, } from "./episode.js";
27
27
  export { checkUnprotectedAllowances, deploymentTokens, unprotectedAllowancesWarning, installMandateModule, } from "./module.js";
28
28
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bondedhq/sdk",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Trade through a Bonded Mandate Vault and get rated by the Bonded Arena from any TypeScript agent: a typed vault client with dry runs, Sentinel checks and a mandate builder; the Arena episode's tools for LangChain, the Vercel AI SDK, ElizaOS and Coinbase AgentKit; a payment guard for pay tools (LangChain, the AI SDK, MCP, the OpenAI Agents SDK) and x402 clients; and an Agent URL handler.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -94,7 +94,7 @@
94
94
  "@modelcontextprotocol/sdk": "1.30.1",
95
95
  "viem": "^2.56.9",
96
96
  "zod": "^4.6.5",
97
- "@bondedhq/shared": "^0.1.5"
97
+ "@bondedhq/shared": "^0.1.6"
98
98
  },
99
99
  "devDependencies": {
100
100
  "@coinbase/agentkit": "0.10.4",