@bondedhq/sdk 0.0.0-stage → 0.1.0
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 +482 -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/agentkit.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { ActionProvider, EvmWalletProvider, } from "@coinbase/agentkit";
|
|
2
|
+
import { mandateVaultAbi } from "@bondedhq/shared";
|
|
3
|
+
import { encodeFunctionData, getAddress, hexToBytes, isAddressEqual, stringToBytes, } from "viem";
|
|
4
|
+
import { readExecuteOutcome } from "./breaches.js";
|
|
5
|
+
import { BlockedByMandateError, BondedError, VaultFrozenError } from "./errors.js";
|
|
6
|
+
import { GuardedVault } from "./guarded.js";
|
|
7
|
+
import { vaultRevertError } from "./revert.js";
|
|
8
|
+
import { zod3FromJsonSchema } from "./json-schema-zod3.js";
|
|
9
|
+
import { resolveTools, resultText } from "./mcp.js";
|
|
10
|
+
import { BondedVault } from "./vault.js";
|
|
11
|
+
/**
|
|
12
|
+
* A Coinbase AgentKit wallet provider whose wallet is a Mandate Vault. Every transaction an
|
|
13
|
+
* action provider sends becomes `vault.execute(to, data)` from the agent key, so the vault checks
|
|
14
|
+
* it against the mandate and records any breach; `getAddress()` is where the funds
|
|
15
|
+
* are. Any AgentKit action provider then trades through the mandate unchanged.
|
|
16
|
+
*
|
|
17
|
+
* What a vault can't do is refused: native value (the vault is ERC-20 only), contract creation,
|
|
18
|
+
* and raw signatures or signed transactions from the agent key, which would bypass the vault.
|
|
19
|
+
* `signMessage` signs with the agent key (e.g. for API logins); it is not the vault's signature.
|
|
20
|
+
*
|
|
21
|
+
* In Enforce mode a blocked transaction still mines; `waitForTransactionReceipt` throws
|
|
22
|
+
* `BlockedByMandateError` for it, so the action reports a failure instead of a success.
|
|
23
|
+
*/
|
|
24
|
+
export class BondedWalletProvider extends EvmWalletProvider {
|
|
25
|
+
vault;
|
|
26
|
+
/** Where the funds are: the vault, or a module's smart account. */
|
|
27
|
+
holder;
|
|
28
|
+
#wallet;
|
|
29
|
+
#guard;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
super();
|
|
32
|
+
this.vault = getAddress(options.vault);
|
|
33
|
+
this.holder = getAddress(options.holder ?? options.vault);
|
|
34
|
+
this.#wallet = options.wallet;
|
|
35
|
+
if (options.guard) {
|
|
36
|
+
const reader = new BondedVault({
|
|
37
|
+
vault: this.vault,
|
|
38
|
+
publicClient: options.wallet.getPublicClient(),
|
|
39
|
+
// Dry runs simulate from the agent key; sending goes through `wallet`.
|
|
40
|
+
account: { address: getAddress(options.wallet.getAddress()), type: "json-rpc" },
|
|
41
|
+
});
|
|
42
|
+
const guard = options.guard === true ? {} : options.guard;
|
|
43
|
+
// Sentinel checks must be signed by the agent key: that is the AgentKit wallet, not the
|
|
44
|
+
// address-only account the reader simulates from (which can't sign).
|
|
45
|
+
const sentinel = guard.sentinel && {
|
|
46
|
+
...guard.sentinel,
|
|
47
|
+
account: guard.sentinel.account ?? agentKitSigner(options.wallet),
|
|
48
|
+
};
|
|
49
|
+
this.#guard = new GuardedVault(reader, sentinel ? { ...guard, sentinel } : guard);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A provider for a vault or a module, with `holder` read from the chain (a module's funds are
|
|
54
|
+
* in its smart account). Use this for modules; `new` assumes a vault unless `holder` is given.
|
|
55
|
+
*/
|
|
56
|
+
static async create(options) {
|
|
57
|
+
if (options.holder)
|
|
58
|
+
return new BondedWalletProvider(options);
|
|
59
|
+
const reader = new BondedVault({
|
|
60
|
+
vault: options.vault,
|
|
61
|
+
publicClient: options.wallet.getPublicClient(),
|
|
62
|
+
});
|
|
63
|
+
const { holder } = await reader.identify();
|
|
64
|
+
return new BondedWalletProvider({ ...options, holder });
|
|
65
|
+
}
|
|
66
|
+
getAddress() {
|
|
67
|
+
return this.holder;
|
|
68
|
+
}
|
|
69
|
+
getNetwork() {
|
|
70
|
+
return this.#wallet.getNetwork();
|
|
71
|
+
}
|
|
72
|
+
getName() {
|
|
73
|
+
return "bonded_vault_wallet_provider";
|
|
74
|
+
}
|
|
75
|
+
/** The native balance where the funds are. The mandate covers ERC-20 tokens; gas is paid by the agent key. */
|
|
76
|
+
async getBalance() {
|
|
77
|
+
return this.#wallet.getPublicClient().getBalance({ address: this.holder });
|
|
78
|
+
}
|
|
79
|
+
getPublicClient() {
|
|
80
|
+
return this.#wallet.getPublicClient();
|
|
81
|
+
}
|
|
82
|
+
// Generic in its parameters: AgentKit types this with its own copy of viem.
|
|
83
|
+
readContract(params) {
|
|
84
|
+
return this.#wallet.readContract(params);
|
|
85
|
+
}
|
|
86
|
+
/** Sends `transaction` as `vault.execute(to, data)` from the agent key. */
|
|
87
|
+
async sendTransaction(transaction) {
|
|
88
|
+
if (transaction.value !== undefined && BigInt(transaction.value) > 0n)
|
|
89
|
+
throw new BondedError("NATIVE_VALUE", "the Mandate Vault holds ERC-20 tokens only; it can't send native value");
|
|
90
|
+
if (!transaction.to)
|
|
91
|
+
throw new BondedError("UNSUPPORTED", "the Mandate Vault can't deploy contracts");
|
|
92
|
+
const target = getAddress(transaction.to);
|
|
93
|
+
const data = transaction.data ?? "0x";
|
|
94
|
+
await this.#guard?.check({ type: "call", target, data });
|
|
95
|
+
try {
|
|
96
|
+
return await this.#wallet.sendTransaction({
|
|
97
|
+
to: this.vault,
|
|
98
|
+
data: encodeFunctionData({
|
|
99
|
+
abi: mandateVaultAbi,
|
|
100
|
+
functionName: "execute",
|
|
101
|
+
args: [target, data],
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw vaultRevertError(error, {
|
|
107
|
+
vault: this.vault,
|
|
108
|
+
target,
|
|
109
|
+
signer: getAddress(this.#wallet.getAddress()),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Waits for the transaction, then reads the vault's verdict from its events: throws
|
|
115
|
+
* `BlockedByMandateError` when Enforce mode stopped the call, `VaultFrozenError` when a
|
|
116
|
+
* freeze skipped it. A Monitor-mode breach goes through and is in the vault's breach log.
|
|
117
|
+
*/
|
|
118
|
+
async waitForTransactionReceipt(txHash) {
|
|
119
|
+
const receipt = (await this.#wallet.waitForTransactionReceipt(txHash));
|
|
120
|
+
if (!receipt.to || !isAddressEqual(receipt.to, this.vault))
|
|
121
|
+
return receipt;
|
|
122
|
+
const outcome = readExecuteOutcome(this.vault, receipt.logs);
|
|
123
|
+
if (receipt.status === "success" && !outcome.executed) {
|
|
124
|
+
const blocked = outcome.breaches.find((b) => b.blocked);
|
|
125
|
+
if (blocked)
|
|
126
|
+
throw new BlockedByMandateError(blocked, txHash);
|
|
127
|
+
throw new VaultFrozenError(this.vault, outcome.breaches[0], txHash);
|
|
128
|
+
}
|
|
129
|
+
return receipt;
|
|
130
|
+
}
|
|
131
|
+
async nativeTransfer() {
|
|
132
|
+
throw new BondedError("NATIVE_VALUE", "the Mandate Vault holds ERC-20 tokens only; it can't send native value");
|
|
133
|
+
}
|
|
134
|
+
/** Signs with the agent key. This is not a signature by the vault. */
|
|
135
|
+
signMessage(message) {
|
|
136
|
+
return this.#wallet.signMessage(message);
|
|
137
|
+
}
|
|
138
|
+
async sign() {
|
|
139
|
+
throw bypass("raw hashes");
|
|
140
|
+
}
|
|
141
|
+
async signTypedData() {
|
|
142
|
+
throw bypass("typed data (e.g. permits)");
|
|
143
|
+
}
|
|
144
|
+
async signTransaction() {
|
|
145
|
+
throw bypass("transactions");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Signs EIP-191 messages with an AgentKit wallet. Text is passed as its UTF-8 bytes, so a
|
|
150
|
+
* message that happens to look like hex is still signed as text.
|
|
151
|
+
*/
|
|
152
|
+
export function agentKitSigner(wallet) {
|
|
153
|
+
return {
|
|
154
|
+
signMessage: ({ message }) => wallet.signMessage(typeof message === "string"
|
|
155
|
+
? stringToBytes(message)
|
|
156
|
+
: typeof message.raw === "string"
|
|
157
|
+
? hexToBytes(message.raw)
|
|
158
|
+
: message.raw),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function bypass(what) {
|
|
162
|
+
return new BondedError("UNSUPPORTED", `the vault holds the funds and can't sign ${what}; a signature from the agent key would act outside the vault`);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* An AgentKit action provider whose actions are the episode's tools, named
|
|
166
|
+
* `BondedEpisodeActionProvider_<tool>` as AgentKit names its own. Each action returns what the
|
|
167
|
+
* Arena returned, as text; a failed call returns `Error: ...` so the model can react. Works on
|
|
168
|
+
* any network and with any wallet provider: the tools don't use the wallet.
|
|
169
|
+
*
|
|
170
|
+
* In tools mode the episode's writes (`swap`, `transfer`, `approve`) are tools too. In chain mode
|
|
171
|
+
* only the reads are, and writes are transactions to the episode vault: use a
|
|
172
|
+
* `BondedWalletProvider` for `episode.vault` with `episode.sessionKey` and `episode.endpoints.rpc`.
|
|
173
|
+
*/
|
|
174
|
+
export class BondedEpisodeActionProvider extends ActionProvider {
|
|
175
|
+
#episode;
|
|
176
|
+
constructor(episode) {
|
|
177
|
+
super("bonded_episode", []);
|
|
178
|
+
this.#episode = episode;
|
|
179
|
+
}
|
|
180
|
+
getActions() {
|
|
181
|
+
return this.#episode.tools.map((t) => ({
|
|
182
|
+
name: `BondedEpisodeActionProvider_${t.name}`,
|
|
183
|
+
description: t.description,
|
|
184
|
+
schema: zod3FromJsonSchema(t.inputSchema),
|
|
185
|
+
invoke: async (args) => resultText(await t.call(args ?? {})),
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
supportsNetwork() {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The episode's tools as an AgentKit action provider. With no argument it reads the episode from
|
|
194
|
+
* the runner's `BONDED_*` variables. Switch to it only while being rated:
|
|
195
|
+
*
|
|
196
|
+
* ```ts
|
|
197
|
+
* const actionProviders = isBondedEpisode()
|
|
198
|
+
* ? [await bondedEpisodeActionProvider()]
|
|
199
|
+
* : [erc20ActionProvider(), myProviders()];
|
|
200
|
+
* const kit = await AgentKit.from({ walletProvider, actionProviders });
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export async function bondedEpisodeActionProvider(from) {
|
|
204
|
+
return new BondedEpisodeActionProvider(await resolveTools(from));
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=agentkit.js.map
|
package/dist/ai-sdk.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Tool } from "ai";
|
|
2
|
+
import { type EpisodeTools, type EpisodeToolsSource } from "./mcp.js";
|
|
3
|
+
import { type BondedVaultToolsOptions } from "./vault-tools.js";
|
|
4
|
+
export type { BondedVaultToolsOptions } from "./vault-tools.js";
|
|
5
|
+
/**
|
|
6
|
+
* The episode's tools as a Vercel AI SDK tool set, for `generateText`/`streamText`/agents. Each
|
|
7
|
+
* keeps the tool's own JSON Schema; `execute` returns the Arena's output, or `{ error }` when the
|
|
8
|
+
* call failed, so the model can react as it would to a production tool.
|
|
9
|
+
*
|
|
10
|
+
* With no argument it reads the episode from the runner's `BONDED_*` variables. Switch to them
|
|
11
|
+
* only while being rated:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* const tools = isBondedEpisode() ? await bondedAiSdkTools() : productionTools;
|
|
15
|
+
* await generateText({ model, tools, stopWhen: stepCountIs(10), prompt: task.goal });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Peer dependency: `ai` 5 or later (tested with 7).
|
|
19
|
+
*/
|
|
20
|
+
export declare function bondedAiSdkTools(from?: EpisodeTools | EpisodeToolsSource): Promise<Record<string, Tool>>;
|
|
21
|
+
/**
|
|
22
|
+
* The agent's Mandate Vault as an AI SDK tool set, for production: `swap`, `transfer`, `approve`
|
|
23
|
+
* and `vault_status`, every write sent through `BondedVault` so the vault checks it against the
|
|
24
|
+
* mandate. Each write is dry-run first and refused on a predicted breach (`guard`, on by
|
|
25
|
+
* default). With `sentinel`, Sentinel is asked too; what the agent read is taken from the tool
|
|
26
|
+
* results earlier in the conversation, unless you pass `context`. A refusal or failure returns
|
|
27
|
+
* `{ error }`, so the model can react.
|
|
28
|
+
*
|
|
29
|
+
* The names and arguments match the episode's write tools, so the agent's prompts work the same
|
|
30
|
+
* while it is rated:
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* const tools = isBondedEpisode()
|
|
34
|
+
* ? await bondedAiSdkTools()
|
|
35
|
+
* : { ...productionTools, ...bondedVaultAiSdkTools({ vault }) };
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function bondedVaultAiSdkTools(options: BondedVaultToolsOptions): Record<string, Tool>;
|
|
39
|
+
//# sourceMappingURL=ai-sdk.d.ts.map
|
package/dist/ai-sdk.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { jsonSchema, tool } from "ai";
|
|
2
|
+
import { SENTINEL_INPUT_CHARS } from "@bondedhq/shared";
|
|
3
|
+
import { resolveTools } from "./mcp.js";
|
|
4
|
+
import { BONDED_VAULT_TOOLS, bondedVaultTools, } from "./vault-tools.js";
|
|
5
|
+
/**
|
|
6
|
+
* The episode's tools as a Vercel AI SDK tool set, for `generateText`/`streamText`/agents. Each
|
|
7
|
+
* keeps the tool's own JSON Schema; `execute` returns the Arena's output, or `{ error }` when the
|
|
8
|
+
* call failed, so the model can react as it would to a production tool.
|
|
9
|
+
*
|
|
10
|
+
* With no argument it reads the episode from the runner's `BONDED_*` variables. Switch to them
|
|
11
|
+
* only while being rated:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* const tools = isBondedEpisode() ? await bondedAiSdkTools() : productionTools;
|
|
15
|
+
* await generateText({ model, tools, stopWhen: stepCountIs(10), prompt: task.goal });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Peer dependency: `ai` 5 or later (tested with 7).
|
|
19
|
+
*/
|
|
20
|
+
export async function bondedAiSdkTools(from) {
|
|
21
|
+
const episode = await resolveTools(from);
|
|
22
|
+
return Object.fromEntries(episode.tools.map((t) => [
|
|
23
|
+
t.name,
|
|
24
|
+
tool({
|
|
25
|
+
description: t.description,
|
|
26
|
+
inputSchema: jsonSchema(t.inputSchema),
|
|
27
|
+
execute: async (args) => {
|
|
28
|
+
const result = await t.call(args ?? {});
|
|
29
|
+
return result.ok ? result.output : { error: result.error };
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
]));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The agent's Mandate Vault as an AI SDK tool set, for production: `swap`, `transfer`, `approve`
|
|
36
|
+
* and `vault_status`, every write sent through `BondedVault` so the vault checks it against the
|
|
37
|
+
* mandate. Each write is dry-run first and refused on a predicted breach (`guard`, on by
|
|
38
|
+
* default). With `sentinel`, Sentinel is asked too; what the agent read is taken from the tool
|
|
39
|
+
* results earlier in the conversation, unless you pass `context`. A refusal or failure returns
|
|
40
|
+
* `{ error }`, so the model can react.
|
|
41
|
+
*
|
|
42
|
+
* The names and arguments match the episode's write tools, so the agent's prompts work the same
|
|
43
|
+
* while it is rated:
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* const tools = isBondedEpisode()
|
|
47
|
+
* ? await bondedAiSdkTools()
|
|
48
|
+
* : { ...productionTools, ...bondedVaultAiSdkTools({ vault }) };
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function bondedVaultAiSdkTools(options) {
|
|
52
|
+
return Object.fromEntries(bondedVaultTools(options).map((t) => [
|
|
53
|
+
t.name,
|
|
54
|
+
tool({
|
|
55
|
+
description: t.description,
|
|
56
|
+
inputSchema: jsonSchema(t.inputSchema),
|
|
57
|
+
execute: async (args, call) => {
|
|
58
|
+
const context = options.sentinel && !options.context
|
|
59
|
+
? { untrustedInputs: toolResultsIn(call?.messages ?? []) }
|
|
60
|
+
: undefined;
|
|
61
|
+
const result = await t.call(args ?? {}, context);
|
|
62
|
+
return result.ok ? result.output : { error: result.error };
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
]));
|
|
66
|
+
}
|
|
67
|
+
/** The tool results the model has seen (the last 20), as Sentinel's untrusted inputs. */
|
|
68
|
+
function toolResultsIn(messages) {
|
|
69
|
+
const own = new Set(BONDED_VAULT_TOOLS);
|
|
70
|
+
const inputs = [];
|
|
71
|
+
for (const message of messages) {
|
|
72
|
+
const m = message;
|
|
73
|
+
if (m.role !== "tool" || !Array.isArray(m.content))
|
|
74
|
+
continue;
|
|
75
|
+
for (const part of m.content) {
|
|
76
|
+
if (part.type !== "tool-result" || !part.toolName || own.has(part.toolName))
|
|
77
|
+
continue;
|
|
78
|
+
const out = part.output;
|
|
79
|
+
const value = out && typeof out === "object" && "value" in out ? out.value : out;
|
|
80
|
+
const text = typeof value === "string" ? value : JSON.stringify(value ?? null);
|
|
81
|
+
inputs.push({ tool: part.toolName, output: text.slice(0, SENTINEL_INPUT_CHARS) });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return inputs.slice(-20);
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=ai-sdk.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type BreachType } from "@bondedhq/shared";
|
|
2
|
+
import { type Address, type Hex, type Log } from "viem";
|
|
3
|
+
/** One entry in the vault's breach log: what pays claims and decides ratings. */
|
|
4
|
+
export interface VaultBreach {
|
|
5
|
+
/** Index in the vault's log (`getBreach(id)`); predicted ids in a dry run. */
|
|
6
|
+
id: bigint;
|
|
7
|
+
type: BreachType;
|
|
8
|
+
/** Plain-language reason, from the breach type. */
|
|
9
|
+
reason: string;
|
|
10
|
+
/** The contract the action called; the zero address for a breach found by `reconcile`. */
|
|
11
|
+
target: Address;
|
|
12
|
+
/** The function called (4-byte selector). */
|
|
13
|
+
selector: Hex;
|
|
14
|
+
/** Value lost, in USD with 6 decimals (0 when blocked). */
|
|
15
|
+
lossUsd6: bigint;
|
|
16
|
+
/** Enforce mode stopped the action: nothing moved. */
|
|
17
|
+
blocked: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** Why the vault records a breach of this type, in plain words. */
|
|
20
|
+
export declare function breachReason(type: BreachType): string;
|
|
21
|
+
/** What the vault's events say about one `execute`: whether the call ran, and what it recorded. */
|
|
22
|
+
export interface ExecuteOutcome {
|
|
23
|
+
/** The call ran (an `Executed` event). False when Enforce mode blocked it or a freeze skipped it. */
|
|
24
|
+
executed: boolean;
|
|
25
|
+
breaches: VaultBreach[];
|
|
26
|
+
/** The vault froze during this action. */
|
|
27
|
+
froze: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** Reads the outcome of `execute` from its logs; only logs emitted by `vault` count. */
|
|
30
|
+
export declare function readExecuteOutcome(vault: Address, logs: readonly Log[]): ExecuteOutcome;
|
|
31
|
+
/**
|
|
32
|
+
* What the vault would do with an action, from simulating `execute` against the chain's
|
|
33
|
+
* current state: the contract's own checks, not a copy of them.
|
|
34
|
+
*/
|
|
35
|
+
export interface DryRunResult {
|
|
36
|
+
/** The action would run and the vault would record no breach. */
|
|
37
|
+
ok: boolean;
|
|
38
|
+
/** The call would run. False when Enforce mode would block it or a freeze would skip it. */
|
|
39
|
+
executed: boolean;
|
|
40
|
+
/** Enforce mode would stop it (the attempt is still recorded as a breach). */
|
|
41
|
+
blocked: boolean;
|
|
42
|
+
/** The breach this action would cause, if any (an earlier unexplained outflow comes first). */
|
|
43
|
+
breach?: VaultBreach;
|
|
44
|
+
/** Every breach the vault would record, in order. */
|
|
45
|
+
breaches: VaultBreach[];
|
|
46
|
+
/** The vault would freeze. */
|
|
47
|
+
wouldFreeze: boolean;
|
|
48
|
+
}
|
|
49
|
+
export declare function toDryRunResult(outcome: ExecuteOutcome): DryRunResult;
|
|
50
|
+
//# sourceMappingURL=breaches.d.ts.map
|
package/dist/breaches.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { breachTypeFromCode, mandateVaultAbi } from "@bondedhq/shared";
|
|
2
|
+
import { isAddressEqual, parseEventLogs } from "viem";
|
|
3
|
+
const REASONS = {
|
|
4
|
+
T1_UNAUTHORIZED_TARGET: "the mandate doesn't allow calling this contract or function",
|
|
5
|
+
T1_UNAUTHORIZED_APPROVAL: "the mandate doesn't allow approving this spender",
|
|
6
|
+
T1_UNAUTHORIZED_TRANSFER: "the recipient is neither the vault nor one of the mandate's destinations",
|
|
7
|
+
T1_DEFERRED: "the vault's balances dropped earlier without an action that explains it",
|
|
8
|
+
T2_LIMIT: "the action goes over the mandate's per-trade or daily limit",
|
|
9
|
+
T3_ASSET: "the action loses more value than the mandate's slippage allows (e.g. a honeypot or lookalike token)",
|
|
10
|
+
};
|
|
11
|
+
/** Why the vault records a breach of this type, in plain words. */
|
|
12
|
+
export function breachReason(type) {
|
|
13
|
+
return REASONS[type];
|
|
14
|
+
}
|
|
15
|
+
/** Reads the outcome of `execute` from its logs; only logs emitted by `vault` count. */
|
|
16
|
+
export function readExecuteOutcome(vault, logs) {
|
|
17
|
+
const own = logs.filter((log) => isAddressEqual(log.address, vault));
|
|
18
|
+
const events = parseEventLogs({
|
|
19
|
+
abi: mandateVaultAbi,
|
|
20
|
+
logs: own,
|
|
21
|
+
eventName: ["Executed", "BreachRecorded", "Frozen"],
|
|
22
|
+
});
|
|
23
|
+
const breaches = [];
|
|
24
|
+
let executed = false;
|
|
25
|
+
let froze = false;
|
|
26
|
+
for (const event of events) {
|
|
27
|
+
if (event.eventName === "Executed")
|
|
28
|
+
executed = true;
|
|
29
|
+
else if (event.eventName === "Frozen")
|
|
30
|
+
froze = true;
|
|
31
|
+
else {
|
|
32
|
+
const type = breachTypeFromCode(event.args.breachType);
|
|
33
|
+
breaches.push({
|
|
34
|
+
id: event.args.breachId,
|
|
35
|
+
type,
|
|
36
|
+
reason: breachReason(type),
|
|
37
|
+
target: event.args.target,
|
|
38
|
+
selector: event.args.selector,
|
|
39
|
+
lossUsd6: event.args.lossUsd6,
|
|
40
|
+
blocked: event.args.blocked,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { executed, breaches, froze };
|
|
45
|
+
}
|
|
46
|
+
export function toDryRunResult(outcome) {
|
|
47
|
+
const own = outcome.breaches.find((b) => b.type !== "T1_DEFERRED") ?? outcome.breaches[0];
|
|
48
|
+
return {
|
|
49
|
+
ok: outcome.executed && outcome.breaches.length === 0,
|
|
50
|
+
executed: outcome.executed,
|
|
51
|
+
blocked: outcome.breaches.some((b) => b.blocked),
|
|
52
|
+
...(own ? { breach: own } : {}),
|
|
53
|
+
breaches: outcome.breaches,
|
|
54
|
+
wouldFreeze: outcome.froze,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=breaches.js.map
|
package/dist/done.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DoneRequest } from "@bondedhq/shared";
|
|
2
|
+
/**
|
|
3
|
+
* Tells the gateway the agent is finished: `POST {done}` with an optional final message and the
|
|
4
|
+
* agent's own model usage (tokens, cost), which Bonded uses to estimate what a full rating costs
|
|
5
|
+
* you. Exiting the process (runner) or the deadline also ends the episode.
|
|
6
|
+
*/
|
|
7
|
+
export declare function finishEpisode(episode: {
|
|
8
|
+
endpoints: {
|
|
9
|
+
done: string;
|
|
10
|
+
};
|
|
11
|
+
}, body?: DoneRequest, options?: {
|
|
12
|
+
fetch?: typeof fetch;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
//# sourceMappingURL=done.d.ts.map
|
package/dist/done.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BondedError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tells the gateway the agent is finished: `POST {done}` with an optional final message and the
|
|
4
|
+
* agent's own model usage (tokens, cost), which Bonded uses to estimate what a full rating costs
|
|
5
|
+
* you. Exiting the process (runner) or the deadline also ends the episode.
|
|
6
|
+
*/
|
|
7
|
+
export async function finishEpisode(episode, body = {}, options = {}) {
|
|
8
|
+
const response = await (options.fetch ?? fetch)(episode.endpoints.done, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: { "content-type": "application/json" },
|
|
11
|
+
body: JSON.stringify(body),
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok)
|
|
14
|
+
throw new BondedError("EPISODE_DONE", `the gateway refused /done: HTTP ${response.status} ${(await response.text()).slice(0, 200)}`);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=done.js.map
|
package/dist/eliza.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts of ElizaOS's plugin interface this plugin uses, written out so the SDK doesn't
|
|
3
|
+
* import @elizaos/core: its type declarations only resolve with `moduleResolution: "bundler"`.
|
|
4
|
+
* test/eliza-types.ts checks against the real types that a plugin built here is an ElizaOS
|
|
5
|
+
* `Plugin` (run by `pnpm typecheck`).
|
|
6
|
+
*/
|
|
7
|
+
export interface ElizaMemory {
|
|
8
|
+
content: {
|
|
9
|
+
text?: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface ElizaState {
|
|
13
|
+
text?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ElizaRuntime {
|
|
16
|
+
useModel(modelType: "OBJECT_SMALL", params: {
|
|
17
|
+
prompt: string;
|
|
18
|
+
}): Promise<unknown>;
|
|
19
|
+
}
|
|
20
|
+
type ElizaCallback = (response: {
|
|
21
|
+
text?: string;
|
|
22
|
+
actions?: string[];
|
|
23
|
+
}) => Promise<unknown>;
|
|
24
|
+
export interface ElizaActionResult {
|
|
25
|
+
success: boolean;
|
|
26
|
+
text?: string;
|
|
27
|
+
data?: Record<string, unknown>;
|
|
28
|
+
error?: string | Error;
|
|
29
|
+
}
|
|
30
|
+
export interface ElizaAction {
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
name: string;
|
|
33
|
+
similes?: string[];
|
|
34
|
+
description: string;
|
|
35
|
+
validate: (runtime: ElizaRuntime, message: ElizaMemory, state?: ElizaState) => Promise<boolean>;
|
|
36
|
+
handler: (runtime: ElizaRuntime, message: ElizaMemory, state?: ElizaState, options?: {
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}, callback?: ElizaCallback) => Promise<ElizaActionResult>;
|
|
39
|
+
}
|
|
40
|
+
export interface ElizaProvider {
|
|
41
|
+
name: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
get: (runtime: ElizaRuntime, message: ElizaMemory, state: ElizaState) => Promise<{
|
|
44
|
+
text?: string;
|
|
45
|
+
values?: Record<string, unknown>;
|
|
46
|
+
data?: Record<string, unknown>;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
export interface ElizaPlugin {
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
actions: ElizaAction[];
|
|
53
|
+
providers: ElizaProvider[];
|
|
54
|
+
/** Runs when the runtime registers the plugin, before it reads `actions` and `providers`. */
|
|
55
|
+
init?: (config: Record<string, string>, runtime: unknown) => Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
import { type SentinelGuardOptions, type TokenInfo } from "./guarded.js";
|
|
58
|
+
import { type BondedEpisode } from "./episode.js";
|
|
59
|
+
import { type EpisodeTools } from "./mcp.js";
|
|
60
|
+
import type { BondedVault } from "./vault.js";
|
|
61
|
+
export interface BondedElizaOptions {
|
|
62
|
+
/** The vault, with the agent key to send from (and a `router` for swaps). */
|
|
63
|
+
vault: BondedVault;
|
|
64
|
+
/** Tokens the agent may name by symbol. Others must be given by address. */
|
|
65
|
+
tokens?: readonly TokenInfo[];
|
|
66
|
+
/**
|
|
67
|
+
* Dry-run each action first and refuse a predicted breach. On by default: an LLM-driven
|
|
68
|
+
* agent should not knowingly breach. The vault judges either way.
|
|
69
|
+
*/
|
|
70
|
+
guard?: boolean;
|
|
71
|
+
/** Ask Sentinel before each action, with the conversation as the untrusted input. */
|
|
72
|
+
sentinel?: SentinelGuardOptions;
|
|
73
|
+
/**
|
|
74
|
+
* Rating mode. With `"auto"` (the default), when the process is running a Bonded episode
|
|
75
|
+
* (`isBondedEpisode()`: the runner set `BONDED_*` variables), the plugin swaps its actions for
|
|
76
|
+
* the episode's, when the runtime registers it: see `bondedEpisodePlugin`. `"off"` keeps the
|
|
77
|
+
* vault actions always.
|
|
78
|
+
*/
|
|
79
|
+
rating?: "auto" | "off";
|
|
80
|
+
}
|
|
81
|
+
/** The action names this plugin adds. */
|
|
82
|
+
export declare const BONDED_ACTIONS: readonly ["BONDED_SWAP", "BONDED_TRANSFER", "BONDED_APPROVE"];
|
|
83
|
+
/**
|
|
84
|
+
* An ElizaOS plugin: `BONDED_SWAP`, `BONDED_TRANSFER` and `BONDED_APPROVE` act through the
|
|
85
|
+
* agent's Mandate Vault, and the `BONDED_VAULT` provider tells the agent the vault's state.
|
|
86
|
+
*
|
|
87
|
+
* Each action takes its parameters from `options.parameters` when the caller passes them, or
|
|
88
|
+
* asks the runtime's `OBJECT_SMALL` model to read them from the conversation. Amounts are whole
|
|
89
|
+
* tokens ("12.5"). By default each action is dry-run against the vault first and refused if the
|
|
90
|
+
* vault would record a breach; with `sentinel`, Sentinel is asked too, with the conversation as
|
|
91
|
+
* what the agent read.
|
|
92
|
+
*/
|
|
93
|
+
export declare function bondedPlugin(options: BondedElizaOptions): ElizaPlugin;
|
|
94
|
+
export interface BondedEpisodeElizaOptions {
|
|
95
|
+
/** The episode. Default: the one the runner set up (`fromBondedEnv`). */
|
|
96
|
+
episode?: BondedEpisode;
|
|
97
|
+
/** An existing connection to the episode's tools (otherwise one is made at `init`). */
|
|
98
|
+
tools?: EpisodeTools;
|
|
99
|
+
/** Chain mode: dry-run each vault action first (default true), as `bondedPlugin` does. */
|
|
100
|
+
guard?: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Rating mode for ElizaOS: a plugin whose actions are the episode's tools, so an Eliza agent is
|
|
104
|
+
* rated with the same character and model it runs in production. Each tool becomes an action
|
|
105
|
+
* named after it in capitals (`get_prices` is `GET_PRICES`); its parameters come from
|
|
106
|
+
* `options.parameters`, or the runtime's `OBJECT_SMALL` model reads them from the conversation
|
|
107
|
+
* against the tool's JSON Schema. The `BONDED_TASK` provider gives the agent the episode's task.
|
|
108
|
+
*
|
|
109
|
+
* In tools mode the writes (`SWAP`, `TRANSFER`, `APPROVE`) are tools too. In chain mode only the
|
|
110
|
+
* reads are, and the plugin adds `BONDED_SWAP`, `BONDED_TRANSFER` and `BONDED_APPROVE` on the
|
|
111
|
+
* episode vault, signed with the episode's session key (`episodeVault`).
|
|
112
|
+
*
|
|
113
|
+
* The actions are filled in by `init`, which the runtime calls when it registers the plugin
|
|
114
|
+
* (before it reads them). `bondedPlugin` does this by itself while an episode is running.
|
|
115
|
+
*/
|
|
116
|
+
export declare function bondedEpisodePlugin(options?: BondedEpisodeElizaOptions): ElizaPlugin;
|
|
117
|
+
export {};
|
|
118
|
+
//# sourceMappingURL=eliza.d.ts.map
|