@agentlayer.tech/wallet 0.1.100 → 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 (85) 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/agent_wallet/providers/solana_rpc.py +1 -1
  17. package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +7 -4
  18. package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +7 -2
  19. package/agent-wallet/agent_wallet/wallet_layer/solana.py +1 -1
  20. package/agent-wallet/openclaw.plugin.json +1 -1
  21. package/agent-wallet/pyproject.toml +4 -2
  22. package/agent-wallet/scripts/build_release_bundle.py +1 -0
  23. package/agent-wallet/scripts/install_agent_wallet.py +1 -1
  24. package/bin/openclaw-agent-wallet.mjs +38 -0
  25. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
  26. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
  27. package/codex/plugins/agent-wallet/server.py +17 -5
  28. package/connectors/AGENTS.md +37 -0
  29. package/connectors/DEVELOPER_GUIDE.md +131 -0
  30. package/connectors/README.md +139 -0
  31. package/connectors/READ_ONLY_BETA.md +54 -0
  32. package/connectors/RELEASING.md +48 -0
  33. package/connectors/conformance/README.md +31 -0
  34. package/connectors/conformance/package.json +54 -0
  35. package/connectors/conformance/scripts/sync-license.mjs +3 -0
  36. package/connectors/conformance/scripts/sync-spec.mjs +16 -0
  37. package/connectors/conformance/src/cli.ts +43 -0
  38. package/connectors/conformance/src/index.ts +7 -0
  39. package/connectors/conformance/src/runner.ts +314 -0
  40. package/connectors/conformance/src/types.ts +27 -0
  41. package/connectors/conformance/test/fixtures/invalid-response-identity.json +14 -0
  42. package/connectors/conformance/test/fixtures/missing-tool-fixture.json +4 -0
  43. package/connectors/conformance/test/runner.test.mjs +151 -0
  44. package/connectors/conformance/tsconfig.json +17 -0
  45. package/connectors/examples/reference-crypto-data/Dockerfile +21 -0
  46. package/connectors/examples/reference-crypto-data/README.md +51 -0
  47. package/connectors/examples/reference-crypto-data/conformance.json +11 -0
  48. package/connectors/examples/reference-crypto-data/connector.json +95 -0
  49. package/connectors/examples/reference-crypto-data/package.json +21 -0
  50. package/connectors/examples/reference-crypto-data/railway.toml +9 -0
  51. package/connectors/examples/reference-crypto-data/src/connector.ts +137 -0
  52. package/connectors/examples/reference-crypto-data/src/server.ts +13 -0
  53. package/connectors/examples/reference-crypto-data/test/connector.test.mjs +97 -0
  54. package/connectors/examples/reference-crypto-data/tsconfig.json +17 -0
  55. package/connectors/package-lock.json +561 -0
  56. package/connectors/package.json +19 -0
  57. package/connectors/scripts/smoke-packed-packages.mjs +121 -0
  58. package/connectors/sdk-typescript/README.md +47 -0
  59. package/connectors/sdk-typescript/package.json +49 -0
  60. package/connectors/sdk-typescript/scripts/sync-license.mjs +3 -0
  61. package/connectors/sdk-typescript/src/connector.ts +204 -0
  62. package/connectors/sdk-typescript/src/errors.ts +11 -0
  63. package/connectors/sdk-typescript/src/http.ts +116 -0
  64. package/connectors/sdk-typescript/src/index.ts +26 -0
  65. package/connectors/sdk-typescript/src/types.ts +101 -0
  66. package/connectors/sdk-typescript/test/sdk.test.mjs +181 -0
  67. package/connectors/sdk-typescript/tsconfig.json +21 -0
  68. package/connectors/spec/connector-manifest.schema.json +340 -0
  69. package/connectors/spec/connector-protocol.md +159 -0
  70. package/connectors/spec/transaction-intent.schema.json +281 -0
  71. package/connectors/templates/read-only/.env.example +1 -0
  72. package/connectors/templates/read-only/Dockerfile +11 -0
  73. package/connectors/templates/read-only/README.md +33 -0
  74. package/connectors/templates/read-only/conformance.json +11 -0
  75. package/connectors/templates/read-only/connector.json +64 -0
  76. package/connectors/templates/read-only/package.json +21 -0
  77. package/connectors/templates/read-only/railway.toml +9 -0
  78. package/connectors/templates/read-only/src/connector.ts +61 -0
  79. package/connectors/templates/read-only/src/server.ts +14 -0
  80. package/connectors/templates/read-only/test/connector.test.mjs +62 -0
  81. package/connectors/templates/read-only/tsconfig.json +17 -0
  82. package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
  83. package/package.json +2 -1
  84. package/wdk-btc-wallet/package.json +1 -1
  85. package/wdk-evm-wallet/package.json +1 -1
@@ -0,0 +1,31 @@
1
+ # AgentLayer connector conformance
2
+
3
+ Run Protocol v1 contract checks against a deployed read-only connector:
4
+
5
+ ```bash
6
+ npx @agentlayer.tech/connector-conformance@beta \
7
+ --manifest ./connector.json \
8
+ --fixture ./conformance.json \
9
+ --endpoint https://your-connector.up.railway.app
10
+ ```
11
+
12
+ The suite validates the official manifest schema, fixture coverage, health
13
+ identity, every declared read tool's input/output schemas, response binding and
14
+ expiry, and rejection of wrong identities and undeclared tools.
15
+
16
+ Example fixture:
17
+
18
+ ```json
19
+ {
20
+ "schema_version": 1,
21
+ "tools": {
22
+ "get_pools": {
23
+ "arguments": { "chain": "base", "limit": 10 }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ Run conformance against the final immutable deployment URL before publishing a
30
+ manifest. A passing report is required for review, but it does not grant
31
+ `verified_write` status.
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@agentlayer.tech/connector-conformance",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "Conformance runner for AgentLayer Connector Protocol v1.",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/lopushok9/Agent-Layer.git",
10
+ "directory": "connectors/conformance"
11
+ },
12
+ "homepage": "https://github.com/lopushok9/Agent-Layer/tree/main/connectors/conformance#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/lopushok9/Agent-Layer/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "sideEffects": false,
20
+ "engines": {
21
+ "node": ">=24"
22
+ },
23
+ "bin": {
24
+ "agentlayer-connector-conformance": "./dist/cli.js"
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist/",
34
+ "spec/",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "scripts": {
39
+ "sync-license": "node scripts/sync-license.mjs",
40
+ "sync-spec": "node scripts/sync-spec.mjs",
41
+ "build": "npm run sync-spec && tsc -p tsconfig.json",
42
+ "check": "npm run build && node --test test/*.test.mjs",
43
+ "prepack": "npm run sync-license && npm run build"
44
+ },
45
+ "dependencies": {
46
+ "ajv": "^8.20.0",
47
+ "ajv-formats": "^3.0.1"
48
+ },
49
+ "devDependencies": {
50
+ "@agentlayer.tech/connector-sdk": "0.1.0-beta.1",
51
+ "@types/node": "^24.0.0",
52
+ "typescript": "^7.0.2"
53
+ }
54
+ }
@@ -0,0 +1,3 @@
1
+ import { copyFileSync } from "node:fs";
2
+
3
+ copyFileSync(new URL("../../../LICENSE", import.meta.url), new URL("../LICENSE", import.meta.url));
@@ -0,0 +1,16 @@
1
+ import { cp, mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
6
+ const source = path.resolve(packageRoot, "..", "spec");
7
+ const destination = path.join(packageRoot, "spec");
8
+
9
+ await mkdir(destination, { recursive: true });
10
+ for (const name of [
11
+ "connector-manifest.schema.json",
12
+ "connector-protocol.md",
13
+ "transaction-intent.schema.json",
14
+ ]) {
15
+ await cp(path.join(source, name), path.join(destination, name));
16
+ }
@@ -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,7 @@
1
+ export { assertFixture, runConformance } from "./runner.js";
2
+ export type {
3
+ ConformanceCheck,
4
+ ConformanceFixture,
5
+ ConformanceOptions,
6
+ ConformanceReport,
7
+ } from "./types.js";
@@ -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
+ }