@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/actions.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { demoRouterAbi } from "@bondedhq/shared";
|
|
2
|
+
import { encodeFunctionData, erc20Abi, formatUnits, getAddress, isAddress, isHex, } from "viem";
|
|
3
|
+
import { BondedError } from "./errors.js";
|
|
4
|
+
/**
|
|
5
|
+
* Bonded's testnet market (`DemoRouter`: oracle prices minus a fee). Its `swap` has no minimum
|
|
6
|
+
* output, so `minOut` is checked against its quote before sending.
|
|
7
|
+
*/
|
|
8
|
+
export function demoRouter(address) {
|
|
9
|
+
const router = getAddress(address);
|
|
10
|
+
return {
|
|
11
|
+
address: router,
|
|
12
|
+
encodeSwap: (p) => encodeFunctionData({
|
|
13
|
+
abi: demoRouterAbi,
|
|
14
|
+
functionName: "swap",
|
|
15
|
+
args: [p.tokenIn, p.tokenOut, p.amountIn, p.recipient],
|
|
16
|
+
}),
|
|
17
|
+
quote: (client, p) => client.readContract({
|
|
18
|
+
address: router,
|
|
19
|
+
abi: demoRouterAbi,
|
|
20
|
+
functionName: "quote",
|
|
21
|
+
args: [p.tokenIn, p.tokenOut, p.amountIn],
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Checks an action's fields and normalises its addresses, with errors that name the field. */
|
|
26
|
+
export function normaliseAction(action) {
|
|
27
|
+
const address = (field, value) => {
|
|
28
|
+
if (typeof value !== "string" || !isAddress(value, { strict: false }))
|
|
29
|
+
throw new BondedError("INVALID_ACTION", `${action.type}: ${field} must be an address`);
|
|
30
|
+
return getAddress(value);
|
|
31
|
+
};
|
|
32
|
+
const amount = (field, value) => {
|
|
33
|
+
if (typeof value !== "bigint" || value < 0n)
|
|
34
|
+
throw new BondedError("INVALID_ACTION", `${action.type}: ${field} must be a non-negative bigint in the token's base units`);
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
37
|
+
switch (action.type) {
|
|
38
|
+
case "swap":
|
|
39
|
+
return {
|
|
40
|
+
type: "swap",
|
|
41
|
+
tokenIn: address("tokenIn", action.tokenIn),
|
|
42
|
+
tokenOut: address("tokenOut", action.tokenOut),
|
|
43
|
+
amountIn: amount("amountIn", action.amountIn),
|
|
44
|
+
...(action.minOut !== undefined ? { minOut: amount("minOut", action.minOut) } : {}),
|
|
45
|
+
};
|
|
46
|
+
case "transfer":
|
|
47
|
+
return {
|
|
48
|
+
type: "transfer",
|
|
49
|
+
token: address("token", action.token),
|
|
50
|
+
to: address("to", action.to),
|
|
51
|
+
amount: amount("amount", action.amount),
|
|
52
|
+
};
|
|
53
|
+
case "approve":
|
|
54
|
+
return {
|
|
55
|
+
type: "approve",
|
|
56
|
+
token: address("token", action.token),
|
|
57
|
+
spender: address("spender", action.spender),
|
|
58
|
+
amount: amount("amount", action.amount),
|
|
59
|
+
};
|
|
60
|
+
case "call":
|
|
61
|
+
if (!isHex(action.data))
|
|
62
|
+
throw new BondedError("INVALID_ACTION", "call: data must be 0x-prefixed hex");
|
|
63
|
+
return { type: "call", target: address("target", action.target), data: action.data };
|
|
64
|
+
default:
|
|
65
|
+
throw new BondedError("INVALID_ACTION", `unknown action type ${action.type}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** ERC-20 calldata for a transfer or approval. */
|
|
69
|
+
export function erc20Call(fn, token, who, amount) {
|
|
70
|
+
return {
|
|
71
|
+
target: token,
|
|
72
|
+
data: encodeFunctionData({ abi: erc20Abi, functionName: fn, args: [who, amount] }),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** An action in the words Sentinel was trained on: a tool name and string arguments. */
|
|
76
|
+
export function describeAction(action, tokens = []) {
|
|
77
|
+
const token = (a) => tokens.find((t) => t.address.toLowerCase() === a.toLowerCase());
|
|
78
|
+
const symbol = (a) => token(a)?.symbol ?? a;
|
|
79
|
+
const units = (a, value) => {
|
|
80
|
+
const t = token(a);
|
|
81
|
+
return t ? formatUnits(value, t.decimals) : value.toString();
|
|
82
|
+
};
|
|
83
|
+
switch (action.type) {
|
|
84
|
+
case "swap":
|
|
85
|
+
return {
|
|
86
|
+
tool: "swap",
|
|
87
|
+
args: {
|
|
88
|
+
tokenIn: symbol(action.tokenIn),
|
|
89
|
+
tokenOut: symbol(action.tokenOut),
|
|
90
|
+
amountIn: units(action.tokenIn, action.amountIn),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
case "transfer":
|
|
94
|
+
return {
|
|
95
|
+
tool: "transfer",
|
|
96
|
+
args: {
|
|
97
|
+
token: symbol(action.token),
|
|
98
|
+
to: action.to,
|
|
99
|
+
amount: units(action.token, action.amount),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
case "approve":
|
|
103
|
+
return {
|
|
104
|
+
tool: "approve",
|
|
105
|
+
args: {
|
|
106
|
+
token: symbol(action.token),
|
|
107
|
+
spender: action.spender,
|
|
108
|
+
amount: units(action.token, action.amount),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
case "call":
|
|
112
|
+
return { tool: "call", args: { target: action.target, data: action.data.slice(0, 2_000) } };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=actions.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { BondedEpisode } from "./episode.js";
|
|
3
|
+
import type { EpisodeUsage } from "./mcp.js";
|
|
4
|
+
/** For your own handler: the proof a verify answer carries, and the whole answer. */
|
|
5
|
+
export { agentUrlVerifyProof, answerAgentUrlVerify } from "@bondedhq/shared/agent-url";
|
|
6
|
+
export { SIGNATURE_HEADER, SIGNATURE_TOLERANCE_SECONDS, signBondedRequest, type SignatureCheck, TIMESTAMP_HEADER, verifyBondedSignature, } from "./signature.js";
|
|
7
|
+
/**
|
|
8
|
+
* Agent URL mode: instead of running the Bonded runner, an agent serves one HTTPS endpoint and
|
|
9
|
+
* Bonded calls it once per episode. Every call is a signed `POST` of one JSON message:
|
|
10
|
+
*
|
|
11
|
+
* - `{ type: "verify", challenge }` when the URL is registered: answer
|
|
12
|
+
* `200 { challenge, proof, version? }`, where `proof` is the hex HMAC-SHA256 of
|
|
13
|
+
* `"verify." + challenge` under the secret.
|
|
14
|
+
* - `{ type: "episode", ... }` (the runner protocol's `EpisodeMessage`): answer `2xx` within
|
|
15
|
+
* 10 seconds, then work: call the episode's tools and `POST {done}` when finished.
|
|
16
|
+
* - `{ type: "cancel", episodeId }`: stop that episode (best effort).
|
|
17
|
+
*
|
|
18
|
+
* `createBondedHandler` does all of that: it checks signatures, answers challenges, acknowledges an
|
|
19
|
+
* episode at once and runs your `onEpisode` in the background, aborts it on cancel or at the
|
|
20
|
+
* deadline, and posts `/done` (with the usage you return) when it resolves.
|
|
21
|
+
*/
|
|
22
|
+
/** What `onEpisode` may return: posted to the episode's `/done`. */
|
|
23
|
+
export interface EpisodeOutcome {
|
|
24
|
+
finalMessage?: string;
|
|
25
|
+
/** Your model's usage for the episode; Bonded uses it to estimate a full rating's cost. */
|
|
26
|
+
usage?: EpisodeUsage;
|
|
27
|
+
}
|
|
28
|
+
export interface EpisodeContext {
|
|
29
|
+
/**
|
|
30
|
+
* Aborted when Bonded cancels the episode or its deadline passes. Stop then: the episode is
|
|
31
|
+
* over and its endpoints refuse further calls. Pass it to your model and HTTP calls.
|
|
32
|
+
*/
|
|
33
|
+
signal: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
export interface BondedHandlerOptions {
|
|
36
|
+
/**
|
|
37
|
+
* The agent secret Bonded showed you when you registered the URL (`bonded_as_...`), or several
|
|
38
|
+
* while you rotate it (any one may match).
|
|
39
|
+
*/
|
|
40
|
+
secret: string | string[];
|
|
41
|
+
/** Runs one episode. Resolve when the agent is finished; the handler then posts `/done`. */
|
|
42
|
+
onEpisode(episode: BondedEpisode, context: EpisodeContext): Promise<EpisodeOutcome | void> | EpisodeOutcome | void;
|
|
43
|
+
/**
|
|
44
|
+
* Your agent's version (a release tag, a git commit). Reported when Bonded verifies the URL;
|
|
45
|
+
* it becomes the rating's code hash when you don't declare one.
|
|
46
|
+
*/
|
|
47
|
+
version?: string;
|
|
48
|
+
/** Called when `onEpisode` throws (default: `console.error`). `/done` is still posted. */
|
|
49
|
+
onError?: (error: unknown, episode: BondedEpisode) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Keeps the background work alive on platforms that stop at the response (Cloudflare's
|
|
52
|
+
* `ctx.waitUntil`, Vercel's `waitUntil`, Next.js `after`). The handler's second argument wins.
|
|
53
|
+
*/
|
|
54
|
+
waitUntil?: (work: Promise<unknown>) => void;
|
|
55
|
+
/** How far a request's timestamp may be from this server's clock. Default 300 seconds. */
|
|
56
|
+
toleranceSeconds?: number;
|
|
57
|
+
/** Largest request body accepted. Default 256 KiB (the most Bonded sends). */
|
|
58
|
+
maxBodyBytes?: number;
|
|
59
|
+
/** Most episodes run at once (Bonded sends one at a time per agent). Default 8. */
|
|
60
|
+
maxConcurrent?: number;
|
|
61
|
+
/** For tests: the clock, in milliseconds. */
|
|
62
|
+
now?: () => number;
|
|
63
|
+
/** For tests: the `fetch` used to post `/done`. */
|
|
64
|
+
fetch?: typeof fetch;
|
|
65
|
+
}
|
|
66
|
+
/** A standard Fetch API handler: Hono, Next.js route handlers, Bun, Deno, Cloudflare Workers. */
|
|
67
|
+
export type BondedHandler = (request: Request, context?: {
|
|
68
|
+
waitUntil?: (work: Promise<unknown>) => void;
|
|
69
|
+
}) => Promise<Response>;
|
|
70
|
+
/**
|
|
71
|
+
* An Agent URL endpoint for your agent.
|
|
72
|
+
*
|
|
73
|
+
* ```ts
|
|
74
|
+
* const handler = createBondedHandler({
|
|
75
|
+
* secret: process.env.BONDED_AGENT_SECRET!,
|
|
76
|
+
* version: process.env.GIT_SHA,
|
|
77
|
+
* async onEpisode(episode, { signal }) {
|
|
78
|
+
* const tools = await bondedAiSdkTools(episode);
|
|
79
|
+
* const { usage } = await generateText({ model, tools, prompt: episode.task.goal, abortSignal: signal });
|
|
80
|
+
* return { usage: { inputTokens: usage.inputTokens ?? 0, outputTokens: usage.outputTokens ?? 0 } };
|
|
81
|
+
* },
|
|
82
|
+
* });
|
|
83
|
+
* export const POST = handler; // Next.js; or app.post("/bonded", (c) => handler(c.req.raw)) in Hono
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export declare function createBondedHandler(options: BondedHandlerOptions): BondedHandler;
|
|
87
|
+
/**
|
|
88
|
+
* The handler as a Node `http` / Express / Fastify-style listener:
|
|
89
|
+
*
|
|
90
|
+
* ```ts
|
|
91
|
+
* app.post("/bonded", toNodeHandler(handler)); // Express: mount it before express.json()
|
|
92
|
+
* http.createServer(toNodeHandler(handler)).listen(8080);
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* The signature covers the raw body, so the body must reach it unparsed: either nothing has read
|
|
96
|
+
* the stream yet, or `req.body` is the raw `Buffer` or string (`express.raw()`/`express.text()`).
|
|
97
|
+
*/
|
|
98
|
+
export declare function toNodeHandler(handler: BondedHandler): (req: IncomingMessage & {
|
|
99
|
+
body?: unknown;
|
|
100
|
+
}, res: ServerResponse) => Promise<void>;
|
|
101
|
+
//# sourceMappingURL=agent-url.d.ts.map
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { AGENT_URL_MAX_BODY_BYTES, AGENT_URL_TOLERANCE_SECONDS, AgentUrlReplayGuard, answerAgentUrlVerify, verifyAgentUrlRequest, } from "@bondedhq/shared/agent-url";
|
|
2
|
+
import { finishEpisode } from "./done.js";
|
|
3
|
+
/** For your own handler: the proof a verify answer carries, and the whole answer. */
|
|
4
|
+
export { agentUrlVerifyProof, answerAgentUrlVerify } from "@bondedhq/shared/agent-url";
|
|
5
|
+
export { SIGNATURE_HEADER, SIGNATURE_TOLERANCE_SECONDS, signBondedRequest, TIMESTAMP_HEADER, verifyBondedSignature, } from "./signature.js";
|
|
6
|
+
/**
|
|
7
|
+
* An Agent URL endpoint for your agent.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const handler = createBondedHandler({
|
|
11
|
+
* secret: process.env.BONDED_AGENT_SECRET!,
|
|
12
|
+
* version: process.env.GIT_SHA,
|
|
13
|
+
* async onEpisode(episode, { signal }) {
|
|
14
|
+
* const tools = await bondedAiSdkTools(episode);
|
|
15
|
+
* const { usage } = await generateText({ model, tools, prompt: episode.task.goal, abortSignal: signal });
|
|
16
|
+
* return { usage: { inputTokens: usage.inputTokens ?? 0, outputTokens: usage.outputTokens ?? 0 } };
|
|
17
|
+
* },
|
|
18
|
+
* });
|
|
19
|
+
* export const POST = handler; // Next.js; or app.post("/bonded", (c) => handler(c.req.raw)) in Hono
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function createBondedHandler(options) {
|
|
23
|
+
if (!options.secret || (Array.isArray(options.secret) && options.secret.length === 0))
|
|
24
|
+
throw new Error("createBondedHandler needs the agent secret");
|
|
25
|
+
const now = options.now ?? Date.now;
|
|
26
|
+
const tolerance = options.toleranceSeconds ?? AGENT_URL_TOLERANCE_SECONDS;
|
|
27
|
+
const maxBody = options.maxBodyBytes ?? AGENT_URL_MAX_BODY_BYTES;
|
|
28
|
+
const maxConcurrent = options.maxConcurrent ?? 8;
|
|
29
|
+
const onError = options.onError ??
|
|
30
|
+
((error, episode) => console.error(`Bonded episode ${episode.episodeId} failed:`, error));
|
|
31
|
+
const secrets = Array.isArray(options.secret) ? options.secret : [options.secret];
|
|
32
|
+
/** Signatures already accepted, until they fall out of the window: replays are refused. */
|
|
33
|
+
const replay = new AgentUrlReplayGuard(tolerance);
|
|
34
|
+
const running = new Map();
|
|
35
|
+
const run = async (episode, controller) => {
|
|
36
|
+
const deadline = setTimeout(() => controller.abort(new Error("the episode's deadline passed")), episode.deadlineMs);
|
|
37
|
+
let outcome;
|
|
38
|
+
let failed = false;
|
|
39
|
+
try {
|
|
40
|
+
outcome = await options.onEpisode(episode, { signal: controller.signal });
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
failed = true;
|
|
44
|
+
if (!controller.signal.aborted)
|
|
45
|
+
onError(error, episode);
|
|
46
|
+
outcome = {
|
|
47
|
+
finalMessage: `the agent failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 4_000),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
clearTimeout(deadline);
|
|
52
|
+
running.delete(episode.episodeId);
|
|
53
|
+
}
|
|
54
|
+
// Cancelled or past its deadline: the episode is already over at the gateway.
|
|
55
|
+
if (controller.signal.aborted)
|
|
56
|
+
return;
|
|
57
|
+
try {
|
|
58
|
+
await finishEpisode(episode, {
|
|
59
|
+
...(outcome?.finalMessage !== undefined
|
|
60
|
+
? { finalMessage: outcome.finalMessage.slice(0, 4_000) }
|
|
61
|
+
: {}),
|
|
62
|
+
...(outcome?.usage ? { usage: outcome.usage } : {}),
|
|
63
|
+
}, options.fetch ? { fetch: options.fetch } : {});
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (!failed)
|
|
67
|
+
onError(error, episode);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return async (request, context) => {
|
|
71
|
+
if (request.method !== "POST")
|
|
72
|
+
return json(405, { error: "Bonded calls this endpoint with POST" }, { allow: "POST" });
|
|
73
|
+
const body = await readCapped(request, maxBody);
|
|
74
|
+
if (body === undefined)
|
|
75
|
+
return json(413, { error: `the body is over ${maxBody} bytes` });
|
|
76
|
+
const check = await verifyAgentUrlRequest({
|
|
77
|
+
secret: options.secret,
|
|
78
|
+
body,
|
|
79
|
+
headers: request.headers,
|
|
80
|
+
nowMs: now(),
|
|
81
|
+
toleranceSeconds: tolerance,
|
|
82
|
+
replay,
|
|
83
|
+
});
|
|
84
|
+
if (!check.ok)
|
|
85
|
+
return json(check.status, { error: check.reason });
|
|
86
|
+
const message = check.message;
|
|
87
|
+
// The answer proves the secret (an HMAC of the challenge), so an echo server can't pass.
|
|
88
|
+
if (message.type === "verify")
|
|
89
|
+
return json(200, await answerAgentUrlVerify(await signingSecret(secrets, body, request.headers, now(), tolerance), message, options.version));
|
|
90
|
+
if (message.type === "cancel") {
|
|
91
|
+
const controller = running.get(message.episodeId);
|
|
92
|
+
controller?.abort(new Error("Bonded cancelled the episode"));
|
|
93
|
+
return json(200, { ok: true, cancelled: controller !== undefined });
|
|
94
|
+
}
|
|
95
|
+
const episode = toEpisode(message);
|
|
96
|
+
if (running.has(episode.episodeId))
|
|
97
|
+
return json(200, { ok: true, episodeId: episode.episodeId, alreadyRunning: true });
|
|
98
|
+
if (running.size >= maxConcurrent)
|
|
99
|
+
return json(429, { error: `already running ${running.size} episodes` });
|
|
100
|
+
const controller = new AbortController();
|
|
101
|
+
running.set(episode.episodeId, controller);
|
|
102
|
+
const work = run(episode, controller);
|
|
103
|
+
// Called as a method: Cloudflare's ctx.waitUntil needs its `this`.
|
|
104
|
+
if (context?.waitUntil)
|
|
105
|
+
context.waitUntil(work);
|
|
106
|
+
else
|
|
107
|
+
options.waitUntil?.(work);
|
|
108
|
+
return json(202, { ok: true, episodeId: episode.episodeId });
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Which of the secrets signed a request already verified (while a secret rotates, the verify
|
|
113
|
+
* proof must be made with the one Bonded holds).
|
|
114
|
+
*/
|
|
115
|
+
async function signingSecret(secrets, body, headers, nowMs, toleranceSeconds) {
|
|
116
|
+
if (secrets.length === 1)
|
|
117
|
+
return secrets[0];
|
|
118
|
+
for (const secret of secrets) {
|
|
119
|
+
const check = await verifyAgentUrlRequest({ secret, body, headers, nowMs, toleranceSeconds });
|
|
120
|
+
if (check.ok)
|
|
121
|
+
return secret;
|
|
122
|
+
}
|
|
123
|
+
return secrets[0];
|
|
124
|
+
}
|
|
125
|
+
/** The episode as the agent sees it (the same shape `fromBondedEnv` returns). */
|
|
126
|
+
function toEpisode(message) {
|
|
127
|
+
// The runner protocol's message, minus `type`, `nonce` and `teeKeyId` (TEE-bound agents use
|
|
128
|
+
// the runner).
|
|
129
|
+
return {
|
|
130
|
+
episodeId: message.episodeId,
|
|
131
|
+
task: message.task,
|
|
132
|
+
mode: message.mode,
|
|
133
|
+
endpoints: message.endpoints,
|
|
134
|
+
vault: message.vault,
|
|
135
|
+
chainId: message.chainId,
|
|
136
|
+
contracts: { exchange: message.contracts.exchange, tokens: message.contracts.tokens },
|
|
137
|
+
...(message.sessionKey ? { sessionKey: message.sessionKey } : {}),
|
|
138
|
+
deadlineMs: message.deadlineMs,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function json(status, body, headers = {}) {
|
|
142
|
+
return new Response(JSON.stringify(body), {
|
|
143
|
+
status,
|
|
144
|
+
headers: { "content-type": "application/json", ...headers },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** The body as text, or undefined when it is over `max` bytes (read no further than that). */
|
|
148
|
+
async function readCapped(request, max) {
|
|
149
|
+
const declared = Number(request.headers.get("content-length") ?? "0");
|
|
150
|
+
if (declared > max)
|
|
151
|
+
return undefined;
|
|
152
|
+
if (!request.body)
|
|
153
|
+
return "";
|
|
154
|
+
const reader = request.body.getReader();
|
|
155
|
+
const chunks = [];
|
|
156
|
+
let size = 0;
|
|
157
|
+
for (;;) {
|
|
158
|
+
const { done, value } = await reader.read();
|
|
159
|
+
if (done)
|
|
160
|
+
break;
|
|
161
|
+
size += value.byteLength;
|
|
162
|
+
if (size > max) {
|
|
163
|
+
await reader.cancel();
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
chunks.push(value);
|
|
167
|
+
}
|
|
168
|
+
const bytes = new Uint8Array(size);
|
|
169
|
+
let offset = 0;
|
|
170
|
+
for (const chunk of chunks) {
|
|
171
|
+
bytes.set(chunk, offset);
|
|
172
|
+
offset += chunk.byteLength;
|
|
173
|
+
}
|
|
174
|
+
return new TextDecoder().decode(bytes);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The handler as a Node `http` / Express / Fastify-style listener:
|
|
178
|
+
*
|
|
179
|
+
* ```ts
|
|
180
|
+
* app.post("/bonded", toNodeHandler(handler)); // Express: mount it before express.json()
|
|
181
|
+
* http.createServer(toNodeHandler(handler)).listen(8080);
|
|
182
|
+
* ```
|
|
183
|
+
*
|
|
184
|
+
* The signature covers the raw body, so the body must reach it unparsed: either nothing has read
|
|
185
|
+
* the stream yet, or `req.body` is the raw `Buffer` or string (`express.raw()`/`express.text()`).
|
|
186
|
+
*/
|
|
187
|
+
export function toNodeHandler(handler) {
|
|
188
|
+
return async (req, res) => {
|
|
189
|
+
let body;
|
|
190
|
+
if (typeof req.body === "string" || req.body instanceof Uint8Array) {
|
|
191
|
+
body = req.body;
|
|
192
|
+
}
|
|
193
|
+
else if (req.body !== undefined && req.body !== null && !isEmptyObject(req.body)) {
|
|
194
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
195
|
+
res.end(JSON.stringify({
|
|
196
|
+
error: "the body was already parsed, so its signature can't be checked: mount the Bonded handler before express.json(), or use express.raw({ type: 'application/json' })",
|
|
197
|
+
}));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
else if (req.method === "POST") {
|
|
201
|
+
body = new ReadableStream({
|
|
202
|
+
start(controller) {
|
|
203
|
+
req.on("data", (chunk) => controller.enqueue(new Uint8Array(chunk)));
|
|
204
|
+
req.on("end", () => controller.close());
|
|
205
|
+
req.on("error", (error) => controller.error(error));
|
|
206
|
+
},
|
|
207
|
+
cancel() {
|
|
208
|
+
req.destroy();
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const headers = new Headers();
|
|
213
|
+
for (const [name, value] of Object.entries(req.headers))
|
|
214
|
+
if (typeof value === "string")
|
|
215
|
+
headers.set(name, value);
|
|
216
|
+
else if (Array.isArray(value))
|
|
217
|
+
headers.set(name, value.join(", "));
|
|
218
|
+
const request = new Request(`http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`, {
|
|
219
|
+
method: req.method ?? "GET",
|
|
220
|
+
headers,
|
|
221
|
+
...(body !== undefined ? { body, duplex: "half" } : {}),
|
|
222
|
+
});
|
|
223
|
+
const response = await handler(request);
|
|
224
|
+
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
225
|
+
res.end(Buffer.from(await response.arrayBuffer()));
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function isEmptyObject(value) {
|
|
229
|
+
return (typeof value === "object" &&
|
|
230
|
+
value !== null &&
|
|
231
|
+
!Array.isArray(value) &&
|
|
232
|
+
Object.keys(value).length === 0);
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=agent-url.js.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type Action, ActionProvider, EvmWalletProvider, type Network, type WalletProvider } from "@coinbase/agentkit";
|
|
2
|
+
import { type Address, type Hex, type TransactionReceipt, type TransactionRequest } from "viem";
|
|
3
|
+
import { type GuardOptions } from "./guarded.js";
|
|
4
|
+
import type { MessageSigner } from "./sentinel.js";
|
|
5
|
+
import { type EpisodeTools, type EpisodeToolsSource } from "./mcp.js";
|
|
6
|
+
export interface BondedWalletProviderOptions {
|
|
7
|
+
/**
|
|
8
|
+
* A wallet provider for the vault's agent key, e.g. AgentKit's `ViemWalletProvider`. It signs,
|
|
9
|
+
* pays gas and reads the chain; it never holds the funds.
|
|
10
|
+
*/
|
|
11
|
+
wallet: EvmWalletProvider;
|
|
12
|
+
/** The Mandate Vault (or MandateModule) whose agent key `wallet` is. */
|
|
13
|
+
vault: Address;
|
|
14
|
+
/**
|
|
15
|
+
* Where the funds are, which AgentKit sees as the wallet's address: the vault itself, or a
|
|
16
|
+
* module's smart account. `BondedWalletProvider.create` reads it from the chain.
|
|
17
|
+
*/
|
|
18
|
+
holder?: Address;
|
|
19
|
+
/**
|
|
20
|
+
* Check each transaction before sending it: a dry run of the vault's `execute`, refused on a
|
|
21
|
+
* predicted breach, and Sentinel if configured. Off by default: the vault judges either way.
|
|
22
|
+
*/
|
|
23
|
+
guard?: boolean | GuardOptions;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A Coinbase AgentKit wallet provider whose wallet is a Mandate Vault. Every transaction an
|
|
27
|
+
* action provider sends becomes `vault.execute(to, data)` from the agent key, so the vault checks
|
|
28
|
+
* it against the mandate and records any breach; `getAddress()` is where the funds
|
|
29
|
+
* are. Any AgentKit action provider then trades through the mandate unchanged.
|
|
30
|
+
*
|
|
31
|
+
* What a vault can't do is refused: native value (the vault is ERC-20 only), contract creation,
|
|
32
|
+
* and raw signatures or signed transactions from the agent key, which would bypass the vault.
|
|
33
|
+
* `signMessage` signs with the agent key (e.g. for API logins); it is not the vault's signature.
|
|
34
|
+
*
|
|
35
|
+
* In Enforce mode a blocked transaction still mines; `waitForTransactionReceipt` throws
|
|
36
|
+
* `BlockedByMandateError` for it, so the action reports a failure instead of a success.
|
|
37
|
+
*/
|
|
38
|
+
export declare class BondedWalletProvider extends EvmWalletProvider {
|
|
39
|
+
#private;
|
|
40
|
+
readonly vault: Address;
|
|
41
|
+
/** Where the funds are: the vault, or a module's smart account. */
|
|
42
|
+
readonly holder: Address;
|
|
43
|
+
constructor(options: BondedWalletProviderOptions);
|
|
44
|
+
/**
|
|
45
|
+
* A provider for a vault or a module, with `holder` read from the chain (a module's funds are
|
|
46
|
+
* in its smart account). Use this for modules; `new` assumes a vault unless `holder` is given.
|
|
47
|
+
*/
|
|
48
|
+
static create(options: BondedWalletProviderOptions): Promise<BondedWalletProvider>;
|
|
49
|
+
getAddress(): string;
|
|
50
|
+
getNetwork(): Network;
|
|
51
|
+
getName(): string;
|
|
52
|
+
/** The native balance where the funds are. The mandate covers ERC-20 tokens; gas is paid by the agent key. */
|
|
53
|
+
getBalance(): Promise<bigint>;
|
|
54
|
+
getPublicClient(): ReturnType<EvmWalletProvider["getPublicClient"]>;
|
|
55
|
+
readContract<P>(params: P): Promise<never>;
|
|
56
|
+
/** Sends `transaction` as `vault.execute(to, data)` from the agent key. */
|
|
57
|
+
sendTransaction(transaction: TransactionRequest): Promise<Hex>;
|
|
58
|
+
/**
|
|
59
|
+
* Waits for the transaction, then reads the vault's verdict from its events: throws
|
|
60
|
+
* `BlockedByMandateError` when Enforce mode stopped the call, `VaultFrozenError` when a
|
|
61
|
+
* freeze skipped it. A Monitor-mode breach goes through and is in the vault's breach log.
|
|
62
|
+
*/
|
|
63
|
+
waitForTransactionReceipt(txHash: Hex): Promise<TransactionReceipt>;
|
|
64
|
+
nativeTransfer(): Promise<Hex>;
|
|
65
|
+
/** Signs with the agent key. This is not a signature by the vault. */
|
|
66
|
+
signMessage(message: string | Uint8Array): Promise<Hex>;
|
|
67
|
+
sign(): Promise<Hex>;
|
|
68
|
+
signTypedData(): Promise<Hex>;
|
|
69
|
+
signTransaction(): Promise<Hex>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Signs EIP-191 messages with an AgentKit wallet. Text is passed as its UTF-8 bytes, so a
|
|
73
|
+
* message that happens to look like hex is still signed as text.
|
|
74
|
+
*/
|
|
75
|
+
export declare function agentKitSigner(wallet: EvmWalletProvider): MessageSigner;
|
|
76
|
+
/**
|
|
77
|
+
* An AgentKit action provider whose actions are the episode's tools, named
|
|
78
|
+
* `BondedEpisodeActionProvider_<tool>` as AgentKit names its own. Each action returns what the
|
|
79
|
+
* Arena returned, as text; a failed call returns `Error: ...` so the model can react. Works on
|
|
80
|
+
* any network and with any wallet provider: the tools don't use the wallet.
|
|
81
|
+
*
|
|
82
|
+
* In tools mode the episode's writes (`swap`, `transfer`, `approve`) are tools too. In chain mode
|
|
83
|
+
* only the reads are, and writes are transactions to the episode vault: use a
|
|
84
|
+
* `BondedWalletProvider` for `episode.vault` with `episode.sessionKey` and `episode.endpoints.rpc`.
|
|
85
|
+
*/
|
|
86
|
+
export declare class BondedEpisodeActionProvider extends ActionProvider<WalletProvider> {
|
|
87
|
+
#private;
|
|
88
|
+
constructor(episode: EpisodeTools);
|
|
89
|
+
getActions(): Action[];
|
|
90
|
+
supportsNetwork(): boolean;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The episode's tools as an AgentKit action provider. With no argument it reads the episode from
|
|
94
|
+
* the runner's `BONDED_*` variables. Switch to it only while being rated:
|
|
95
|
+
*
|
|
96
|
+
* ```ts
|
|
97
|
+
* const actionProviders = isBondedEpisode()
|
|
98
|
+
* ? [await bondedEpisodeActionProvider()]
|
|
99
|
+
* : [erc20ActionProvider(), myProviders()];
|
|
100
|
+
* const kit = await AgentKit.from({ walletProvider, actionProviders });
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare function bondedEpisodeActionProvider(from?: EpisodeTools | EpisodeToolsSource): Promise<BondedEpisodeActionProvider>;
|
|
104
|
+
//# sourceMappingURL=agentkit.d.ts.map
|