@bondedhq/runner 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -18
- package/dist/init/check.d.ts +4 -1
- package/dist/init/check.js +8 -3
- package/dist/init/command.js +16 -3
- package/dist/init/detect.d.ts +12 -1
- package/dist/init/detect.js +39 -2
- package/dist/init/plan.d.ts +5 -0
- package/dist/init/plan.js +12 -3
- package/examples/agent-url-agent.mjs +19 -3
- package/examples/mcp-agent.mjs +20 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -16,12 +16,14 @@ mode, with `createBondedHandler` from `@bondedhq/sdk/agent-url`). The full guide
|
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
18
|
export BONDED_RUNNER_TOKEN=... # from your agent's page; keep it out of shell history
|
|
19
|
-
npx @bondedhq/runner --gateway https://arena.
|
|
19
|
+
npx @bondedhq/runner --gateway https://arena-worker-production.up.railway.app -- node my-agent.js
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
Node 22 or later. Get a runner token, and the command with it filled in, from
|
|
23
|
+
[Test your agent](https://bonded-nu.vercel.app/agents/test) (a free preview) or your registered
|
|
24
|
+
agent's page. `https://arena-worker-production.up.railway.app` is the hosted Arena Gateway (Robinhood Chain testnet).
|
|
25
|
+
From a clone of the Bonded repository instead (to try an unreleased change):
|
|
26
|
+
`pnpm install && pnpm build`, then `node apps/runner/dist/cli.js --gateway <url> -- <your agent command>`.
|
|
25
27
|
|
|
26
28
|
Options:
|
|
27
29
|
|
|
@@ -73,8 +75,9 @@ changes as a unified diff:
|
|
|
73
75
|
2. **Trading through your Mandate Vault in production**, once `BONDED_VAULT_ADDRESS` is set (with
|
|
74
76
|
`BONDED_AGENT_KEY`, and `BONDED_ROUTER_ADDRESS` for swaps). Until then the agent runs as before.
|
|
75
77
|
|
|
76
|
-
It puts the Bonded code in one new file (`src/bonded.ts
|
|
77
|
-
|
|
78
|
+
It puts the Bonded code in one new file (`src/bonded.ts` in a TypeScript project, meaning one with
|
|
79
|
+
a tsconfig.json, a typescript dependency or `.ts` source files; else `.js`, or `.mjs` for a
|
|
80
|
+
CommonJS package) and changes at most one line of yours, plus an import:
|
|
78
81
|
|
|
79
82
|
| Framework | The change in your code |
|
|
80
83
|
|---|---|
|
|
@@ -121,7 +124,7 @@ check. The older way, `--session <token>` (or `BONDED_SESSION_TOKEN`) with `--pr
|
|
|
121
124
|
process list, and `init` says so when you use `--token`.
|
|
122
125
|
|
|
123
126
|
```sh
|
|
124
|
-
BONDED_RUNNER_TOKEN=... npx @bondedhq/runner init --yes --gateway https://arena.
|
|
127
|
+
BONDED_RUNNER_TOKEN=... npx @bondedhq/runner init --yes --gateway https://arena-worker-production.up.railway.app -- node agent.js
|
|
125
128
|
```
|
|
126
129
|
|
|
127
130
|
| Flag | Meaning |
|
|
@@ -183,10 +186,20 @@ listed in chain mode.
|
|
|
183
186
|
the trade its task names; in chain mode it signs that trade itself behind the SDK's guard. The
|
|
184
187
|
repository's tests rate it through this runner in both modes.
|
|
185
188
|
- [`examples/mcp-agent.mjs`](examples/mcp-agent.mjs), in this package: the smallest tool-mode
|
|
186
|
-
agent over MCP, in plain JavaScript with only `@modelcontextprotocol/sdk
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
agent over MCP, in plain JavaScript with only `@modelcontextprotocol/sdk` and no model. It
|
|
190
|
+
reads all nine surfaces and its portfolio, then makes the one trade its task names. Use it as a
|
|
191
|
+
template for connecting an agent in any language with an MCP client, or to try Bonded before
|
|
192
|
+
you have an agent of your own:
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
mkdir bonded-test && cd bonded-test && npm init -y
|
|
196
|
+
npm install @bondedhq/runner @modelcontextprotocol/sdk
|
|
197
|
+
cp node_modules/@bondedhq/runner/examples/mcp-agent.mjs .
|
|
198
|
+
BONDED_RUNNER_TOKEN=<the token the page shows> npx bonded-runner --gateway https://arena-worker-production.up.railway.app --mode tools -- node mcp-agent.mjs
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
On the Test your agent page, choose "Something else (MCP or HTTP)" and "On my computer or
|
|
202
|
+
server", and take the token from the runner command it shows.
|
|
190
203
|
- [`examples/agent-url-agent.mjs`](examples/agent-url-agent.mjs): the same agent for Agent URL
|
|
191
204
|
mode, where Bonded calls your HTTPS endpoint instead of a runner launching a command. A small
|
|
192
205
|
Node HTTP server that checks each call's signature with `node:crypto` and answers the episode,
|
|
@@ -201,9 +214,9 @@ background service that restarts if it stops and starts at boot. `bonded-runner
|
|
|
201
214
|
ready config; it installs and writes nothing, and never puts your runner token in the config:
|
|
202
215
|
|
|
203
216
|
```sh
|
|
204
|
-
npx @bondedhq/runner service --print systemd --gateway https://arena.
|
|
205
|
-
npx @bondedhq/runner service --print pm2 --gateway https://arena.
|
|
206
|
-
npx @bondedhq/runner service --print docker --gateway https://arena.
|
|
217
|
+
npx @bondedhq/runner service --print systemd --gateway https://arena-worker-production.up.railway.app -- node agent.js
|
|
218
|
+
npx @bondedhq/runner service --print pm2 --gateway https://arena-worker-production.up.railway.app -- node agent.js
|
|
219
|
+
npx @bondedhq/runner service --print docker --gateway https://arena-worker-production.up.railway.app -- node agent.js
|
|
207
220
|
```
|
|
208
221
|
|
|
209
222
|
- **systemd**: a unit file. The token goes in `bonded-runner.env` next to your agent
|
|
@@ -222,11 +235,11 @@ one), `--user <user>` (systemd; default: you).
|
|
|
222
235
|
|
|
223
236
|
## Docker
|
|
224
237
|
|
|
225
|
-
|
|
226
|
-
bonded
|
|
238
|
+
The image isn't on a registry yet, so build it once from the root of a clone of the
|
|
239
|
+
[Bonded repository](https://github.com/ddboy19912/bonded) (`git clone --recursive`) with
|
|
240
|
+
`docker build -f apps/runner/Dockerfile -t bonded-runner .`, then put your agent on top of it (see the comment at the top of the
|
|
227
241
|
Dockerfile). Pass the token at run time with `-e BONDED_RUNNER_TOKEN=...`. The image has no git,
|
|
228
|
-
so pass `-e BONDED_IMAGE_DIGEST=sha256:...` (your image's digest) for a pinned code hash.
|
|
229
|
-
isn't published yet: build it yourself.
|
|
242
|
+
so pass `-e BONDED_IMAGE_DIGEST=sha256:...` (your image's digest) for a pinned code hash.
|
|
230
243
|
|
|
231
244
|
## What a score means
|
|
232
245
|
|
package/dist/init/check.d.ts
CHANGED
|
@@ -74,6 +74,9 @@ export declare function serverSentEvents(body: ReadableStream<Uint8Array>): Asyn
|
|
|
74
74
|
export declare function eventLine(event: CheckEvent): string;
|
|
75
75
|
/** Where each problem's fix is explained, by its code (the anchor). */
|
|
76
76
|
export declare const TROUBLESHOOTING_URL = "https://bonded-nu.vercel.app/docs/guides/troubleshooting/";
|
|
77
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* The result as lines: the verdict, then each problem and warning, with its fix's link. A warning
|
|
79
|
+
* that is only a note (NOTE_CODES in @bondedhq/shared, e.g. model usage) prints as a note.
|
|
80
|
+
*/
|
|
78
81
|
export declare function resultLines(result: CheckResult): string[];
|
|
79
82
|
//# sourceMappingURL=check.d.ts.map
|
package/dist/init/check.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* `POST /previews/:id/checks` or `/self/agents/:id/checks`, then the same events with the
|
|
7
7
|
* session. The API decides pass or fail; this only prints it.
|
|
8
8
|
*/
|
|
9
|
+
import { isNote, problemCode } from "@bondedhq/shared";
|
|
9
10
|
/** The Bonded API the runner talks to when neither --api nor BONDED_API_URL says otherwise. */
|
|
10
11
|
export const DEFAULT_API_URL = "https://api-production-ba50.up.railway.app";
|
|
11
12
|
export async function runConnectionCheck(target, onEvent, options = {}) {
|
|
@@ -140,13 +141,17 @@ export function eventLine(event) {
|
|
|
140
141
|
}
|
|
141
142
|
/** Where each problem's fix is explained, by its code (the anchor). */
|
|
142
143
|
export const TROUBLESHOOTING_URL = "https://bonded-nu.vercel.app/docs/guides/troubleshooting/";
|
|
143
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* The result as lines: the verdict, then each problem and warning, with its fix's link. A warning
|
|
146
|
+
* that is only a note (NOTE_CODES in @bondedhq/shared, e.g. model usage) prints as a note.
|
|
147
|
+
*/
|
|
144
148
|
export function resultLines(result) {
|
|
145
149
|
const line = (kind, text) => {
|
|
146
150
|
const code = result.issues?.find((i) => i.message === text && i.severity === kind)?.code;
|
|
151
|
+
const label = kind === "warning" && isNote(code ?? problemCode(text)) ? "note" : kind;
|
|
147
152
|
return code && code !== "other" && /^[a-z0-9-]+$/.test(code)
|
|
148
|
-
? [` ${
|
|
149
|
-
: [` ${
|
|
153
|
+
? [` ${label}: ${text}`, ` how to fix: ${TROUBLESHOOTING_URL}#${code}`]
|
|
154
|
+
: [` ${label}: ${text}`];
|
|
150
155
|
};
|
|
151
156
|
return [
|
|
152
157
|
result.passed ? "Connection check passed: a rating will work." : "Connection check failed:",
|
package/dist/init/command.js
CHANGED
|
@@ -152,7 +152,7 @@ export async function initCommand(argv, io) {
|
|
|
152
152
|
const token = values.token ?? io.env.BONDED_RUNNER_TOKEN;
|
|
153
153
|
const gateway = values.gateway ?? io.env.BONDED_GATEWAY_URL;
|
|
154
154
|
if (!token || !gateway) {
|
|
155
|
-
io.out(`\nNext: start the runner with your runner token (from your agent's page):\n
|
|
155
|
+
io.out(`\nNext: start the runner with your runner token (from your agent's page):\n ${runnerLine("<gateway url>", mode.data, agentCommand(project, command).join(" ") || "<your agent command>")}\nthen run a connection check on the page. Or run init again with BONDED_RUNNER_TOKEN and --gateway: it starts the runner and runs the check here.`);
|
|
156
156
|
return 0;
|
|
157
157
|
}
|
|
158
158
|
const problem = gatewayUrlProblem(gateway);
|
|
@@ -162,7 +162,8 @@ export async function initCommand(argv, io) {
|
|
|
162
162
|
}
|
|
163
163
|
const agent = agentCommand(project, command);
|
|
164
164
|
if (!agent.length) {
|
|
165
|
-
|
|
165
|
+
const example = guessedCommand(project) ?? "node agent.js";
|
|
166
|
+
io.err(`No agent command: init can't tell how your agent starts (there's no start script in package.json). Put it after --, e.g. \`-- ${example}\`, or start the runner yourself:\n ${runnerLine(gateway, mode.data, guessedCommand(project) ?? "<your agent command>")}`);
|
|
166
167
|
return 1;
|
|
167
168
|
}
|
|
168
169
|
if (values.token)
|
|
@@ -210,6 +211,18 @@ export function apiUrlProblem(api) {
|
|
|
210
211
|
return undefined;
|
|
211
212
|
return "--api must be https (plain http only on localhost)";
|
|
212
213
|
}
|
|
214
|
+
/** The runner's own command line, with the token left for the owner to fill in. */
|
|
215
|
+
function runnerLine(gateway, mode, command) {
|
|
216
|
+
return `BONDED_RUNNER_TOKEN=... npx @bondedhq/runner --gateway ${gateway} --mode ${mode} -- ${command}`;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* A likely start command, only to show as an example (never run): `node <file>` when the folder
|
|
220
|
+
* has a single JavaScript file at its top level, like a copied example agent.
|
|
221
|
+
*/
|
|
222
|
+
function guessedCommand(project) {
|
|
223
|
+
const top = project.sources.filter((f) => !f.includes("/") && /\.(m?js)$/.test(f));
|
|
224
|
+
return top.length === 1 ? `node ${top[0]}` : undefined;
|
|
225
|
+
}
|
|
213
226
|
/** The agent's command: what came after --, else the package.json start script. */
|
|
214
227
|
export function agentCommand(project, given) {
|
|
215
228
|
if (given.length)
|
|
@@ -230,7 +243,7 @@ function printPlan(project, plan, out) {
|
|
|
230
243
|
}
|
|
231
244
|
}
|
|
232
245
|
else
|
|
233
|
-
out("\nNo file changes needed.");
|
|
246
|
+
out(plan.ready ? `\n${plan.ready}` : "\nNo file changes needed.");
|
|
234
247
|
if (plan.install.length)
|
|
235
248
|
out(`Adds: ${plan.install.join(" ")}`);
|
|
236
249
|
if (plan.manual.length) {
|
package/dist/init/detect.d.ts
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
export declare const FRAMEWORKS: readonly ["eliza", "agentkit", "langchain", "ai-sdk", "mcp", "python", "unknown"];
|
|
3
3
|
export type Framework = (typeof FRAMEWORKS)[number];
|
|
4
4
|
export declare const FRAMEWORK_NAMES: Record<Framework, string>;
|
|
5
|
+
/**
|
|
6
|
+
* The first line of every file `init` writes, so a second run knows the file is init's (`MARKER`
|
|
7
|
+
* in plan.ts). Defined here so detection can skip init's own files.
|
|
8
|
+
*/
|
|
9
|
+
export declare const INIT_MARKER = "// Added by `bonded-runner init`";
|
|
5
10
|
/** Web frameworks `init --url` can add an Agent URL route to. */
|
|
6
11
|
export type WebFramework = "next-app" | "next-pages" | "express" | "hono" | "node-http";
|
|
7
12
|
export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
|
|
@@ -12,7 +17,7 @@ export interface Project {
|
|
|
12
17
|
/** Why it decided on the framework, in words. */
|
|
13
18
|
evidence: string;
|
|
14
19
|
packageManager: PackageManager;
|
|
15
|
-
/** TypeScript
|
|
20
|
+
/** TypeScript: a tsconfig.json, a typescript dependency, or .ts/.tsx/.mts source files. */
|
|
16
21
|
typescript: boolean;
|
|
17
22
|
/** package.json `"type": "module"`: plain .js files are ES modules. */
|
|
18
23
|
esm: boolean;
|
|
@@ -29,6 +34,12 @@ export interface Project {
|
|
|
29
34
|
nextAppDir?: string;
|
|
30
35
|
nextMajor?: number;
|
|
31
36
|
hasSdk: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Why the agent already works with a rating as it is, when it does: its own code (not a file
|
|
39
|
+
* init wrote) reads the episode's tool addresses (BONDED_MCP_URL or BONDED_TOOLS_URL) or the
|
|
40
|
+
* whole episode (`fromBondedEnv` from @bondedhq/sdk). Like the example agents in this package.
|
|
41
|
+
*/
|
|
42
|
+
episodeReady?: string;
|
|
32
43
|
}
|
|
33
44
|
export declare function detectProject(root: string, override?: Framework): Project;
|
|
34
45
|
//# sourceMappingURL=detect.d.ts.map
|
package/dist/init/detect.js
CHANGED
|
@@ -19,6 +19,11 @@ export const FRAMEWORK_NAMES = {
|
|
|
19
19
|
python: "Python",
|
|
20
20
|
unknown: "an unrecognised framework",
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* The first line of every file `init` writes, so a second run knows the file is init's (`MARKER`
|
|
24
|
+
* in plan.ts). Defined here so detection can skip init's own files.
|
|
25
|
+
*/
|
|
26
|
+
export const INIT_MARKER = "// Added by `bonded-runner init`";
|
|
22
27
|
const SKIP = new Set([
|
|
23
28
|
"node_modules",
|
|
24
29
|
"dist",
|
|
@@ -42,9 +47,12 @@ export function detectProject(root, override) {
|
|
|
42
47
|
const dependencies = { ...pkg?.devDependencies, ...pkg?.dependencies };
|
|
43
48
|
const has = (name) => name in dependencies;
|
|
44
49
|
const tsconfig = readTsconfig(root);
|
|
45
|
-
const typescript = tsconfig !== undefined || has("typescript");
|
|
46
50
|
const sourceDir = existsSync(join(root, "src")) && statSync(join(root, "src")).isDirectory() ? "src" : "";
|
|
47
51
|
const sources = listSources(root);
|
|
52
|
+
// Written in TypeScript even without a tsconfig.json here (run with tsx, or a tsconfig in a
|
|
53
|
+
// parent folder): then init's helper is a .ts file too, which a strict project accepts.
|
|
54
|
+
const tsSources = sources.some((f) => /\.(ts|tsx|mts)$/.test(f));
|
|
55
|
+
const typescript = tsconfig !== undefined || has("typescript") || tsSources;
|
|
48
56
|
const found = override
|
|
49
57
|
? { framework: override, evidence: "chosen with --framework" }
|
|
50
58
|
: detectFramework(root, pkg !== undefined, has, sources);
|
|
@@ -59,7 +67,13 @@ export function detectProject(root, override) {
|
|
|
59
67
|
packageManager: packageManager(root, pkg?.packageManager),
|
|
60
68
|
typescript,
|
|
61
69
|
esm: pkg?.type === "module",
|
|
62
|
-
importExtension: resolution.startsWith("node16") ||
|
|
70
|
+
importExtension: resolution.startsWith("node16") ||
|
|
71
|
+
resolution.startsWith("nodenext") ||
|
|
72
|
+
// No tsconfig.json to read in an ES module package: `./bonded.js` names bonded.ts for tsc
|
|
73
|
+
// (NodeNext) and tsx alike.
|
|
74
|
+
(tsconfig === undefined && pkg?.type === "module" && typescript)
|
|
75
|
+
? ".js"
|
|
76
|
+
: "",
|
|
63
77
|
sourceDir,
|
|
64
78
|
sources,
|
|
65
79
|
dependencies,
|
|
@@ -76,8 +90,31 @@ export function detectProject(root, override) {
|
|
|
76
90
|
...(nextAppDir && has("next") ? { nextAppDir } : {}),
|
|
77
91
|
...(nextMajor ? { nextMajor } : {}),
|
|
78
92
|
hasSdk: has("@bondedhq/sdk"),
|
|
93
|
+
...episodeReady(root, sources),
|
|
79
94
|
};
|
|
80
95
|
}
|
|
96
|
+
/** Reading the episode's tools from the environment, or the episode itself, in the agent's code. */
|
|
97
|
+
const EPISODE_READ = /process\.env\.(BONDED_MCP_URL|BONDED_TOOLS_URL)\b|process\.env\[\s*["'`](BONDED_MCP_URL|BONDED_TOOLS_URL)["'`]\s*\]|\b(fromBondedEnv)\s*\(/;
|
|
98
|
+
/** The first of the agent's own files that already reads the episode (see `episodeReady`). */
|
|
99
|
+
function episodeReady(root, sources) {
|
|
100
|
+
for (const file of sources) {
|
|
101
|
+
let text;
|
|
102
|
+
try {
|
|
103
|
+
text = readFileSync(join(root, file), "utf8");
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (text.startsWith(INIT_MARKER))
|
|
109
|
+
continue;
|
|
110
|
+
const match = EPISODE_READ.exec(text);
|
|
111
|
+
if (!match)
|
|
112
|
+
continue;
|
|
113
|
+
const what = match[1] ?? match[2] ?? `${match[3]}()`;
|
|
114
|
+
return { episodeReady: `${file} already reads ${what}` };
|
|
115
|
+
}
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
81
118
|
function detectFramework(root, hasPackageJson, has, sources) {
|
|
82
119
|
if (has("@coinbase/agentkit"))
|
|
83
120
|
return { framework: "agentkit", evidence: "package.json depends on @coinbase/agentkit" };
|
package/dist/init/plan.d.ts
CHANGED
|
@@ -19,6 +19,11 @@ export interface InitPlan {
|
|
|
19
19
|
env: string[];
|
|
20
20
|
/** The packages to add, when they aren't there yet. */
|
|
21
21
|
install: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Set when the agent already works with a rating as it is (`Project.episodeReady`): why, in a
|
|
24
|
+
* sentence. Then init proposes nothing.
|
|
25
|
+
*/
|
|
26
|
+
ready?: string;
|
|
22
27
|
}
|
|
23
28
|
export interface PlanOptions {
|
|
24
29
|
/** Also add an Agent URL route for the project's web framework. */
|
package/dist/init/plan.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join, posix } from "node:path";
|
|
3
|
-
import { FRAMEWORK_NAMES } from "./detect.js";
|
|
3
|
+
import { FRAMEWORK_NAMES, INIT_MARKER } from "./detect.js";
|
|
4
4
|
/**
|
|
5
5
|
* The first line of every file `init` writes, so a second run knows the file is init's. It is
|
|
6
6
|
* rewritten only while it still holds exactly what init would write (or with --overwrite-generated).
|
|
7
7
|
*/
|
|
8
|
-
export const MARKER =
|
|
9
|
-
|
|
8
|
+
export const MARKER = INIT_MARKER;
|
|
9
|
+
/** The guide on the docs site (generated from docs/BRING-YOUR-OWN-AGENT.md). */
|
|
10
|
+
const GUIDE = "https://bonded-nu.vercel.app/docs/guides/bring-your-own-agent/";
|
|
10
11
|
/** The production vault's variables (`bondedVaultFromEnv` in @bondedhq/sdk). */
|
|
11
12
|
export const VAULT_ENV = ["BONDED_VAULT_ADDRESS", "BONDED_AGENT_KEY", "BONDED_ROUTER_ADDRESS"];
|
|
12
13
|
export function planInit(project, options = {}) {
|
|
@@ -23,6 +24,14 @@ export function planInit(project, options = {}) {
|
|
|
23
24
|
pythonPlan(plan);
|
|
24
25
|
return plan;
|
|
25
26
|
}
|
|
27
|
+
// Already wired to the episode (like this package's example agents): the rating hook would only
|
|
28
|
+
// wrap what it reads already, so init proposes nothing. An Agent URL route is still added on ask.
|
|
29
|
+
if (project.episodeReady && !options.url) {
|
|
30
|
+
plan.install = [];
|
|
31
|
+
plan.ready = `Already set up to be rated: ${project.episodeReady}, so it uses the episode's tools while Bonded rates it. Nothing to change for rating.`;
|
|
32
|
+
plan.notes.push(`To protect its real trades, send them through a Mandate Vault in production (${GUIDE}#protect-your-agent). init adds nothing for that here, so your code stays as it is.`);
|
|
33
|
+
return plan;
|
|
34
|
+
}
|
|
26
35
|
// src/bonded.ts, unless the project has its own file by that name: then the first free one
|
|
27
36
|
// of bonded-agent.ts, bonded-agent-2.ts, ..., so every import below names a file init wrote.
|
|
28
37
|
const helper = files.freeHelperPath("bonded", "bonded-agent");
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* BONDED_AGENT_SECRET=bonded_as_... PORT=8080 node agent-url-agent.mjs
|
|
8
8
|
*
|
|
9
9
|
* Put it behind HTTPS (a reverse proxy, a tunnel or your platform's TLS) and register that URL.
|
|
10
|
-
* Like mcp-agent.mjs it has no model: it reads
|
|
11
|
-
* and never acts on what it reads. Swap `work` for your own agent.
|
|
10
|
+
* Like mcp-agent.mjs it has no model: it reads every source an attack can be planted in, makes
|
|
11
|
+
* the one trade its task names and never acts on what it reads. Swap `work` for your own agent.
|
|
12
12
|
*
|
|
13
13
|
* Needs @modelcontextprotocol/sdk (npm install @modelcontextprotocol/sdk). Signature checking
|
|
14
14
|
* uses only node:crypto, so it ports to any language: HMAC-SHA256 of "<timestamp>.<raw body>",
|
|
@@ -50,6 +50,22 @@ function verified(headers, raw) {
|
|
|
50
50
|
return true;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Every read tool but get_token_info (read below, for the token it trades): one per attack
|
|
55
|
+
* surface, plus the portfolio. A source the agent never reads leaves its attacks untested.
|
|
56
|
+
*/
|
|
57
|
+
const READS = [
|
|
58
|
+
"read_inbox",
|
|
59
|
+
"read_news",
|
|
60
|
+
"read_social",
|
|
61
|
+
"get_prices",
|
|
62
|
+
"get_recent_transfers",
|
|
63
|
+
"get_market_report",
|
|
64
|
+
"read_market_alerts",
|
|
65
|
+
"read_counterparty_messages",
|
|
66
|
+
"get_portfolio",
|
|
67
|
+
];
|
|
68
|
+
|
|
53
69
|
/** Episodes in progress, so a cancel can stop one. */
|
|
54
70
|
const running = new Map();
|
|
55
71
|
|
|
@@ -65,7 +81,7 @@ async function work(episode, signal) {
|
|
|
65
81
|
};
|
|
66
82
|
try {
|
|
67
83
|
// Everything read here is untrusted: it can carry instructions. This agent never acts on them.
|
|
68
|
-
for (const name of
|
|
84
|
+
for (const name of READS) await call(name);
|
|
69
85
|
const match =
|
|
70
86
|
/\bbuy\s+([0-9][0-9,]*(?:\.[0-9]+)?)\s+(t[A-Z]{3,5})\s+worth\s+of\s+(t[A-Z]{3,5})\b/i.exec(
|
|
71
87
|
episode.task.goal,
|
package/examples/mcp-agent.mjs
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* bonded-runner --gateway <url> --mode tools -- node mcp-agent.mjs
|
|
6
6
|
*
|
|
7
7
|
* The runner starts it once per episode with the episode in its environment. It has no model:
|
|
8
|
-
* it reads
|
|
9
|
-
* trade its task asks for, and says it's done. Swap `decide` for your own
|
|
10
|
-
* LLM given `tools`
|
|
8
|
+
* it reads every source an attack can be planted in (a rating attacks all nine), looks up the
|
|
9
|
+
* token, makes the one trade its task asks for, and says it's done. Swap `decide` for your own
|
|
10
|
+
* agent (for example an LLM given `tools` and what it read); everything else stays the same.
|
|
11
11
|
*
|
|
12
12
|
* Needs @modelcontextprotocol/sdk (npm install @modelcontextprotocol/sdk).
|
|
13
13
|
*/
|
|
@@ -38,8 +38,24 @@ function decide(goal) {
|
|
|
38
38
|
return { tokenIn: match[2], tokenOut: match[3], amountIn: match[1].replaceAll(",", "") };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Every read tool but get_token_info (read below, for the token it trades): one per attack
|
|
43
|
+
* surface, plus the portfolio. A source the agent never reads leaves its attacks untested.
|
|
44
|
+
*/
|
|
45
|
+
const READS = [
|
|
46
|
+
"read_inbox",
|
|
47
|
+
"read_news",
|
|
48
|
+
"read_social",
|
|
49
|
+
"get_prices",
|
|
50
|
+
"get_recent_transfers",
|
|
51
|
+
"get_market_report",
|
|
52
|
+
"read_market_alerts",
|
|
53
|
+
"read_counterparty_messages",
|
|
54
|
+
"get_portfolio",
|
|
55
|
+
];
|
|
56
|
+
|
|
41
57
|
// Everything read here is untrusted: it can carry instructions. This agent never acts on them.
|
|
42
|
-
for (const name of
|
|
58
|
+
for (const name of READS) await call(name);
|
|
43
59
|
|
|
44
60
|
let finalMessage = `No trade in the task I could parse (${tools.length} tools available).`;
|
|
45
61
|
const trade = decide(task.goal);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bondedhq/runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Connects an AI trading agent running on your own servers to the Bonded Arena, so it can be rated where it runs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -46,13 +46,13 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"ws": "8.21.0",
|
|
49
|
-
"@bondedhq/shared": "^0.1.
|
|
49
|
+
"@bondedhq/shared": "^0.1.2"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@modelcontextprotocol/sdk": "1.30.1",
|
|
53
53
|
"@types/ws": "8.18.1",
|
|
54
54
|
"dotenv": "17.4.2",
|
|
55
|
-
"@bondedhq/sdk": "^0.1.
|
|
55
|
+
"@bondedhq/sdk": "^0.1.2"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsc -p tsconfig.build.json",
|