@agentlayer.tech/wallet 0.1.101 → 0.1.102

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.
Files changed (81) hide show
  1. package/.openclaw/extensions/agent-wallet/dist/index.js +46 -1
  2. package/.openclaw/extensions/agent-wallet/index.ts +46 -1
  3. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
  4. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  5. package/README.md +18 -0
  6. package/VERSION +1 -1
  7. package/agent-wallet/agent_wallet/__init__.py +1 -1
  8. package/agent-wallet/agent_wallet/connector_cli.py +198 -0
  9. package/agent-wallet/agent_wallet/connectors/__init__.py +28 -0
  10. package/agent-wallet/agent_wallet/connectors/catalog.py +68 -0
  11. package/agent-wallet/agent_wallet/connectors/client.py +308 -0
  12. package/agent-wallet/agent_wallet/connectors/intent_policy.py +243 -0
  13. package/agent-wallet/agent_wallet/connectors/manifest.py +192 -0
  14. package/agent-wallet/agent_wallet/connectors/registry.py +341 -0
  15. package/agent-wallet/agent_wallet/openclaw_adapter.py +56 -5
  16. package/agent-wallet/openclaw.plugin.json +1 -1
  17. package/agent-wallet/pyproject.toml +4 -2
  18. package/agent-wallet/scripts/build_release_bundle.py +1 -0
  19. package/agent-wallet/scripts/install_agent_wallet.py +1 -1
  20. package/bin/openclaw-agent-wallet.mjs +38 -0
  21. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
  22. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
  23. package/codex/plugins/agent-wallet/server.py +4 -1
  24. package/connectors/AGENTS.md +37 -0
  25. package/connectors/DEVELOPER_GUIDE.md +131 -0
  26. package/connectors/README.md +139 -0
  27. package/connectors/READ_ONLY_BETA.md +54 -0
  28. package/connectors/RELEASING.md +48 -0
  29. package/connectors/conformance/README.md +31 -0
  30. package/connectors/conformance/package.json +54 -0
  31. package/connectors/conformance/scripts/sync-license.mjs +3 -0
  32. package/connectors/conformance/scripts/sync-spec.mjs +16 -0
  33. package/connectors/conformance/src/cli.ts +43 -0
  34. package/connectors/conformance/src/index.ts +7 -0
  35. package/connectors/conformance/src/runner.ts +314 -0
  36. package/connectors/conformance/src/types.ts +27 -0
  37. package/connectors/conformance/test/fixtures/invalid-response-identity.json +14 -0
  38. package/connectors/conformance/test/fixtures/missing-tool-fixture.json +4 -0
  39. package/connectors/conformance/test/runner.test.mjs +151 -0
  40. package/connectors/conformance/tsconfig.json +17 -0
  41. package/connectors/examples/reference-crypto-data/Dockerfile +21 -0
  42. package/connectors/examples/reference-crypto-data/README.md +51 -0
  43. package/connectors/examples/reference-crypto-data/conformance.json +11 -0
  44. package/connectors/examples/reference-crypto-data/connector.json +95 -0
  45. package/connectors/examples/reference-crypto-data/package.json +21 -0
  46. package/connectors/examples/reference-crypto-data/railway.toml +9 -0
  47. package/connectors/examples/reference-crypto-data/src/connector.ts +137 -0
  48. package/connectors/examples/reference-crypto-data/src/server.ts +13 -0
  49. package/connectors/examples/reference-crypto-data/test/connector.test.mjs +97 -0
  50. package/connectors/examples/reference-crypto-data/tsconfig.json +17 -0
  51. package/connectors/package-lock.json +561 -0
  52. package/connectors/package.json +19 -0
  53. package/connectors/scripts/smoke-packed-packages.mjs +121 -0
  54. package/connectors/sdk-typescript/README.md +47 -0
  55. package/connectors/sdk-typescript/package.json +49 -0
  56. package/connectors/sdk-typescript/scripts/sync-license.mjs +3 -0
  57. package/connectors/sdk-typescript/src/connector.ts +204 -0
  58. package/connectors/sdk-typescript/src/errors.ts +11 -0
  59. package/connectors/sdk-typescript/src/http.ts +116 -0
  60. package/connectors/sdk-typescript/src/index.ts +26 -0
  61. package/connectors/sdk-typescript/src/types.ts +101 -0
  62. package/connectors/sdk-typescript/test/sdk.test.mjs +181 -0
  63. package/connectors/sdk-typescript/tsconfig.json +21 -0
  64. package/connectors/spec/connector-manifest.schema.json +340 -0
  65. package/connectors/spec/connector-protocol.md +159 -0
  66. package/connectors/spec/transaction-intent.schema.json +281 -0
  67. package/connectors/templates/read-only/.env.example +1 -0
  68. package/connectors/templates/read-only/Dockerfile +11 -0
  69. package/connectors/templates/read-only/README.md +33 -0
  70. package/connectors/templates/read-only/conformance.json +11 -0
  71. package/connectors/templates/read-only/connector.json +64 -0
  72. package/connectors/templates/read-only/package.json +21 -0
  73. package/connectors/templates/read-only/railway.toml +9 -0
  74. package/connectors/templates/read-only/src/connector.ts +61 -0
  75. package/connectors/templates/read-only/src/server.ts +14 -0
  76. package/connectors/templates/read-only/test/connector.test.mjs +62 -0
  77. package/connectors/templates/read-only/tsconfig.json +17 -0
  78. package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
  79. package/package.json +2 -1
  80. package/wdk-btc-wallet/package.json +1 -1
  81. package/wdk-evm-wallet/package.json +1 -1
@@ -0,0 +1,314 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import { createRequire } from "node:module";
4
+ import { Ajv2020, type ValidateFunction } from "ajv/dist/2020.js";
5
+ import type { FormatsPlugin } from "ajv-formats";
6
+
7
+ import type {
8
+ ConformanceCheck,
9
+ ConformanceFixture,
10
+ ConformanceOptions,
11
+ ConformanceReport,
12
+ } from "./types.js";
13
+
14
+ const require = createRequire(import.meta.url);
15
+ const addFormats = require("ajv-formats") as FormatsPlugin;
16
+ const MAX_RESPONSE_TTL_MS = 300_000;
17
+ const MAX_RESPONSE_BYTES = 1024 * 1024;
18
+ const RESERVED_WRITE_RESULT_KEYS = new Set([
19
+ "approval_token",
20
+ "broadcast_request",
21
+ "evm_transaction_intent",
22
+ "payment_intent",
23
+ "raw_transaction",
24
+ "signed_transaction",
25
+ "signing_request",
26
+ "solana_transaction_intent",
27
+ "transaction_intent",
28
+ ]);
29
+
30
+ function object(value: unknown): Record<string, unknown> | null {
31
+ return value && typeof value === "object" && !Array.isArray(value)
32
+ ? (value as Record<string, unknown>)
33
+ : null;
34
+ }
35
+
36
+ async function readJsonResponse(response: Response): Promise<unknown> {
37
+ const contentType = String(response.headers.get("content-type") ?? "").toLowerCase();
38
+ if (!contentType.startsWith("application/json")) {
39
+ throw new Error("Response Content-Type must be application/json.");
40
+ }
41
+ const contentLength = Number(response.headers.get("content-length") ?? 0);
42
+ if (Number.isFinite(contentLength) && contentLength > MAX_RESPONSE_BYTES) {
43
+ throw new Error("Response exceeds the 1 MiB protocol limit.");
44
+ }
45
+ if (!response.body) throw new Error("Response body is missing.");
46
+ const reader = response.body.getReader();
47
+ const chunks: Uint8Array[] = [];
48
+ let size = 0;
49
+ while (true) {
50
+ const { done, value } = await reader.read();
51
+ if (done) break;
52
+ size += value.byteLength;
53
+ if (size > MAX_RESPONSE_BYTES) {
54
+ await reader.cancel();
55
+ throw new Error("Response exceeds the 1 MiB protocol limit.");
56
+ }
57
+ chunks.push(value);
58
+ }
59
+ try {
60
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
61
+ } catch {
62
+ throw new Error("Response body is not valid JSON.");
63
+ }
64
+ }
65
+
66
+ async function defaultManifestSchema(): Promise<Record<string, unknown>> {
67
+ const url = new URL("../spec/connector-manifest.schema.json", import.meta.url);
68
+ return JSON.parse(await readFile(url, "utf8")) as Record<string, unknown>;
69
+ }
70
+
71
+ function buildAjv(): Ajv2020 {
72
+ const ajv = new Ajv2020({ allErrors: true, strict: true });
73
+ addFormats(ajv);
74
+ return ajv;
75
+ }
76
+
77
+ function identity(manifest: Record<string, unknown>): Record<string, string> {
78
+ return {
79
+ id: String(manifest.id),
80
+ version: String(manifest.version),
81
+ ...(manifest.artifact_digest ? { artifact_digest: String(manifest.artifact_digest) } : {}),
82
+ };
83
+ }
84
+
85
+ function assertNoWritePayload(value: unknown): void {
86
+ const pending: unknown[] = [value];
87
+ while (pending.length > 0) {
88
+ const current = pending.pop();
89
+ if (Array.isArray(current)) {
90
+ pending.push(...current);
91
+ } else if (current && typeof current === "object") {
92
+ for (const [key, item] of Object.entries(current)) {
93
+ if (RESERVED_WRITE_RESULT_KEYS.has(key.toLowerCase())) {
94
+ throw new Error(`Read result contains reserved write field: ${key}.`);
95
+ }
96
+ pending.push(item);
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ function checkResponseBinding(
103
+ payload: unknown,
104
+ manifest: Record<string, unknown>,
105
+ requestId: string,
106
+ toolName: string,
107
+ validateOutput: ValidateFunction
108
+ ): void {
109
+ const response = object(payload);
110
+ if (!response) throw new Error("Response root must be an object.");
111
+ if (response.protocol_version !== 1 || response.request_id !== requestId) {
112
+ throw new Error("Response protocol/request binding does not match.");
113
+ }
114
+ const responseIdentity = object(response.connector);
115
+ const expectedIdentity = identity(manifest);
116
+ if (
117
+ !responseIdentity ||
118
+ responseIdentity.id !== expectedIdentity.id ||
119
+ responseIdentity.version !== expectedIdentity.version ||
120
+ responseIdentity.artifact_digest !== expectedIdentity.artifact_digest
121
+ ) {
122
+ throw new Error("Response connector identity does not match.");
123
+ }
124
+ if (response.tool !== toolName || response.kind !== "read_result") {
125
+ throw new Error("Response tool or kind does not match the read request.");
126
+ }
127
+ const expiresAt = Date.parse(String(response.expires_at ?? ""));
128
+ const remaining = expiresAt - Date.now();
129
+ if (!Number.isFinite(expiresAt) || remaining <= 0 || remaining > MAX_RESPONSE_TTL_MS) {
130
+ throw new Error("Response expiry must be in the next 300 seconds.");
131
+ }
132
+ assertNoWritePayload(response.result);
133
+ if (!validateOutput(response.result)) {
134
+ throw new Error("Response result does not match the declared output_schema.");
135
+ }
136
+ }
137
+
138
+ async function post(
139
+ fetchImpl: typeof fetch,
140
+ endpoint: string,
141
+ payload: Record<string, unknown>,
142
+ timeoutMs: number
143
+ ): Promise<Response> {
144
+ return fetchImpl(`${endpoint}/invoke`, {
145
+ method: "POST",
146
+ headers: { accept: "application/json", "content-type": "application/json" },
147
+ body: JSON.stringify(payload),
148
+ redirect: "error",
149
+ signal: AbortSignal.timeout(timeoutMs),
150
+ });
151
+ }
152
+
153
+ export async function runConformance(options: ConformanceOptions): Promise<ConformanceReport> {
154
+ const endpoint = String(
155
+ options.endpoint ?? object(options.manifest.transport)?.url ?? ""
156
+ ).replace(/\/$/, "");
157
+ let endpointUrl: URL;
158
+ try {
159
+ endpointUrl = new URL(endpoint);
160
+ } catch {
161
+ throw new Error("A connector endpoint URL is required.");
162
+ }
163
+ const localHttp =
164
+ endpointUrl.protocol === "http:" &&
165
+ ["127.0.0.1", "::1", "localhost"].includes(endpointUrl.hostname);
166
+ if (endpointUrl.protocol !== "https:" && !localHttp) {
167
+ throw new Error("Connector endpoints must use HTTPS; HTTP is allowed only for loopback tests.");
168
+ }
169
+ const timeoutMs = options.timeoutMs ?? 10_000;
170
+ const fetchImpl = options.fetchImpl ?? fetch;
171
+ const checks: ConformanceCheck[] = [];
172
+ const runCheck = async (name: string, callback: () => void | Promise<void>): Promise<void> => {
173
+ try {
174
+ await callback();
175
+ checks.push({ name, ok: true });
176
+ } catch (error) {
177
+ checks.push({ name, ok: false, error: String(error instanceof Error ? error.message : error) });
178
+ }
179
+ };
180
+
181
+ const ajv = buildAjv();
182
+ const manifestSchema = options.manifestSchema ?? (await defaultManifestSchema());
183
+ const validateManifest = ajv.compile(manifestSchema);
184
+ await runCheck("manifest_schema", () => {
185
+ if (!validateManifest(options.manifest)) throw new Error("Manifest does not match Protocol v1 schema.");
186
+ });
187
+
188
+ const tools = Array.isArray(options.manifest.tools)
189
+ ? options.manifest.tools.map(object).filter((tool): tool is Record<string, unknown> => tool !== null)
190
+ : [];
191
+ const readTools = tools.filter((tool) => tool.read_only === true);
192
+ await runCheck("fixture_coverage", () => {
193
+ const declared = new Set(readTools.map((tool) => String(tool.name)));
194
+ for (const name of declared) {
195
+ if (!object(options.fixture.tools?.[name])?.arguments) {
196
+ throw new Error(`Missing fixture arguments for ${name}.`);
197
+ }
198
+ }
199
+ for (const name of Object.keys(options.fixture.tools ?? {})) {
200
+ if (!declared.has(name)) throw new Error(`Fixture names an undeclared read tool: ${name}.`);
201
+ }
202
+ });
203
+
204
+ await runCheck("healthz", async () => {
205
+ const response = await fetchImpl(`${endpoint}/healthz`, {
206
+ headers: { accept: "application/json" },
207
+ redirect: "error",
208
+ signal: AbortSignal.timeout(timeoutMs),
209
+ });
210
+ if (response.status !== 200) throw new Error(`Health endpoint returned HTTP ${response.status}.`);
211
+ const payload = object(await readJsonResponse(response));
212
+ const connector = object(payload?.connector);
213
+ if (
214
+ payload?.ok !== true ||
215
+ connector?.id !== options.manifest.id ||
216
+ connector?.version !== options.manifest.version
217
+ ) {
218
+ throw new Error("Health endpoint identity does not match the manifest.");
219
+ }
220
+ });
221
+
222
+ for (const tool of readTools) {
223
+ const toolName = String(tool.name);
224
+ const fixture = options.fixture.tools[toolName];
225
+ if (!fixture) continue;
226
+ let validateInput: ValidateFunction;
227
+ let validateOutput: ValidateFunction;
228
+ try {
229
+ validateInput = ajv.compile(tool.input_schema as Record<string, unknown>);
230
+ validateOutput = ajv.compile(tool.output_schema as Record<string, unknown>);
231
+ } catch (error) {
232
+ checks.push({ name: `tool:${toolName}:schemas`, ok: false, error: String(error) });
233
+ continue;
234
+ }
235
+ await runCheck(`tool:${toolName}:invoke`, async () => {
236
+ if (!validateInput(fixture.arguments)) throw new Error("Fixture arguments violate input_schema.");
237
+ const requestId = `conformance-${toolName}-${Date.now()}`;
238
+ const response = await post(
239
+ fetchImpl,
240
+ endpoint,
241
+ {
242
+ protocol_version: 1,
243
+ request_id: requestId,
244
+ connector: identity(options.manifest),
245
+ tool: toolName,
246
+ arguments: fixture.arguments,
247
+ context: {},
248
+ },
249
+ timeoutMs
250
+ );
251
+ if (response.status !== 200) throw new Error(`Invoke returned HTTP ${response.status}.`);
252
+ checkResponseBinding(
253
+ await readJsonResponse(response),
254
+ options.manifest,
255
+ requestId,
256
+ toolName,
257
+ validateOutput
258
+ );
259
+ });
260
+ }
261
+
262
+ await runCheck("reject_wrong_identity", async () => {
263
+ const response = await post(
264
+ fetchImpl,
265
+ endpoint,
266
+ {
267
+ protocol_version: 1,
268
+ request_id: `conformance-wrong-id-${Date.now()}`,
269
+ connector: { id: "com.agentlayer.conformance.invalid", version: "0.0.0" },
270
+ tool: String(readTools[0]?.name ?? "unknown"),
271
+ arguments: options.fixture.tools[String(readTools[0]?.name ?? "")]?.arguments ?? {},
272
+ context: {},
273
+ },
274
+ timeoutMs
275
+ );
276
+ if (response.status >= 200 && response.status < 300) {
277
+ throw new Error("Connector accepted a request for a different identity.");
278
+ }
279
+ });
280
+
281
+ await runCheck("reject_unknown_tool", async () => {
282
+ const response = await post(
283
+ fetchImpl,
284
+ endpoint,
285
+ {
286
+ protocol_version: 1,
287
+ request_id: `conformance-unknown-tool-${Date.now()}`,
288
+ connector: identity(options.manifest),
289
+ tool: "__conformance_unknown_tool__",
290
+ arguments: {},
291
+ context: {},
292
+ },
293
+ timeoutMs
294
+ );
295
+ if (response.status >= 200 && response.status < 300) {
296
+ throw new Error("Connector accepted an undeclared tool.");
297
+ }
298
+ });
299
+
300
+ return {
301
+ ok: checks.every((check) => check.ok),
302
+ connector_id: String(options.manifest.id ?? ""),
303
+ connector_version: String(options.manifest.version ?? ""),
304
+ endpoint,
305
+ checks,
306
+ };
307
+ }
308
+
309
+ export function assertFixture(value: unknown): asserts value is ConformanceFixture {
310
+ const fixture = object(value);
311
+ if (fixture?.schema_version !== 1 || !object(fixture.tools)) {
312
+ throw new Error("Conformance fixture must use schema_version 1 and contain a tools object.");
313
+ }
314
+ }
@@ -0,0 +1,27 @@
1
+ export interface ConformanceFixture {
2
+ schema_version: 1;
3
+ tools: Record<string, { arguments: Record<string, unknown> }>;
4
+ }
5
+
6
+ export interface ConformanceCheck {
7
+ name: string;
8
+ ok: boolean;
9
+ error?: string;
10
+ }
11
+
12
+ export interface ConformanceReport {
13
+ ok: boolean;
14
+ connector_id: string;
15
+ connector_version: string;
16
+ endpoint: string;
17
+ checks: ConformanceCheck[];
18
+ }
19
+
20
+ export interface ConformanceOptions {
21
+ manifest: Record<string, unknown>;
22
+ fixture: ConformanceFixture;
23
+ endpoint?: string;
24
+ timeoutMs?: number;
25
+ fetchImpl?: typeof fetch;
26
+ manifestSchema?: Record<string, unknown>;
27
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "request_id": "replaced-by-test",
4
+ "connector": {
5
+ "id": "com.attacker",
6
+ "version": "1.0.0"
7
+ },
8
+ "tool": "get_pools",
9
+ "kind": "read_result",
10
+ "result": {
11
+ "pools": []
12
+ },
13
+ "expires_at": "replaced-by-test"
14
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "schema_version": 1,
3
+ "tools": {}
4
+ }
@@ -0,0 +1,151 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import { createServer } from "node:http";
4
+ import test from "node:test";
5
+
6
+ import {
7
+ createConnectorHttpHandler,
8
+ defineReadOnlyConnector,
9
+ } from "@agentlayer.tech/connector-sdk";
10
+ import { runConformance } from "../dist/index.js";
11
+
12
+ const manifest = JSON.parse(
13
+ await readFile(new URL("../../templates/read-only/connector.json", import.meta.url), "utf8")
14
+ );
15
+ const fixture = { schema_version: 1, tools: { get_pools: { arguments: { chain: "base" } } } };
16
+ const invalidIdentityResponse = JSON.parse(
17
+ await readFile(new URL("./fixtures/invalid-response-identity.json", import.meta.url), "utf8")
18
+ );
19
+ const missingToolFixture = JSON.parse(
20
+ await readFile(new URL("./fixtures/missing-tool-fixture.json", import.meta.url), "utf8")
21
+ );
22
+
23
+ function response(status, payload) {
24
+ return new Response(JSON.stringify(payload), {
25
+ status,
26
+ headers: { "content-type": "application/json" },
27
+ });
28
+ }
29
+
30
+ function conformingFetch(url, options = {}) {
31
+ if (String(url).endsWith("/healthz")) {
32
+ return Promise.resolve(
33
+ response(200, { ok: true, connector: { id: manifest.id, version: manifest.version } })
34
+ );
35
+ }
36
+ const request = JSON.parse(options.body);
37
+ if (request.connector.id !== manifest.id || request.tool === "__conformance_unknown_tool__") {
38
+ return Promise.resolve(response(409, { ok: false }));
39
+ }
40
+ return Promise.resolve(
41
+ response(200, {
42
+ protocol_version: 1,
43
+ request_id: request.request_id,
44
+ connector: request.connector,
45
+ tool: request.tool,
46
+ kind: "read_result",
47
+ result: { pools: [] },
48
+ expires_at: new Date(Date.now() + 60_000).toISOString(),
49
+ })
50
+ );
51
+ }
52
+
53
+ test("passes a conforming read-only endpoint", async () => {
54
+ const report = await runConformance({
55
+ manifest,
56
+ fixture,
57
+ endpoint: "https://connector.example.com",
58
+ fetchImpl: conformingFetch,
59
+ });
60
+ assert.equal(report.ok, true, JSON.stringify(report.checks));
61
+ assert.ok(report.checks.length >= 6);
62
+ });
63
+
64
+ test("rejects public plaintext HTTP endpoints", async () => {
65
+ await assert.rejects(
66
+ runConformance({
67
+ manifest,
68
+ fixture,
69
+ endpoint: "http://connector.example.com",
70
+ fetchImpl: conformingFetch,
71
+ }),
72
+ /must use HTTPS/
73
+ );
74
+ });
75
+
76
+ test("reports response identity drift", async () => {
77
+ const report = await runConformance({
78
+ manifest,
79
+ fixture,
80
+ endpoint: "https://connector.example.com",
81
+ fetchImpl: async (url, options = {}) => {
82
+ const result = await conformingFetch(url, options);
83
+ if (!String(url).endsWith("/invoke") || result.status !== 200) return result;
84
+ const payload = await result.json();
85
+ return response(200, {
86
+ ...invalidIdentityResponse,
87
+ request_id: payload.request_id,
88
+ expires_at: payload.expires_at,
89
+ });
90
+ },
91
+ });
92
+ assert.equal(report.ok, false);
93
+ assert.equal(report.checks.find((check) => check.name === "tool:get_pools:invoke")?.ok, false);
94
+ });
95
+
96
+ test("reports write envelopes embedded in read-only output", async () => {
97
+ const permissiveManifest = structuredClone(manifest);
98
+ permissiveManifest.tools[0].output_schema = { type: "object" };
99
+ const report = await runConformance({
100
+ manifest: permissiveManifest,
101
+ fixture,
102
+ endpoint: "https://connector.example.com",
103
+ fetchImpl: async (url, options = {}) => {
104
+ const result = await conformingFetch(url, options);
105
+ if (!String(url).endsWith("/invoke") || result.status !== 200) return result;
106
+ const payload = await result.json();
107
+ return response(200, {
108
+ ...payload,
109
+ result: { data: { payment_intent: { amount: "1" } } },
110
+ });
111
+ },
112
+ });
113
+ assert.equal(report.ok, false);
114
+ const invoke = report.checks.find((check) => check.name === "tool:get_pools:invoke");
115
+ assert.equal(invoke?.ok, false);
116
+ assert.match(invoke?.error ?? "", /reserved write field/);
117
+ });
118
+
119
+ test("fails when a declared tool has no fixture", async () => {
120
+ const report = await runConformance({
121
+ manifest,
122
+ fixture: missingToolFixture,
123
+ endpoint: "https://connector.example.com",
124
+ fetchImpl: conformingFetch,
125
+ });
126
+ assert.equal(report.ok, false);
127
+ assert.equal(report.checks.find((check) => check.name === "fixture_coverage")?.ok, false);
128
+ });
129
+
130
+ test("passes against the real SDK HTTP wire contract", async (context) => {
131
+ const connector = defineReadOnlyConnector({
132
+ manifest,
133
+ handlers: { get_pools: () => ({ pools: [] }) },
134
+ });
135
+ const handler = createConnectorHttpHandler(connector);
136
+ const server = createServer((request, response_) => void handler(request, response_));
137
+ await new Promise((resolve, reject) => {
138
+ server.once("error", reject);
139
+ server.listen(0, "127.0.0.1", resolve);
140
+ });
141
+ context.after(() => new Promise((resolve) => server.close(resolve)));
142
+ const address = server.address();
143
+ assert.ok(address && typeof address === "object");
144
+
145
+ const report = await runConformance({
146
+ manifest,
147
+ fixture,
148
+ endpoint: `http://127.0.0.1:${address.port}`,
149
+ });
150
+ assert.equal(report.ok, true, JSON.stringify(report.checks));
151
+ });
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "rootDir": "src",
7
+ "outDir": "dist",
8
+ "declaration": true,
9
+ "strict": true,
10
+ "noUncheckedIndexedAccess": true,
11
+ "exactOptionalPropertyTypes": true,
12
+ "verbatimModuleSyntax": true,
13
+ "types": ["node"],
14
+ "skipLibCheck": true
15
+ },
16
+ "include": ["src/**/*.ts"]
17
+ }
@@ -0,0 +1,21 @@
1
+ FROM node:24-alpine AS build
2
+
3
+ WORKDIR /app/connectors
4
+ COPY connectors/package.json connectors/package-lock.json ./
5
+ COPY connectors/sdk-typescript ./sdk-typescript
6
+ COPY connectors/conformance ./conformance
7
+ COPY connectors/templates/read-only ./templates/read-only
8
+ COPY connectors/examples/reference-crypto-data ./examples/reference-crypto-data
9
+ RUN npm ci
10
+ RUN npm run build --workspace @agentlayer.tech/connector-sdk
11
+ RUN npm run build --workspace @agentlayer.tech/reference-crypto-data-connector
12
+
13
+ FROM node:24-alpine
14
+
15
+ ENV NODE_ENV=production
16
+ WORKDIR /app/connectors
17
+ COPY --from=build --chown=node:node /app/connectors /app/connectors
18
+ USER node
19
+ EXPOSE 3000
20
+
21
+ CMD ["npm", "run", "start", "--workspace", "@agentlayer.tech/reference-crypto-data-connector"]
@@ -0,0 +1,51 @@
1
+ # AgentLayer crypto-data reference connector
2
+
3
+ This is a neutral, read-only reference implementation for Connector Protocol
4
+ v1. It exposes public spot-price and asset-metadata tools. It does not receive a
5
+ wallet address, construct transactions, make payments, sign, or broadcast.
6
+
7
+ The upstream is Coinbase Exchange's public market-data API. The reference uses
8
+ the documented public ticker and currency endpoints and requires no API key:
9
+
10
+ - https://docs.cdp.coinbase.com/api-reference/exchange-api/rest-api/products/get-product-ticker
11
+ - https://docs.cdp.coinbase.com/api-reference/exchange-api/rest-api/currencies/get-a-currency
12
+
13
+ Run locally:
14
+
15
+ ```bash
16
+ npm ci --prefix connectors
17
+ npm run build --workspace @agentlayer.tech/reference-crypto-data-connector --prefix connectors
18
+ npm run start --workspace @agentlayer.tech/reference-crypto-data-connector --prefix connectors
19
+ ```
20
+
21
+ The reviewed beta deployment is available at
22
+ `https://reference-crypto-data-production.up.railway.app`. Its manifest is
23
+ already pinned to that exact HTTPS endpoint. Re-run its live conformance check
24
+ after any immutable version release:
25
+
26
+ ```bash
27
+ cd connectors
28
+ node conformance/dist/cli.js \
29
+ --manifest examples/reference-crypto-data/connector.json \
30
+ --fixture examples/reference-crypto-data/conformance.json \
31
+ --endpoint https://reference-crypto-data-production.up.railway.app
32
+ ```
33
+
34
+ ## Railway deployment
35
+
36
+ Deploy this directory as one stateless service in a new, isolated Railway
37
+ project. Keep the repository root as the Docker build context and point the
38
+ service Dockerfile at
39
+ `connectors/examples/reference-crypto-data/Dockerfile` (for a root deployment,
40
+ set Railway's `RAILWAY_DOCKERFILE_PATH` service variable to that value).
41
+ Configure the Railway deployment health-check path as `/healthz` with a
42
+ 10-second timeout and an `ON_FAILURE` restart policy. The service needs no
43
+ application variables, volume, database, wallet material, or connection to the
44
+ AgentLayer provider gateway.
45
+
46
+ After Railway generates the public domain:
47
+
48
+ 1. update `connector.json` to that exact HTTPS URL;
49
+ 2. redeploy the immutable manifest version;
50
+ 3. run both live conformance and wallet invocation tests;
51
+ 4. install the reviewed manifest with `wallet connectors install ... --enable --yes`.
@@ -0,0 +1,11 @@
1
+ {
2
+ "schema_version": 1,
3
+ "tools": {
4
+ "get_spot_price": {
5
+ "arguments": { "base": "BTC", "quote": "USD" }
6
+ },
7
+ "get_asset_metadata": {
8
+ "arguments": { "asset": "BTC" }
9
+ }
10
+ }
11
+ }