The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit f1f8fc237d6aCommit detailsView latest

tests/api.test.ts 275 lines · 9.3 KB · typescript
1
import { describe, expect, it } from 'vitest';
2
import { buildApi } from '../apps/api/src/app.js';
3
4
describe('HTTP API', () => {
5
  it('serves health, readiness, and OpenAPI', async () => {
6
    const app = buildApi();
7
    await app.ready();
8
    expect((await app.inject({ method: 'GET', url: '/health' })).statusCode).toBe(200);
9
    expect((await app.inject({ method: 'GET', url: '/ready' })).json()).toEqual({
10
      status: 'ready',
11
    });
12
    expect(
13
      (await app.inject({ method: 'GET', url: '/extensions/trust/v0.1' })).json(),
14
    ).toMatchObject({ version: '0.1', transportsMessages: false });
15
    const specification = (await app.inject({ method: 'GET', url: '/openapi.json' })).json();
16
    expect(specification.info.title).toBe('OpenClasp API');
17
    expect(specification.paths).toHaveProperty('/v0.1/risk/assess');
18
    await app.close();
19
  });
20
21
  it('isolates hosted dashboard and settings by authenticated operator', async () => {
22
    const calls: string[] = [];
23
    const repository = {
24
      dashboard: async (operatorId: string) => {
25
        calls.push(`dashboard:${operatorId}`);
26
        return {
27
          agents: [],
28
          projects: [],
29
          installations: [],
30
          setupRequests: [],
31
          publications: [],
32
          interactions: [],
33
          federatedInteractions: [],
34
          liveSessions: [],
35
          events: [],
36
          conflicts: [],
37
          receipts: [],
38
          profiles: [],
39
          runtimes: [],
40
        };
41
      },
42
      getSettings: async (operatorId: string) => {
43
        calls.push(`settings:${operatorId}`);
44
        return {
45
          displayName: '',
46
          contributionEnabled: false,
47
          retentionDays: 30,
48
          evidenceSharing: 'ask' as const,
49
          rawConversationsStored: false as const,
50
        };
51
      },
52
      saveSettings: async (operatorId: string, value: any) => {
53
        calls.push(`save:${operatorId}`);
54
        return { ...value, rawConversationsStored: false as const };
55
      },
56
      upsert: async () => undefined,
57
      list: async () => [],
58
      publishAgent: async (_operatorId: string, card: any) => card,
59
      unpublishAgent: async () => true,
60
      getPublishedAgent: async () => undefined,
61
      searchPublishedAgents: async () => [],
62
      registerAgentRuntime: async (operatorId: string, agentId: string, endpoint: string) => {
63
        calls.push(`runtime:${operatorId}:${agentId}:${endpoint}`);
64
        return {
65
          agentId,
66
          endpoint,
67
          a2aEndpoint: endpoint,
68
          status: 'verified' as const,
69
          verifiedAt: new Date().toISOString(),
70
          verificationKey: 'public-key',
71
        };
72
      },
73
      disableAgentRuntime: async (operatorId: string, agentId: string) => {
74
        calls.push(`disable-runtime:${operatorId}:${agentId}`);
75
        return { agentId, status: 'disabled' as const };
76
      },
77
    };
78
    const app = buildApi(undefined, undefined, repository);
79
    await app.ready();
80
    expect((await app.inject({ method: 'GET', url: '/v0.1/dashboard' })).statusCode).toBe(401);
81
    expect(
82
      (
83
        await app.inject({
84
          method: 'GET',
85
          url: '/v0.1/dashboard',
86
          headers: { 'x-openclasp-operator': 'user-a' },
87
        })
88
      ).statusCode,
89
    ).toBe(200);
90
    expect(
91
      (
92
        await app.inject({
93
          method: 'GET',
94
          url: '/v0.1/settings',
95
          headers: { 'x-openclasp-operator': 'user-b' },
96
        })
97
      ).statusCode,
98
    ).toBe(200);
99
    expect(
100
      (
101
        await app.inject({
102
          method: 'PUT',
103
          url: '/v0.1/agents/agent-a/runtime',
104
          headers: { 'x-openclasp-operator': 'user-a' },
105
          payload: { endpoint: 'https://agent.example/openclasp' },
106
        })
107
      ).statusCode,
108
    ).toBe(200);
109
    expect(calls).toEqual([
110
      'dashboard:user-a',
111
      'settings:user-b',
112
      'runtime:user-a:agent-a:https://agent.example/openclasp',
113
    ]);
114
    await app.close();
115
  });
116
117
  it('publishes only an owned agent public card', async () => {
118
    const published: any[] = [];
119
    const repository = {
120
      dashboard: async () => ({}) as any,
121
      getSettings: async () => ({}) as any,
122
      saveSettings: async () => ({}) as any,
123
      upsert: async () => undefined,
124
      list: async () => [
125
        {
126
          kind: 'agent_profile' as const,
127
          recordId: 'agent-one',
128
          payload: {
129
            agentId: 'agent-one',
130
            projectId: 'secret-project',
131
            name: 'Research agent',
132
            description: 'Finds primary sources',
133
            framework: 'Codex',
134
            agentVersion: '1.0.0',
135
            a2aEndpoint: 'https://agent.example/a2a',
136
            capabilities: ['research'],
137
            limitations: ['no purchases'],
138
            identityMode: 'oauth_installation' as const,
139
            status: 'active' as const,
140
            createdAt: '2026-01-01T00:00:00.000Z',
141
            updatedAt: '2026-01-01T00:00:00.000Z',
142
          },
143
        },
144
      ],
145
      publishAgent: async (_operatorId: string, card: any) => {
146
        published.push(card);
147
        return card;
148
      },
149
      unpublishAgent: async () => true,
150
      getPublishedAgent: async () => published[0],
151
      searchPublishedAgents: async () => published,
152
    };
153
    const app = buildApi(undefined, undefined, repository);
154
    await app.ready();
155
    const response = await app.inject({
156
      method: 'POST',
157
      url: '/v0.1/agents/agent-one/publication',
158
      headers: { 'x-openclasp-operator': 'owner-one' },
159
      payload: { published: true },
160
    });
161
    expect(response.statusCode).toBe(200);
162
    expect(published[0]).toMatchObject({
163
      agentId: 'agent-one',
164
      capabilities: ['research'],
165
      assurance: 'oauth_authenticated',
166
    });
167
    expect(published[0]).not.toHaveProperty('projectId');
168
    expect(published[0]).not.toHaveProperty('operatorId');
169
    const publicCard = await app.inject({
170
      method: 'GET',
171
      url: '/agents/agent-one/card.json',
172
    });
173
    expect(publicCard.statusCode).toBe(200);
174
    expect(publicCard.json()).not.toHaveProperty('projectId');
175
    const a2aCard = await app.inject({
176
      method: 'GET',
177
      url: '/agents/agent-one/a2a-agent-card.json',
178
    });
179
    expect(a2aCard.statusCode).toBe(200);
180
    expect(a2aCard.json().supportedInterfaces[0]).toMatchObject({
181
      url: 'https://agent.example/a2a',
182
      protocolVersion: '1.0',
183
    });
184
    const automation = await app.inject({
185
      method: 'PUT',
186
      url: '/v0.1/agents/agent-one/automation',
187
      headers: { 'x-openclasp-operator': 'owner-one' },
188
      payload: {
189
        a2aEndpoint: 'https://agent.example/a2a',
190
        autoPublish: true,
191
        autoAcceptPolicy: 'safe_matching',
192
        autoAcceptTaskCategories: ['research'],
193
      },
194
    });
195
    expect(automation.statusCode).toBe(200);
196
    expect(automation.json()).toMatchObject({
197
      autoPublish: true,
198
      autoAcceptPolicy: 'safe_matching',
199
    });
200
    await app.close();
201
  });
202
203
  it('accepts only structured live-session events at the reporting endpoint', async () => {
204
    const events: any[] = [];
205
    const repository = {
206
      dashboard: async () => ({}) as any,
207
      getSettings: async () => ({}) as any,
208
      saveSettings: async () => ({}) as any,
209
      upsert: async () => undefined,
210
      list: async () => [],
211
      publishAgent: async (_operatorId: string, card: any) => card,
212
      unpublishAgent: async () => true,
213
      getPublishedAgent: async () => undefined,
214
      searchPublishedAgents: async () => [],
215
      recordLiveSessionEvent: async (token: string, value: any) => {
216
        events.push({ token, value });
217
        return {
218
          recorded: true,
219
          deduplicated: false,
220
          eventId: value.eventId,
221
          attestation: {
222
            algorithm: 'Ed25519' as const,
223
            keyId: 'openclasp:test',
224
            value: 'signature',
225
            digest: 'digest',
226
          },
227
        };
228
      },
229
    };
230
    const app = buildApi(undefined, undefined, repository);
231
    await app.ready();
232
    const interactionId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
233
    expect(
234
      (await app.inject({ method: 'POST', url: `/sessions/${interactionId}/events` })).statusCode,
235
    ).toBe(401);
236
    const response = await app.inject({
237
      method: 'POST',
238
      url: `/sessions/${interactionId}/events`,
239
      headers: { authorization: 'Bearer live-session-token' },
240
      payload: {
241
        eventId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
242
        interactionId,
243
        agentId: 'agent-a',
244
        sequence: 1,
245
        type: 'message_sent',
246
        occurredAt: new Date().toISOString(),
247
        messageHash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
248
        evidenceReferences: [],
249
        details: {},
250
      },
251
    });
252
    expect(response.statusCode).toBe(200);
253
    expect(events[0]).toMatchObject({
254
      token: 'live-session-token',
255
      value: { interactionId, agentId: 'agent-a', type: 'message_sent' },
256
    });
257
    const rawMessageAttempt = await app.inject({
258
      method: 'POST',
259
      url: `/sessions/${interactionId}/events`,
260
      headers: { authorization: 'Bearer live-session-token' },
261
      payload: {
262
        eventId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc',
263
        interactionId,
264
        agentId: 'agent-a',
265
        sequence: 2,
266
        type: 'message_sent',
267
        occurredAt: new Date().toISOString(),
268
        details: { message: 'raw conversation text' },
269
      },
270
    });
271
    expect(rawMessageAttempt.statusCode).toBe(400);
272
    expect(events).toHaveLength(1);
273
    await app.close();
274
  });
275
});