@bondedhq/sdk 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +483 -2
- package/dist/actions.d.ts +76 -0
- package/dist/actions.js +115 -0
- package/dist/agent-url.d.ts +101 -0
- package/dist/agent-url.js +234 -0
- package/dist/agentkit.d.ts +104 -0
- package/dist/agentkit.js +206 -0
- package/dist/ai-sdk.d.ts +39 -0
- package/dist/ai-sdk.js +86 -0
- package/dist/breaches.d.ts +50 -0
- package/dist/breaches.js +57 -0
- package/dist/done.d.ts +14 -0
- package/dist/done.js +16 -0
- package/dist/eliza.d.ts +118 -0
- package/dist/eliza.js +281 -0
- package/dist/episode.d.ts +108 -0
- package/dist/episode.js +65 -0
- package/dist/errors.d.ts +93 -0
- package/dist/errors.js +97 -0
- package/dist/guarded.d.ts +80 -0
- package/dist/guarded.js +97 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +19 -0
- package/dist/json-schema-zod3.d.ts +9 -0
- package/dist/json-schema-zod3.js +54 -0
- package/dist/langchain.d.ts +39 -0
- package/dist/langchain.js +53 -0
- package/dist/mandate.d.ts +36 -0
- package/dist/mandate.js +26 -0
- package/dist/mcp-config-cli.d.ts +3 -0
- package/dist/mcp-config-cli.js +44 -0
- package/dist/mcp.d.ts +131 -0
- package/dist/mcp.js +111 -0
- package/dist/module.d.ts +37 -0
- package/dist/module.js +32 -0
- package/dist/revert.d.ts +12 -0
- package/dist/revert.js +71 -0
- package/dist/sentinel.d.ts +43 -0
- package/dist/sentinel.js +94 -0
- package/dist/signature.d.ts +34 -0
- package/dist/signature.js +52 -0
- package/dist/solana-agent-kit.d.ts +141 -0
- package/dist/solana-agent-kit.js +333 -0
- package/dist/vault-tools.d.ts +67 -0
- package/dist/vault-tools.js +335 -0
- package/dist/vault.d.ts +116 -0
- package/dist/vault.js +326 -0
- package/package.json +134 -3
package/dist/eliza.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { erc20Abi, getAddress, isAddress, parseUnits } from "viem";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { BondedError } from "./errors.js";
|
|
4
|
+
import { GuardedVault, } from "./guarded.js";
|
|
5
|
+
import { SENTINEL_INPUT_CHARS } from "@bondedhq/shared";
|
|
6
|
+
import { episodeVault, fromBondedEnv } from "./episode.js";
|
|
7
|
+
import { isBondedEpisode, resolveTools, resultText, } from "./mcp.js";
|
|
8
|
+
/** The action names this plugin adds. */
|
|
9
|
+
export const BONDED_ACTIONS = ["BONDED_SWAP", "BONDED_TRANSFER", "BONDED_APPROVE"];
|
|
10
|
+
const amount = z.string().regex(/^\d+(\.\d+)?$/, "a decimal amount, e.g. 250 or 12.5");
|
|
11
|
+
const PARAMS = {
|
|
12
|
+
BONDED_SWAP: z.object({ tokenIn: z.string(), tokenOut: z.string(), amount }),
|
|
13
|
+
BONDED_TRANSFER: z.object({ token: z.string(), to: z.string(), amount }),
|
|
14
|
+
BONDED_APPROVE: z.object({ token: z.string(), spender: z.string(), amount }),
|
|
15
|
+
};
|
|
16
|
+
const DESCRIPTIONS = {
|
|
17
|
+
BONDED_SWAP: "Swap one token for another with the funds in the agent's Bonded Mandate Vault.",
|
|
18
|
+
BONDED_TRANSFER: "Send tokens from the agent's Bonded Mandate Vault to an address.",
|
|
19
|
+
BONDED_APPROVE: "Let an address spend tokens from the agent's Bonded Mandate Vault.",
|
|
20
|
+
};
|
|
21
|
+
const FIELDS = {
|
|
22
|
+
BONDED_SWAP: '{"tokenIn": symbol or address, "tokenOut": symbol or address, "amount": "decimal"}',
|
|
23
|
+
BONDED_TRANSFER: '{"token": symbol or address, "to": "0x address", "amount": "decimal"}',
|
|
24
|
+
BONDED_APPROVE: '{"token": symbol or address, "spender": "0x address", "amount": "decimal"}',
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* An ElizaOS plugin: `BONDED_SWAP`, `BONDED_TRANSFER` and `BONDED_APPROVE` act through the
|
|
28
|
+
* agent's Mandate Vault, and the `BONDED_VAULT` provider tells the agent the vault's state.
|
|
29
|
+
*
|
|
30
|
+
* Each action takes its parameters from `options.parameters` when the caller passes them, or
|
|
31
|
+
* asks the runtime's `OBJECT_SMALL` model to read them from the conversation. Amounts are whole
|
|
32
|
+
* tokens ("12.5"). By default each action is dry-run against the vault first and refused if the
|
|
33
|
+
* vault would record a breach; with `sentinel`, Sentinel is asked too, with the conversation as
|
|
34
|
+
* what the agent read.
|
|
35
|
+
*/
|
|
36
|
+
export function bondedPlugin(options) {
|
|
37
|
+
const guard = options.guard ?? true;
|
|
38
|
+
const guarded = new GuardedVault(options.vault, options.sentinel ? { sentinel: options.sentinel } : {});
|
|
39
|
+
const action = (name) => ({
|
|
40
|
+
name,
|
|
41
|
+
similes: [],
|
|
42
|
+
description: DESCRIPTIONS[name],
|
|
43
|
+
validate: async () => options.vault.walletClient !== undefined,
|
|
44
|
+
handler: async (runtime, message, state, handlerOptions, callback) => {
|
|
45
|
+
try {
|
|
46
|
+
const params = await readParams(name, runtime, message, state, handlerOptions);
|
|
47
|
+
const vaultAction = await toVaultAction(name, params, options);
|
|
48
|
+
const context = { untrustedInputs: conversation(message, state) };
|
|
49
|
+
const result = guard || options.sentinel
|
|
50
|
+
? await guarded.execute(vaultAction, context)
|
|
51
|
+
: await options.vault.execute(vaultAction);
|
|
52
|
+
const text = result.breaches.length
|
|
53
|
+
? `Done, but the vault recorded a breach: ${result.breaches.map((b) => `${b.type} (${b.reason})`).join("; ")}. The vault may have frozen the agent.`
|
|
54
|
+
: `Done within the mandate (${result.hashes.length} transaction${result.hashes.length === 1 ? "" : "s"}).`;
|
|
55
|
+
await callback?.({ text, actions: [name] });
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
text,
|
|
59
|
+
data: {
|
|
60
|
+
hashes: result.hashes,
|
|
61
|
+
breaches: result.breaches.map((b) => ({
|
|
62
|
+
...b,
|
|
63
|
+
id: b.id.toString(),
|
|
64
|
+
lossUsd6: b.lossUsd6.toString(),
|
|
65
|
+
})),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const text = `Not done: ${error instanceof Error ? error.message : String(error)}`;
|
|
71
|
+
await callback?.({ text, actions: [name] });
|
|
72
|
+
return { success: false, text, error: error instanceof Error ? error : String(error) };
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const provider = {
|
|
77
|
+
name: "BONDED_VAULT",
|
|
78
|
+
description: "The agent's Bonded Mandate Vault: mode, frozen or not, and breaches recorded.",
|
|
79
|
+
get: async () => {
|
|
80
|
+
const status = await options.vault.status();
|
|
81
|
+
const text = (status.kind === "module"
|
|
82
|
+
? `Funds are in smart account ${status.holder}, guarded by Bonded mandate module ${options.vault.address} (${status.mode} mode). `
|
|
83
|
+
: `Funds are in Bonded Mandate Vault ${options.vault.address} (${status.mode} mode). `) +
|
|
84
|
+
(status.frozen
|
|
85
|
+
? "It is frozen: no action will run until its guardian unfreezes it. "
|
|
86
|
+
: "It is active. ") +
|
|
87
|
+
`${status.breachCount} breach${status.breachCount === 1n ? "" : "es"} recorded.`;
|
|
88
|
+
return {
|
|
89
|
+
text,
|
|
90
|
+
values: { bondedVaultFrozen: status.frozen, bondedVaultMode: status.mode },
|
|
91
|
+
data: {
|
|
92
|
+
vault: options.vault.address,
|
|
93
|
+
kind: status.kind,
|
|
94
|
+
holder: status.holder,
|
|
95
|
+
frozen: status.frozen,
|
|
96
|
+
mode: status.mode,
|
|
97
|
+
breachCount: status.breachCount.toString(),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
const plugin = {
|
|
103
|
+
name: "bonded",
|
|
104
|
+
description: "Trade through a Bonded Mandate Vault, checked against the agent's mandate.",
|
|
105
|
+
actions: BONDED_ACTIONS.map(action),
|
|
106
|
+
providers: [provider],
|
|
107
|
+
};
|
|
108
|
+
if ((options.rating ?? "auto") === "auto")
|
|
109
|
+
plugin.init = async (config, runtime) => {
|
|
110
|
+
if (!isBondedEpisode())
|
|
111
|
+
return;
|
|
112
|
+
const rated = bondedEpisodePlugin({ guard });
|
|
113
|
+
await rated.init(config, runtime);
|
|
114
|
+
plugin.actions = rated.actions;
|
|
115
|
+
plugin.providers = rated.providers;
|
|
116
|
+
};
|
|
117
|
+
return plugin;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Rating mode for ElizaOS: a plugin whose actions are the episode's tools, so an Eliza agent is
|
|
121
|
+
* rated with the same character and model it runs in production. Each tool becomes an action
|
|
122
|
+
* named after it in capitals (`get_prices` is `GET_PRICES`); its parameters come from
|
|
123
|
+
* `options.parameters`, or the runtime's `OBJECT_SMALL` model reads them from the conversation
|
|
124
|
+
* against the tool's JSON Schema. The `BONDED_TASK` provider gives the agent the episode's task.
|
|
125
|
+
*
|
|
126
|
+
* In tools mode the writes (`SWAP`, `TRANSFER`, `APPROVE`) are tools too. In chain mode only the
|
|
127
|
+
* reads are, and the plugin adds `BONDED_SWAP`, `BONDED_TRANSFER` and `BONDED_APPROVE` on the
|
|
128
|
+
* episode vault, signed with the episode's session key (`episodeVault`).
|
|
129
|
+
*
|
|
130
|
+
* The actions are filled in by `init`, which the runtime calls when it registers the plugin
|
|
131
|
+
* (before it reads them). `bondedPlugin` does this by itself while an episode is running.
|
|
132
|
+
*/
|
|
133
|
+
export function bondedEpisodePlugin(options = {}) {
|
|
134
|
+
const plugin = {
|
|
135
|
+
name: "bonded-episode",
|
|
136
|
+
description: "The tools of a Bonded Arena episode.",
|
|
137
|
+
actions: [],
|
|
138
|
+
providers: [],
|
|
139
|
+
init: async () => {
|
|
140
|
+
const episode = options.episode ?? fromBondedEnv();
|
|
141
|
+
const tools = await resolveTools(options.tools ?? episode);
|
|
142
|
+
const actions = tools.tools.map(toolAction);
|
|
143
|
+
if (episode.mode === "chain" && episode.sessionKey)
|
|
144
|
+
actions.push(...bondedPlugin({
|
|
145
|
+
vault: episodeVault(episode),
|
|
146
|
+
tokens: episode.contracts.tokens,
|
|
147
|
+
guard: options.guard ?? true,
|
|
148
|
+
rating: "off",
|
|
149
|
+
}).actions);
|
|
150
|
+
plugin.actions = actions;
|
|
151
|
+
plugin.providers = [taskProvider(episode)];
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
return plugin;
|
|
155
|
+
}
|
|
156
|
+
/** One episode tool as an Eliza action. */
|
|
157
|
+
function toolAction(t) {
|
|
158
|
+
const name = t.name.toUpperCase();
|
|
159
|
+
const takesArgs = Object.keys(t.inputSchema.properties ?? {}).length > 0;
|
|
160
|
+
return {
|
|
161
|
+
name,
|
|
162
|
+
similes: [t.name],
|
|
163
|
+
description: t.description,
|
|
164
|
+
validate: async () => true,
|
|
165
|
+
handler: async (runtime, message, state, handlerOptions, callback) => {
|
|
166
|
+
const given = handlerOptions?.parameters;
|
|
167
|
+
const args = given && typeof given === "object"
|
|
168
|
+
? given
|
|
169
|
+
: takesArgs
|
|
170
|
+
? await runtime.useModel("OBJECT_SMALL", {
|
|
171
|
+
prompt: [
|
|
172
|
+
`Extract the parameters for ${name} (${t.description}) from the conversation.`,
|
|
173
|
+
`Answer with JSON only, matching this JSON Schema: ${JSON.stringify(t.inputSchema)}`,
|
|
174
|
+
"Use only values the conversation states.",
|
|
175
|
+
"",
|
|
176
|
+
state?.text ?? message.content.text ?? "",
|
|
177
|
+
].join("\n"),
|
|
178
|
+
})
|
|
179
|
+
: {};
|
|
180
|
+
const result = await t.call(args && typeof args === "object" ? args : {});
|
|
181
|
+
const text = resultText(result);
|
|
182
|
+
await callback?.({ text, actions: [name] });
|
|
183
|
+
return result.ok
|
|
184
|
+
? { success: true, text, data: { tool: t.name, output: result.output } }
|
|
185
|
+
: { success: false, text, error: result.error, data: { tool: t.name } };
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/** The episode's task, as the agent's current instructions. */
|
|
190
|
+
function taskProvider(episode) {
|
|
191
|
+
const text = [
|
|
192
|
+
`Your task: ${episode.task.goal}`,
|
|
193
|
+
`Your mandate: ${episode.task.mandate}`,
|
|
194
|
+
`Tokens: ${episode.contracts.tokens.map((t) => t.symbol).join(", ")}.`,
|
|
195
|
+
].join("\n");
|
|
196
|
+
return {
|
|
197
|
+
name: "BONDED_TASK",
|
|
198
|
+
description: "The task the agent is working on, and its mandate.",
|
|
199
|
+
get: async () => ({
|
|
200
|
+
text,
|
|
201
|
+
values: { bondedTaskGoal: episode.task.goal },
|
|
202
|
+
data: { episodeId: episode.episodeId, task: episode.task },
|
|
203
|
+
}),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** The action's parameters: given by the caller, or read from the conversation by the model. */
|
|
207
|
+
async function readParams(name, runtime, message, state, handlerOptions) {
|
|
208
|
+
const given = handlerOptions?.parameters;
|
|
209
|
+
const raw = given && typeof given === "object"
|
|
210
|
+
? given
|
|
211
|
+
: await runtime.useModel("OBJECT_SMALL", {
|
|
212
|
+
prompt: [
|
|
213
|
+
`Extract the parameters for ${name} (${DESCRIPTIONS[name]}) from the conversation.`,
|
|
214
|
+
`Answer with JSON only: ${FIELDS[name]}.`,
|
|
215
|
+
"Use only values the conversation states. Amounts are in whole tokens.",
|
|
216
|
+
"",
|
|
217
|
+
state?.text ?? message.content.text ?? "",
|
|
218
|
+
].join("\n"),
|
|
219
|
+
});
|
|
220
|
+
const parsed = PARAMS[name].safeParse(raw);
|
|
221
|
+
if (!parsed.success)
|
|
222
|
+
throw new BondedError("INVALID_ACTION", `${name} needs ${FIELDS[name]}: ${parsed.error.issues.map((i) => `${i.path.join(".")} ${i.message}`).join(", ")}`);
|
|
223
|
+
return parsed.data;
|
|
224
|
+
}
|
|
225
|
+
async function toVaultAction(name, params, options) {
|
|
226
|
+
const token = (value) => resolveToken(value, options);
|
|
227
|
+
const units = async (value, amount) => parseUnits(amount, (await token(value)).decimals);
|
|
228
|
+
const recipient = (field, value) => {
|
|
229
|
+
if (!isAddress(value, { strict: false }))
|
|
230
|
+
throw new BondedError("INVALID_ACTION", `${field} must be a 0x address, not "${value}"`);
|
|
231
|
+
return getAddress(value);
|
|
232
|
+
};
|
|
233
|
+
switch (name) {
|
|
234
|
+
case "BONDED_SWAP":
|
|
235
|
+
return {
|
|
236
|
+
type: "swap",
|
|
237
|
+
tokenIn: (await token(params.tokenIn)).address,
|
|
238
|
+
tokenOut: (await token(params.tokenOut)).address,
|
|
239
|
+
amountIn: await units(params.tokenIn, params.amount),
|
|
240
|
+
};
|
|
241
|
+
case "BONDED_TRANSFER":
|
|
242
|
+
return {
|
|
243
|
+
type: "transfer",
|
|
244
|
+
token: (await token(params.token)).address,
|
|
245
|
+
to: recipient("to", params.to),
|
|
246
|
+
amount: await units(params.token, params.amount),
|
|
247
|
+
};
|
|
248
|
+
case "BONDED_APPROVE":
|
|
249
|
+
return {
|
|
250
|
+
type: "approve",
|
|
251
|
+
token: (await token(params.token)).address,
|
|
252
|
+
spender: recipient("spender", params.spender),
|
|
253
|
+
amount: await units(params.token, params.amount),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** A configured token by symbol (any case) or address; an unknown address's decimals are read. */
|
|
258
|
+
async function resolveToken(value, options) {
|
|
259
|
+
const tokens = options.tokens ?? [];
|
|
260
|
+
const known = tokens.find((t) => t.symbol.toLowerCase() === value.toLowerCase() ||
|
|
261
|
+
t.address.toLowerCase() === value.toLowerCase());
|
|
262
|
+
if (known)
|
|
263
|
+
return known;
|
|
264
|
+
if (!isAddress(value, { strict: false }))
|
|
265
|
+
throw new BondedError("INVALID_ACTION", `unknown token "${value}"; use one of ${tokens.map((t) => t.symbol).join(", ") || "the configured tokens"} or a token address`);
|
|
266
|
+
const address = getAddress(value);
|
|
267
|
+
const decimals = await options.vault.publicClient.readContract({
|
|
268
|
+
address,
|
|
269
|
+
abi: erc20Abi,
|
|
270
|
+
functionName: "decimals",
|
|
271
|
+
});
|
|
272
|
+
return { symbol: address, address, decimals };
|
|
273
|
+
}
|
|
274
|
+
/** What the agent read before acting: the message and recent conversation, for Sentinel. */
|
|
275
|
+
function conversation(message, state) {
|
|
276
|
+
const inputs = [{ tool: "message", output: message.content.text ?? "" }];
|
|
277
|
+
if (state?.text)
|
|
278
|
+
inputs.push({ tool: "conversation", output: state.text.slice(-SENTINEL_INPUT_CHARS) });
|
|
279
|
+
return inputs.filter((i) => i.output.length > 0);
|
|
280
|
+
}
|
|
281
|
+
//# sourceMappingURL=eliza.js.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { type AgentTask } from "@bondedhq/shared";
|
|
2
|
+
import { type Address, type Hex } from "viem";
|
|
3
|
+
import type { TokenInfo } from "./guarded.js";
|
|
4
|
+
import { BondedVault } from "./vault.js";
|
|
5
|
+
/**
|
|
6
|
+
* An Arena episode, as the Bonded runner hands it to an agent it launches: the task, where the
|
|
7
|
+
* episode's tools are, and in chain mode a fork RPC and the key to sign with. The agent acts
|
|
8
|
+
* exactly as in production, against these endpoints, and the Arena judges it by the episode
|
|
9
|
+
* vault's breach log.
|
|
10
|
+
*/
|
|
11
|
+
export interface BondedEpisode {
|
|
12
|
+
episodeId: string;
|
|
13
|
+
task: AgentTask;
|
|
14
|
+
/** tools: writes are the `swap`/`transfer`/`approve` tools. chain: writes are signed `execute` txs to `endpoints.rpc`. */
|
|
15
|
+
mode: "tools" | "chain";
|
|
16
|
+
endpoints: {
|
|
17
|
+
/** MCP over streamable HTTP. */
|
|
18
|
+
mcp: string;
|
|
19
|
+
/** Plain HTTP: `GET {tools}` lists them, `POST {tools}/{name}` with JSON args calls one. */
|
|
20
|
+
tools: string;
|
|
21
|
+
openapi: string;
|
|
22
|
+
/** `POST` here when finished (`{ finalMessage? }`). */
|
|
23
|
+
done: string;
|
|
24
|
+
/** Chain mode: the episode's fork. Only signed `execute` calls to the episode vault are accepted. */
|
|
25
|
+
rpc?: string;
|
|
26
|
+
};
|
|
27
|
+
vault: Address;
|
|
28
|
+
chainId: number;
|
|
29
|
+
/** The episode's market: the exchange (swap router) and the tokens the agent can trade. */
|
|
30
|
+
contracts: {
|
|
31
|
+
exchange: Address;
|
|
32
|
+
tokens: TokenInfo[];
|
|
33
|
+
};
|
|
34
|
+
/** Chain mode: the episode's agent key. Absent when the agent's key lives in a TEE. */
|
|
35
|
+
sessionKey?: Hex;
|
|
36
|
+
/** How long the agent has, in milliseconds from when it starts. */
|
|
37
|
+
deadlineMs: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The BONDED_* variables the runner sets for each episode. The schema lives in @bondedhq/shared,
|
|
41
|
+
* next to `episodeEnv` that writes them, so the runner, the gateway and the SDK can't drift.
|
|
42
|
+
*/
|
|
43
|
+
export declare const bondedEpisodeEnv: import("zod").ZodObject<{
|
|
44
|
+
BONDED_EPISODE_ID: import("zod").ZodString;
|
|
45
|
+
BONDED_MODE: import("zod").ZodEnum<{
|
|
46
|
+
tools: "tools";
|
|
47
|
+
chain: "chain";
|
|
48
|
+
}>;
|
|
49
|
+
BONDED_TASK: import("zod").ZodPipe<import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodTransform<unknown, string>>, import("zod").ZodObject<{
|
|
50
|
+
goal: import("zod").ZodString;
|
|
51
|
+
mandate: import("zod").ZodString;
|
|
52
|
+
policy: import("zod").ZodOptional<import("zod").ZodObject<{
|
|
53
|
+
tokens: import("zod").ZodArray<import("zod").ZodString>;
|
|
54
|
+
maxTradeUsd: import("zod").ZodNumber;
|
|
55
|
+
maxDailyUsd: import("zod").ZodNumber;
|
|
56
|
+
transferAllowlist: import("zod").ZodArray<import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodTransform<`0x${string}`, string>>>;
|
|
57
|
+
approvalAllowlist: import("zod").ZodArray<import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodTransform<`0x${string}`, string>>>;
|
|
58
|
+
}, import("zod/v4/core").$strip>>;
|
|
59
|
+
maxSteps: import("zod").ZodNumber;
|
|
60
|
+
}, import("zod/v4/core").$strip>>;
|
|
61
|
+
BONDED_MCP_URL: import("zod").ZodURL;
|
|
62
|
+
BONDED_TOOLS_URL: import("zod").ZodURL;
|
|
63
|
+
BONDED_OPENAPI_URL: import("zod").ZodURL;
|
|
64
|
+
BONDED_DONE_URL: import("zod").ZodURL;
|
|
65
|
+
BONDED_RPC_URL: import("zod").ZodOptional<import("zod").ZodURL>;
|
|
66
|
+
BONDED_SESSION_KEY: import("zod").ZodOptional<import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodTransform<`0x${string}`, string>>>;
|
|
67
|
+
BONDED_TEE_KEY_ID: import("zod").ZodOptional<import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodTransform<`0x${string}`, string>>>;
|
|
68
|
+
BONDED_VAULT: import("zod").ZodPipe<import("zod").ZodString & import("zod").ZodType<`0x${string}`, string, import("zod/v4/core").$ZodTypeInternals<`0x${string}`, string>>, import("zod").ZodTransform<`0x${string}`, `0x${string}`>>;
|
|
69
|
+
BONDED_CHAIN_ID: import("zod").ZodCoercedNumber<unknown>;
|
|
70
|
+
BONDED_CONTRACTS: import("zod").ZodPipe<import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodTransform<unknown, string>>, import("zod").ZodObject<{
|
|
71
|
+
tokens: import("zod").ZodArray<import("zod").ZodObject<{
|
|
72
|
+
symbol: import("zod").ZodString;
|
|
73
|
+
address: import("zod").ZodPipe<import("zod").ZodString & import("zod").ZodType<`0x${string}`, string, import("zod/v4/core").$ZodTypeInternals<`0x${string}`, string>>, import("zod").ZodTransform<`0x${string}`, `0x${string}`>>;
|
|
74
|
+
decimals: import("zod").ZodNumber;
|
|
75
|
+
}, import("zod/v4/core").$strip>>;
|
|
76
|
+
exchange: import("zod").ZodPipe<import("zod").ZodString & import("zod").ZodType<`0x${string}`, string, import("zod/v4/core").$ZodTypeInternals<`0x${string}`, string>>, import("zod").ZodTransform<`0x${string}`, `0x${string}`>>;
|
|
77
|
+
}, import("zod/v4/core").$strip>>;
|
|
78
|
+
BONDED_DEADLINE_MS: import("zod").ZodCoercedNumber<unknown>;
|
|
79
|
+
}, import("zod/v4/core").$strip>;
|
|
80
|
+
/**
|
|
81
|
+
* Whether this process is running a Bonded episode: the runner set valid `BONDED_*` variables.
|
|
82
|
+
* Lets an agent pick the episode's tools when it is being rated and its own otherwise:
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* const tools = isBondedEpisode() ? await bondedLangChainTools() : productionTools;
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare function isBondedEpisode(env?: Record<string, string | undefined>): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Reads the episode the Bonded runner set up from the environment. Throws `BondedError`
|
|
91
|
+
* (`EPISODE_ENV`) naming every missing or malformed variable.
|
|
92
|
+
*
|
|
93
|
+
* ```ts
|
|
94
|
+
* const episode = fromBondedEnv();
|
|
95
|
+
* if (episode.mode === "chain") {
|
|
96
|
+
* const vault = new BondedVault({ vault: episode.vault, rpcUrl: episode.endpoints.rpc, ... });
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export declare function fromBondedEnv(env?: Record<string, string | undefined>): BondedEpisode;
|
|
101
|
+
/**
|
|
102
|
+
* Chain mode: the episode vault as a `BondedVault`, signing with the episode's session key
|
|
103
|
+
* through the episode's fork RPC, and swapping on the episode's exchange. Writes are then signed
|
|
104
|
+
* `execute` calls, exactly as in production. Throws `BondedError` (`EPISODE_MODE`) in tools mode
|
|
105
|
+
* or when the key is held by a TEE (no session key): use your own signer there.
|
|
106
|
+
*/
|
|
107
|
+
export declare function episodeVault(episode: BondedEpisode): BondedVault;
|
|
108
|
+
//# sourceMappingURL=episode.d.ts.map
|
package/dist/episode.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { EpisodeEnv, parseEpisodeEnv } from "@bondedhq/shared";
|
|
2
|
+
import { defineChain } from "viem";
|
|
3
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
4
|
+
import { BondedError } from "./errors.js";
|
|
5
|
+
import { BondedVault } from "./vault.js";
|
|
6
|
+
/**
|
|
7
|
+
* The BONDED_* variables the runner sets for each episode. The schema lives in @bondedhq/shared,
|
|
8
|
+
* next to `episodeEnv` that writes them, so the runner, the gateway and the SDK can't drift.
|
|
9
|
+
*/
|
|
10
|
+
export const bondedEpisodeEnv = EpisodeEnv;
|
|
11
|
+
/**
|
|
12
|
+
* Whether this process is running a Bonded episode: the runner set valid `BONDED_*` variables.
|
|
13
|
+
* Lets an agent pick the episode's tools when it is being rated and its own otherwise:
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const tools = isBondedEpisode() ? await bondedLangChainTools() : productionTools;
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function isBondedEpisode(env = typeof process === "undefined" ? {} : process.env) {
|
|
20
|
+
return parseEpisodeEnv(env).ok;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reads the episode the Bonded runner set up from the environment. Throws `BondedError`
|
|
24
|
+
* (`EPISODE_ENV`) naming every missing or malformed variable.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* const episode = fromBondedEnv();
|
|
28
|
+
* if (episode.mode === "chain") {
|
|
29
|
+
* const vault = new BondedVault({ vault: episode.vault, rpcUrl: episode.endpoints.rpc, ... });
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function fromBondedEnv(env = process.env) {
|
|
34
|
+
const parsed = parseEpisodeEnv(env);
|
|
35
|
+
if (!parsed.ok)
|
|
36
|
+
throw new BondedError("EPISODE_ENV", `not a valid Bonded episode environment: ${parsed.problems.join("; ")}`);
|
|
37
|
+
const { contracts, ...episode } = parsed.episode;
|
|
38
|
+
return { ...episode, contracts: { exchange: contracts.exchange, tokens: contracts.tokens } };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Chain mode: the episode vault as a `BondedVault`, signing with the episode's session key
|
|
42
|
+
* through the episode's fork RPC, and swapping on the episode's exchange. Writes are then signed
|
|
43
|
+
* `execute` calls, exactly as in production. Throws `BondedError` (`EPISODE_MODE`) in tools mode
|
|
44
|
+
* or when the key is held by a TEE (no session key): use your own signer there.
|
|
45
|
+
*/
|
|
46
|
+
export function episodeVault(episode) {
|
|
47
|
+
const rpc = episode.endpoints.rpc;
|
|
48
|
+
if (episode.mode !== "chain" || !rpc)
|
|
49
|
+
throw new BondedError("EPISODE_MODE", "only a chain-mode episode has a vault to sign for; in tools mode, writes are tool calls");
|
|
50
|
+
if (!episode.sessionKey)
|
|
51
|
+
throw new BondedError("EPISODE_MODE", "this episode has no session key (the agent's key is in a TEE); build a BondedVault with your own signer");
|
|
52
|
+
return new BondedVault({
|
|
53
|
+
vault: episode.vault,
|
|
54
|
+
chain: defineChain({
|
|
55
|
+
id: episode.chainId,
|
|
56
|
+
name: `Bonded episode ${episode.episodeId}`,
|
|
57
|
+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
58
|
+
rpcUrls: { default: { http: [rpc] } },
|
|
59
|
+
}),
|
|
60
|
+
rpcUrl: rpc,
|
|
61
|
+
account: privateKeyToAccount(episode.sessionKey),
|
|
62
|
+
router: episode.contracts.exchange,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=episode.js.map
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Hex } from "viem";
|
|
2
|
+
import type { DryRunResult, VaultBreach } from "./breaches.js";
|
|
3
|
+
/** Base class for every error the SDK throws on purpose. `code` is stable; messages may change. */
|
|
4
|
+
export declare class BondedError extends Error {
|
|
5
|
+
readonly code: string;
|
|
6
|
+
readonly name: string;
|
|
7
|
+
constructor(code: string, message: string, options?: {
|
|
8
|
+
cause?: unknown;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
/** The signing key isn't the vault's agent key: only that key can call `execute`. */
|
|
12
|
+
export declare class NotAgentError extends BondedError {
|
|
13
|
+
readonly vault: Hex;
|
|
14
|
+
readonly signer: Hex | undefined;
|
|
15
|
+
readonly name = "NotAgentError";
|
|
16
|
+
constructor(vault: Hex, signer: Hex | undefined, options?: {
|
|
17
|
+
cause?: unknown;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The vault is frozen: its guardian froze it, or a breach did (freeze on breach). Nothing the
|
|
22
|
+
* agent sends will run until the guardian unfreezes it.
|
|
23
|
+
*/
|
|
24
|
+
export declare class VaultFrozenError extends BondedError {
|
|
25
|
+
readonly vault: Hex;
|
|
26
|
+
/** Set when this action froze it: the vault found an earlier unexplained outflow first. */
|
|
27
|
+
readonly breach?: VaultBreach | undefined;
|
|
28
|
+
/** The transaction that froze it, when this action did. */
|
|
29
|
+
readonly hash?: Hex | undefined;
|
|
30
|
+
readonly name = "VaultFrozenError";
|
|
31
|
+
constructor(vault: Hex,
|
|
32
|
+
/** Set when this action froze it: the vault found an earlier unexplained outflow first. */
|
|
33
|
+
breach?: VaultBreach | undefined,
|
|
34
|
+
/** The transaction that froze it, when this action did. */
|
|
35
|
+
hash?: Hex | undefined, options?: {
|
|
36
|
+
cause?: unknown;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Enforce mode stopped the action: the vault recorded the attempt as a breach and moved nothing. */
|
|
40
|
+
export declare class BlockedByMandateError extends BondedError {
|
|
41
|
+
readonly breach: VaultBreach;
|
|
42
|
+
readonly hash: Hex;
|
|
43
|
+
readonly name = "BlockedByMandateError";
|
|
44
|
+
constructor(breach: VaultBreach, hash: Hex);
|
|
45
|
+
}
|
|
46
|
+
/** A guarded vault refused to send an action because its dry run predicts a breach. */
|
|
47
|
+
export declare class PredictedBreachError extends BondedError {
|
|
48
|
+
readonly dryRun: DryRunResult;
|
|
49
|
+
readonly name = "PredictedBreachError";
|
|
50
|
+
constructor(dryRun: DryRunResult);
|
|
51
|
+
}
|
|
52
|
+
/** Sentinel's answer for a proposed action, as `POST /sentinel/check` returns it. */
|
|
53
|
+
export interface SentinelVerdict {
|
|
54
|
+
/** True when Sentinel advises against the action (manipulated at or above the threshold). */
|
|
55
|
+
blocked: boolean;
|
|
56
|
+
/** How strongly the action looks driven by the untrusted inputs, 0 to 1. */
|
|
57
|
+
manipulated: number;
|
|
58
|
+
/** How strongly the inputs look like they contain instructions for the agent, 0 to 1. */
|
|
59
|
+
injection: number;
|
|
60
|
+
blockThreshold: number;
|
|
61
|
+
model: string;
|
|
62
|
+
latencyMs: number;
|
|
63
|
+
advice: string;
|
|
64
|
+
}
|
|
65
|
+
/** A guarded vault refused an action because Sentinel advised against it. */
|
|
66
|
+
export declare class SentinelRefusedError extends BondedError {
|
|
67
|
+
readonly verdict: SentinelVerdict;
|
|
68
|
+
readonly name = "SentinelRefusedError";
|
|
69
|
+
constructor(verdict: SentinelVerdict);
|
|
70
|
+
}
|
|
71
|
+
/** The Sentinel API didn't give an answer: refused the request, was busy, or couldn't be reached. */
|
|
72
|
+
export declare class SentinelError extends BondedError {
|
|
73
|
+
/** HTTP status, when the API answered. */
|
|
74
|
+
readonly status?: number | undefined;
|
|
75
|
+
readonly name = "SentinelError";
|
|
76
|
+
constructor(message: string,
|
|
77
|
+
/** HTTP status, when the API answered. */
|
|
78
|
+
status?: number | undefined, options?: {
|
|
79
|
+
cause?: unknown;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** The call the agent asked the vault to make reverted (e.g. not enough balance). Nothing changed. */
|
|
83
|
+
export declare class CallRevertedError extends BondedError {
|
|
84
|
+
/** The revert data, when the node returned it. */
|
|
85
|
+
readonly data?: Hex | undefined;
|
|
86
|
+
readonly name = "CallRevertedError";
|
|
87
|
+
constructor(message: string,
|
|
88
|
+
/** The revert data, when the node returned it. */
|
|
89
|
+
data?: Hex | undefined, options?: {
|
|
90
|
+
cause?: unknown;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Base class for every error the SDK throws on purpose. `code` is stable; messages may change. */
|
|
2
|
+
export class BondedError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
name = "BondedError";
|
|
5
|
+
constructor(code, message, options) {
|
|
6
|
+
super(message, options);
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** The signing key isn't the vault's agent key: only that key can call `execute`. */
|
|
11
|
+
export class NotAgentError extends BondedError {
|
|
12
|
+
vault;
|
|
13
|
+
signer;
|
|
14
|
+
name = "NotAgentError";
|
|
15
|
+
constructor(vault, signer, options) {
|
|
16
|
+
super("NOT_AGENT", `${signer ?? "this account"} is not the agent key of vault ${vault}; only the vault's agent can act through it`, options);
|
|
17
|
+
this.vault = vault;
|
|
18
|
+
this.signer = signer;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The vault is frozen: its guardian froze it, or a breach did (freeze on breach). Nothing the
|
|
23
|
+
* agent sends will run until the guardian unfreezes it.
|
|
24
|
+
*/
|
|
25
|
+
export class VaultFrozenError extends BondedError {
|
|
26
|
+
vault;
|
|
27
|
+
breach;
|
|
28
|
+
hash;
|
|
29
|
+
name = "VaultFrozenError";
|
|
30
|
+
constructor(vault,
|
|
31
|
+
/** Set when this action froze it: the vault found an earlier unexplained outflow first. */
|
|
32
|
+
breach,
|
|
33
|
+
/** The transaction that froze it, when this action did. */
|
|
34
|
+
hash, options) {
|
|
35
|
+
super("VAULT_FROZEN", breach
|
|
36
|
+
? `vault ${vault} froze before acting: it found an earlier outflow it couldn't explain (breach ${breach.id})`
|
|
37
|
+
: `vault ${vault} is frozen; its guardian has to unfreeze it`, options);
|
|
38
|
+
this.vault = vault;
|
|
39
|
+
this.breach = breach;
|
|
40
|
+
this.hash = hash;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Enforce mode stopped the action: the vault recorded the attempt as a breach and moved nothing. */
|
|
44
|
+
export class BlockedByMandateError extends BondedError {
|
|
45
|
+
breach;
|
|
46
|
+
hash;
|
|
47
|
+
name = "BlockedByMandateError";
|
|
48
|
+
constructor(breach, hash) {
|
|
49
|
+
super("BLOCKED_BY_MANDATE", `the vault blocked this action (${breach.type}): ${breach.reason}. The attempt is recorded as breach ${breach.id}`);
|
|
50
|
+
this.breach = breach;
|
|
51
|
+
this.hash = hash;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** A guarded vault refused to send an action because its dry run predicts a breach. */
|
|
55
|
+
export class PredictedBreachError extends BondedError {
|
|
56
|
+
dryRun;
|
|
57
|
+
name = "PredictedBreachError";
|
|
58
|
+
constructor(dryRun) {
|
|
59
|
+
const breach = dryRun.breach;
|
|
60
|
+
super("PREDICTED_BREACH", breach
|
|
61
|
+
? `refused: the vault would record a breach (${breach.type}): ${breach.reason}`
|
|
62
|
+
: "refused: the vault would not run this action");
|
|
63
|
+
this.dryRun = dryRun;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** A guarded vault refused an action because Sentinel advised against it. */
|
|
67
|
+
export class SentinelRefusedError extends BondedError {
|
|
68
|
+
verdict;
|
|
69
|
+
name = "SentinelRefusedError";
|
|
70
|
+
constructor(verdict) {
|
|
71
|
+
super("SENTINEL_REFUSED", `refused on Sentinel's advice (manipulated ${verdict.manipulated.toFixed(2)} ≥ ${verdict.blockThreshold}): ${verdict.advice}`);
|
|
72
|
+
this.verdict = verdict;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** The Sentinel API didn't give an answer: refused the request, was busy, or couldn't be reached. */
|
|
76
|
+
export class SentinelError extends BondedError {
|
|
77
|
+
status;
|
|
78
|
+
name = "SentinelError";
|
|
79
|
+
constructor(message,
|
|
80
|
+
/** HTTP status, when the API answered. */
|
|
81
|
+
status, options) {
|
|
82
|
+
super("SENTINEL_ERROR", message, options);
|
|
83
|
+
this.status = status;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** The call the agent asked the vault to make reverted (e.g. not enough balance). Nothing changed. */
|
|
87
|
+
export class CallRevertedError extends BondedError {
|
|
88
|
+
data;
|
|
89
|
+
name = "CallRevertedError";
|
|
90
|
+
constructor(message,
|
|
91
|
+
/** The revert data, when the node returned it. */
|
|
92
|
+
data, options) {
|
|
93
|
+
super("CALL_REVERTED", message, options);
|
|
94
|
+
this.data = data;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=errors.js.map
|