@agentlayer.tech/wallet 0.1.101 → 0.1.103
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/.claude-plugin/marketplace.json +4 -1
- package/.openclaw/extensions/agent-wallet/dist/index.js +46 -1
- package/.openclaw/extensions/agent-wallet/index.ts +46 -1
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/README.md +18 -0
- package/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/connector_cli.py +198 -0
- package/agent-wallet/agent_wallet/connectors/__init__.py +28 -0
- package/agent-wallet/agent_wallet/connectors/catalog.py +68 -0
- package/agent-wallet/agent_wallet/connectors/client.py +308 -0
- package/agent-wallet/agent_wallet/connectors/intent_policy.py +243 -0
- package/agent-wallet/agent_wallet/connectors/manifest.py +192 -0
- package/agent-wallet/agent_wallet/connectors/registry.py +341 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +56 -5
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +4 -2
- package/agent-wallet/scripts/build_release_bundle.py +1 -0
- package/agent-wallet/scripts/install_agent_wallet.py +1 -1
- package/bin/openclaw-agent-wallet.mjs +38 -0
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +2 -2
- package/claude-code/plugins/agent-wallet/commands/wallet-evm.md +1 -0
- package/claude-code/plugins/agent-wallet/commands/wallet-setup.md +1 -0
- package/claude-code/plugins/agent-wallet/skills/wallet-operator/SKILL.md +1 -0
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/server.py +4 -1
- package/connectors/AGENTS.md +37 -0
- package/connectors/DEVELOPER_GUIDE.md +131 -0
- package/connectors/README.md +139 -0
- package/connectors/READ_ONLY_BETA.md +54 -0
- package/connectors/RELEASING.md +48 -0
- package/connectors/conformance/README.md +31 -0
- package/connectors/conformance/package.json +54 -0
- package/connectors/conformance/scripts/sync-license.mjs +3 -0
- package/connectors/conformance/scripts/sync-spec.mjs +16 -0
- package/connectors/conformance/src/cli.ts +43 -0
- package/connectors/conformance/src/index.ts +7 -0
- package/connectors/conformance/src/runner.ts +314 -0
- package/connectors/conformance/src/types.ts +27 -0
- package/connectors/conformance/test/fixtures/invalid-response-identity.json +14 -0
- package/connectors/conformance/test/fixtures/missing-tool-fixture.json +4 -0
- package/connectors/conformance/test/runner.test.mjs +151 -0
- package/connectors/conformance/tsconfig.json +17 -0
- package/connectors/examples/reference-crypto-data/Dockerfile +21 -0
- package/connectors/examples/reference-crypto-data/README.md +51 -0
- package/connectors/examples/reference-crypto-data/conformance.json +11 -0
- package/connectors/examples/reference-crypto-data/connector.json +95 -0
- package/connectors/examples/reference-crypto-data/package.json +21 -0
- package/connectors/examples/reference-crypto-data/railway.toml +9 -0
- package/connectors/examples/reference-crypto-data/src/connector.ts +137 -0
- package/connectors/examples/reference-crypto-data/src/server.ts +13 -0
- package/connectors/examples/reference-crypto-data/test/connector.test.mjs +97 -0
- package/connectors/examples/reference-crypto-data/tsconfig.json +17 -0
- package/connectors/package-lock.json +561 -0
- package/connectors/package.json +19 -0
- package/connectors/scripts/smoke-packed-packages.mjs +121 -0
- package/connectors/sdk-typescript/README.md +47 -0
- package/connectors/sdk-typescript/package.json +49 -0
- package/connectors/sdk-typescript/scripts/sync-license.mjs +3 -0
- package/connectors/sdk-typescript/src/connector.ts +204 -0
- package/connectors/sdk-typescript/src/errors.ts +11 -0
- package/connectors/sdk-typescript/src/http.ts +116 -0
- package/connectors/sdk-typescript/src/index.ts +26 -0
- package/connectors/sdk-typescript/src/types.ts +101 -0
- package/connectors/sdk-typescript/test/sdk.test.mjs +181 -0
- package/connectors/sdk-typescript/tsconfig.json +21 -0
- package/connectors/spec/connector-manifest.schema.json +340 -0
- package/connectors/spec/connector-protocol.md +159 -0
- package/connectors/spec/transaction-intent.schema.json +281 -0
- package/connectors/templates/read-only/.env.example +1 -0
- package/connectors/templates/read-only/Dockerfile +11 -0
- package/connectors/templates/read-only/README.md +33 -0
- package/connectors/templates/read-only/conformance.json +11 -0
- package/connectors/templates/read-only/connector.json +64 -0
- package/connectors/templates/read-only/package.json +21 -0
- package/connectors/templates/read-only/railway.toml +9 -0
- package/connectors/templates/read-only/src/connector.ts +61 -0
- package/connectors/templates/read-only/src/server.ts +14 -0
- package/connectors/templates/read-only/test/connector.test.mjs +62 -0
- package/connectors/templates/read-only/tsconfig.json +17 -0
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +2 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/package.json +1 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { assertFixture, runConformance } from "./index.js";
|
|
7
|
+
|
|
8
|
+
function valueAfter(args: string[], flag: string): string | undefined {
|
|
9
|
+
const index = args.indexOf(flag);
|
|
10
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function readJson(file: string): Promise<unknown> {
|
|
14
|
+
return JSON.parse(await readFile(path.resolve(file), "utf8"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function main(): Promise<void> {
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const manifestPath = valueAfter(args, "--manifest");
|
|
20
|
+
const fixturePath = valueAfter(args, "--fixture");
|
|
21
|
+
const endpoint = valueAfter(args, "--endpoint");
|
|
22
|
+
if (!manifestPath || !fixturePath) {
|
|
23
|
+
throw new Error("Usage: agentlayer-connector-conformance --manifest connector.json --fixture conformance.json [--endpoint URL]");
|
|
24
|
+
}
|
|
25
|
+
const manifest = await readJson(manifestPath);
|
|
26
|
+
const fixture = await readJson(fixturePath);
|
|
27
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
28
|
+
throw new Error("Manifest root must be an object.");
|
|
29
|
+
}
|
|
30
|
+
assertFixture(fixture);
|
|
31
|
+
const report = await runConformance({
|
|
32
|
+
manifest: manifest as Record<string, unknown>,
|
|
33
|
+
fixture,
|
|
34
|
+
...(endpoint ? { endpoint } : {}),
|
|
35
|
+
});
|
|
36
|
+
console.log(JSON.stringify(report, null, 2));
|
|
37
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
main().catch((error) => {
|
|
41
|
+
console.error(JSON.stringify({ ok: false, error: String(error instanceof Error ? error.message : error) }));
|
|
42
|
+
process.exitCode = 2;
|
|
43
|
+
});
|
|
@@ -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,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`.
|