@haiai/haiai 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 +127 -0
- package/bin/haiai.cjs +70 -0
- package/dist/cjs/a2a.js +352 -0
- package/dist/cjs/a2a.js.map +1 -0
- package/dist/cjs/agent.js +236 -0
- package/dist/cjs/agent.js.map +1 -0
- package/dist/cjs/client.js +2168 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/config.js +176 -0
- package/dist/cjs/config.js.map +1 -0
- package/dist/cjs/errors.js +102 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/hash.js +52 -0
- package/dist/cjs/hash.js.map +1 -0
- package/dist/cjs/index.js +84 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/integrations.js +193 -0
- package/dist/cjs/integrations.js.map +1 -0
- package/dist/cjs/jacs.js +66 -0
- package/dist/cjs/jacs.js.map +1 -0
- package/dist/cjs/mime.js +100 -0
- package/dist/cjs/mime.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/signing.js +190 -0
- package/dist/cjs/signing.js.map +1 -0
- package/dist/cjs/sse.js +76 -0
- package/dist/cjs/sse.js.map +1 -0
- package/dist/cjs/types.js +6 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/verify.js +76 -0
- package/dist/cjs/verify.js.map +1 -0
- package/dist/cjs/ws.js +206 -0
- package/dist/cjs/ws.js.map +1 -0
- package/dist/esm/a2a.js +305 -0
- package/dist/esm/a2a.js.map +1 -0
- package/dist/esm/agent.js +231 -0
- package/dist/esm/agent.js.map +1 -0
- package/dist/esm/client.js +2131 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/config.js +171 -0
- package/dist/esm/config.js.map +1 -0
- package/dist/esm/errors.js +88 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/hash.js +49 -0
- package/dist/esm/hash.js.map +1 -0
- package/dist/esm/index.js +27 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/integrations.js +147 -0
- package/dist/esm/integrations.js.map +1 -0
- package/dist/esm/jacs.js +61 -0
- package/dist/esm/jacs.js.map +1 -0
- package/dist/esm/mime.js +97 -0
- package/dist/esm/mime.js.map +1 -0
- package/dist/esm/signing.js +183 -0
- package/dist/esm/signing.js.map +1 -0
- package/dist/esm/sse.js +73 -0
- package/dist/esm/sse.js.map +1 -0
- package/dist/esm/types.js +5 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/verify.js +72 -0
- package/dist/esm/verify.js.map +1 -0
- package/dist/esm/ws.js +168 -0
- package/dist/esm/ws.js.map +1 -0
- package/dist/types/a2a.d.ts +52 -0
- package/dist/types/a2a.d.ts.map +1 -0
- package/dist/types/agent.d.ts +202 -0
- package/dist/types/agent.d.ts.map +1 -0
- package/dist/types/client.d.ts +486 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/config.d.ts +31 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/errors.d.ts +50 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/hash.d.ts +32 -0
- package/dist/types/hash.d.ts.map +1 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/integrations.d.ts +25 -0
- package/dist/types/integrations.d.ts.map +1 -0
- package/dist/types/jacs.d.ts +26 -0
- package/dist/types/jacs.d.ts.map +1 -0
- package/dist/types/mime.d.ts +39 -0
- package/dist/types/mime.d.ts.map +1 -0
- package/dist/types/signing.d.ts +58 -0
- package/dist/types/signing.d.ts.map +1 -0
- package/dist/types/sse.d.ts +8 -0
- package/dist/types/sse.d.ts.map +1 -0
- package/dist/types/types.d.ts +652 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/verify.d.ts +20 -0
- package/dist/types/verify.d.ts.map +1 -0
- package/dist/types/ws.d.ts +30 -0
- package/dist/types/ws.d.ts.map +1 -0
- package/examples/a2a_quickstart.ts +138 -0
- package/examples/hai_quickstart.ts +111 -0
- package/examples/mcp_quickstart.ts +53 -0
- package/npm/@haiai/cli-darwin-arm64/package.json +16 -0
- package/npm/@haiai/cli-darwin-x64/package.json +16 -0
- package/npm/@haiai/cli-linux-arm64/package.json +16 -0
- package/npm/@haiai/cli-linux-x64/package.json +16 -0
- package/npm/@haiai/cli-win32-x64/package.json +16 -0
- package/package.json +68 -0
- package/scripts/build-platform-packages.js +132 -0
- package/scripts/smoke-package.cjs +114 -0
- package/scripts/write-cjs-package.cjs +9 -0
- package/src/a2a.ts +463 -0
- package/src/agent.ts +302 -0
- package/src/client.ts +2504 -0
- package/src/config.ts +204 -0
- package/src/errors.ts +99 -0
- package/src/hash.ts +66 -0
- package/src/index.ts +163 -0
- package/src/integrations.ts +210 -0
- package/src/jacs.ts +86 -0
- package/src/mime.ts +131 -0
- package/src/signing.ts +233 -0
- package/src/sse.ts +86 -0
- package/src/types.ts +773 -0
- package/src/verify.ts +89 -0
- package/src/ws.ts +198 -0
- package/tests/_debug_jacs.cjs +29 -0
- package/tests/a2a-contract.test.ts +271 -0
- package/tests/a2a-fixtures.test.ts +73 -0
- package/tests/a2a.test.ts +379 -0
- package/tests/binary.test.ts +90 -0
- package/tests/client-api-methods.test.ts +176 -0
- package/tests/client-path-escaping.test.ts +80 -0
- package/tests/client-register.test.ts +61 -0
- package/tests/config.test.ts +281 -0
- package/tests/contract.test.ts +360 -0
- package/tests/cross-lang-contract.test.ts +67 -0
- package/tests/email-conformance.test.ts +289 -0
- package/tests/email-integration.test.ts +217 -0
- package/tests/email.test.ts +767 -0
- package/tests/errors.test.ts +167 -0
- package/tests/init-contract.test.ts +129 -0
- package/tests/integrations.test.ts +132 -0
- package/tests/jacs-passthrough.test.ts +125 -0
- package/tests/key-cache.test.ts +201 -0
- package/tests/key-integration.test.ts +119 -0
- package/tests/key-lookups.test.ts +187 -0
- package/tests/key-rotation.test.ts +362 -0
- package/tests/mime.test.ts +127 -0
- package/tests/security.test.ts +109 -0
- package/tests/setup.ts +60 -0
- package/tests/signing.test.ts +142 -0
- package/tests/sse.test.ts +125 -0
- package/tests/types.test.ts +294 -0
- package/tests/verify-link.test.ts +81 -0
- package/tests/ws.test.ts +213 -0
- package/tsconfig.cjs.json +11 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('a2a facade wrappers', () => {
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
vi.resetModules();
|
|
7
|
+
vi.unmock('@hai.ai/jacs/a2a');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns a clear error when optional A2A exports are unavailable', async () => {
|
|
11
|
+
vi.doMock('@hai.ai/jacs/a2a', () => ({
|
|
12
|
+
JACSA2AIntegration: undefined,
|
|
13
|
+
}));
|
|
14
|
+
const mod = await import('../src/a2a.js');
|
|
15
|
+
|
|
16
|
+
await expect(mod.getA2AIntegration({})).rejects.toThrow(
|
|
17
|
+
"Module '@hai.ai/jacs/a2a' does not export class 'JACSA2AIntegration'",
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('delegates A2A calls to JACSA2AIntegration', async () => {
|
|
22
|
+
const calls: Record<string, unknown[]> = {};
|
|
23
|
+
vi.doMock('@hai.ai/jacs/a2a', () => {
|
|
24
|
+
class FakeA2AIntegration {
|
|
25
|
+
private readonly client: unknown;
|
|
26
|
+
private readonly trustPolicy: unknown;
|
|
27
|
+
|
|
28
|
+
constructor(client: unknown, trustPolicy?: unknown) {
|
|
29
|
+
this.client = client;
|
|
30
|
+
this.trustPolicy = trustPolicy;
|
|
31
|
+
calls.ctor = [client, trustPolicy as unknown];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static quickstart(options: Record<string, unknown>): Record<string, unknown> {
|
|
35
|
+
calls.quickstart = [options];
|
|
36
|
+
return { quickstart: true, options };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
exportAgentCard(agentData: Record<string, unknown>): Record<string, unknown> {
|
|
40
|
+
calls.exportAgentCard = [this.client, this.trustPolicy, agentData];
|
|
41
|
+
return { op: 'exportAgentCard', agentData };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
signArtifact(
|
|
45
|
+
artifact: Record<string, unknown>,
|
|
46
|
+
artifactType: string,
|
|
47
|
+
parentSignatures: Record<string, unknown>[] | null,
|
|
48
|
+
): Record<string, unknown> {
|
|
49
|
+
calls.signArtifact = [this.client, this.trustPolicy, artifact, artifactType, parentSignatures];
|
|
50
|
+
return { op: 'signArtifact', artifactType };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
verifyWrappedArtifact(
|
|
54
|
+
wrappedArtifact: string | Record<string, unknown>,
|
|
55
|
+
): Record<string, unknown> {
|
|
56
|
+
calls.verifyWrappedArtifact = [this.client, this.trustPolicy, wrappedArtifact];
|
|
57
|
+
return { op: 'verifyWrappedArtifact' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
createChainOfCustody(artifacts: Record<string, unknown>[]): Record<string, unknown> {
|
|
61
|
+
calls.createChainOfCustody = [this.client, this.trustPolicy, artifacts];
|
|
62
|
+
return { op: 'createChainOfCustody', count: artifacts.length };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
generateWellKnownDocuments(
|
|
66
|
+
agentCard: unknown,
|
|
67
|
+
jwsSignature: string,
|
|
68
|
+
publicKeyB64: string,
|
|
69
|
+
agentData: Record<string, unknown>,
|
|
70
|
+
): Record<string, unknown> {
|
|
71
|
+
calls.generateWellKnownDocuments = [
|
|
72
|
+
this.client,
|
|
73
|
+
this.trustPolicy,
|
|
74
|
+
agentCard,
|
|
75
|
+
jwsSignature,
|
|
76
|
+
publicKeyB64,
|
|
77
|
+
agentData,
|
|
78
|
+
];
|
|
79
|
+
return { op: 'generateWellKnownDocuments' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
assessRemoteAgent(agentCardJson: string | Record<string, unknown>): Record<string, unknown> {
|
|
83
|
+
calls.assessRemoteAgent = [this.client, this.trustPolicy, agentCardJson];
|
|
84
|
+
return { op: 'assessRemoteAgent', allowed: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
trustA2AAgent(agentCardJson: string | Record<string, unknown>): string {
|
|
88
|
+
calls.trustA2AAgent = [this.client, this.trustPolicy, agentCardJson];
|
|
89
|
+
return 'trusted-agent';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
JACSA2AIntegration: FakeA2AIntegration,
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const mod = await import('../src/a2a.js');
|
|
99
|
+
const fakeClient = { kind: 'jacs-client' };
|
|
100
|
+
const options = { trustPolicy: 'strict' as const };
|
|
101
|
+
|
|
102
|
+
const integration = await mod.getA2AIntegration(fakeClient, options);
|
|
103
|
+
expect(calls.ctor).toEqual([fakeClient, 'strict']);
|
|
104
|
+
|
|
105
|
+
await expect(mod.quickstartA2A({
|
|
106
|
+
name: 'hai-agent',
|
|
107
|
+
domain: 'agent.example.com',
|
|
108
|
+
description: 'HAIAI agent',
|
|
109
|
+
algorithm: 'pq2025',
|
|
110
|
+
})).resolves.toEqual({
|
|
111
|
+
quickstart: true,
|
|
112
|
+
options: {
|
|
113
|
+
name: 'hai-agent',
|
|
114
|
+
domain: 'agent.example.com',
|
|
115
|
+
description: 'HAIAI agent',
|
|
116
|
+
algorithm: 'pq2025',
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await expect(mod.exportAgentCard(fakeClient, { jacsId: 'agent-1' }, options)).resolves.toEqual({
|
|
121
|
+
op: 'exportAgentCard',
|
|
122
|
+
agentData: { jacsId: 'agent-1' },
|
|
123
|
+
});
|
|
124
|
+
expect(calls.exportAgentCard).toEqual([fakeClient, 'strict', { jacsId: 'agent-1' }]);
|
|
125
|
+
|
|
126
|
+
await expect(
|
|
127
|
+
mod.signArtifact(fakeClient, { taskId: 't-1' }, 'task', [{ parent: true }], options),
|
|
128
|
+
).resolves.toEqual({ op: 'signArtifact', artifactType: 'task' });
|
|
129
|
+
expect(calls.signArtifact).toEqual([
|
|
130
|
+
fakeClient,
|
|
131
|
+
'strict',
|
|
132
|
+
{ taskId: 't-1' },
|
|
133
|
+
'task',
|
|
134
|
+
[{ parent: true }],
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
await expect(mod.verifyArtifact(fakeClient, '{"wrapped":true}', options)).resolves.toEqual({
|
|
138
|
+
op: 'verifyWrappedArtifact',
|
|
139
|
+
});
|
|
140
|
+
expect(calls.verifyWrappedArtifact).toEqual([fakeClient, 'strict', '{"wrapped":true}']);
|
|
141
|
+
|
|
142
|
+
await expect(
|
|
143
|
+
mod.createChainOfCustody(fakeClient, [{ one: 1 }, { two: 2 }], options),
|
|
144
|
+
).resolves.toEqual({
|
|
145
|
+
op: 'createChainOfCustody',
|
|
146
|
+
count: 2,
|
|
147
|
+
});
|
|
148
|
+
expect(calls.createChainOfCustody).toEqual([fakeClient, 'strict', [{ one: 1 }, { two: 2 }]]);
|
|
149
|
+
|
|
150
|
+
await expect(
|
|
151
|
+
mod.generateWellKnownDocuments(
|
|
152
|
+
fakeClient,
|
|
153
|
+
{ name: 'Agent Card' },
|
|
154
|
+
'jws-signature',
|
|
155
|
+
'pubkey-b64',
|
|
156
|
+
{ jacsId: 'agent-1' },
|
|
157
|
+
options,
|
|
158
|
+
),
|
|
159
|
+
).resolves.toEqual({
|
|
160
|
+
op: 'generateWellKnownDocuments',
|
|
161
|
+
});
|
|
162
|
+
expect(calls.generateWellKnownDocuments).toEqual([
|
|
163
|
+
fakeClient,
|
|
164
|
+
'strict',
|
|
165
|
+
{ name: 'Agent Card' },
|
|
166
|
+
'jws-signature',
|
|
167
|
+
'pubkey-b64',
|
|
168
|
+
{ jacsId: 'agent-1' },
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
await expect(mod.assessRemoteAgent(fakeClient, '{"card":true}', options)).resolves.toEqual({
|
|
172
|
+
op: 'assessRemoteAgent',
|
|
173
|
+
allowed: true,
|
|
174
|
+
});
|
|
175
|
+
expect(calls.assessRemoteAgent).toEqual([fakeClient, 'strict', '{"card":true}']);
|
|
176
|
+
|
|
177
|
+
await expect(mod.trustA2AAgent(fakeClient, '{"card":true}', options)).resolves.toBe('trusted-agent');
|
|
178
|
+
expect(calls.trustA2AAgent).toEqual([fakeClient, 'strict', '{"card":true}']);
|
|
179
|
+
expect(integration).toBeTruthy();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('registerWithAgentCard merges card metadata and calls HaiClient.register', async () => {
|
|
183
|
+
vi.doMock('@hai.ai/jacs/a2a', () => {
|
|
184
|
+
class FakeA2AIntegration {
|
|
185
|
+
exportAgentCard(): Record<string, unknown> {
|
|
186
|
+
return {
|
|
187
|
+
name: 'Demo Agent',
|
|
188
|
+
supportedInterfaces: [{ url: 'https://agent.example.com', protocolBinding: 'jsonrpc', protocolVersion: '1.0' }],
|
|
189
|
+
capabilities: {},
|
|
190
|
+
skills: [{ id: 's1' }],
|
|
191
|
+
metadata: {},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { JACSA2AIntegration: FakeA2AIntegration };
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const mod = await import('../src/a2a.js');
|
|
199
|
+
const registerCalls: unknown[] = [];
|
|
200
|
+
const fakeHaiClient = {
|
|
201
|
+
jacsId: 'agent-1',
|
|
202
|
+
agentName: 'Agent One',
|
|
203
|
+
exportKeys: () => ({ publicKeyPem: '-----BEGIN PUBLIC KEY-----\nABC\n-----END PUBLIC KEY-----' }),
|
|
204
|
+
register: async (opts: Record<string, unknown>) => {
|
|
205
|
+
registerCalls.push(opts);
|
|
206
|
+
return { agentId: 'agent-1' };
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const result = await mod.registerWithAgentCard(
|
|
211
|
+
fakeHaiClient,
|
|
212
|
+
{},
|
|
213
|
+
{ jacsId: 'agent-1', jacsName: 'Agent One' },
|
|
214
|
+
{ ownerEmail: 'owner@hai.ai', trustPolicy: 'verified' },
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
expect(registerCalls).toHaveLength(1);
|
|
218
|
+
const sent = registerCalls[0] as Record<string, unknown>;
|
|
219
|
+
expect(sent.ownerEmail).toBe('owner@hai.ai');
|
|
220
|
+
expect(typeof sent.agentJson).toBe('string');
|
|
221
|
+
const merged = JSON.parse(sent.agentJson as string) as Record<string, unknown>;
|
|
222
|
+
expect(merged.a2aAgentCard).toBeTruthy();
|
|
223
|
+
expect((merged.metadata as Record<string, unknown>).a2aProfile).toBe('1.0');
|
|
224
|
+
expect(result.agentCard).toBeTruthy();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('onMediatedBenchmarkJob retries and submits signed artifacts', async () => {
|
|
228
|
+
const behavior = {
|
|
229
|
+
trustAllowed: true,
|
|
230
|
+
signatureValid: true,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
vi.doMock('@hai.ai/jacs/a2a', () => {
|
|
234
|
+
class FakeA2AIntegration {
|
|
235
|
+
signArtifact(
|
|
236
|
+
artifact: Record<string, unknown>,
|
|
237
|
+
artifactType: string,
|
|
238
|
+
): Record<string, unknown> {
|
|
239
|
+
return {
|
|
240
|
+
jacsType: `a2a-${artifactType}`,
|
|
241
|
+
a2aArtifact: artifact,
|
|
242
|
+
jacsSignature: { agentID: 'agent-1', signature: 'sig' },
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
verifyWrappedArtifact(): Record<string, unknown> {
|
|
247
|
+
return { valid: behavior.signatureValid, error: behavior.signatureValid ? '' : 'invalid' };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
assessRemoteAgent(): Record<string, unknown> {
|
|
251
|
+
return { allowed: behavior.trustAllowed, reason: behavior.trustAllowed ? 'ok' : 'blocked' };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { JACSA2AIntegration: FakeA2AIntegration };
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const mod = await import('../src/a2a.js');
|
|
258
|
+
const submitCalls: unknown[] = [];
|
|
259
|
+
const emailCalls: unknown[] = [];
|
|
260
|
+
let onBenchmarkAttempts = 0;
|
|
261
|
+
const fakeHaiClient = {
|
|
262
|
+
onBenchmarkJob: async (handler: (job: Record<string, unknown>) => Promise<void>) => {
|
|
263
|
+
onBenchmarkAttempts += 1;
|
|
264
|
+
if (onBenchmarkAttempts === 1) {
|
|
265
|
+
throw new Error('temporary transport failure');
|
|
266
|
+
}
|
|
267
|
+
await handler({
|
|
268
|
+
runId: 'job-1',
|
|
269
|
+
data: {
|
|
270
|
+
job_id: 'job-1',
|
|
271
|
+
remoteAgentCard: { metadata: { jacsId: 'trusted-agent' } },
|
|
272
|
+
a2aTask: { wrapped: true },
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
},
|
|
276
|
+
submitResponse: async (...args: unknown[]) => {
|
|
277
|
+
submitCalls.push(args);
|
|
278
|
+
return { success: true };
|
|
279
|
+
},
|
|
280
|
+
sendEmail: async (opts: Record<string, unknown>) => {
|
|
281
|
+
emailCalls.push(opts);
|
|
282
|
+
return { status: 'sent' };
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
await mod.onMediatedBenchmarkJob(
|
|
287
|
+
fakeHaiClient,
|
|
288
|
+
{},
|
|
289
|
+
async () => ({ message: 'handled' }),
|
|
290
|
+
{
|
|
291
|
+
trustPolicy: 'strict',
|
|
292
|
+
transport: 'ws',
|
|
293
|
+
maxReconnectAttempts: 1,
|
|
294
|
+
enforceTrustPolicy: true,
|
|
295
|
+
verifyInboundArtifact: true,
|
|
296
|
+
notifyEmail: 'ops@hai.ai',
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(onBenchmarkAttempts).toBe(2);
|
|
301
|
+
expect(submitCalls).toHaveLength(1);
|
|
302
|
+
expect(emailCalls).toHaveLength(1);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('onMediatedBenchmarkJob rejects trust/signature failures', async () => {
|
|
306
|
+
const state = {
|
|
307
|
+
trustAllowed: false,
|
|
308
|
+
signatureValid: true,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
vi.doMock('@hai.ai/jacs/a2a', () => {
|
|
312
|
+
class FakeA2AIntegration {
|
|
313
|
+
signArtifact(
|
|
314
|
+
artifact: Record<string, unknown>,
|
|
315
|
+
artifactType: string,
|
|
316
|
+
): Record<string, unknown> {
|
|
317
|
+
return {
|
|
318
|
+
jacsType: `a2a-${artifactType}`,
|
|
319
|
+
a2aArtifact: artifact,
|
|
320
|
+
jacsSignature: { agentID: 'agent-1', signature: 'sig' },
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
verifyWrappedArtifact(): Record<string, unknown> {
|
|
325
|
+
return { valid: state.signatureValid, error: state.signatureValid ? '' : 'invalid' };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
assessRemoteAgent(): Record<string, unknown> {
|
|
329
|
+
return { allowed: state.trustAllowed, reason: 'blocked' };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return { JACSA2AIntegration: FakeA2AIntegration };
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const mod = await import('../src/a2a.js');
|
|
336
|
+
const fakeHaiClient = {
|
|
337
|
+
onBenchmarkJob: async (handler: (job: Record<string, unknown>) => Promise<void>) => {
|
|
338
|
+
await handler({
|
|
339
|
+
runId: 'job-1',
|
|
340
|
+
data: {
|
|
341
|
+
job_id: 'job-1',
|
|
342
|
+
remoteAgentCard: { metadata: { jacsId: 'unknown' } },
|
|
343
|
+
a2aTask: { wrapped: true },
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
},
|
|
347
|
+
submitResponse: async () => ({ success: true }),
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
await expect(
|
|
351
|
+
mod.onMediatedBenchmarkJob(
|
|
352
|
+
fakeHaiClient,
|
|
353
|
+
{},
|
|
354
|
+
async () => ({ message: 'handled' }),
|
|
355
|
+
{
|
|
356
|
+
transport: 'ws',
|
|
357
|
+
enforceTrustPolicy: true,
|
|
358
|
+
verifyInboundArtifact: true,
|
|
359
|
+
},
|
|
360
|
+
),
|
|
361
|
+
).rejects.toThrow('trust policy rejected remote agent');
|
|
362
|
+
|
|
363
|
+
state.trustAllowed = true;
|
|
364
|
+
state.signatureValid = false;
|
|
365
|
+
|
|
366
|
+
await expect(
|
|
367
|
+
mod.onMediatedBenchmarkJob(
|
|
368
|
+
fakeHaiClient,
|
|
369
|
+
{},
|
|
370
|
+
async () => ({ message: 'handled' }),
|
|
371
|
+
{
|
|
372
|
+
transport: 'ws',
|
|
373
|
+
enforceTrustPolicy: true,
|
|
374
|
+
verifyInboundArtifact: true,
|
|
375
|
+
},
|
|
376
|
+
),
|
|
377
|
+
).rejects.toThrow('inbound a2a task signature invalid');
|
|
378
|
+
});
|
|
379
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { spawnSync } from "child_process";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
const PLATFORMS: Record<string, string> = {
|
|
7
|
+
"darwin-arm64": "@haiai/cli-darwin-arm64",
|
|
8
|
+
"darwin-x64": "@haiai/cli-darwin-x64",
|
|
9
|
+
"linux-x64": "@haiai/cli-linux-x64",
|
|
10
|
+
"linux-arm64": "@haiai/cli-linux-arm64",
|
|
11
|
+
"win32-x64": "@haiai/cli-win32-x64",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const binWrapper = path.resolve(__dirname, "..", "bin", "haiai.cjs");
|
|
15
|
+
|
|
16
|
+
describe("binary wrapper", () => {
|
|
17
|
+
it("wrapper script exists and is valid JS", () => {
|
|
18
|
+
expect(existsSync(binWrapper)).toBe(true);
|
|
19
|
+
const content = require("fs").readFileSync(binWrapper, "utf-8");
|
|
20
|
+
expect(content).toContain("findBinary");
|
|
21
|
+
expect(content).toContain("PLATFORMS");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("wrapper script can be parsed by Node.js without errors", () => {
|
|
25
|
+
const result = spawnSync(process.execPath, ["--check", binWrapper]);
|
|
26
|
+
expect(result.status).toBe(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("wrapper exits gracefully when no binary is available", () => {
|
|
30
|
+
// Run with empty PATH and no platform packages to trigger fallback
|
|
31
|
+
const result = spawnSync(process.execPath, [binWrapper], {
|
|
32
|
+
env: { ...process.env, HAIAI_BINARY_PATH: "", PATH: "" },
|
|
33
|
+
timeout: 5000,
|
|
34
|
+
});
|
|
35
|
+
const stderr = result.stderr?.toString() ?? "";
|
|
36
|
+
// Should not crash with a ReferenceError (CJS/ESM mismatch)
|
|
37
|
+
expect(stderr).not.toContain("ReferenceError");
|
|
38
|
+
// Should not crash with a SyntaxError
|
|
39
|
+
expect(stderr).not.toContain("SyntaxError");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("platform key matches a known package", () => {
|
|
43
|
+
const key = `${process.platform}-${process.arch}`;
|
|
44
|
+
expect(PLATFORMS[key]).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("platform package.json exists for current platform", () => {
|
|
48
|
+
const key = `${process.platform}-${process.arch}`;
|
|
49
|
+
const pkgName = PLATFORMS[key];
|
|
50
|
+
const pkgJsonPath = path.resolve(
|
|
51
|
+
__dirname,
|
|
52
|
+
"..",
|
|
53
|
+
"npm",
|
|
54
|
+
...pkgName.split("/"),
|
|
55
|
+
"package.json"
|
|
56
|
+
);
|
|
57
|
+
expect(existsSync(pkgJsonPath)).toBe(true);
|
|
58
|
+
|
|
59
|
+
const pkg = JSON.parse(require("fs").readFileSync(pkgJsonPath, "utf-8"));
|
|
60
|
+
expect(pkg.name).toBe(pkgName);
|
|
61
|
+
expect(pkg.os).toContain(process.platform);
|
|
62
|
+
expect(pkg.cpu).toContain(process.arch);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("all 5 platform packages have valid package.json", () => {
|
|
66
|
+
for (const [key, pkgName] of Object.entries(PLATFORMS)) {
|
|
67
|
+
const pkgJsonPath = path.resolve(
|
|
68
|
+
__dirname,
|
|
69
|
+
"..",
|
|
70
|
+
"npm",
|
|
71
|
+
...pkgName.split("/"),
|
|
72
|
+
"package.json"
|
|
73
|
+
);
|
|
74
|
+
expect(existsSync(pkgJsonPath)).toBe(true);
|
|
75
|
+
|
|
76
|
+
const pkg = JSON.parse(require("fs").readFileSync(pkgJsonPath, "utf-8"));
|
|
77
|
+
expect(pkg.name).toBe(pkgName);
|
|
78
|
+
expect(pkg.os).toBeDefined();
|
|
79
|
+
expect(pkg.cpu).toBeDefined();
|
|
80
|
+
expect(pkg.bin).toBeDefined();
|
|
81
|
+
expect(pkg.bin.haiai).toBeDefined();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("wrapper falls back gracefully when binary not present", () => {
|
|
86
|
+
const content = require("fs").readFileSync(binWrapper, "utf-8");
|
|
87
|
+
expect(content).toContain("native binary not found");
|
|
88
|
+
expect(content).toContain("process.exit(1)");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { HaiClient } from '../src/client.js';
|
|
3
|
+
import { generateTestKeypair as generateKeypair } from './setup.js';
|
|
4
|
+
|
|
5
|
+
async function makeClient(jacsId: string = 'agent/with/slash'): Promise<HaiClient> {
|
|
6
|
+
const keypair = generateKeypair();
|
|
7
|
+
return HaiClient.fromCredentials(jacsId, keypair.privateKeyPem, { url: 'https://hai.example', privateKeyPassphrase: 'keygen-password' });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('client additional API methods', () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.unstubAllGlobals();
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('escapes updateUsername agentId and uses PUT', async () => {
|
|
17
|
+
const client = await makeClient();
|
|
18
|
+
const fetchMock = vi.fn(async (url: string | URL, init?: RequestInit) => {
|
|
19
|
+
expect(String(url)).toBe('https://hai.example/api/v1/agents/agent%2F..%2Fescape/username');
|
|
20
|
+
expect(init?.method).toBe('PUT');
|
|
21
|
+
expect(init?.body).toBe(JSON.stringify({ username: 'new-name' }));
|
|
22
|
+
return new Response(
|
|
23
|
+
JSON.stringify({
|
|
24
|
+
username: 'new-name',
|
|
25
|
+
email: 'new-name@hai.ai',
|
|
26
|
+
previous_username: 'old-name',
|
|
27
|
+
}),
|
|
28
|
+
{
|
|
29
|
+
status: 200,
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
35
|
+
|
|
36
|
+
const result = await client.updateUsername('agent/../escape', 'new-name');
|
|
37
|
+
expect(result.username).toBe('new-name');
|
|
38
|
+
expect(result.previousUsername).toBe('old-name');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('escapes deleteUsername agentId and uses DELETE', async () => {
|
|
42
|
+
const client = await makeClient();
|
|
43
|
+
const fetchMock = vi.fn(async (url: string | URL, init?: RequestInit) => {
|
|
44
|
+
expect(String(url)).toBe('https://hai.example/api/v1/agents/agent%2F..%2Fescape/username');
|
|
45
|
+
expect(init?.method).toBe('DELETE');
|
|
46
|
+
return new Response(
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
released_username: 'old-name',
|
|
49
|
+
cooldown_until: '2026-03-01T00:00:00Z',
|
|
50
|
+
message: 'released',
|
|
51
|
+
}),
|
|
52
|
+
{
|
|
53
|
+
status: 200,
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
59
|
+
|
|
60
|
+
const result = await client.deleteUsername('agent/../escape');
|
|
61
|
+
expect(result.releasedUsername).toBe('old-name');
|
|
62
|
+
expect(result.cooldownUntil).toBe('2026-03-01T00:00:00Z');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('verifyDocument POSTs to public /api/jacs/verify without auth header', async () => {
|
|
66
|
+
const client = await makeClient();
|
|
67
|
+
const fetchMock = vi.fn(async (url: string | URL, init?: RequestInit) => {
|
|
68
|
+
expect(String(url)).toBe('https://hai.example/api/jacs/verify');
|
|
69
|
+
expect(init?.method).toBe('POST');
|
|
70
|
+
expect(init?.body).toBe(JSON.stringify({ document: '{"jacsId":"a"}' }));
|
|
71
|
+
const headers = new Headers(init?.headers as HeadersInit | undefined);
|
|
72
|
+
expect(headers.has('Authorization')).toBe(false);
|
|
73
|
+
return new Response(
|
|
74
|
+
JSON.stringify({
|
|
75
|
+
valid: true,
|
|
76
|
+
verified_at: '2026-01-01T00:00:00Z',
|
|
77
|
+
document_type: 'JacsDocument',
|
|
78
|
+
issuer_verified: true,
|
|
79
|
+
signature_verified: true,
|
|
80
|
+
signer_id: 'agent-1',
|
|
81
|
+
signed_at: '2026-01-01T00:00:00Z',
|
|
82
|
+
}),
|
|
83
|
+
{
|
|
84
|
+
status: 200,
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
90
|
+
|
|
91
|
+
const result = await client.verifyDocument({ jacsId: 'a' });
|
|
92
|
+
expect(result.valid).toBe(true);
|
|
93
|
+
expect(result.documentType).toBe('JacsDocument');
|
|
94
|
+
expect(result.signerId).toBe('agent-1');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('getVerification GETs public advanced verification endpoint without auth header', async () => {
|
|
98
|
+
const client = await makeClient();
|
|
99
|
+
const fetchMock = vi.fn(async (url: string | URL, init?: RequestInit) => {
|
|
100
|
+
expect(String(url)).toBe('https://hai.example/api/v1/agents/agent%2F..%2Fescape/verification');
|
|
101
|
+
expect(init?.method).toBe('GET');
|
|
102
|
+
const headers = new Headers(init?.headers as HeadersInit | undefined);
|
|
103
|
+
expect(headers.has('Authorization')).toBe(false);
|
|
104
|
+
return new Response(
|
|
105
|
+
JSON.stringify({
|
|
106
|
+
agent_id: 'agent/../escape',
|
|
107
|
+
verification: {
|
|
108
|
+
jacs_valid: true,
|
|
109
|
+
dns_valid: true,
|
|
110
|
+
hai_registered: false,
|
|
111
|
+
badge: 'domain',
|
|
112
|
+
},
|
|
113
|
+
hai_signatures: ['ed25519:abc...'],
|
|
114
|
+
verified_at: '2026-01-02T00:00:00Z',
|
|
115
|
+
errors: [],
|
|
116
|
+
}),
|
|
117
|
+
{
|
|
118
|
+
status: 200,
|
|
119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
124
|
+
|
|
125
|
+
const result = await client.getVerification('agent/../escape');
|
|
126
|
+
expect(result.agentId).toBe('agent/../escape');
|
|
127
|
+
expect(result.verification.badge).toBe('domain');
|
|
128
|
+
expect(result.verification.jacsValid).toBe(true);
|
|
129
|
+
expect(result.verification.dnsValid).toBe(true);
|
|
130
|
+
expect(result.verification.haiRegistered).toBe(false);
|
|
131
|
+
expect(result.haiSignatures).toEqual(['ed25519:abc...']);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('verifyAgentDocumentOnHai POSTs public /api/v1/agents/verify without auth header', async () => {
|
|
135
|
+
const client = await makeClient();
|
|
136
|
+
const fetchMock = vi.fn(async (url: string | URL, init?: RequestInit) => {
|
|
137
|
+
expect(String(url)).toBe('https://hai.example/api/v1/agents/verify');
|
|
138
|
+
expect(init?.method).toBe('POST');
|
|
139
|
+
expect(init?.body).toBe(
|
|
140
|
+
JSON.stringify({
|
|
141
|
+
agent_json: '{"jacsId":"agent-1","jacsAgentDomain":"example.com"}',
|
|
142
|
+
domain: 'override.example.com',
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
const headers = new Headers(init?.headers as HeadersInit | undefined);
|
|
146
|
+
expect(headers.has('Authorization')).toBe(false);
|
|
147
|
+
return new Response(
|
|
148
|
+
JSON.stringify({
|
|
149
|
+
agent_id: 'agent-1',
|
|
150
|
+
verification: {
|
|
151
|
+
jacs_valid: true,
|
|
152
|
+
dns_valid: true,
|
|
153
|
+
hai_registered: true,
|
|
154
|
+
badge: 'attested',
|
|
155
|
+
},
|
|
156
|
+
hai_signatures: ['ed25519:def...'],
|
|
157
|
+
verified_at: '2026-01-02T00:00:00Z',
|
|
158
|
+
errors: [],
|
|
159
|
+
}),
|
|
160
|
+
{
|
|
161
|
+
status: 200,
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
167
|
+
|
|
168
|
+
const result = await client.verifyAgentDocumentOnHai(
|
|
169
|
+
{ jacsId: 'agent-1', jacsAgentDomain: 'example.com' },
|
|
170
|
+
{ domain: 'override.example.com' },
|
|
171
|
+
);
|
|
172
|
+
expect(result.agentId).toBe('agent-1');
|
|
173
|
+
expect(result.verification.badge).toBe('attested');
|
|
174
|
+
expect(result.verification.haiRegistered).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
});
|